pax_global_header00006660000000000000000000000064152650121740014515gustar00rootroot0000000000000052 comment=c2365bea39b2059a6bba3c9dba98be2dabf31070 guzzle-test-server-c2365be/000077500000000000000000000000001526501217400157315ustar00rootroot00000000000000guzzle-test-server-c2365be/AGENTS.md000066400000000000000000000075341526501217400172450ustar00rootroot00000000000000# Agent Guidelines ## Code and tooling - All code must remain compatible with PHP 7.4. Run PHPStan and PHP-CS-Fixer only under a PHP 7.4.x runtime; never run either tool under any other PHP major.minor version. - Use fully qualified `#[\SensitiveParameter]` on concrete executable parameters when their established role normally carries a secret, credential-bearing aggregate, or confidential Guzzle-owned container, and the active frame can throw or invoke throwing code. - Repeat the attribute on every qualifying owned caller, callee, concrete trait method, and closure parameter. Do not add it to interfaces, abstract-only declarations, pure/no-realistic-throw helpers, assignment-only sites, arbitrary generic payloads, or completed non-recoverable derivatives. - For PHP 7.4 compatibility, put `#[\SensitiveParameter]` on its own line and the parameter on the following line, expand the complete parameter list, and never add a comma after the final parameter. Native trace redaction starts on PHP 8.2 and does not redact logs, messages, properties, wire traffic, captured variables, return values, or the separate backtrace `$this`/`object`. - Always pass an explicit character list to `trim()`, `ltrim()`, and `rtrim()`; never rely on the default characters. - Handle `preg_*` engine failures: when the result is used as data, test for `false` or `null` and throw a `\RuntimeException` including `preg_last_error_msg()`; boolean validation guards must compare strictly, such as `=== 1`, so an engine failure can only ever fail closed. Diagnostic escaping is the narrow exception: use a deterministic bytewise fallback rather than throwing, so it cannot obscure the original exception. - Anchor validation patterns to the true end of input with the `D` modifier or `\z`; a bare `$` accepts a trailing newline. - Never embed raw control bytes in exception messages and other diagnostics; escape or redact the offending value first. - Helper classes that expose only public static methods are `final` and have a private constructor. - Resist native PHP serialization when a class holds live state (streams, resources, handles, callbacks, credentials); plain data holders remain serializable. - To resist, `__serialize()` and `__unserialize()` both throw `\LogicException(static::class.' should never be serialized')` and its unserialized counterpart, implemented directly on the guarded class. - In general, numeric inputs should not accept non-finite floats. In situations where they are accepted and we need to cast to a string, we should branch on `\is_finite($value)`, using `(string) $value` for the finite case and `\is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF')` otherwise. - This package has no test suite of its own; it is exercised by Guzzle's HTTP handler integration tests, which use it as a development dependency. - Never call `strtolower()`, `strtoupper()`, `strcasecmp()`, `stripos()`, or other locale-sensitive case functions; use the locale-independent `GuzzleHttp\Psr7\Utils::asciiToLower()`, `asciiToUpper()`, `caselessEquals()`, and `caselessContains()` helpers instead. - Changes in behavior need a `CHANGELOG.md` entry in the unreleased section of the target branch and an `UPGRADING.md` note when the behavior differs between major versions. ## Documentation - Wrap markdown prose and PHPDoc text to 80 columns using greedy wrapping. Never split a markdown link or an inline code span across a line break; a line that cannot be broken may exceed the limit. Avoid em dashes. - Keep PHPDoc and the corresponding `docs/` pages in sync: shared prose is deliberately word-for-word identical, including boilerplate copied verbatim between related functions, so apply the same edit to every copy. Only formatting and linking may differ, such as a docs link becoming a PHPDoc `@see` tag; the wording must never drift. guzzle-test-server-c2365be/CHANGELOG.md000066400000000000000000000050741526501217400175500ustar00rootroot00000000000000# Change Log ## 1.1.0 - 2026-10-18 - Added `Server::enqueueRawByteParts()` to queue verbatim-byte responses written in delayed parts ## 1.0.1 - 2026-08-24 - Added support for PHP 8.6 ## 1.0.0 - 2026-07-20 - Dropped support for PHP 7.2 and 7.3 - Require `guzzlehttp/guzzle` ^8.0 and `guzzlehttp/psr7` ^3.0 - Reject native PHP serialization of `Server` - Made `Server` final and non-instantiable - Added native return types to `Server` control methods - Added native parameter types to `Server` utility methods - Added native string and int types to the public `Server::$url` and `Server::$port` configuration properties - Added a `guzzle-server/read-timeout-gzip` endpoint that stalls mid-body - Added `guzzle-server/drip-timeout` and `guzzle-server/drip-timeout-gzip` endpoints that drip the body every 100ms - Added a `guzzle-server/drip-timeout-headers` endpoint that drips the header block every 100ms - Added a `guzzle-server/stall-brief` endpoint that stalls mid-body for 1.5s, then completes - Added `Server::enqueueRawBytes()` to queue verbatim-byte responses that bypass Node's HTTP handling - Inline Digest auth so secure endpoints avoid optional `http-auth` and no-qop is deterministic - Hide queued and received raw HTTP records in stack traces on PHP 8.2+ ## 0.7.0 - 2026-07-16 - Require `guzzlehttp/guzzle` ^7.13.3 and `guzzlehttp/psr7` ^2.12.4 - Decode received request data without Guzzle's deprecated JSON utility ## 0.6.0 - 2026-06-23 - Require `guzzlehttp/guzzle` ^7.12.3 and `guzzlehttp/psr7` ^2.12.3 - Improve diagnostics for invalid responses and malformed received requests ## 0.5.1 - 2026-06-18 - Disable proxying on the control client so an ambient `http_proxy` cannot misroute it - Return a 500 instead of crashing when the response queue is empty ## 0.5.0 - 2026-06-02 - Require `guzzlehttp/guzzle` ^7.11 and `guzzlehttp/psr7` ^2.11 - Harden received request reconstruction from node.js server data ## 0.4.0 - 2026-05-25 - Parse ports from Host headers when reconstructing received requests ## 0.3.3 - 2026-05-25 - Fixed `Server::enqueue()` to accept a single PSR-7 response as documented ## 0.3.2 - 2026-05-18 - Start the node.js server without shell backgrounding ## 0.3.1 - 2026-05-18 - Fix node.js server startup on Windows ## 0.3.0 - 2026-05-18 - Harden node.js server startup and shutdown handling - Improve JSON handling for queued and received server data - Fix digest authentication qop handling ## 0.2.0 - 2026-05-18 - Require Node.js `^20.19 || ^22.13 || >=24` and convert the server to ESM ## 0.1.0 - 2021-10-05 - First release extracted from Guzzle 7.3 guzzle-test-server-c2365be/CLAUDE.md000066400000000000000000000000131526501217400172020ustar00rootroot00000000000000@AGENTS.md guzzle-test-server-c2365be/LICENSE000066400000000000000000000021201526501217400167310ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2011 Michael Dowling Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guzzle-test-server-c2365be/README.md000066400000000000000000000030031526501217400172040ustar00rootroot00000000000000# Guzzle Test Server `guzzlehttp/test-server` is a testing helper for PHP HTTP clients. It starts a local Node.js HTTP server, lets your tests queue responses, and records the requests your client sends. Most Guzzle client tests should use Guzzle's MockHandler and History Middleware instead of a real server. Use this package for handler tests and integration tests that need a predictable HTTP endpoint. It is not intended to be a general-purpose web server or a production dependency. ## Installation ```bash composer require --dev guzzlehttp/test-server ``` The server requires Node.js `^20.19 || ^22.13 || >=24` available as `node`. ## Version Guidance | Version | Status | PHP Version | |---------|--------------|--------------| | 1.1 | Latest | >=7.4,<8.7 | | 0.7 | Maintenance | >=7.2.5,<8.7 | ## Quick Start ```php use GuzzleHttp\Client; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Server\Server; $client = new Client(); Server::start(); register_shutdown_function(static function (): void { Server::stop(); }); Server::enqueue([ new Response(201), ]); $response = $client->request('GET', Server::$url); echo $response->getStatusCode(); // 201 $requests = Server::received(); echo $requests[0]->getMethod(); // GET ``` ## Documentation - [Test Server Usage](docs/test-server-usage.md) - [Upgrade Guide](UPGRADING.md) - [Changelog](CHANGELOG.md) ## License Guzzle Test Server is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information. guzzle-test-server-c2365be/UPGRADING.md000066400000000000000000000053501526501217400175760ustar00rootroot00000000000000Guzzle Test Server Upgrade Guide ================================ 0.7 to 1.0 ---------- Guzzle Test Server 1.0 is a major release that raises the minimum PHP version, updates the supported Guzzle dependency stack for Guzzle 8 and Guzzle PSR-7 3.x, and tightens the `Server` class contract. #### PHP Version and Dependencies Guzzle Test Server 1.0 requires PHP `^7.4 || ^8.0`, [Guzzle 8.x](https://github.com/guzzle/guzzle/blob/8.0/UPGRADING.md), and [Guzzle PSR-7 3.x](https://github.com/guzzle/psr7/blob/3.0/UPGRADING.md). The Node.js runtime requirement is unchanged from 0.7 and remains documented in the README and package metadata. If your application still supports PHP 7.2 or 7.3, or still uses Guzzle 7 or Guzzle PSR-7 2, continue using Guzzle Test Server 0.7 until your minimum requirements are raised. #### Server Class Shape `GuzzleHttp\Server\Server` is now final and cannot be constructed. Use its static control API directly instead of subclassing or instantiating it. #### Native PHP Serialization `Server` no longer supports native PHP `serialize()` or `unserialize()`. Configure the test server during test setup instead of persisting it. #### Native Method Signatures `Server` control and utility methods now declare native parameter and return types. Update callers and wrappers to pass values that match the documented types. #### Public Configuration Properties `Server::$url` is now typed as `string`, and `Server::$port` is now typed as `int`. Assign deliberately typed values before starting the server or before the internal control client is created. Cast and validate values loaded from environment variables or test configuration before assigning them to these properties; `Server::start()` also validates the configured port. ```php use GuzzleHttp\Server\Server; $port = getenv('GUZZLE_TEST_SERVER_PORT'); if (!is_string($port)) { throw new \RuntimeException('GUZZLE_TEST_SERVER_PORT must be set'); } $validatedPort = filter_var($port, FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1, 'max_range' => 65535, ], ]); if (!is_int($validatedPort)) { throw new \RuntimeException('GUZZLE_TEST_SERVER_PORT must be a valid TCP port'); } Server::$port = $validatedPort; Server::$url = sprintf('http://127.0.0.1:%d/', Server::$port); ``` #### Sensitive Stack Trace Arguments Queued and received raw HTTP records are marked with `#[\SensitiveParameter]`. PHP 8.2 and later replace those arguments in stack traces with `SensitiveParameterValue`. PHP 7.4 through 8.1 do not redact trace arguments. This does not redact the Node server log, other logs, exception messages, object properties, wire traffic, captured variables, return values, user callbacks, or the separate executing object in an explicit backtrace. guzzle-test-server-c2365be/composer.json000066400000000000000000000024401526501217400204530ustar00rootroot00000000000000{ "name": "guzzlehttp/test-server", "description": "Node.js server and PHP controller", "license": "MIT", "authors": [ { "name": "Graham Campbell", "email": "hello@gjcampbell.co.uk", "homepage": "https://github.com/GrahamCampbell" }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", "homepage": "https://github.com/Nyholm" } ], "require": { "php": "^7.4 || ^8.0", "guzzlehttp/guzzle": "^8.1", "guzzlehttp/psr7": "^3.1", "symfony/polyfill-php80": "^1.25", "symfony/polyfill-php82": "^1.27" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2" }, "autoload": { "psr-4": { "GuzzleHttp\\Server\\": "src/" } }, "config": { "allow-plugins": { "bamarni/composer-bin-plugin": true }, "preferred-install": "dist", "sort-packages": true }, "extra": { "bamarni-bin": { "bin-links": true, "forward-command": false } } } guzzle-test-server-c2365be/docs/000077500000000000000000000000001526501217400166615ustar00rootroot00000000000000guzzle-test-server-c2365be/docs/test-server-usage.md000066400000000000000000000116001526501217400225660ustar00rootroot00000000000000# Test Server Usage Use the test server for tests that need a local HTTP endpoint with queued responses and recorded requests. The server is controlled from PHP, but the HTTP server process itself runs on Node.js. This guide covers the PHP control API and request/response behavior; setup and version requirements are documented in the project README. ## When to Use the Test Server You almost never need this package when testing normal Guzzle clients. Prefer Guzzle's [Mock Handler](https://github.com/guzzle/guzzle/blob/8.0/docs/testing-guzzle-clients.md#mock-handler) and [History Middleware](https://github.com/guzzle/guzzle/blob/8.0/docs/testing-guzzle-clients.md#history-middleware) for most client tests. Use the test server when implementing HTTP handlers or when a test must exercise an actual local HTTP server. ## Basic Usage ```php use GuzzleHttp\Client; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Server\Server; $client = new Client(); Server::start(); register_shutdown_function(static function (): void { Server::stop(); }); Server::enqueue([ new Response(201), ]); $response = $client->request('GET', Server::$url); echo $response->getStatusCode(); // 201 $requests = Server::received(); echo $requests[0]->getMethod(); // GET ``` ## Lifecycle `GuzzleHttp\Server\Server` provides a static interface to the test server. Call `Server::start()` before sending requests to `Server::$url`. `Server::start()` starts the Node.js process when needed and waits until it can receive requests before returning. `Server::start()` and `Server::stop()` are idempotent. Calling `Server::start()` while the server is already started returns immediately, and calling `Server::stop()` when it is not started is safe. Operations that use the control API, such as `Server::enqueue()` and `Server::flush()`, create the internal control client and require the server to be reachable. They do not universally start the server for you. `Server::received()` is the exception: before the server has been started, it returns an empty array. Registering `Server::stop()` as a shutdown function helps avoid leaving the local server running after a failing test. ## Configuration By default, the test server listens on `127.0.0.1:8126` and `Server::$url` is `http://127.0.0.1:8126/`. Set `Server::$port` and `Server::$url` before the first server operation or internal control client use if a test needs a different port. ```php Server::$port = 8127; Server::$url = 'http://127.0.0.1:8127/'; Server::start(); ``` ## Queuing Responses Use `Server::enqueue()` to define the responses the server should return. It accepts a `Psr\Http\Message\ResponseInterface` or an array of `ResponseInterface` objects. When responses are queued, they replace any previously queued responses. As the server receives requests, queued responses are returned in FIFO order. When the queue is empty, the server returns a 500 response and does not record the request. ```php Server::enqueue([ new Response(200, [], 'first'), new Response(500, [], 'second'), ]); ``` Use `Server::enqueueRaw()` when a PSR-7 response is not suitable, such as when you need exact headers or a raw reason phrase. The response is still written through Node's HTTP response handling. ```php Server::enqueueRaw(200, 'OK', ['X-Test' => 'raw'], 'response body'); ``` Use `Server::enqueueRawBytes()` to queue a single response as verbatim bytes written straight to the client socket, bypassing Node's HTTP response handling entirely. The connection is closed after the bytes are written. Use this to emulate misbehaving servers that send protocol-invalid bytes, such as one that sends a body in response to a HEAD request. ```php Server::enqueueRawBytes("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); ``` Use `Server::enqueueRawByteParts()` to queue a single response as verbatim bytes written straight to the client socket in parts, bypassing Node's HTTP response handling entirely. An integer part waits that many milliseconds before the next part is written. The connection is closed after the last part, so a trailing delay keeps it open after the response. Use this to emulate servers that stall mid-response or hold the connection open after a response. ```php Server::enqueueRawByteParts([ "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhel", 1500, "lo\r\n0\r\n\r\n", 3000, ]); ``` ## Inspecting Requests Use `Server::received()` to retrieve the requests sent to the server. Before the server has been started, it returns an empty array. ```php $requests = Server::received(); echo $requests[0]->getUri(); echo $requests[0]->getBody(); ``` ## Clearing Received Requests Use `Server::flush()` to clear the list of received requests. It does not change the queued responses. ```php Server::flush(); echo count(Server::received()); // 0 ``` ## Related - [Guzzle Testing Documentation](https://github.com/guzzle/guzzle/blob/8.0/docs/testing-guzzle-clients.md) guzzle-test-server-c2365be/package.json000066400000000000000000000001611526501217400202150ustar00rootroot00000000000000{ "private": true, "type": "module", "engines": { "node": "^20.19 || ^22.13 || >=24" } } guzzle-test-server-c2365be/src/000077500000000000000000000000001526501217400165205ustar00rootroot00000000000000guzzle-test-server-c2365be/src/Server.php000066400000000000000000000327521526501217400205100ustar00rootroot00000000000000request('DELETE', 'guzzle-server/requests'); } /** * Queue an array of responses or a single response on the server. * * When responses are queued, they replace any previously queued responses. * As the server receives requests, queued responses are returned in FIFO * order. When the queue is empty, the server returns a 500 response and * does not record the request. * * @param array|ResponseInterface $responses A single or array of Responses * to queue. * * @throws InvalidArgumentException */ public static function enqueue( #[\SensitiveParameter] $responses ): void { if ($responses instanceof ResponseInterface) { $responses = [$responses]; } $data = []; foreach ((array) $responses as $response) { if (!$response instanceof ResponseInterface) { throw new InvalidArgumentException(\sprintf('Invalid response given; got %s.', \get_debug_type($response))); } $headers = \array_map(static function (array $h): string { return \implode(' ,', $h); }, $response->getHeaders()); $data[] = [ 'status' => (string) $response->getStatusCode(), 'reason' => $response->getReasonPhrase(), 'headers' => $headers, 'body' => \base64_encode((string) $response->getBody()), ]; } self::getClient()->request('PUT', 'guzzle-server/responses', [ 'json' => $data, ]); } /** * Queue a single response manually, for cases where a PSR-7 response is not * suitable, such as when you need exact headers or a raw reason phrase. * * The response is still written through Node's HTTP response handling. * * @param int|string $statusCode Status code for the response, e.g. 200 * @param string $reasonPhrase Status reason response e.g "OK" * @param array $headers Array of headers to send in response * @param string|null $body Body to send in response * * @throws \GuzzleHttp\Exception\GuzzleException */ public static function enqueueRaw( $statusCode, string $reasonPhrase, #[\SensitiveParameter] array $headers, #[\SensitiveParameter] ?string $body ): void { $data = [ [ 'status' => (string) $statusCode, 'reason' => $reasonPhrase, 'headers' => $headers, 'body' => \base64_encode((string) $body), ], ]; self::getClient()->request('PUT', 'guzzle-server/responses', [ 'json' => $data, ]); } /** * Queue a single response as verbatim bytes written straight to the client * socket, bypassing Node's HTTP response handling entirely. The connection * is closed after the bytes are written. Use this to emulate misbehaving * servers that send protocol-invalid bytes, such as one that sends a body * in response to a HEAD request. * * @throws \GuzzleHttp\Exception\GuzzleException */ public static function enqueueRawBytes( #[\SensitiveParameter] string $bytes ): void { self::getClient()->request('PUT', 'guzzle-server/responses', [ 'json' => [ [ 'raw' => true, 'body' => \base64_encode($bytes), ], ], ]); } /** * Queue a single response as verbatim bytes written straight to the client * socket in parts, bypassing Node's HTTP response handling entirely. An * integer part waits that many milliseconds before the next part is * written. The connection is closed after the last part, so a trailing * delay keeps it open after the response. Use this to emulate servers that * stall mid-response or hold the connection open after a response. * * @param array $parts Byte strings and non-negative millisecond * delays, in write order. * * @throws InvalidArgumentException * @throws \GuzzleHttp\Exception\GuzzleException */ public static function enqueueRawByteParts( #[\SensitiveParameter] array $parts ): void { $encoded = []; foreach ($parts as $part) { if (\is_string($part)) { $encoded[] = ['bytes' => \base64_encode($part)]; } elseif (\is_int($part) && $part >= 0) { $encoded[] = ['delay' => $part]; } else { throw new InvalidArgumentException(\sprintf('Invalid raw byte part given; got %s.', \is_int($part) ? (string) $part : \get_debug_type($part))); } } self::getClient()->request('PUT', 'guzzle-server/responses', [ 'json' => [ [ 'raw' => true, 'parts' => $encoded, ], ], ]); } /** * Get all of the received requests. * * Before the server has been started, it returns an empty array. * * @return RequestInterface[] * * @throws InvalidArgumentException * @throws \JsonException * @throws \RuntimeException */ public static function received(): array { if (!self::$started) { return []; } $response = self::getClient()->request('GET', 'guzzle-server/requests'); $data = \json_decode((string) $response->getBody(), true, 512, \JSON_THROW_ON_ERROR); if (!\is_array($data)) { throw new \RuntimeException(\sprintf('Expected JSON array of received requests from node.js server; got %s', \get_debug_type($data))); } return \array_map( static function ( #[\SensitiveParameter] $message ): RequestInterface { if (!\is_array($message)) { throw new \RuntimeException('Expected each received request from node.js server to be an array'); } foreach (['http_method', 'uri', 'version'] as $key) { if (!\array_key_exists($key, $message)) { throw new \RuntimeException(\sprintf('Expected received request "%s" from node.js server to be present', $key)); } if (!\is_scalar($message[$key])) { throw new \RuntimeException(\sprintf('Expected received request "%s" from node.js server to be a scalar value; got %s', $key, \get_debug_type($message[$key]))); } } if (!\array_key_exists('headers', $message) || !\is_array($message['headers'])) { throw new \RuntimeException('Expected received request "headers" from node.js server to be an array'); } if (!\array_key_exists('body', $message) || (!\is_scalar($message['body']) && null !== $message['body'])) { throw new \RuntimeException('Expected received request "body" from node.js server to be a scalar value or null'); } $uri = (string) $message['uri']; if (\array_key_exists('query_string', $message) && null !== $message['query_string']) { if (!\is_scalar($message['query_string'])) { throw new \RuntimeException('Expected received request "query_string" from node.js server to be a scalar value or null'); } $uri .= '?'.(string) $message['query_string']; } $response = new Psr7\Request( (string) $message['http_method'], $uri, $message['headers'], (string) $message['body'], (string) $message['version'] ); $uri = $response->getUri()->withScheme('http'); $host = $response->getHeaderLine('host'); if ($host !== '') { $parts = \parse_url('http://'.$host); if (\is_array($parts)) { if (isset($parts['host'])) { $uri = $uri->withHost($parts['host']); } if (isset($parts['port'])) { $uri = $uri->withPort($parts['port']); } } } return $response->withUri($uri); }, $data ); } /** * Stop running the node.js server */ public static function stop(): void { try { if (self::$started) { self::getClient()->request('DELETE', 'guzzle-server'); } } finally { self::$started = false; self::closeProcess(); } } public static function wait(int $maxTries = 5): void { $tries = 0; while (!self::isListening() && ++$tries < $maxTries) { \usleep(100000); } if (!self::isListening()) { throw new \RuntimeException('Unable to contact node.js server'); } } public static function start(): void { if (self::$started) { return; } if (!self::isListening()) { $port = \filter_var(self::$port, \FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1, 'max_range' => 65535, ], ]); if (false === $port) { throw new InvalidArgumentException('Invalid node.js server port'); } $script = __DIR__.\DIRECTORY_SEPARATOR.'server.js'; $logFile = \sys_get_temp_dir().\DIRECTORY_SEPARATOR.'server.log'; $process = \proc_open( 'node '.\escapeshellarg($script).' '.$port, [ 0 => ['pipe', 'r'], 1 => ['file', $logFile, 'a'], 2 => ['file', $logFile, 'a'], ], $pipes ); if (!\is_resource($process)) { throw new \RuntimeException('Unable to start node.js server'); } self::$process = $process; foreach ($pipes as $pipe) { if (\is_resource($pipe)) { \fclose($pipe); } } try { self::wait(50); } catch (\Exception $e) { self::closeProcess(); throw $e; } } self::$started = true; } private static function isListening(): bool { try { self::getClient()->request('GET', 'guzzle-server/perf', [ 'connect_timeout' => 5, 'timeout' => 5, ]); return true; } catch (\Exception $e) { return false; } } private static function getClient(): Client { if (null === self::$client) { self::$client = new Client([ 'base_uri' => self::$url, 'proxy' => '', 'sync' => true, ]); } return self::$client; } private static function closeProcess(): void { if (!\is_resource(self::$process)) { self::$process = null; return; } $status = \proc_get_status(self::$process); if (!empty($status['running'])) { \proc_terminate(self::$process); } \proc_close(self::$process); self::$process = null; } } guzzle-test-server-c2365be/src/server.js000066400000000000000000000404041526501217400203660ustar00rootroot00000000000000/** * Guzzle node.js test server to return queued responses to HTTP requests and * expose a RESTful API for enqueueing responses and retrieving the requests * that have been received. * * - Delete all requests that have been received: * > DELETE /guzzle-server/requests * > Host: 127.0.0.1:8126 * * - Enqueue responses * > PUT /guzzle-server/responses * > Host: 127.0.0.1:8126 * > * > [{'status': 200, 'reason': 'OK', 'headers': {}, 'body': '' }] * * - Get the received requests * > GET /guzzle-server/requests * > Host: 127.0.0.1:8126 * * < HTTP/1.1 200 OK * < * < [{'http_method': 'GET', 'uri': '/', 'headers': {}, 'body': 'string'}] * * - Attempt access to the secure area * > GET /secure/by-digest/qop-auth/guzzle-server/requests * > Host: 127.0.0.1:8126 * * < HTTP/1.1 401 Unauthorized * < WWW-Authenticate: Digest realm="Digest Test", nonce="<32 hex chars>", algorithm=MD5, qop="auth" * < Content-Length: 0 * * - Shutdown the server * > DELETE /guzzle-server * > Host: 127.0.0.1:8126 * * @package Guzzle PHP * @license See the LICENSE file that was distributed with this source code. */ import * as crypto from 'node:crypto'; import * as http from 'node:http'; import * as url from 'node:url'; import * as zlib from 'node:zlib'; /** * Guzzle node.js server * @class */ var GuzzleServer = function(port, log) { this.port = port; this.log = log; this.responses = []; this.requests = []; var that = this; var md5 = function(input) { var hasher = crypto.createHash('md5'); hasher.update(input); return hasher.digest('hex'); }; var digestCredentials = {realm: 'Digest Test', login: 'me', password: 'test'}; var issuedDigestNonces = new Set(); var digestChallengeHeader = function(qop) { var nonce = crypto.randomBytes(16).toString('hex'); issuedDigestNonces.add(nonce); var header = 'Digest realm="' + digestCredentials.realm + '", nonce="' + nonce + '", algorithm=MD5'; if (qop) { header += ', qop="' + qop + '"'; } return header; }; var parseDigestAuthorization = function(header) { var scheme = /^Digest[ \t]+/i.exec(header || ''); if (!scheme) { return null; } var params = Object.create(null); var offset = scheme[0].length; var pattern = /([!#$%&'*+\-.^_`|~0-9A-Za-z]+)=(?:"((?:[^"\\]|\\.)*)"|([^,\s]+))/y; var match; while (offset < header.length) { while (header[offset] === ' ' || header[offset] === '\t') { offset++; } pattern.lastIndex = offset; match = pattern.exec(header); if (!match) { return null; } var name = match[1].toLowerCase(); if (Object.prototype.hasOwnProperty.call(params, name)) { return null; } params[name] = match[2] !== undefined ? match[2].replace(/\\(.)/g, '$1') : match[3]; offset = pattern.lastIndex; while (header[offset] === ' ' || header[offset] === '\t') { offset++; } if (offset === header.length) { return params; } if (header[offset] !== ',') { return null; } offset++; if (offset === header.length) { return null; } } return params; }; var hasOnlyDigestParams = function(params, names) { var allowed = Object.create(null); for (var i = 0; i < names.length; i++) { allowed[names[i]] = true; } for (var name in params) { if (!allowed[name]) { return false; } } return true; }; var digestResponseEquals = function(expected, actual) { if (!/^[0-9a-f]{32}$/i.test(actual || '')) { return false; } var expectedBuffer = Buffer.from(expected, 'hex'); var actualBuffer = Buffer.from(actual, 'hex'); return expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer); }; var checkDigestAuthorization = function(req, qop) { var params = parseDigestAuthorization(req.headers['authorization']); var expectedParams = qop ? ['username', 'realm', 'nonce', 'uri', 'response', 'algorithm', 'qop', 'nc', 'cnonce'] : ['username', 'realm', 'nonce', 'uri', 'response', 'algorithm']; if (!params || !hasOnlyDigestParams(params, expectedParams) || params.username !== digestCredentials.login || params.realm !== digestCredentials.realm || !params.nonce || !issuedDigestNonces.has(params.nonce) || params.uri !== req.url || !params.algorithm || params.algorithm.toUpperCase() !== 'MD5') { return false; } var ha1 = md5(digestCredentials.login + ':' + digestCredentials.realm + ':' + digestCredentials.password); var ha2 = md5(req.method + ':' + req.url); var expected; if (qop) { if (params.qop !== qop || !/^[0-9a-f]{8}$/i.test(params.nc || '') || params.nc === '00000000' || !params.cnonce) { return false; } expected = md5(ha1 + ':' + params.nonce + ':' + params.nc + ':' + params.cnonce + ':' + qop + ':' + ha2); } else { expected = md5(ha1 + ':' + params.nonce + ':' + ha2); } return digestResponseEquals(expected, params.response); }; var firewallRequest = function(request, req, res, requestHandlerCallback) { var securedAreaUriParts = request.uri.match(/^\/secure\/by-(digest)(\/qop-([^\/]*))?(\/.*)$/); if (securedAreaUriParts) { var qop = securedAreaUriParts[3] || null; if (!checkDigestAuthorization(req, qop)) { res.writeHead(401, 'Unauthorized', { 'WWW-Authenticate': digestChallengeHeader(qop), 'Content-Length': 0 }); res.end(); return; } req.url = securedAreaUriParts[4]; requestHandlerCallback(request, req, res); } else { requestHandlerCallback(request, req, res); } }; var controlRequest = function(request, req, res) { if (req.url == '/guzzle-server/garbage') { if (that.log) { console.log('returning garbage') } res.socket.end("220 example.com ESMTP\r\n200 This is garbage\r\n\r\n"); } else if (req.url == '/guzzle-server/bad-status') { if (that.log) { console.log('returning bad status code') } res.writeHead(700, 'BAD', {'Content-Length': 16}); res.end('Body of response'); } else if (req.url == '/guzzle-server/perf') { res.writeHead(200, 'OK', {'Content-Length': 16}); res.end('Body of response'); } else if (req.method == 'DELETE') { if (req.url == '/guzzle-server/requests') { // Clear the received requests that.requests = []; res.writeHead(200, 'OK', { 'Content-Length': 0 }); res.end(); if (that.log) { console.log('Flushing requests'); } } else if (req.url == '/guzzle-server') { // Shutdown the server res.writeHead(200, 'OK', { 'Content-Length': 0, 'Connection': 'close' }); res.end(); if (that.log) { console.log('Shutting down'); } that.server.close(); } } else if (req.method == 'GET') { if (req.url === '/guzzle-server/requests') { if (that.log) { console.log('Sending received requests'); } // Get received requests var body = JSON.stringify(that.requests); res.writeHead(200, 'OK', { 'Content-Length': Buffer.byteLength(body) }); res.end(body); } else if (req.url == '/guzzle-server/read-timeout') { if (that.log) { console.log('Sleeping'); } res.writeHead(200, 'OK'); res.write("sleeping 60 seconds ...\n"); setTimeout(function () { res.end("slept 60 seconds\n"); }, 60*1000); } else if (req.url == '/guzzle-server/read-timeout-gzip') { if (that.log) { console.log('Sleeping (gzip)'); } var gzipped = zlib.gzipSync( Buffer.from('hi there ... this gzip body never finishes\n'.repeat(64)) ); var half = Math.floor(gzipped.length / 2); res.writeHead(200, 'OK', { 'Content-Encoding': 'gzip' }); // Send a valid but incomplete gzip prefix, then stall so the client // times out mid-body while inflating. res.write(gzipped.slice(0, half)); setTimeout(function () { res.end(gzipped.slice(half)); }, 60*1000); } else if (req.url == '/guzzle-server/drip-timeout') { if (that.log) { console.log('Dripping response body'); } res.writeHead(200, 'OK'); // Flush the headers now; Node would otherwise hold them until the // first timer tick, risking the client's header-phase timeout. res.flushHeaders(); // Send one body byte every 100ms so no single read stalls, while the // whole body takes ~2 seconds to arrive. The disconnect check runs // inside the tick because the request 'close' event fires when the // message completes on current Node, not when the connection closes. var dripped = 0; var dripInterval = setInterval(function () { if (res.writableEnded || res.destroyed || !res.socket || res.socket.destroyed) { clearInterval(dripInterval); return; } res.write('.'); if (++dripped >= 20) { clearInterval(dripInterval); res.end(); } }, 100); } else if (req.url == '/guzzle-server/drip-timeout-gzip') { if (that.log) { console.log('Dripping response body (gzip)'); } var gzippedDrip = zlib.gzipSync( Buffer.from('hi there ... this gzip body arrives slowly\n'.repeat(64)) ); var pieceLength = Math.ceil(gzippedDrip.length / 20); var offset = 0; res.writeHead(200, 'OK', { 'Content-Encoding': 'gzip' }); // Flush the headers now; Node would otherwise hold them until the // first timer tick, risking the client's header-phase timeout. res.flushHeaders(); // Send a valid gzip slice every 100ms so no single read stalls, while // the whole body takes ~2 seconds to arrive. var gzipDripInterval = setInterval(function () { if (res.writableEnded || res.destroyed || !res.socket || res.socket.destroyed) { clearInterval(gzipDripInterval); return; } res.write(gzippedDrip.slice(offset, offset + pieceLength)); offset += pieceLength; if (offset >= gzippedDrip.length) { clearInterval(gzipDripInterval); res.end(); } }, 100); } else if (req.url == '/guzzle-server/drip-timeout-headers') { if (that.log) { console.log('Dripping response headers'); } // Write the header block to the raw socket so the header phase itself // arrives slowly (~0.8 seconds) without any single read stalling. res.socket.write('HTTP/1.1 200 OK\r\n'); var headerCount = 0; var headerInterval = setInterval(function () { if (!res.socket || res.socket.destroyed) { clearInterval(headerInterval); return; } res.socket.write('X-Drip-' + headerCount + ': ' + headerCount + '\r\n'); if (++headerCount >= 8) { clearInterval(headerInterval); res.socket.end('Content-Length: 2\r\nConnection: close\r\n\r\nok'); } }, 100); } else if (req.url == '/guzzle-server/stall-brief') { if (that.log) { console.log('Stalling briefly'); } // Stall mid-body for longer than a small socket timeout, then finish, // so a completed response can be observed without a long wait. res.writeHead(200, 'OK'); res.write('partial-'); setTimeout(function () { res.end('rest'); }, 1500); } } else if (req.method == 'PUT' && req.url == '/guzzle-server/responses') { if (that.log) { console.log('Adding responses...'); } if (!request.body) { if (that.log) { console.log('No response data was provided'); } res.writeHead(400, 'NO RESPONSES IN REQUEST', { 'Content-Length': 0 }); } else { var responses; try { responses = JSON.parse(request.body); } catch (e) { if (that.log) { console.log('Invalid response JSON: ' + e.message); } res.writeHead(400, 'INVALID RESPONSE JSON', { 'Content-Length': 0 }); res.end(); return; } if (!Array.isArray(responses)) { if (that.log) { console.log('Response data must be an array'); } res.writeHead(400, 'RESPONSES MUST BE AN ARRAY', { 'Content-Length': 0 }); res.end(); return; } that.responses = responses; for (var i = 0; i < that.responses.length; i++) { if (Object.prototype.hasOwnProperty.call(that.responses[i], 'body') && that.responses[i].body !== null) { that.responses[i].body = Buffer.from(that.responses[i].body, 'base64'); } if (Array.isArray(that.responses[i].parts)) { that.responses[i].parts = that.responses[i].parts.map(function (part) { return Object.prototype.hasOwnProperty.call(part, 'bytes') ? Buffer.from(part.bytes, 'base64') : part.delay; }); } } if (that.log) { console.log(that.responses); } res.writeHead(200, 'OK', { 'Content-Length': 0 }); } res.end(); } }; var rawPartsSockets = new WeakSet(); var writeRawParts = function(socket, parts) { var index = 0; while (index < parts.length && typeof parts[index] !== 'number') { socket.write(parts[index++]); } if (index === parts.length) { socket.end(); return; } setTimeout(function () { if (!socket.destroyed) { writeRawParts(socket, parts.slice(index + 1)); } }, parts[index]); }; var receivedRequest = function(request, req, res) { if (req.url.indexOf('/guzzle-server') === 0) { controlRequest(request, req, res); } else if (req.url.indexOf('/guzzle-server') == -1 && !that.responses.length) { res.writeHead(500); res.end('No responses in queue'); } else { if (that.log) { console.log('Returning response from queue and adding request'); } that.requests.push(request); var response = that.responses.shift(); if (!response) { res.writeHead(500); res.end('No responses in queue'); return; } if (response.raw === true) { if (that.log) { console.log('Returning raw response from queue'); } if (Array.isArray(response.parts)) { rawPartsSockets.add(res.socket); writeRawParts(res.socket, response.parts); return; } res.socket.end(response.body); return; } res.writeHead(response.status, response.reason, response.headers); res.end(response.body); } }; this.start = function() { that.server = http.createServer(function(req, res) { // Node never responds again on a connection that served raw parts. if (rawPartsSockets.has(req.socket)) { req.socket.destroy(); return; } var parts = url.parse(req.url, false); var request = { http_method: req.method, scheme: parts.scheme, uri: parts.pathname, query_string: parts.query, headers: req.headers, version: req.httpVersion, body: '' }; // Receive each chunk of the request body req.addListener('data', function(chunk) { request.body += chunk; }); // Called when the request completes req.addListener('end', function() { firewallRequest(request, req, res, receivedRequest); }); }); that.server.listen(this.port, '127.0.0.1'); if (this.log) { console.log('Server running at http://127.0.0.1:8126/'); } }; }; // Get the port from the arguments const port = process.argv.length >= 3 ? process.argv[2] : 8126; const log = process.argv.length >= 4 ? process.argv[3] : false; // Start the server const server = new GuzzleServer(port, log); server.start();