-
Notifications
You must be signed in to change notification settings - Fork 1
Websockets support #121
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
Merged
Merged
Websockets support #121
Changes from all commits
Commits
Show all changes
83 commits
Select commit
Hold shift + click to select a range
de8f9ac
Integrate websocket support
thekid 0a3bee0
Add dependency on xp-forge/websockets
thekid b061fae
Fix HTTP protocol test
thekid 2fd7308
Fix integration test
thekid 8d54e89
Add test for WebSocket routing handler
thekid f6ce581
Bump dependency on xp-forge/websockets to 4.1+
thekid 260aca2
Implement integration tests for websockets
thekid ee8fa87
Remove PHP 7.0 and 7.1 compatibility
thekid 274bc84
Refactor logging API
thekid ebfb5dd
Prevent exception inside handleDisconnect() and handleError()
thekid d85e29f
Remove sequential server, it does not support multiple connections
thekid feee4e4
Remove test for sequential server
thekid 70db24d
Fix logging
thekid a0e258a
Yield all other events after "connection"
thekid abfc4d5
Run the PHP development webserver as a backend, forwarding requests t…
thekid a314a06
Test binary messages
thekid d013905
QA: Imports
thekid db560b2
Add test for EventSource implementation
thekid 0ea9a10
Simplify test, add new variation
thekid 933349e
Fix logging
thekid b8a57df
Fix reference to undefined property
thekid 001e056
Return an empty string when reading after end of chunked data
thekid b03894e
Correctly end chunked output stream
thekid 57bf8cf
QA: Remove unused helper
thekid f7ab152
Extract common parts from the implementation of WS<->SSE translation
thekid 04ad79b
Add tests for WS<->SSE translation
thekid af6f908
Test handling of backend errors when translating SSE to WebSockets
thekid 0b88cf0
Test "close" message when translating SSE to WebSockets
thekid 5d88f89
Test unexpected events when translating SSE to WebSockets
thekid 7139ed6
Pass websocket ID via (non-standard) header "Sec-WebSocket-Id"
thekid 4e47393
Test invoking websocket with invalid utf-8
thekid f5d8504
Fix chunked transfer-encoding in requests and responses
thekid d4c3b59
Make ReadLength consistent with ReadChunks on EOF handling
thekid d3350b9
Make compatible with xp-forge/uri 2.0-RELEASE
thekid b9c69dc
Stacktrace appears in hints, omit printing to STDERR
thekid 9f9fe9f
Forward synchronously, the PHP development webserver can only handle …
thekid 470db1c
Simplify code by using the URI::resource() accessor
thekid 98cdcdf
Prevent double-flush
thekid 3e4a6b0
Test translating text and binary messages to SSE
thekid 7edfaca
Show backend port during server startup
thekid 9d0f80e
Rename WsProtocol -> WebsocketProtocol for naming consistency
thekid 170e39b
Add tests for web.io.EventSink
thekid f409f0c
Rename TranslateMessages -> ForwardMessages
thekid ee3a34b
Consistently use "resource" instead of "uri" in logging API
thekid 9d2bd54
Test implicit and explicit text messages
thekid 9d97940
Fix integration tests
thekid e24aedd
QA: Apidoc types
thekid 3ba8c75
Fix "Cannot unpack Traversable with string keys" (PHP 7.4, 8.0)
thekid 1460267
Rename "uri" to "resource" in web.io.Input
thekid d06d384
Ensure socket to backend is closed
thekid bb27996
Move HttpProtocolTest into dedicated package
thekid c2fd23b
Add tests for WebsocketProtocol
thekid 7e1d2fa
Test ping and close handling
thekid 29e4d7d
Simplify test code by extracting common code to instance variable
thekid 11428a4
Test logging messages
thekid 5490ca6
Remove compatibility with older xp-framework/networking libraries
thekid d0baa95
Consistently spell `WebSocket`, step 1
thekid 8ae6e95
Consistently spell `WebSocket`, step 2
thekid f195e9c
Consistently spell `WebSocket`, step 3
thekid c5414d9
Extract worker logic into dedicated classes
thekid df80df8
Call shutdown()
thekid 3de6518
Make ForwardRequests / ForwardMessages accept Worker instances
thekid 209df50
Add tests for workers
thekid a62ac31
Reuse worker instance
thekid d9e880e
Add workaround for PHP 7.4 not supporting ephemeral ports
thekid 3cb3755
Pass through `opcache.enable` flag to (hopefully) prevent "JIT is inc…
thekid c3f326a
Prevent startup warnings from creating problems with parsing log line
thekid b61335a
Parse over all warnings
thekid 5afae7c
Ensure launch() always selects a new free port
thekid 7d22b85
Fix possible problem when passing `127.0.0.1:0` as command line argument
thekid 0a1d730
Remove SOMAXCONN discovery
thekid 26d5ca6
Fix "Creation of dynamic property [...] is deprecated"
thekid a5f430a
MFH
thekid d70f064
MFH
thekid bf74ae4
QA: Canonicalize link to PHP docs
thekid 92c1334
Add possibility to use multiple workers in development webserver
thekid 0ff034a
Fix tests for forwarding messages and requests with workers
thekid ce26d46
Add test for request distribution
thekid 9972b95
Remove `sequential` server model
thekid 81d6dbb
Add test waits_for_worker_to_become_idle()
thekid a0b0412
Merge branch 'master' into feature/websockets
thekid 9369f41
Merge branch 'master' into feature/websockets
thekid 28ceba1
Test named argument selection
thekid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<?php namespace web\handler; | ||
|
||
use web\Handler; | ||
use web\io\EventSink; | ||
use websocket\Listeners; | ||
|
||
/** | ||
* WebSocket handler used for routing websocket handshake requests | ||
* | ||
* @test web.unittest.handler.WebSocketTest | ||
* @see https://www.rfc-editor.org/rfc/rfc6455 | ||
*/ | ||
class WebSocket implements Handler { | ||
const GUID= '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; | ||
|
||
private $listener; | ||
|
||
/** @param function(websocket.protocol.Connection, string|util.Bytes): var|websocket.Listener $listener */ | ||
public function __construct($listener) { | ||
$this->listener= Listeners::cast($listener); | ||
} | ||
|
||
/** | ||
* Handles a request | ||
* | ||
* @param web.Request $request | ||
* @param web.Response $response | ||
* @return var | ||
*/ | ||
public function handle($request, $response) { | ||
switch ($version= (int)$request->header('Sec-WebSocket-Version')) { | ||
case 13: // RFC 6455 | ||
$key= $request->header('Sec-WebSocket-Key'); | ||
$response->answer(101); | ||
$response->header('Sec-WebSocket-Accept', base64_encode(sha1($key.self::GUID, true))); | ||
foreach ($this->listener->protocols ?? [] as $protocol) { | ||
$response->header('Sec-WebSocket-Protocol', $protocol, true); | ||
} | ||
break; | ||
|
||
case 9: // Reserved version, use for WS <-> SSE translation | ||
$response->answer(200); | ||
$response->header('Content-Type', 'text/event-stream'); | ||
$response->header('Transfer-Encoding', 'chunked'); | ||
$response->trace('websocket', $request->header('Sec-WebSocket-Id')); | ||
|
||
$events= new EventSink($request, $response); | ||
foreach ($events->receive() as $message) { | ||
$this->listener->message($events, $message); | ||
} | ||
return; | ||
|
||
case 0: | ||
$response->answer(426); | ||
$response->send('This service requires use of the WebSocket protocol', 'text/plain'); | ||
return; | ||
|
||
default: | ||
$response->answer(400); | ||
$response->send('This service does not support WebSocket version '.$version, 'text/plain'); | ||
return; | ||
} | ||
|
||
yield 'connection' => ['websocket', [ | ||
'path' => $request->uri()->resource(), | ||
'headers' => $request->headers(), | ||
'listener' => $this->listener, | ||
]]; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?php namespace web\io; | ||
|
||
use io\streams\Streams; | ||
use lang\IllegalStateException; | ||
use util\Bytes; | ||
use websocket\protocol\{Opcodes, Connection}; | ||
|
||
/** @test web.unittest.io.EventSinkTest */ | ||
class EventSink extends Connection { | ||
private $request, $out; | ||
|
||
/** | ||
* Creates a new event sink | ||
* | ||
* @param web.Request $request | ||
* @param web.Response $response | ||
*/ | ||
public function __construct($request, $response) { | ||
$this->request= $request; | ||
$this->out= $response->stream(); | ||
parent::__construct(null, null, null, $request->uri()->resource(), $request->headers()); | ||
} | ||
|
||
/** | ||
* Receives messages | ||
* | ||
* @return iterable | ||
*/ | ||
public function receive() { | ||
switch ($mime= $this->request->header('Content-Type')) { | ||
case 'text/plain': yield Opcodes::TEXT => Streams::readAll($this->request->stream()); break; | ||
case 'application/octet-stream': yield Opcodes::BINARY => new Bytes(Streams::readAll($this->request->stream())); break; | ||
default: throw new IllegalStateException('Unexpected content type '.$mime); | ||
} | ||
} | ||
|
||
/** | ||
* Sends a websocket message | ||
* | ||
* @param string|util.Bytes $message | ||
* @return void | ||
*/ | ||
public function send($message) { | ||
if ($message instanceof Bytes) { | ||
$this->out->write("event: bytes\ndata: ".addcslashes($message, "\r\n")."\n\n"); | ||
} else { | ||
$this->out->write("data: ".addcslashes($message, "\r\n")."\n\n"); | ||
} | ||
} | ||
|
||
/** | ||
* Closes the websocket connection | ||
* | ||
* @param int $code | ||
* @param string $reason | ||
* @return void | ||
*/ | ||
public function close($code= 1000, $reason= '') { | ||
$this->out->write("event: close\ndata: ".$code.':'.addcslashes($reason, "\r\n")."\n\n"); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php namespace web\io; | ||
|
||
use IteratorAggregate, Traversable; | ||
use io\streams\{InputStream, StringReader}; | ||
|
||
/** | ||
* Event source is the receiving end for server-sent events, handling the | ||
* `text/event-stream` wire format. | ||
* | ||
* @test web.unittest.io.EventSourceTest | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource | ||
*/ | ||
class EventSource implements IteratorAggregate { | ||
private $reader; | ||
|
||
/** Creates a new event source */ | ||
public function __construct(InputStream $in) { | ||
$this->reader= new StringReader($in); | ||
} | ||
|
||
/** Yields events and associated data */ | ||
public function getIterator(): Traversable { | ||
$event= null; | ||
while (null !== ($line= $this->reader->readLine())) { | ||
if (0 === strncmp($line, 'event: ', 7)) { | ||
$event= substr($line, 7); | ||
} else if (0 === strncmp($line, 'data: ', 6)) { | ||
yield $event => substr($line, 6); | ||
$event= null; | ||
} | ||
} | ||
$this->reader->close(); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first array element will become the value of the
Upgrade
header, e.g.:The second argument is then passed to the
handleSwitch()
method's $context parameter.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTTP/2 cleartext uses the following: