Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions tests/phpt/server/chaos/001-h2-rapid-reset.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ $client = spawn(function () use ($port, $server) {
}

/* Open-then-RST burst: HEADERS(END_STREAM) immediately followed by
* RST_STREAM(CANCEL) on the same stream id, BURST times. */
for ($i = 0; $i < BURST; $i++) {
$sid = $c->sendRequest('GET', '/', 'x');
$c->sendRstStream($sid, 0x8 /* CANCEL */);
* RST_STREAM(CANCEL) on the same stream id, BURST times.
*
* Tearing the connection down is one legitimate answer to a rapid-reset
* attack; the burst then writes into a socket the server has closed, and
* the client reports that by throwing. What this test asserts is the
* liveness probe below, which opens a connection of its own. */
try {
for ($i = 0; $i < BURST; $i++) {
$sid = $c->sendRequest('GET', '/', 'x');
$c->sendRstStream($sid, 0x8 /* CANCEL */);
}
} catch (\Throwable $e) {
}
$c->close();

Expand Down
18 changes: 14 additions & 4 deletions tests/phpt/server/compression/060-h1-request-malformed.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,29 @@ $enc = HttpServerConfig::getSupportedEncodings();
$has_br = in_array('br', $enc, true);
$has_zstd = in_array('zstd', $enc, true);

/* One line per encoding the build has, for the failure diff, and a verdict that
* holds whatever that set is. Brotli without zstd is a supported build, and the
* property under test — a malformed body is refused — says nothing about how
* many encodings were compiled in. */
$client = spawn(function () use ($port, $server, $has_br, $has_zstd) {
delay(20);

$status = [];

if ($has_br) {
/* random bytes are not valid brotli → decoder error → 400 */
echo "br garbage: ", post($port, str_repeat("\xAA", 32), 'br'), "\n";
$status['br'] = post($port, str_repeat("\xAA", 32), 'br');
echo "br garbage: ", $status['br'], "\n";
}
if ($has_zstd) {
/* random bytes — no zstd magic → 400 */
echo "zstd garbage: ", post($port, str_repeat("\xAA", 32), 'zstd'), "\n";
$status['zstd'] = post($port, str_repeat("\xAA", 32), 'zstd');
echo "zstd garbage: ", $status['zstd'], "\n";
}

echo "every malformed body refused: ",
($status !== [] && array_unique(array_values($status)) === [400]) ? 'yes' : 'no', "\n";

delay(50);
$server->stop();
});
Expand All @@ -79,6 +90,5 @@ await($client);
echo "Done\n";
?>
--EXPECTF--
%Agarbage: 400
%Agarbage: 400
%Aevery malformed body refused: yes
Done
81 changes: 68 additions & 13 deletions tests/phpt/server/h2/_h2_client.inc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ class H2TestClient
* a reset is the exception, and nine call sites destructure that tuple. */
private ?int $last_reset_code = null;

/* Latched once a frame write reaches a peer that is gone. A server under a
* flood is entitled to tear the connection down mid-write, and on Windows
* every further fwrite() on a reset socket raises a warning that lands in
* the test's own output. The latch turns the run of warnings into one
* exception the caller can catch. */
private bool $peer_gone = false;

/** @param int $timeout_sec read timeout */
public function __construct(string $host, int $port, int $timeout_sec = 5)
{
Expand Down Expand Up @@ -115,17 +122,58 @@ class H2TestClient

/* ----- Frame primitives ----- */

/**
* A frame the caller asked for: reaching a peer that is gone is that
* caller's business, so it arrives as an exception.
*
* @throws RuntimeException when the peer is gone, on this call and every
* later one.
*/
private function writeFrame(int $type, int $flags, int $stream_id, string $payload): void
{
if (!$this->tryWriteFrame($type, $flags, $stream_id, $payload)) {
throw new RuntimeException(
'h2 client: the peer is gone, or stopped reading long enough to time the write out');
}
}

/**
* A frame this client emits on its own — a SETTINGS or PING ack answering
* what it just read. Those run inside collectResponse, whose callers
* destructure its tuple and would meet an exception where they expect the
* empty response a closed connection gives them.
*
* @return bool false once the peer is gone, on this call and every later one.
*/
private function tryWriteFrame(int $type, int $flags, int $stream_id, string $payload): bool
{
if ($this->peer_gone) {
return false;
}

$len = strlen($payload);
$hdr = chr(($len >> 16) & 0xff)
. chr(($len >> 8) & 0xff)
. chr( $len & 0xff)
. chr($type)
. chr($flags)
. pack('N', $stream_id & 0x7fffffff);
fwrite($this->sock, $hdr . $payload);
$frame = chr(($len >> 16) & 0xff)
. chr(($len >> 8) & 0xff)
. chr( $len & 0xff)
. chr($type)
. chr($flags)
. pack('N', $stream_id & 0x7fffffff)
. $payload;
$frame_len = strlen($frame);

/* @ rather than a warning per frame: a peer that went away is reported
* once, by the latch, and a run of warnings would land in the output
* the test is compared against. */
$written = @fwrite($this->sock, $frame);

if ($written === false || $written < $frame_len) {
$this->peer_gone = true;
return false;
}

fflush($this->sock);

return true;
}

/**
Expand Down Expand Up @@ -238,8 +286,14 @@ class H2TestClient
*/
public function sendWindowUpdate(int $stream_id, int $increment): void
{
$payload = pack('N', $increment & 0x7fffffff);
$this->writeFrame(H2_FRAME_WINDOW_UPDATE, 0, $stream_id, $payload);
$this->writeFrame(H2_FRAME_WINDOW_UPDATE, 0, $stream_id,
self::windowUpdatePayload($increment));
}

/** @param int $increment 1..2^31-1; the reserved high bit is masked off. */
private static function windowUpdatePayload(int $increment): string
{
return pack('N', $increment & 0x7fffffff);
}

public function sendSettingsAck(): void
Expand Down Expand Up @@ -357,7 +411,7 @@ class H2TestClient
if ($type === H2_FRAME_SETTINGS) {
if (!($flags & H2_FLAG_ACK)) {
/* Server settings — ack. */
$this->sendSettingsAck();
$this->tryWriteFrame(H2_FRAME_SETTINGS, H2_FLAG_ACK, 0, '');
}
continue;
}
Expand All @@ -370,7 +424,7 @@ class H2TestClient
if ($type === H2_FRAME_PING) {
if (!($flags & H2_FLAG_ACK)) {
/* Reflect payload with ACK bit set. */
$this->writeFrame(H2_FRAME_PING, H2_FLAG_ACK, 0, $payload);
$this->tryWriteFrame(H2_FRAME_PING, H2_FLAG_ACK, 0, $payload);
}
continue;
}
Expand Down Expand Up @@ -400,8 +454,9 @@ class H2TestClient

if ($auto_window && $consumed_since_ack >= $ack_threshold) {
/* Refill both stream + connection windows. */
$this->sendWindowUpdate($stream_id, $consumed_since_ack);
$this->sendWindowUpdate(0, $consumed_since_ack);
$credit = self::windowUpdatePayload($consumed_since_ack);
$this->tryWriteFrame(H2_FRAME_WINDOW_UPDATE, 0, $stream_id, $credit);
$this->tryWriteFrame(H2_FRAME_WINDOW_UPDATE, 0, 0, $credit);
$consumed_since_ack = 0;
}

Expand Down
18 changes: 9 additions & 9 deletions tests/phpt/server/tls/006-tls-keepalive.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ $config = (new HttpServerConfig())

$server = new HttpServer($config);
$request_count = 0;
$server->addHttpHandler(function ($req, $res) use (&$request_count, $server) {
/* Peer port per request. Three requests over one kept-alive connection share a
* port; three fresh connections get three. This is the server's own view of
* reuse — which is what the test is about, and what curl's verbose log only
* reports at second hand, in wording that changes between curl releases. */
$peer_ports = [];
$server->addHttpHandler(function ($req, $res) use (&$request_count, &$peer_ports, $server) {
$request_count++;
$peer_ports[] = $req->getRemotePort();
$res->setStatusCode(200)
->setHeader('Content-Type', 'text/plain')
->setHeader('X-Seq', (string)$request_count)
Expand All @@ -56,10 +62,6 @@ $client = spawn(function () use ($port) {
/* curl --next fires 3 requests in sequence on the SAME connection.
* Without --next, curl still keeps the conn alive by default,
* but --next makes the reuse explicit and comprehensible. */
/* Use curl's verbose output; the string "Re-using existing connection"
* (sometimes "Reusing existing connection") is curl's own indicator
* that it kept the TLS/TCP session. More robust than parsing
* num_connects, which is per-transfer. */
$cmd = sprintf(
'curl -kv --http1.1 -m 5 '
. 'https://127.0.0.1:%d/first '
Expand All @@ -78,10 +80,8 @@ spawn(function () use ($server) {
$server->start();
$out = await($client);

$reuse_count = preg_match_all('/Re-?using existing connection/i', $out);

echo "count: $request_count\n";
echo "reuses: $reuse_count\n";
echo "connections: ", count(array_unique($peer_ports)), "\n";
echo "bodies: "
. (strpos($out, 'r1:/first') !== false ? '1' : '_')
. (strpos($out, 'r2:/second') !== false ? '2' : '_')
Expand All @@ -91,6 +91,6 @@ echo "bodies: "
echo "Done\n";
--EXPECT--
count: 3
reuses: 2
connections: 1
bodies: 123
Done
Loading