Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to pass path to constructor #7

Merged
merged 7 commits into from
Jan 12, 2025
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
10 changes: 8 additions & 2 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 23 additions & 10 deletions src/main/php/websocket/WebSocket.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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 */
Expand All @@ -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; }
Expand Down Expand Up @@ -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".
Expand All @@ -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");
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down
56 changes: 48 additions & 8 deletions src/test/php/websocket/unittest/WebSocketTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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
);
Expand All @@ -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
);
Expand All @@ -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
);
Expand Down