diff --git a/composer.json b/composer.json index cb15665..52a0868 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/src/Rfc6455Acceptor.php b/src/Rfc6455Acceptor.php index 6c0f415..e5d7a5a 100644 --- a/src/Rfc6455Acceptor.php +++ b/src/Rfc6455Acceptor.php @@ -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')) { + $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)) { @@ -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', diff --git a/src/Websocket.php b/src/Websocket.php index 52b1b10..750d17f 100644 --- a/src/Websocket.php +++ b/src/Websocket.php @@ -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);