Skip to content

Commit

Permalink
Implement Websockets over HTTP/2 and HTTP/3
Browse files Browse the repository at this point in the history
  • Loading branch information
bwoebi committed Feb 15, 2024
1 parent a19a0d2 commit c3a64a9
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 42 deletions.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@
"php": ">=8.1",
"amphp/amp": "^3",
"amphp/byte-stream": "^2.1",
"amphp/http": "^2.1",
"amphp/http-server": "^3.2",
"amphp/http": "dev-structured-fields as v2.1.0",
"amphp/http-server": "dev-http3 as 3.4",
"amphp/socket": "^2.2",
"amphp/websocket": "^2",
"psr/log": "^1|^2|^3",
"revolt/event-loop": "^1"
},
"minimum-stability": "dev",
"require-dev": {
"amphp/http-client": "^5",
"amphp/http-server-static-content": "^2",
Expand Down
86 changes: 50 additions & 36 deletions src/Rfc6455Acceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,57 +21,67 @@ public function __construct(private readonly ErrorHandler $errorHandler = new In

public function handleHandshake(Request $request): Response
{
if ($request->getMethod() !== 'GET') {
$response = $this->errorHandler->handleError(HttpStatus::METHOD_NOT_ALLOWED, request: $request);
$response->setHeader('allow', 'GET');
return $response;
}

if ($request->getProtocolVersion() !== '1.1') {
if ($request->getProtocolVersion() < '1.1') {
$response = $this->errorHandler->handleError(HttpStatus::HTTP_VERSION_NOT_SUPPORTED, request: $request);
$response->setHeader('upgrade', 'websocket');
return $response;
}

if ('' !== $request->getBody()->buffer()) {
return $this->errorHandler->handleError(HttpStatus::BAD_REQUEST, request: $request);
}
$useExtendedConnect = $request->getProtocolVersion() !== "1.1";

$hasUpgradeWebsocket = false;
foreach ($request->getHeaderArray('upgrade') as $value) {
if (\strcasecmp($value, 'websocket') === 0) {
$hasUpgradeWebsocket = true;
break;
}
}
if (!$hasUpgradeWebsocket) {
$response = $this->errorHandler->handleError(HttpStatus::UPGRADE_REQUIRED, request: $request);
$response->setHeader('upgrade', 'websocket');
$requiredMethod = $useExtendedConnect ? "CONNECT" : "GET";
if ($request->getMethod() !== $requiredMethod) {
$response = $this->errorHandler->handleError(HttpStatus::METHOD_NOT_ALLOWED, request: $request);
$response->setHeader('allow', $requiredMethod);
return $response;
}

$hasConnectionUpgrade = false;
foreach ($request->getHeaderArray('connection') as $value) {
$values = \array_map('trim', \explode(',', $value));
if ($useExtendedConnect) {
if ($request->getProtocol() !== "websocket") {
$reason = 'Bad request: ":protocol: websocket" required';
return $this->errorHandler->handleError(HttpStatus::BAD_REQUEST, $reason, $request);
}
} else {
if ('' !== $request->getBody()->buffer()) {
return $this->errorHandler->handleError(HttpStatus::BAD_REQUEST, request: $request);
}

foreach ($values as $token) {
if (\strcasecmp($token, 'upgrade') === 0) {
$hasConnectionUpgrade = true;
$hasUpgradeWebsocket = false;
foreach ($request->getHeaderArray('upgrade') as $value) {
if (\strcasecmp($value, 'websocket') === 0) {
$hasUpgradeWebsocket = true;
break;
}
}
}
if (!$hasUpgradeWebsocket) {
$response = $this->errorHandler->handleError(HttpStatus::UPGRADE_REQUIRED, request: $request);
$response->setHeader('upgrade', 'websocket');
return $response;
}

if (!$hasConnectionUpgrade) {
$reason = 'Bad Request: "Connection: Upgrade" header required';
$response = $this->errorHandler->handleError(HttpStatus::UPGRADE_REQUIRED, $reason, $request);
$response->setHeader('upgrade', 'websocket');
return $response;
}
$hasConnectionUpgrade = false;
foreach ($request->getHeaderArray('connection') as $value) {
$values = \array_map('trim', \explode(',', $value));

foreach ($values as $token) {
if (\strcasecmp($token, 'upgrade') === 0) {
$hasConnectionUpgrade = true;
break;
}
}
}

if (!$hasConnectionUpgrade) {
$reason = 'Bad Request: "Connection: Upgrade" header required';
$response = $this->errorHandler->handleError(HttpStatus::UPGRADE_REQUIRED, $reason, $request);
$response->setHeader('upgrade', 'websocket');
return $response;
}

if (!$acceptKey = $request->getHeader('sec-websocket-key')) {
$reason = 'Bad Request: "Sec-Websocket-Key" header required';
return $this->errorHandler->handleError(HttpStatus::BAD_REQUEST, $reason, $request);
if (!$acceptKey = $request->getHeader('sec-websocket-key')) {

Check failure on line 81 in src/Rfc6455Acceptor.php

View workflow job for this annotation

GitHub Actions / PHP 8.1

RiskyTruthyFalsyComparison

src/Rfc6455Acceptor.php:81:17: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 81 in src/Rfc6455Acceptor.php

View workflow job for this annotation

GitHub Actions / PHP 8.2

RiskyTruthyFalsyComparison

src/Rfc6455Acceptor.php:81:17: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
$reason = 'Bad Request: "Sec-Websocket-Key" header required';
return $this->errorHandler->handleError(HttpStatus::BAD_REQUEST, $reason, $request);
}
}

if (!\in_array('13', $request->getHeaderArray('sec-websocket-version'), true)) {
Expand All @@ -81,6 +91,10 @@ public function handleHandshake(Request $request): Response
return $response;
}

if ($useExtendedConnect) {
return new Response;
}

return new Response(HttpStatus::SWITCHING_PROTOCOLS, [
'connection' => 'upgrade',
'upgrade' => 'websocket',
Expand Down
18 changes: 14 additions & 4 deletions src/Websocket.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,21 @@ public function handleRequest(Request $request): Response
{
$response = $this->acceptor->handleHandshake($request);

if ($response->getStatus() !== HttpStatus::SWITCHING_PROTOCOLS) {
$response->removeHeader('sec-websocket-accept');
$response->setHeader('connection', 'close');
if ($request->getProtocolVersion() < 2) {
if ($response->getStatus() !== HttpStatus::SWITCHING_PROTOCOLS) {
$response->removeHeader('sec-websocket-accept');
$response->setHeader('connection', 'close');

return $response;
return $response;
}
} else {
if ($response->getStatus() >= 300 /* not an OK status */) {
return $response;
}
// Avoid having websocket handlers to take care of versions manually
if ($response->getStatus() === HttpStatus::SWITCHING_PROTOCOLS) {
$response->setStatus(HttpStatus::OK);
}
}

$compressionContext = $this->negotiateCompression($request, $response);
Expand Down

0 comments on commit c3a64a9

Please sign in to comment.