diff --git a/ChangeLog.md b/ChangeLog.md index aa6ede0..297663a 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,8 +3,14 @@ WebSockets change log ## ?.?.? / ????-??-?? -* Fix "Call to a member function message() on null" errors when using an - already connected socket in the `WebSocket` constructor. +* **Heads up**: Deprecated passing origin to `WebSocket` constructor. It + should be passed inside the headers when calling *connect()*. + (@thekid) +* Merged PR #7: Added ability to pass path and query string to `WebSocket` + constructor + (@thekid) +* Fixed "Call to a member function message() on null" errors when using + an already connected socket in the `WebSocket` constructor. (@thekid) ## 4.0.0 / 2024-10-05 diff --git a/src/main/php/websocket/WebSocket.class.php b/src/main/php/websocket/WebSocket.class.php index 9b1798c..68d8ffe 100755 --- a/src/main/php/websocket/WebSocket.class.php +++ b/src/main/php/websocket/WebSocket.class.php @@ -11,7 +11,7 @@ * @test websocket.unittest.WebSocketTest */ class WebSocket implements Closeable { - private $socket, $path, $origin; + private $socket, $path, $headers; private $conn= null; private $listener= null; private $random= 'random_bytes'; @@ -20,14 +20,16 @@ class WebSocket implements Closeable { * Creates a new instance * * @param peer.Socket|string $endpoint, e.g. "wss://example.com" - * @param string $origin + * @param ?string $path */ - public function __construct($endpoint, $origin= 'localhost') { + public function __construct($endpoint, $path= null) { if ($endpoint instanceof Socket) { $this->socket= $endpoint; + $this->headers= ['Host' => $this->socket->host]; $this->path= '/'; } else { $url= parse_url($endpoint); + $this->headers= ['Host' => $url['host']]; if ('wss' === $url['scheme']) { $this->socket= new CryptoSocket($url['host'], $url['port'] ?? 443); $this->socket->cryptoImpl= STREAM_CRYPTO_METHOD_ANY_CLIENT; @@ -37,7 +39,16 @@ public function __construct($endpoint, $origin= 'localhost') { $this->path= $url['path'] ?? '/'; isset($url['query']) && $this->path.= '?'.$url['query']; } - $this->origin= $origin; + + // BC: Older versions accepted origin as second parameter + if (null === $path) { + // NOOP + } else if ('/' === $path[0] ?? null) { + $this->path= $path; + } else { + $this->path= '/'; + $this->headers['Origin']= $path; + } } /** @return peer.Socket */ @@ -46,8 +57,13 @@ public function socket() { return $this->socket; } /** @return string */ public function path() { return $this->path; } - /** @return string */ - public function origin() { return $this->origin; } + /** + * Returns origin set via constructor + * + * @deprecated Pass the origin to `connect()` instead! + * @return ?string + */ + public function origin() { return $this->headers['Origin'] ?? null; } /** @return bool */ public function connected() { return null !== $this->conn; } @@ -80,7 +96,6 @@ public function connect($headers= []) { if ($this->conn) return; $key= base64_encode(($this->random)(16)); - $headers+= ['Host' => $this->socket->host, 'Origin' => $this->origin]; $this->socket->isConnected() || $this->socket->connect(); $this->socket->write( "GET {$this->path} HTTP/1.1\r\n". @@ -89,7 +104,7 @@ public function connect($headers= []) { "Sec-WebSocket-Version: 13\r\n". "Connection: Upgrade\r\n" ); - foreach ($headers as $name => $values) { + foreach ($headers + $this->headers as $name => $values) { foreach ((array)$values as $value) { $this->socket->write("{$name}: {$value}\r\n"); } @@ -194,7 +209,6 @@ public function receive($timeout= null) { $close= unpack('ncode/a*reason', $packet); $this->conn->close($close['code'], $close['reason']); $this->conn= null; - $this->socket->close(); // 1000 is a normal close, all others indicate an error if (1000 === $close['code']) return null; @@ -222,7 +236,6 @@ public function close($code= 1000, $reason= '') { $this->conn->close($code, $reason); $this->conn= null; - $this->socket->close(); } /** Destructor - ensures connection is closed */ diff --git a/src/test/php/websocket/unittest/WebSocketTest.class.php b/src/test/php/websocket/unittest/WebSocketTest.class.php index 7ad878b..4f84e69 100755 --- a/src/test/php/websocket/unittest/WebSocketTest.class.php +++ b/src/test/php/websocket/unittest/WebSocketTest.class.php @@ -31,13 +31,15 @@ public function query($url, $expected) { Assert::equals($expected, (new WebSocket($url))->path()); } + /** @deprecated */ #[Test] public function default_origin() { - Assert::equals('localhost', (new WebSocket('ws://example.com'))->origin()); + Assert::null((new WebSocket('ws://example.com'))->origin()); } + /** @deprecated */ #[Test] - public function origin() { + public function origin_via_constructor() { Assert::equals('example.com', (new WebSocket('ws://example.com', 'example.com'))->origin()); } @@ -47,6 +49,12 @@ public function socket_argument() { Assert::equals($s, (new WebSocket($s))->socket()); } + #[Test, Values([[null, '/'], ['/', '/'], ['/sub', '/sub'], ['/?test=1&l=de', '/?test=1&l=de']])] + public function socket_path($path, $expected) { + $s= new Socket('example.com', 8443); + Assert::equals($expected, (new WebSocket($s, $path))->path()); + } + #[Test, Values([['ws://example.com', 80], ['wss://example.com', 443]])] public function default_port($url, $expected) { Assert::equals($expected, (new WebSocket($url))->socket()->port); @@ -153,6 +161,41 @@ public function handle_server_error() { Assert::false($fixture->connected()); } + #[Test] + public function handshake() { + $fixture= $this->fixture(); + $fixture->connect(); + + Assert::equals( + "GET / HTTP/1.1\r\n". + "Upgrade: websocket\r\n". + "Sec-WebSocket-Key: KioqKioqKioqKioqKioqKg==\r\n". + "Sec-WebSocket-Version: 13\r\n". + "Connection: Upgrade\r\n". + "Host: test\r\n\r\n", + $fixture->socket()->out + ); + } + + #[Test] + public function sends_headers() { + $fixture= $this->fixture(); + $fixture->connect(['Origin' => 'example.com', 'Sec-WebSocket-Protocol' => ['wamp', 'soap']]); + + Assert::equals( + "GET / HTTP/1.1\r\n". + "Upgrade: websocket\r\n". + "Sec-WebSocket-Key: KioqKioqKioqKioqKioqKg==\r\n". + "Sec-WebSocket-Version: 13\r\n". + "Connection: Upgrade\r\n". + "Origin: example.com\r\n". + "Sec-WebSocket-Protocol: wamp\r\n". + "Sec-WebSocket-Protocol: soap\r\n". + "Host: test\r\n\r\n", + $fixture->socket()->out + ); + } + #[Test] public function send_text() { $fixture= $this->fixture(); @@ -165,8 +208,7 @@ public function send_text() { "Sec-WebSocket-Key: KioqKioqKioqKioqKioqKg==\r\n". "Sec-WebSocket-Version: 13\r\n". "Connection: Upgrade\r\n". - "Host: test\r\n". - "Origin: localhost\r\n\r\n". + "Host: test\r\n\r\n". "\x81\x84****\176\117\131\136", $fixture->socket()->out ); @@ -184,8 +226,7 @@ public function send_bytes() { "Sec-WebSocket-Key: KioqKioqKioqKioqKioqKg==\r\n". "Sec-WebSocket-Version: 13\r\n". "Connection: Upgrade\r\n". - "Host: test\r\n". - "Origin: localhost\r\n\r\n". + "Host: test\r\n\r\n". "\x82\x88****\155\143\154\022\023\004\004\004", $fixture->socket()->out ); @@ -203,8 +244,7 @@ public function pings_are_answered() { "Sec-WebSocket-Key: KioqKioqKioqKioqKioqKg==\r\n". "Sec-WebSocket-Version: 13\r\n". "Connection: Upgrade\r\n". - "Host: test\r\n". - "Origin: localhost\r\n\r\n". + "Host: test\r\n\r\n". "\x8a\x81****\013", $fixture->socket()->out );