diff --git a/composer.json b/composer.json index 9183825..815f20c 100644 --- a/composer.json +++ b/composer.json @@ -27,16 +27,16 @@ }, "require": { "php": "^8.0", - "phrity/net-uri": "^1.2", - "phrity/net-stream": "^1.3", - "phrity/util-errorhandler": "^1.0", + "phrity/net-uri": "^2.0", + "phrity/net-stream": "^2.0", + "phrity/util-errorhandler": "^1.1", "psr/http-message": "^1.0 | ^2.0", "psr/log": "^1.0 | ^2.0" }, "require-dev": { "php-coveralls/php-coveralls": "^2.0", "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0", - "phrity/net-mock": "1.3", + "phrity/net-mock": "^2.0", "squizlabs/php_codesniffer": "^3.5" } } diff --git a/docs/Contributing.md b/docs/Contributing.md index a3a1ab0..68c85b5 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -24,10 +24,10 @@ Base your patch on corresponding version branch, and target that version branch | Version | Branch | PHP | Status | | --- | --- | --- | --- | -| [`2.2`](https://github.com/sirn-se/websocket-php/tree/2.2.0) | `v2.2-main` | `^8.1` | Future version | -| [`2.1`](https://github.com/sirn-se/websocket-php/tree/2.1.0) | `v2.1-main` | `^8.0` | Current version | -| [`2.0`](https://github.com/sirn-se/websocket-php/tree/2.0.0) | `v2.0-main` | `^8.0` | Bug fixes only | -| [`1.7`](https://github.com/sirn-se/websocket-php/tree/1.7.0) | `v1.7-master` | `^7.4\|^8.0` | Bug fixes only | +| [`2.2`](https://github.com/sirn-se/websocket-php/tree/2.2.0) | `v2.2-main` | `^8.0` | Current version | +| [`2.1`](https://github.com/sirn-se/websocket-php/tree/2.1.0) | `v2.1-main` | `^8.0` | Bug fixes only | +| [`2.0`](https://github.com/sirn-se/websocket-php/tree/2.0.0) | - | `^8.0` | Not supported | +| [`1.7`](https://github.com/sirn-se/websocket-php/tree/1.7.0) | - | `^7.4\|^8.0` | Not supported | | [`1.6`](https://github.com/sirn-se/websocket-php/tree/1.6.0) | - | `^7.4\|^8.0` | Not supported | | [`1.5`](https://github.com/sirn-se/websocket-php/tree/1.5.0) | - | `^7.4\|^8.0` | Not supported | | [`1.4`](https://github.com/sirn-se/websocket-php/tree/1.4.0) | - | `^7.1` | Not supported | @@ -70,7 +70,7 @@ make coverage ## Contributors * Sören Jensen (maintainer) -* Fredrik Liljegren +* Fredrik Liljegren (orginator) * Armen Baghumian Sankbarani * Ruslan Bekenev * Joshua Thijssen diff --git a/docs/Examples.md b/docs/Examples.md index 591b5ea..b6875b5 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -83,7 +83,7 @@ The random client will use random options and continuously send/receive random m Example use: ``` php examples/random_client.php --uri ws://echo.websocket.org // Connect to echo.websocket.org -php examples/random_client.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_client.php --timeout 5 --framesize 16 // Specify settings php examples/random_client.php --debug // Use runtime debugging ``` @@ -96,6 +96,6 @@ The random server will use random options and continuously send/receive random m Example use: ``` php examples/random_server.php --port 8080 // // Listen on port 8080 -php examples/random_server.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_server.php --timeout 5 --framesize 16 // Specify settings php examples/random_server.php --debug // Use runtime debugging ``` diff --git a/src/Client.php b/src/Client.php index 7ee1116..aa1307f 100644 --- a/src/Client.php +++ b/src/Client.php @@ -375,7 +375,7 @@ public function connect(): void $host_uri = (new Uri()) ->withScheme($this->socketUri->getScheme() == 'wss' ? 'ssl' : 'tcp') - ->withHost($this->socketUri->getHost(Uri::IDNA)) + ->withHost($this->socketUri->getHost(Uri::IDN_ENCODE)) ->withPort($this->socketUri->getPort(Uri::REQUIRE_PORT)); $stream = null; @@ -408,7 +408,7 @@ public function connect(): void } if (!$this->persistent || $stream->tell() == 0) { - $response = $this->performHandshake($host_uri); + $response = $this->performHandshake($this->socketUri); } $this->logger->info("[client] Client connected to {$this->socketUri}"); @@ -464,19 +464,14 @@ public function getHandshakeResponse(): Response|null * Perform upgrade handshake on new connections. * @throws HandshakeException On failed handshake */ - protected function performHandshake(Uri $host_uri): Response + protected function performHandshake(Uri $uri): Response { - $http_uri = (new Uri()) - ->withPath($this->socketUri->getPath(), Uri::ABSOLUTE_PATH) - ->withQuery($this->socketUri->getQuery()); - // Generate the WebSocket key. $key = $this->generateKey(); - $request = new Request('GET', $http_uri); + $request = new Request('GET', $uri); $request = $request - ->withHeader('Host', $host_uri->getAuthority()) ->withHeader('User-Agent', 'websocket-client-php') ->withHeader('Connection', 'Upgrade') ->withHeader('Upgrade', 'websocket') @@ -484,8 +479,8 @@ protected function performHandshake(Uri $host_uri): Response ->withHeader('Sec-WebSocket-Version', '13'); // Handle basic authentication. - if ($userinfo = $this->socketUri->getUserInfo()) { - $request = $request->withHeader('authorization', 'Basic ' . base64_encode($userinfo)); + if ($userinfo = $uri->getUserInfo()) { + $request = $request->withHeader('Authorization', 'Basic ' . base64_encode($userinfo)); } // Add and override with headers. @@ -503,7 +498,7 @@ protected function performHandshake(Uri $host_uri): Response if (empty($response->getHeaderLine('Sec-WebSocket-Accept'))) { throw new HandshakeException( - "Connection to '{$this->socketUri}' failed: Server sent invalid upgrade response.", + "Connection to '{$uri}' failed: Server sent invalid upgrade response.", $response ); } @@ -521,7 +516,7 @@ protected function performHandshake(Uri $host_uri): Response throw $e; } - $this->logger->debug("[client] Handshake on {$http_uri->getPath()}"); + $this->logger->debug("[client] Handshake on {$uri->getPath()}"); $this->connection->setHandshakeRequest($request); $this->connection->setHandshakeResponse($response); @@ -542,7 +537,7 @@ protected function generateKey(): string } /** - * Ensure URI insatnce to use in client. + * Ensure URI instance to use in client. * @param UriInterface|string $uri A ws/wss-URI * @return Uri * @throws BadUriException On invalid URI diff --git a/src/Http/HttpHandler.php b/src/Http/HttpHandler.php index 4594dd0..2717922 100644 --- a/src/Http/HttpHandler.php +++ b/src/Http/HttpHandler.php @@ -83,12 +83,16 @@ public function pull(): MessageInterface foreach ($headers as $header) { $parts = explode(':', $header, 2); if (count($parts) == 2) { - $message = $message->withAddedHeader($parts[0], $parts[1]); + if ($message->getheaderLine($parts[0]) === '') { + $message = $message->withHeader($parts[0], trim($parts[1])); + } else { + $message = $message->withAddedHeader($parts[0], trim($parts[1])); + } } } if ($message instanceof Request) { - $uri = new Uri("//{$message->getHeaderLine('host')}{$path}"); - $message = $message->withUri($uri); + $uri = new Uri("//{$message->getHeaderLine('Host')}{$path}"); + $message = $message->withUri($uri, true); } return $message; diff --git a/src/Http/Message.php b/src/Http/Message.php index cd20e3f..67b1e9e 100644 --- a/src/Http/Message.php +++ b/src/Http/Message.php @@ -164,22 +164,15 @@ public function getAsArray(): array private function handleHeader(string $name, mixed $value): void { - // @todo: Add all available characters, these are just some of them. if (!preg_match('|^[0-9a-zA-Z#_-]+$|', $name)) { throw new InvalidArgumentException("'{$name}' is not a valid header field name."); } $value = is_array($value) ? $value : [$value]; - if (empty($value)) { - throw new InvalidArgumentException("Invalid header value(s) provided."); - } foreach ($value as $content) { if (!is_string($content) && !is_numeric($content)) { throw new InvalidArgumentException("Invalid header value(s) provided."); } $content = trim($content); - if ('' === $content) { - throw new InvalidArgumentException("Invalid header value(s) provided."); - } $this->headers[strtolower($name)][$name][] = $content; } } diff --git a/src/Http/Request.php b/src/Http/Request.php index 01cd465..f14b3f0 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -28,9 +28,7 @@ public function __construct(string $method = 'GET', UriInterface|string|null $ur { $this->uri = $uri instanceof Uri ? $uri : new Uri((string)$uri); $this->method = $method; - if ($this->uri->getHost()) { - $this->headers = ['host' => ['Host' => [$this->uri->getAuthority()]]]; - } + $this->headers = ['host' => ['Host' => [$this->formatHostHeader($this->uri)]]]; } /** @@ -104,9 +102,7 @@ public function withUri(UriInterface $uri, bool $preserveHost = false): self if (isset($new->headers['host'])) { unset($new->headers['host']); } - if ($host = $uri->getHost()) { - $new->headers = array_merge(['host' => ['Host' => [$uri->getAuthority()]]], $new->headers); - } + $new->headers = array_merge(['host' => ['Host' => [$this->formatHostHeader($uri)]]], $new->headers); } return $new; } @@ -122,4 +118,11 @@ public function getAsArray(): array "{$this->getMethod()} {$this->getRequestTarget()} HTTP/{$this->getProtocolVersion()}", ], parent::getAsArray()); } + + private function formatHostHeader(Uri $uri): string + { + $host = $uri->getHost(); + $port = $uri->getPort(); + return $host && $port ? "{$host}:{$port}" : $host; + } } diff --git a/tests/suites/client/ConfigTest.php b/tests/suites/client/ConfigTest.php index 01ab0b1..bd84897 100644 --- a/tests/suites/client/ConfigTest.php +++ b/tests/suites/client/ConfigTest.php @@ -105,7 +105,7 @@ public function testUriInstanceWsDefaultPort(): void $client->setStreamFactory(new StreamFactory()); $this->expectWsClientConnect(port: 80); - $this->expectWsClientPerformHandshake('localhost:80', '/my/mock/path'); + $this->expectWsClientPerformHandshake('localhost', '/my/mock/path'); $client->connect(); $this->expectSocketStreamIsConnected(); @@ -123,7 +123,7 @@ public function testUriInstanceWssDefaultPort(): void $client->setStreamFactory(new StreamFactory()); $this->expectWsClientConnect(scheme: 'ssl', port: 443); - $this->expectWsClientPerformHandshake('localhost:443', '/my/mock/path'); + $this->expectWsClientPerformHandshake('localhost', '/my/mock/path'); $client->connect(); $this->expectSocketStreamIsConnected(); @@ -141,7 +141,7 @@ public function testUriStringAuthorization(): void $this->expectWsClientPerformHandshake( 'localhost:8000', '/my/mock/path', - "authorization: Basic dXNlbmFtZTpwYXNzd29yZA==\r\n" + "Authorization: Basic dXNlbmFtZTpwYXNzd29yZA==\r\n" ); $client->connect(); diff --git a/tests/suites/http/HttpHandlerTest.php b/tests/suites/http/HttpHandlerTest.php index 03ce93e..74e8e15 100644 --- a/tests/suites/http/HttpHandlerTest.php +++ b/tests/suites/http/HttpHandlerTest.php @@ -104,14 +104,14 @@ public function testPullServerRequest(): void $this->assertInstanceOf(HttpHandler::class, $handler); $this->expectSocketStreamReadLine()->setReturn(function () { - return "GET /a/path?a=b HTTP/1.1\r\nHost: test.com:123\r\n\r\n"; + return "GET /a/path?a=b HTTP/1.1\r\nA: \r\nA: 0\r\nA: B\r\nHost: test.com:123\r\n\r\n"; }); $request = $handler->pull(); $this->assertInstanceOf(ServerRequest::class, $request); $this->assertEquals('/a/path?a=b', $request->getRequestTarget()); $this->assertEquals('GET', $request->getMethod()); $this->assertEquals('1.1', $request->getProtocolVersion()); - $this->assertEquals(['Host' => ['test.com:123']], $request->getHeaders()); + $this->assertEquals(['Host' => ['test.com:123'], 'A' => ['0', 'B']], $request->getHeaders()); $this->assertTrue($request->hasHeader('Host')); $uri = $request->getUri(); $this->assertInstanceOf(UriInterface::class, $uri); diff --git a/tests/suites/http/RequestTest.php b/tests/suites/http/RequestTest.php index 4d014d0..547cd28 100644 --- a/tests/suites/http/RequestTest.php +++ b/tests/suites/http/RequestTest.php @@ -46,7 +46,7 @@ public function testEmptyRequest(): void $this->assertEquals('GET', $request->getMethod()); $this->assertInstanceOf(UriInterface::class, $request->getUri()); $this->assertEquals('1.1', $request->getProtocolVersion()); - $this->assertEquals([], $request->getHeaders()); + $this->assertEquals(['Host' => ['']], $request->getHeaders()); $this->assertFalse($request->hasHeader('none')); $this->assertEquals([], $request->getHeader('none')); $this->assertEquals('', $request->getHeaderLine('none')); @@ -54,6 +54,7 @@ public function testEmptyRequest(): void $this->assertEquals('WebSocket\Http\Request(GET )', "{$request}"); $this->assertEquals([ 'GET / HTTP/1.1', + 'Host: ', ], $request->getAsArray()); } @@ -239,15 +240,10 @@ public function testHeaderValueInvalidVariants(mixed $value): void public static function provideInvalidHeaderValues(): Generator { - yield ['']; - yield [' ']; - yield [['0', '']]; yield [[null]]; yield [[[0]]]; - yield [[]]; } - /** * @dataProvider provideValidHeaderValues */ @@ -257,15 +253,19 @@ public function testHeaderValueValidVariants(mixed $value, array $expected): voi $request = new Request(); $request = $request->withHeader('name', $value); $this->assertInstanceOf(Request::class, $request); - $this->assertEquals($expected, $request->getHeaders()); + $this->assertEquals($expected, $request->getHeader('name')); } public static function provideValidHeaderValues(): Generator { - yield ['null', ['name' => ['null']]]; - yield ['0 ', ['name' => ['0']]]; - yield [' 0', ['name' => ['0']]]; - yield [['0', '1'], ['name' => ['0', '1']]]; - yield [0, ['name' => ['0']]]; + yield ['', ['']]; + yield [' ', ['']]; + yield [['0', ''], ['0', '']]; + yield [[], []]; + yield ['null', ['null']]; + yield ['0 ', ['0']]; + yield [' 0', ['0']]; + yield [['0', '1'], ['0', '1']]; + yield [0, ['0']]; } } diff --git a/tests/suites/http/ResponseTest.php b/tests/suites/http/ResponseTest.php index 3e1e29c..d1ae3f8 100644 --- a/tests/suites/http/ResponseTest.php +++ b/tests/suites/http/ResponseTest.php @@ -117,7 +117,7 @@ public function testWithBodyError(): void $response->withBody($factory->createStream()); } - public function testHaederNameError(): void + public function testHeaderNameError(): void { $response = new Response(); $this->expectException(InvalidArgumentException::class); @@ -125,13 +125,4 @@ public function testHaederNameError(): void $this->expectExceptionMessage("'.' is not a valid header field name."); $response->withHeader('.', 'invaid name'); } - - public function testHaederValueError(): void - { - $response = new Response(); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionCode(0); - $this->expectExceptionMessage("Invalid header value(s) provided."); - $response->withHeader('name', ''); - } } diff --git a/tests/suites/http/ServerRequestTest.php b/tests/suites/http/ServerRequestTest.php index 8172c18..f630be2 100644 --- a/tests/suites/http/ServerRequestTest.php +++ b/tests/suites/http/ServerRequestTest.php @@ -42,7 +42,7 @@ public function testEmptyRequest(): void $this->assertEquals('GET', $request->getMethod()); $this->assertInstanceOf(UriInterface::class, $request->getUri()); $this->assertEquals('1.1', $request->getProtocolVersion()); - $this->assertEquals([], $request->getHeaders()); + $this->assertEquals(['Host' => ['']], $request->getHeaders()); $this->assertFalse($request->hasHeader('none')); $this->assertEquals([], $request->getHeader('none')); $this->assertEquals('', $request->getHeaderLine('none')); @@ -51,6 +51,7 @@ public function testEmptyRequest(): void $this->assertEquals('WebSocket\Http\ServerRequest(GET /)', "{$request}"); $this->assertEquals([ 'GET / HTTP/1.1', + 'Host: ', ], $request->getAsArray()); }