From 917c4c32692d0bdf4e6b6bf8a868b575a23469f7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Jan 2025 11:59:24 +0100 Subject: [PATCH 1/7] Add ability to pass path to constructor --- src/main/php/websocket/WebSocket.class.php | 16 +++--- .../unittest/WebSocketTest.class.php | 53 +++++++++++++------ 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/main/php/websocket/WebSocket.class.php b/src/main/php/websocket/WebSocket.class.php index 9b1798c..d1650c7 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; private $conn= null; private $listener= null; private $random= 'random_bytes'; @@ -20,12 +20,12 @@ 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= '/') { if ($endpoint instanceof Socket) { $this->socket= $endpoint; - $this->path= '/'; + $this->path= $path; } else { $url= parse_url($endpoint); if ('wss' === $url['scheme']) { @@ -34,10 +34,9 @@ public function __construct($endpoint, $origin= 'localhost') { } else { $this->socket= new Socket($url['host'], $url['port'] ?? 80); } - $this->path= $url['path'] ?? '/'; + $this->path= $url['path'] ?? $path; isset($url['query']) && $this->path.= '?'.$url['query']; } - $this->origin= $origin; } /** @return peer.Socket */ @@ -46,9 +45,6 @@ public function socket() { return $this->socket; } /** @return string */ public function path() { return $this->path; } - /** @return string */ - public function origin() { return $this->origin; } - /** @return bool */ public function connected() { return null !== $this->conn; } @@ -80,7 +76,7 @@ public function connect($headers= []) { if ($this->conn) return; $key= base64_encode(($this->random)(16)); - $headers+= ['Host' => $this->socket->host, 'Origin' => $this->origin]; + $headers+= ['Host' => $this->socket->host]; $this->socket->isConnected() || $this->socket->connect(); $this->socket->write( "GET {$this->path} HTTP/1.1\r\n". diff --git a/src/test/php/websocket/unittest/WebSocketTest.class.php b/src/test/php/websocket/unittest/WebSocketTest.class.php index 7ad878b..c179770 100755 --- a/src/test/php/websocket/unittest/WebSocketTest.class.php +++ b/src/test/php/websocket/unittest/WebSocketTest.class.php @@ -31,16 +31,6 @@ public function query($url, $expected) { Assert::equals($expected, (new WebSocket($url))->path()); } - #[Test] - public function default_origin() { - Assert::equals('localhost', (new WebSocket('ws://example.com'))->origin()); - } - - #[Test] - public function origin() { - Assert::equals('example.com', (new WebSocket('ws://example.com', 'example.com'))->origin()); - } - #[Test] public function socket_argument() { $s= new Socket('example.com', 8443); @@ -153,6 +143,40 @@ 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']); + + 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". + "Host: test\r\n\r\n", + $fixture->socket()->out + ); + } + #[Test] public function send_text() { $fixture= $this->fixture(); @@ -165,8 +189,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 +207,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 +225,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 ); From e81698bc4e3611052ca90d862210fe35fc120a99 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Jan 2025 12:14:54 +0100 Subject: [PATCH 2/7] Restore backwards compatibility for origin headers --- src/main/php/websocket/WebSocket.class.php | 33 +++++++++++++++---- .../unittest/WebSocketTest.class.php | 18 ++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/main/php/websocket/WebSocket.class.php b/src/main/php/websocket/WebSocket.class.php index d1650c7..4b34fb4 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; + private $socket, $path, $headers; private $conn= null; private $listener= null; private $random= 'random_bytes'; @@ -20,23 +20,35 @@ class WebSocket implements Closeable { * Creates a new instance * * @param peer.Socket|string $endpoint, e.g. "wss://example.com" - * @param string $path + * @param ?string $path */ - public function __construct($endpoint, $path= '/') { + public function __construct($endpoint, $path= null) { if ($endpoint instanceof Socket) { $this->socket= $endpoint; - $this->path= $path; + $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; } else { $this->socket= new Socket($url['host'], $url['port'] ?? 80); } - $this->path= $url['path'] ?? $path; + $this->path= $url['path'] ?? '/'; isset($url['query']) && $this->path.= '?'.$url['query']; } + + // 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 */ @@ -45,6 +57,14 @@ public function socket() { return $this->socket; } /** @return string */ public function path() { return $this->path; } + /** + * 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; } @@ -76,7 +96,6 @@ public function connect($headers= []) { if ($this->conn) return; $key= base64_encode(($this->random)(16)); - $headers+= ['Host' => $this->socket->host]; $this->socket->isConnected() || $this->socket->connect(); $this->socket->write( "GET {$this->path} HTTP/1.1\r\n". @@ -85,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"); } diff --git a/src/test/php/websocket/unittest/WebSocketTest.class.php b/src/test/php/websocket/unittest/WebSocketTest.class.php index c179770..51aa2bd 100755 --- a/src/test/php/websocket/unittest/WebSocketTest.class.php +++ b/src/test/php/websocket/unittest/WebSocketTest.class.php @@ -31,12 +31,30 @@ public function query($url, $expected) { Assert::equals($expected, (new WebSocket($url))->path()); } + /** @deprecated */ + #[Test] + public function default_origin() { + Assert::null((new WebSocket('ws://example.com'))->origin()); + } + + /** @deprecated */ + #[Test] + public function origin_via_constructor() { + Assert::equals('example.com', (new WebSocket('ws://example.com', 'example.com'))->origin()); + } + #[Test] public function socket_argument() { $s= new Socket('example.com', 8443); Assert::equals($s, (new WebSocket($s))->socket()); } + #[Test, Values([[null, '/'], ['/', '/'], ['/sub', '/sub']])] + 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); From 7ae09db02eb38d73422b2ed392963b59a8c7de30 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Jan 2025 12:22:58 +0100 Subject: [PATCH 3/7] Verify query string works --- src/test/php/websocket/unittest/WebSocketTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/websocket/unittest/WebSocketTest.class.php b/src/test/php/websocket/unittest/WebSocketTest.class.php index 51aa2bd..c911404 100755 --- a/src/test/php/websocket/unittest/WebSocketTest.class.php +++ b/src/test/php/websocket/unittest/WebSocketTest.class.php @@ -49,7 +49,7 @@ public function socket_argument() { Assert::equals($s, (new WebSocket($s))->socket()); } - #[Test, Values([[null, '/'], ['/', '/'], ['/sub', '/sub']])] + #[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()); From 85865d91c85218ae430b403de445c2b4e273bb2e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Jan 2025 12:23:44 +0100 Subject: [PATCH 4/7] Document changes --- ChangeLog.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index aa6ede0..1fa5f49 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,8 +3,13 @@ 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) +* 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 From f10d730b1f2d64bb981bc978aee46b59a3ac89b0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Jan 2025 12:25:52 +0100 Subject: [PATCH 5/7] Test sending headers with multiple values See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Protocol --- src/test/php/websocket/unittest/WebSocketTest.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/php/websocket/unittest/WebSocketTest.class.php b/src/test/php/websocket/unittest/WebSocketTest.class.php index c911404..4f84e69 100755 --- a/src/test/php/websocket/unittest/WebSocketTest.class.php +++ b/src/test/php/websocket/unittest/WebSocketTest.class.php @@ -180,7 +180,7 @@ public function handshake() { #[Test] public function sends_headers() { $fixture= $this->fixture(); - $fixture->connect(['Origin' => 'example.com', 'Sec-WebSocket-Protocol' => 'wamp']); + $fixture->connect(['Origin' => 'example.com', 'Sec-WebSocket-Protocol' => ['wamp', 'soap']]); Assert::equals( "GET / HTTP/1.1\r\n". @@ -190,6 +190,7 @@ public function sends_headers() { "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 ); From b294e20bc62a1e6568b4e41a69eaebd542b82b87 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Jan 2025 12:29:56 +0100 Subject: [PATCH 6/7] Remove superfluous Socket::close() calls --- src/main/php/websocket/WebSocket.class.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/php/websocket/WebSocket.class.php b/src/main/php/websocket/WebSocket.class.php index 4b34fb4..68d8ffe 100755 --- a/src/main/php/websocket/WebSocket.class.php +++ b/src/main/php/websocket/WebSocket.class.php @@ -209,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; @@ -237,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 */ From 4dd11f61df5029d9bb2abf2fcdd1c314f22d5350 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Jan 2025 12:32:45 +0100 Subject: [PATCH 7/7] Link to PR [skip ci] --- ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 1fa5f49..297663a 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,7 +6,8 @@ WebSockets change log * **Heads up**: Deprecated passing origin to `WebSocket` constructor. It should be passed inside the headers when calling *connect()*. (@thekid) -* Added ability to pass path and query string to `WebSocket` constructor +* 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.