diff --git a/CHANGELOG.md b/CHANGELOG.md index 2debb2e..93aa6fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,24 @@ All notable changes to ext-websocket are documented here. -## 0.10.0 - 2026-05-18 +## 1.1.0 - 2026-05-23 + +### Added + +- Added `maxConnections`, `handshakeTimeoutMs`, and `idleTimeoutMs` server options to limit idle and slowloris-style connection pressure. +- Added UTF-8 validation for text messages and close reason payloads. +- Added validation for reserved WebSocket close codes on outgoing close frames. + +### Changed + +- Changed masked frame generation to use random mask keys instead of a deterministic mask. + +### Fixed + +- Fixed compatibility with PHP 8.1 and 8.2 when compiling generated class constants. +- Removed dependency on newer `ext/random` C headers when generating WebSocket mask keys. + +## 1.0.0 - 2026-05-18 ### Added diff --git a/README.md b/README.md index df577ef..c66f256 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,9 @@ Options: |---|---| | `maxMessageSize` | Maximum incoming text/binary message size; defaults to 16 MiB | | `maxQueuedBytes` | Maximum outgoing queued bytes per connection; defaults to 16 MiB | +| `maxConnections` | Maximum concurrently accepted TCP connections; defaults to 10000 | +| `handshakeTimeoutMs` | Maximum idle time before HTTP Upgrade completes; defaults to 10000 ms | +| `idleTimeoutMs` | Maximum idle time after HTTP Upgrade completes; defaults to 120000 ms | Prefer `WebSocket\ServerOptions` for explicit configuration. Associative arrays remain supported for compatibility. diff --git a/docs/production.md b/docs/production.md index bcd9423..85954a8 100644 --- a/docs/production.md +++ b/docs/production.md @@ -33,10 +33,13 @@ Set explicit limits for your workload: $server = new WebSocket\Server(new WebSocket\ServerOptions( maxMessageSize: 1024 * 1024, maxQueuedBytes: 8 * 1024 * 1024, + maxConnections: 1000, + handshakeTimeoutMs: 5000, + idleTimeoutMs: 60000, )); ``` -`maxMessageSize` protects incoming frames and fragmented messages. `maxQueuedBytes` protects memory when a client reads slowly and outgoing writes need to be queued. +`maxMessageSize` protects incoming frames and fragmented messages. `maxQueuedBytes` protects memory when a client reads slowly and outgoing writes need to be queued. `maxConnections`, `handshakeTimeoutMs`, and `idleTimeoutMs` protect file descriptors and event-loop work from slowloris-style or idle-connection pressure. ## Slow Clients @@ -68,4 +71,5 @@ The extension intentionally keeps the public API small, so production metrics sh - Disable Xdebug for benchmarks and production. - Put `wss://` behind a TLS proxy. - Start with conservative `maxMessageSize` and `maxQueuedBytes`. +- Set `maxConnections`, `handshakeTimeoutMs`, and `idleTimeoutMs` for the deployment envelope. - Keep the extension and your PHP runtime on the same PHP minor version used during build. diff --git a/php_websocket.h b/php_websocket.h index cd2f43e..fd01cc8 100644 --- a/php_websocket.h +++ b/php_websocket.h @@ -13,16 +13,20 @@ #include "php.h" #include "php_network.h" #include "Zend/zend_closures.h" +#include "Zend/zend_enum.h" #include extern zend_module_entry websocket_module_entry; #define phpext_websocket_ptr &websocket_module_entry -#define PHP_WEBSOCKET_VERSION "0.10.0" +#define PHP_WEBSOCKET_VERSION "1.1.0" #define WEBSOCKET_HTTP_MAX_REQUEST_SIZE 8192 #define WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE (16 * 1024 * 1024) #define WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES (16 * 1024 * 1024) +#define WEBSOCKET_DEFAULT_MAX_CONNECTIONS 10000 +#define WEBSOCKET_DEFAULT_HANDSHAKE_TIMEOUT_MS 10000 +#define WEBSOCKET_DEFAULT_IDLE_TIMEOUT_MS 120000 #define WEBSOCKET_CLOSE_REASON_MAX_LEN 123 #define WEBSOCKET_OPCODE_CONTINUATION 0x0 @@ -44,6 +48,7 @@ extern zend_module_entry websocket_module_entry; #define WEBSOCKET_CLOSE_NORMAL 1000 #define WEBSOCKET_CLOSE_PROTOCOL_ERROR 1002 +#define WEBSOCKET_CLOSE_INVALID_PAYLOAD 1007 #define WEBSOCKET_CLOSE_MESSAGE_TOO_BIG 1009 #ifdef ZTS @@ -128,6 +133,8 @@ typedef struct _websocket_connection_object { bool fragmented; uint8_t fragmented_opcode; zend_string *fragmented_payload; + uint64_t accepted_at_usec; + uint64_t last_activity_usec; zend_object std; } websocket_connection_object; @@ -175,6 +182,8 @@ uint8_t websocket_protocol_message_type_opcode(zval *type); zend_object *websocket_protocol_message_type_from_opcode(uint8_t opcode); bool websocket_protocol_opcode_is_valid(zend_long opcode); bool websocket_protocol_opcode_is_control(zend_long opcode); +bool websocket_protocol_close_code_is_valid(zend_long code); +bool websocket_protocol_is_valid_utf8(const char *payload, size_t payload_len); zend_string *websocket_protocol_pack_payload(zend_string *payload, uint8_t opcode, uint8_t flags); zend_string *websocket_protocol_close_payload(zend_long code, zend_string *reason); websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, size_t len, zend_string **accept_key, size_t *bytes_consumed); diff --git a/php_websocket_compat.h b/php_websocket_compat.h index 67d8a49..317138d 100644 --- a/php_websocket_compat.h +++ b/php_websocket_compat.h @@ -27,6 +27,11 @@ static inline zend_class_entry *zend_register_internal_class_with_flags( } #endif +#if PHP_VERSION_ID < 80300 +# define zend_declare_typed_class_constant(ce, name, value, access_type, attributes, type) \ + zend_declare_class_constant((ce), ZSTR_VAL(name), ZSTR_LEN(name), (value)) +#endif + #if PHP_VERSION_ID >= 80300 # define WEBSOCKET_SET_DEFAULT_HANDLERS(ce, h) \ (ce)->default_object_handlers = (h) diff --git a/tests/001-contracts.phpt b/tests/001-contracts.phpt index 904c5c2..d13a3b4 100644 --- a/tests/001-contracts.phpt +++ b/tests/001-contracts.phpt @@ -15,9 +15,12 @@ var_dump(method_exists(WebSocket\Server::class, 'send')); var_dump(method_exists(WebSocket\Server::class, 'close')); var_dump((new ReflectionMethod(WebSocket\Connection::class, 'send'))->getNumberOfParameters()); var_dump((new ReflectionMethod(WebSocket\ServerOptions::class, '__construct'))->getNumberOfParameters()); -$options = new WebSocket\ServerOptions(maxMessageSize: 1024, maxQueuedBytes: 2048); +$options = new WebSocket\ServerOptions(maxMessageSize: 1024, maxQueuedBytes: 2048, maxConnections: 16, handshakeTimeoutMs: 250, idleTimeoutMs: 500); var_dump($options->maxMessageSize); var_dump($options->maxQueuedBytes); +var_dump($options->maxConnections); +var_dump($options->handshakeTimeoutMs); +var_dump($options->idleTimeoutMs); try { new WebSocket\ServerOptions(maxMessageSize: 0); } catch (ValueError $e) { @@ -45,9 +48,12 @@ bool(true) bool(false) bool(false) int(2) -int(2) +int(5) int(1024) int(2048) +int(16) +int(250) +int(500) WebSocket\ServerOptions::__construct(): Argument #1 ($maxMessageSize) must be at least 1 int(3) int(2) diff --git a/tests/002-protocol.phpt b/tests/002-protocol.phpt index e542996..958e762 100644 --- a/tests/002-protocol.phpt +++ b/tests/002-protocol.phpt @@ -24,8 +24,10 @@ var_dump($decoded->bytesConsumed); var_dump(Protocol::decode(substr($frame, 0, 1))); $masked = Protocol::encode('hi', MessageType::Text, true); -var_dump(bin2hex($masked)); +var_dump(strlen($masked)); +var_dump((ord($masked[1]) & 0x80) !== 0); var_dump(Protocol::decode($masked)->payload); +var_dump($masked !== Protocol::encode('hi', MessageType::Text, true)); ?> --EXPECT-- string(28) "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" @@ -36,5 +38,7 @@ string(5) "hello" bool(true) int(7) NULL -string(16) "8182123456787a5d" +int(8) +bool(true) string(2) "hi" +bool(true) diff --git a/tests/003-protocol-primitives.phpt b/tests/003-protocol-primitives.phpt index edb23b6..2d028c0 100644 --- a/tests/003-protocol-primitives.phpt +++ b/tests/003-protocol-primitives.phpt @@ -54,6 +54,29 @@ try { } catch (\Error $e) { var_dump($e instanceof \Error); } + +try { + Protocol::encode("\xff"); +} catch (\ValueError $e) { + var_dump($e instanceof \ValueError); +} + +var_dump(bin2hex(Protocol::decode(Protocol::encode("\xff", MessageType::Binary))->payload)); + +try { + new CloseFrame(1000, "\xff"); +} catch (\ValueError $e) { + var_dump($e instanceof \ValueError); +} + +foreach ([1004, 1005, 1006, 1015] as $code) { + try { + new CloseFrame($code); + } catch (\ValueError $e) { + var_dump($e instanceof \ValueError); + } +} + ?> --EXPECT-- int(0) @@ -76,3 +99,10 @@ int(1) bool(true) bool(true) bool(true) +bool(true) +string(2) "ff" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/tests/013-server-connection-limits.phpt b/tests/013-server-connection-limits.phpt new file mode 100644 index 0000000..2e94530 --- /dev/null +++ b/tests/013-server-connection-limits.phpt @@ -0,0 +1,135 @@ +--TEST-- +WebSocket\Server enforces connection limits and handshake timeouts +--EXTENSIONS-- +websocket +--SKIPIF-- + +--FILE-- +listen('127.0.0.1', PORT_PLACEHOLDER); +$server->run(); +PHP; + +$serverCode = str_replace('PORT_PLACEHOLDER', (string) $port, $serverCode); +file_put_contents($serverFile, $serverCode); + +$process = proc_open( + [PHP_BINARY, '-n', '-d', 'extension=' . $extension, $serverFile], + [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, +); + +if (!is_resource($process)) { + echo "cannot start server\n"; + exit; +} + +$first = false; +$deadline = microtime(true) + 5.0; +do { + $first = @stream_socket_client('tcp://127.0.0.1:' . $port, $errno, $errstr, 0.1); + if ($first !== false) { + break; + } + + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + + usleep(10000); +} while (microtime(true) < $deadline); + +if ($first !== false) { + fwrite($first, "GET /slow HTTP/1.1\r\n"); +} + +usleep(200000); + +$second = @stream_socket_client('tcp://127.0.0.1:' . $port, $errno, $errstr, 0.5); +$secondResponse = ''; +if ($second !== false) { + stream_set_timeout($second, 1); + $secondResponse = fread($second, 4096); + fclose($second); +} + +if ($first !== false) { + stream_set_timeout($first, 2); + usleep(1200000); + $firstResponse = fread($first, 4096); + $firstMeta = stream_get_meta_data($first); + fclose($first); +} else { + $firstResponse = null; + $firstMeta = ['eof' => false]; +} + +$status = proc_get_status($process); +if ($status['running']) { + proc_terminate($process); +} + +$stdout = stream_get_contents($pipes[1]); +$stderr = stream_get_contents($pipes[2]); +fclose($pipes[1]); +fclose($pipes[2]); +proc_close($process); + +var_dump($first !== false); +var_dump($second !== false); +var_dump(str_contains($secondResponse, "HTTP/1.1 503 Service Unavailable\r\n")); +var_dump($firstResponse === '' && $firstMeta['eof']); +var_dump($stdout === ''); +var_dump($stderr === ''); + +@unlink($serverFile); +@rmdir($tmpDir); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/tests/014-server-utf8-validation.phpt b/tests/014-server-utf8-validation.phpt new file mode 100644 index 0000000..c4984cd --- /dev/null +++ b/tests/014-server-utf8-validation.phpt @@ -0,0 +1,264 @@ +--TEST-- +WebSocket\Server rejects invalid UTF-8 text and close reason payloads +--EXTENSIONS-- +websocket +--SKIPIF-- + +--FILE-- +listen('127.0.0.1', PORT_PLACEHOLDER); + +$server->onOpen(static function (Connection $connection): void { + file_put_contents(EVENTS_PLACEHOLDER, "open\n"); +}); + +$server->onMessage(static function (Connection $connection, string $message): void { + file_put_contents(EVENTS_PLACEHOLDER, "message:" . bin2hex($message) . "\n", FILE_APPEND); +}); + +$server->onClose(static function (Connection $connection) use ($server): void { + file_put_contents(EVENTS_PLACEHOLDER, "close\n", FILE_APPEND); + $server->stop(); +}); + +$server->run(); +file_put_contents(EVENTS_PLACEHOLDER, "returned\n", FILE_APPEND); +PHP; + + $serverCode = str_replace( + ['PORT_PLACEHOLDER', 'EVENTS_PLACEHOLDER'], + [(string) $port, var_export($eventsFile, true)], + $serverCode, + ); + file_put_contents($serverFile, $serverCode); + + $process = proc_open( + [PHP_BINARY, '-n', '-d', 'extension=' . $extension, $serverFile], + [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + ); + + if (!is_resource($process)) { + echo "cannot start server\n"; + exit; + } + + $client = false; + $deadline = microtime(true) + 5.0; + + do { + $client = @stream_socket_client('tcp://127.0.0.1:' . $port, $errno, $errstr, 0.1); + if ($client !== false) { + break; + } + + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + + usleep(10000); + } while (microtime(true) < $deadline); + + $readFrame = static function ($client): CloseFrame|null { + $buffer = ''; + $deadline = microtime(true) + 5.0; + + do { + $chunk = fread($client, 4096); + if (is_string($chunk) && $chunk !== '') { + $buffer .= $chunk; + $frame = Protocol::unpack($buffer); + if ($frame instanceof CloseFrame) { + return $frame; + } + } + + usleep(10000); + } while (microtime(true) < $deadline); + + return null; + }; + + if ($client !== false) { + fwrite($client, implode("\r\n", [ + 'GET / HTTP/1.1', + 'Host: 127.0.0.1:' . $port, + 'Upgrade: websocket', + 'Connection: Upgrade', + 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version: 13', + '', + '', + ])); + + stream_set_timeout($client, 1); + fread($client, 4096); + + foreach ($frames as $frame) { + fwrite($client, $frame); + } + + $close = $readFrame($client); + fclose($client); + } else { + $close = null; + } + + $deadline = microtime(true) + 5.0; + do { + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + + usleep(10000); + } while (microtime(true) < $deadline); + + $status = proc_get_status($process); + if ($status['running']) { + proc_terminate($process); + } + + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + $events = file_exists($eventsFile) ? file($eventsFile, FILE_IGNORE_NEW_LINES) : []; + + @unlink($eventsFile); + @unlink($serverFile); + @rmdir($tmpDir); + + return [$close, $events, $stdout, $stderr]; +}; + +$packRawMasked = static function (string $payload, int $opcode, bool $final = true): string { + $mask = "\x12\x34\x56\x78"; + $header = chr(($final ? 0x80 : 0) | $opcode); + $len = strlen($payload); + + if ($len < 126) { + $header .= chr(0x80 | $len); + } elseif ($len <= 0xffff) { + $header .= chr(0x80 | 126) . pack('n', $len); + } else { + $header .= chr(0x80 | 127) . pack('NN', 0, $len); + } + + $masked = ''; + for ($i = 0; $i < $len; $i++) { + $masked .= $payload[$i] ^ $mask[$i & 3]; + } + + return $header . $mask . $masked; +}; + +$invalidText = $runCase('text', [ + $packRawMasked("\xff", Protocol::OPCODE_TEXT), +]); + +$invalidFragmentedText = $runCase('fragmented', [ + $packRawMasked("\xf0\x9f", Protocol::OPCODE_TEXT, false), + $packRawMasked("\xff", Protocol::OPCODE_CONTINUATION), +]); + +$invalidCloseReason = $runCase('close', [ + $packRawMasked(pack('n', Protocol::CLOSE_NORMAL) . "\xff", Protocol::OPCODE_CLOSE), +]); + +foreach ([$invalidText, $invalidFragmentedText, $invalidCloseReason] as [$close, $events, $stdout, $stderr]) { + var_dump($close instanceof CloseFrame); + var_dump($close?->code); + var_dump(in_array('message:ff', $events, true)); + var_dump($events); + var_dump($stdout === ''); + var_dump($stderr === ''); +} +?> +--EXPECT-- +bool(true) +int(1007) +bool(false) +array(3) { + [0]=> + string(4) "open" + [1]=> + string(5) "close" + [2]=> + string(8) "returned" +} +bool(true) +bool(true) +bool(true) +int(1007) +bool(false) +array(3) { + [0]=> + string(4) "open" + [1]=> + string(5) "close" + [2]=> + string(8) "returned" +} +bool(true) +bool(true) +bool(true) +int(1007) +bool(false) +array(3) { + [0]=> + string(4) "open" + [1]=> + string(5) "close" + [2]=> + string(8) "returned" +} +bool(true) +bool(true) diff --git a/websocket.stub.php b/websocket.stub.php index 4bedb2e..737ab39 100644 --- a/websocket.stub.php +++ b/websocket.stub.php @@ -22,15 +22,18 @@ final class Server * Supported options: * - maxMessageSize: maximum accepted text/binary message size in bytes. * - maxQueuedBytes: maximum queued outgoing bytes per connection. + * - maxConnections: maximum concurrently accepted TCP connections. + * - handshakeTimeoutMs: maximum idle time before HTTP Upgrade completes. + * - idleTimeoutMs: maximum idle time after HTTP Upgrade completes. * - * @param ServerOptions|array{maxMessageSize?: positive-int, maxQueuedBytes?: positive-int} $options + * @param ServerOptions|array{maxMessageSize?: int, maxQueuedBytes?: int, maxConnections?: int, handshakeTimeoutMs?: int, idleTimeoutMs?: int} $options */ public function __construct(ServerOptions|array $options = []) {} /** * Bind the TCP listener used by run(). * - * @param int<1, 65535> $port + * @param int $port * * @throws \ValueError If the host contains null bytes or the port is outside TCP range. */ @@ -94,24 +97,54 @@ final class ServerOptions /** * Maximum accepted text/binary message size in bytes. * - * @var positive-int + * @var int */ public readonly int $maxMessageSize; /** * Maximum queued outgoing bytes per connection. * - * @var positive-int + * @var int */ public readonly int $maxQueuedBytes; /** - * @param positive-int $maxMessageSize - * @param positive-int $maxQueuedBytes + * Maximum concurrently accepted TCP connections. + * + * @var int + */ + public readonly int $maxConnections; + + /** + * Maximum idle time before HTTP Upgrade completes, in milliseconds. + * + * @var int + */ + public readonly int $handshakeTimeoutMs; + + /** + * Maximum idle time after HTTP Upgrade completes, in milliseconds. + * + * @var int + */ + public readonly int $idleTimeoutMs; + + /** + * @param int $maxMessageSize + * @param int $maxQueuedBytes + * @param int $maxConnections + * @param int $handshakeTimeoutMs + * @param int $idleTimeoutMs * * @throws \ValueError If a limit is less than 1. */ - public function __construct(int $maxMessageSize = 16 * 1024 * 1024, int $maxQueuedBytes = 16 * 1024 * 1024) {} + public function __construct( + int $maxMessageSize = 16 * 1024 * 1024, + int $maxQueuedBytes = 16 * 1024 * 1024, + int $maxConnections = 10000, + int $handshakeTimeoutMs = 10000, + int $idleTimeoutMs = 120000, + ) {} } /** @@ -147,7 +180,7 @@ public function send(string $payload, MessageType $type = MessageType::Text): vo /** * Send a close frame and close the connection. * - * @param int<1000, 4999> $code + * @param int $code * * @throws \ValueError If the close code or reason length is invalid. */ @@ -315,13 +348,13 @@ public static function decode(string $buffer): Frame|CloseFrame|null {} /** * Encode a raw WebSocket frame. * - * @param self::OPCODE_CONTINUATION|self::OPCODE_TEXT|self::OPCODE_BINARY|self::OPCODE_CLOSE|self::OPCODE_PING|self::OPCODE_PONG $opcode - * @param int-mask-of $flags + * @param int $opcode + * @param int $flags * * @throws \TypeError If $data is not a supported payload or frame value object. * @throws \ValueError If opcode, flags, or control payload length is invalid. */ - public static function pack(string|Frame|CloseFrame $data, int $opcode = self::OPCODE_TEXT, int $flags = self::FLAG_FIN): string {} + public static function pack(string|Frame|CloseFrame $data, int $opcode = \WebSocket\Protocol::OPCODE_TEXT, int $flags = \WebSocket\Protocol::FLAG_FIN): string {} /** * Decode one raw WebSocket frame from a buffer. diff --git a/websocket_arginfo.h b/websocket_arginfo.h index 555e2db..4256180 100644 --- a/websocket_arginfo.h +++ b/websocket_arginfo.h @@ -1,20 +1,8 @@ -/* - * Copyright (c) 2026 Aleksandr Cherednikov - * Licensed under the MIT License. See LICENSE for details. - */ - -/* Generated-by-hand bootstrap arginfo. - * Keep this compatible with PHP 8.1+ until gen_stub.php can run in CI. */ - -#include "Zend/zend_enum.h" +/* This is a generated file, edit the .stub.php file instead. + * Stub hash: 0b1d9b784f9f267416396b70e5c2d1523bc87820 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_Server___construct, 0, 0, 0) - { "options", ZEND_TYPE_INIT_CLASS_CONST_MASK("WebSocket\\ServerOptions", MAY_BE_ARRAY | _ZEND_ARG_INFO_FLAGS(0, 0, 0)), "[]" }, -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_ServerOptions___construct, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maxMessageSize, IS_LONG, 0, "16 * 1024 * 1024") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maxQueuedBytes, IS_LONG, 0, "16 * 1024 * 1024") + ZEND_ARG_OBJ_TYPE_MASK(0, options, WebSocket\\ServerOptions, MAY_BE_ARRAY, "[]") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_listen, 0, 2, IS_VOID, 0) @@ -27,7 +15,9 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_onOpen, 0 ZEND_END_ARG_INFO() #define arginfo_class_WebSocket_Server_onMessage arginfo_class_WebSocket_Server_onOpen + #define arginfo_class_WebSocket_Server_onClose arginfo_class_WebSocket_Server_onOpen + #define arginfo_class_WebSocket_Server_onError arginfo_class_WebSocket_Server_onOpen ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_run, 0, 0, IS_VOID, 0) @@ -38,28 +28,36 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_getDriver, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_ServerOptions___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maxMessageSize, IS_LONG, 0, "16 * 1024 * 1024") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maxQueuedBytes, IS_LONG, 0, "16 * 1024 * 1024") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maxConnections, IS_LONG, 0, "10000") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, handshakeTimeoutMs, IS_LONG, 0, "10000") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, idleTimeoutMs, IS_LONG, 0, "120000") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Connection_send, 0, 1, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_OBJECT, 0, "WebSocket\\MessageType::Text") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, type, WebSocket\\MessageType, 0, "WebSocket\\MessageType::Text") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Connection_close, 0, 0, IS_VOID, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, code, IS_LONG, 0, "1000") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "''") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "\'\'") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Connection_isOpen, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_Frame___construct, 0, 0, 2) - ZEND_ARG_TYPE_INFO(0, type, IS_OBJECT, 0) + ZEND_ARG_OBJ_INFO(0, type, WebSocket\\MessageType, 0) ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, final, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_CloseFrame___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, code, IS_LONG, 0, "WebSocket\\Protocol::CLOSE_NORMAL") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "''") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, reason, IS_STRING, 0, "\'\'") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Protocol_acceptKey, 0, 1, IS_STRING, 0) @@ -68,16 +66,16 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Protocol_encode, 0, 1, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, payload, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_OBJECT, 0, "WebSocket\\MessageType::Text") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, type, WebSocket\\MessageType, 0, "WebSocket\\MessageType::Text") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, masked, _IS_BOOL, 0, "false") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_WebSocket_Protocol_decode, 0, 1, MAY_BE_OBJECT|MAY_BE_NULL) +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_WebSocket_Protocol_decode, 0, 1, WebSocket\\Frame|WebSocket\\CloseFrame, MAY_BE_NULL) ZEND_ARG_TYPE_INFO(0, buffer, IS_STRING, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Protocol_pack, 0, 1, IS_STRING, 0) - ZEND_ARG_TYPE_MASK(0, data, MAY_BE_STRING|MAY_BE_OBJECT, NULL) + ZEND_ARG_OBJ_TYPE_MASK(0, data, WebSocket\\Frame|WebSocket\\CloseFrame, MAY_BE_STRING, NULL) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, opcode, IS_LONG, 0, "WebSocket\\Protocol::OPCODE_TEXT") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "WebSocket\\Protocol::FLAG_FIN") ZEND_END_ARG_INFO() @@ -162,17 +160,39 @@ static zend_class_entry *register_class_WebSocket_Server(void) static zend_class_entry *register_class_WebSocket_ServerOptions(void) { zend_class_entry ce, *class_entry; - zval property_maxMessageSize_default_value; - zval property_maxQueuedBytes_default_value; INIT_NS_CLASS_ENTRY(ce, "WebSocket", "ServerOptions", class_WebSocket_ServerOptions_methods); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + zval property_maxMessageSize_default_value; ZVAL_UNDEF(&property_maxMessageSize_default_value); - zend_declare_typed_property(class_entry, zend_string_init("maxMessageSize", sizeof("maxMessageSize") - 1, 1), &property_maxMessageSize_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_maxMessageSize_name = zend_string_init("maxMessageSize", sizeof("maxMessageSize") - 1, 1); + zend_declare_typed_property(class_entry, property_maxMessageSize_name, &property_maxMessageSize_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_maxMessageSize_name); + zval property_maxQueuedBytes_default_value; ZVAL_UNDEF(&property_maxQueuedBytes_default_value); - zend_declare_typed_property(class_entry, zend_string_init("maxQueuedBytes", sizeof("maxQueuedBytes") - 1, 1), &property_maxQueuedBytes_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_maxQueuedBytes_name = zend_string_init("maxQueuedBytes", sizeof("maxQueuedBytes") - 1, 1); + zend_declare_typed_property(class_entry, property_maxQueuedBytes_name, &property_maxQueuedBytes_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_maxQueuedBytes_name); + + zval property_maxConnections_default_value; + ZVAL_UNDEF(&property_maxConnections_default_value); + zend_string *property_maxConnections_name = zend_string_init("maxConnections", sizeof("maxConnections") - 1, 1); + zend_declare_typed_property(class_entry, property_maxConnections_name, &property_maxConnections_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_maxConnections_name); + + zval property_handshakeTimeoutMs_default_value; + ZVAL_UNDEF(&property_handshakeTimeoutMs_default_value); + zend_string *property_handshakeTimeoutMs_name = zend_string_init("handshakeTimeoutMs", sizeof("handshakeTimeoutMs") - 1, 1); + zend_declare_typed_property(class_entry, property_handshakeTimeoutMs_name, &property_handshakeTimeoutMs_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_handshakeTimeoutMs_name); + + zval property_idleTimeoutMs_default_value; + ZVAL_UNDEF(&property_idleTimeoutMs_default_value); + zend_string *property_idleTimeoutMs_name = zend_string_init("idleTimeoutMs", sizeof("idleTimeoutMs") - 1, 1); + zend_declare_typed_property(class_entry, property_idleTimeoutMs_name, &property_idleTimeoutMs_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_idleTimeoutMs_name); return class_entry; } @@ -180,17 +200,21 @@ static zend_class_entry *register_class_WebSocket_ServerOptions(void) static zend_class_entry *register_class_WebSocket_Connection(void) { zend_class_entry ce, *class_entry; - zval property_id_default_value; - zval property_remoteAddress_default_value; INIT_NS_CLASS_ENTRY(ce, "WebSocket", "Connection", class_WebSocket_Connection_methods); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + zval property_id_default_value; ZVAL_UNDEF(&property_id_default_value); - zend_declare_typed_property(class_entry, zend_string_init("id", sizeof("id") - 1, 1), &property_id_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string *property_id_name = zend_string_init("id", sizeof("id") - 1, 1); + zend_declare_typed_property(class_entry, property_id_name, &property_id_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_id_name); + zval property_remoteAddress_default_value; ZVAL_UNDEF(&property_remoteAddress_default_value); - zend_declare_typed_property(class_entry, zend_string_init("remoteAddress", sizeof("remoteAddress") - 1, 1), &property_remoteAddress_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string *property_remoteAddress_name = zend_string_init("remoteAddress", sizeof("remoteAddress") - 1, 1); + zend_declare_typed_property(class_entry, property_remoteAddress_name, &property_remoteAddress_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_remoteAddress_name); return class_entry; } @@ -200,10 +224,15 @@ static zend_class_entry *register_class_WebSocket_MessageType(void) zend_class_entry *class_entry = zend_register_internal_enum("WebSocket\\MessageType", IS_UNDEF, NULL); zend_enum_add_case_cstr(class_entry, "Continuation", NULL); + zend_enum_add_case_cstr(class_entry, "Text", NULL); + zend_enum_add_case_cstr(class_entry, "Binary", NULL); + zend_enum_add_case_cstr(class_entry, "Ping", NULL); + zend_enum_add_case_cstr(class_entry, "Pong", NULL); + zend_enum_add_case_cstr(class_entry, "Close", NULL); return class_entry; @@ -212,35 +241,46 @@ static zend_class_entry *register_class_WebSocket_MessageType(void) static zend_class_entry *register_class_WebSocket_Frame(void) { zend_class_entry ce, *class_entry; - zend_string *property_type_class_WebSocket_MessageType; - zval property_type_default_value; - zval property_opcode_default_value; - zval property_flags_default_value; - zval property_payload_default_value; - zval property_final_default_value; - zval property_bytesConsumed_default_value; INIT_NS_CLASS_ENTRY(ce, "WebSocket", "Frame", class_WebSocket_Frame_methods); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); - property_type_class_WebSocket_MessageType = zend_string_init("WebSocket\\MessageType", sizeof("WebSocket\\MessageType") - 1, 1); + zval property_type_default_value; ZVAL_UNDEF(&property_type_default_value); - zend_declare_typed_property(class_entry, zend_string_init("type", sizeof("type") - 1, 1), &property_type_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_type_class_WebSocket_MessageType, 0, 0)); + zend_string *property_type_name = zend_string_init("type", sizeof("type") - 1, 1); + zend_string *property_type_class_WebSocket_MessageType = zend_string_init("WebSocket\\MessageType", sizeof("WebSocket\\MessageType")-1, 1); + zend_declare_typed_property(class_entry, property_type_name, &property_type_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_type_class_WebSocket_MessageType, 0, 0)); + zend_string_release(property_type_name); + zval property_opcode_default_value; ZVAL_UNDEF(&property_opcode_default_value); - zend_declare_typed_property(class_entry, zend_string_init("opcode", sizeof("opcode") - 1, 1), &property_opcode_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_opcode_name = zend_string_init("opcode", sizeof("opcode") - 1, 1); + zend_declare_typed_property(class_entry, property_opcode_name, &property_opcode_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_opcode_name); + zval property_flags_default_value; ZVAL_UNDEF(&property_flags_default_value); - zend_declare_typed_property(class_entry, zend_string_init("flags", sizeof("flags") - 1, 1), &property_flags_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_flags_name = zend_string_init("flags", sizeof("flags") - 1, 1); + zend_declare_typed_property(class_entry, property_flags_name, &property_flags_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_flags_name); + zval property_payload_default_value; ZVAL_UNDEF(&property_payload_default_value); - zend_declare_typed_property(class_entry, zend_string_init("payload", sizeof("payload") - 1, 1), &property_payload_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string *property_payload_name = zend_string_init("payload", sizeof("payload") - 1, 1); + zend_declare_typed_property(class_entry, property_payload_name, &property_payload_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_payload_name); + zval property_final_default_value; ZVAL_UNDEF(&property_final_default_value); - zend_declare_typed_property(class_entry, zend_string_init("final", sizeof("final") - 1, 1), &property_final_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL)); + zend_string *property_final_name = zend_string_init("final", sizeof("final") - 1, 1); + zend_declare_typed_property(class_entry, property_final_name, &property_final_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL)); + zend_string_release(property_final_name); + zval property_bytesConsumed_default_value; ZVAL_UNDEF(&property_bytesConsumed_default_value); - zend_declare_typed_property(class_entry, zend_string_init("bytesConsumed", sizeof("bytesConsumed") - 1, 1), &property_bytesConsumed_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_bytesConsumed_name = zend_string_init("bytesConsumed", sizeof("bytesConsumed") - 1, 1); + zend_declare_typed_property(class_entry, property_bytesConsumed_name, &property_bytesConsumed_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_bytesConsumed_name); return class_entry; } @@ -248,25 +288,33 @@ static zend_class_entry *register_class_WebSocket_Frame(void) static zend_class_entry *register_class_WebSocket_CloseFrame(void) { zend_class_entry ce, *class_entry; - zval property_code_default_value; - zval property_reason_default_value; - zval property_flags_default_value; - zval property_bytesConsumed_default_value; INIT_NS_CLASS_ENTRY(ce, "WebSocket", "CloseFrame", class_WebSocket_CloseFrame_methods); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + zval property_code_default_value; ZVAL_UNDEF(&property_code_default_value); - zend_declare_typed_property(class_entry, zend_string_init("code", sizeof("code") - 1, 1), &property_code_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_code_name = zend_string_init("code", sizeof("code") - 1, 1); + zend_declare_typed_property(class_entry, property_code_name, &property_code_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_code_name); + zval property_reason_default_value; ZVAL_UNDEF(&property_reason_default_value); - zend_declare_typed_property(class_entry, zend_string_init("reason", sizeof("reason") - 1, 1), &property_reason_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string *property_reason_name = zend_string_init("reason", sizeof("reason") - 1, 1); + zend_declare_typed_property(class_entry, property_reason_name, &property_reason_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_reason_name); + zval property_flags_default_value; ZVAL_UNDEF(&property_flags_default_value); - zend_declare_typed_property(class_entry, zend_string_init("flags", sizeof("flags") - 1, 1), &property_flags_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_flags_name = zend_string_init("flags", sizeof("flags") - 1, 1); + zend_declare_typed_property(class_entry, property_flags_name, &property_flags_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_flags_name); + zval property_bytesConsumed_default_value; ZVAL_UNDEF(&property_bytesConsumed_default_value); - zend_declare_typed_property(class_entry, zend_string_init("bytesConsumed", sizeof("bytesConsumed") - 1, 1), &property_bytesConsumed_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string *property_bytesConsumed_name = zend_string_init("bytesConsumed", sizeof("bytesConsumed") - 1, 1); + zend_declare_typed_property(class_entry, property_bytesConsumed_name, &property_bytesConsumed_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_bytesConsumed_name); return class_entry; } @@ -278,32 +326,149 @@ static zend_class_entry *register_class_WebSocket_Protocol(void) INIT_NS_CLASS_ENTRY(ce, "WebSocket", "Protocol", class_WebSocket_Protocol_methods); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); - zend_declare_class_constant_long(class_entry, "OPCODE_CONTINUATION", sizeof("OPCODE_CONTINUATION") - 1, 0x0); - zend_declare_class_constant_long(class_entry, "OPCODE_TEXT", sizeof("OPCODE_TEXT") - 1, 0x1); - zend_declare_class_constant_long(class_entry, "OPCODE_BINARY", sizeof("OPCODE_BINARY") - 1, 0x2); - zend_declare_class_constant_long(class_entry, "OPCODE_CLOSE", sizeof("OPCODE_CLOSE") - 1, 0x8); - zend_declare_class_constant_long(class_entry, "OPCODE_PING", sizeof("OPCODE_PING") - 1, 0x9); - zend_declare_class_constant_long(class_entry, "OPCODE_PONG", sizeof("OPCODE_PONG") - 1, 0xA); - - zend_declare_class_constant_long(class_entry, "FLAG_FIN", sizeof("FLAG_FIN") - 1, 1 << 0); - zend_declare_class_constant_long(class_entry, "FLAG_COMPRESS", sizeof("FLAG_COMPRESS") - 1, 1 << 1); - zend_declare_class_constant_long(class_entry, "FLAG_RSV1", sizeof("FLAG_RSV1") - 1, 1 << 2); - zend_declare_class_constant_long(class_entry, "FLAG_RSV2", sizeof("FLAG_RSV2") - 1, 1 << 3); - zend_declare_class_constant_long(class_entry, "FLAG_RSV3", sizeof("FLAG_RSV3") - 1, 1 << 4); - zend_declare_class_constant_long(class_entry, "FLAG_MASK", sizeof("FLAG_MASK") - 1, 1 << 5); - - zend_declare_class_constant_long(class_entry, "CLOSE_NORMAL", sizeof("CLOSE_NORMAL") - 1, 1000); - zend_declare_class_constant_long(class_entry, "CLOSE_GOING_AWAY", sizeof("CLOSE_GOING_AWAY") - 1, 1001); - zend_declare_class_constant_long(class_entry, "CLOSE_PROTOCOL_ERROR", sizeof("CLOSE_PROTOCOL_ERROR") - 1, 1002); - zend_declare_class_constant_long(class_entry, "CLOSE_UNSUPPORTED_DATA", sizeof("CLOSE_UNSUPPORTED_DATA") - 1, 1003); - zend_declare_class_constant_long(class_entry, "CLOSE_NO_STATUS", sizeof("CLOSE_NO_STATUS") - 1, 1005); - zend_declare_class_constant_long(class_entry, "CLOSE_ABNORMAL", sizeof("CLOSE_ABNORMAL") - 1, 1006); - zend_declare_class_constant_long(class_entry, "CLOSE_INVALID_PAYLOAD", sizeof("CLOSE_INVALID_PAYLOAD") - 1, 1007); - zend_declare_class_constant_long(class_entry, "CLOSE_POLICY_VIOLATION", sizeof("CLOSE_POLICY_VIOLATION") - 1, 1008); - zend_declare_class_constant_long(class_entry, "CLOSE_MESSAGE_TOO_BIG", sizeof("CLOSE_MESSAGE_TOO_BIG") - 1, 1009); - zend_declare_class_constant_long(class_entry, "CLOSE_EXTENSION_MISSING", sizeof("CLOSE_EXTENSION_MISSING") - 1, 1010); - zend_declare_class_constant_long(class_entry, "CLOSE_SERVER_ERROR", sizeof("CLOSE_SERVER_ERROR") - 1, 1011); - zend_declare_class_constant_long(class_entry, "CLOSE_TLS", sizeof("CLOSE_TLS") - 1, 1015); + zval const_OPCODE_CONTINUATION_value; + ZVAL_LONG(&const_OPCODE_CONTINUATION_value, 0x0); + zend_string *const_OPCODE_CONTINUATION_name = zend_string_init_interned("OPCODE_CONTINUATION", sizeof("OPCODE_CONTINUATION") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_OPCODE_CONTINUATION_name, &const_OPCODE_CONTINUATION_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_OPCODE_CONTINUATION_name); + + zval const_OPCODE_TEXT_value; + ZVAL_LONG(&const_OPCODE_TEXT_value, 0x1); + zend_string *const_OPCODE_TEXT_name = zend_string_init_interned("OPCODE_TEXT", sizeof("OPCODE_TEXT") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_OPCODE_TEXT_name, &const_OPCODE_TEXT_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_OPCODE_TEXT_name); + + zval const_OPCODE_BINARY_value; + ZVAL_LONG(&const_OPCODE_BINARY_value, 0x2); + zend_string *const_OPCODE_BINARY_name = zend_string_init_interned("OPCODE_BINARY", sizeof("OPCODE_BINARY") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_OPCODE_BINARY_name, &const_OPCODE_BINARY_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_OPCODE_BINARY_name); + + zval const_OPCODE_CLOSE_value; + ZVAL_LONG(&const_OPCODE_CLOSE_value, 0x8); + zend_string *const_OPCODE_CLOSE_name = zend_string_init_interned("OPCODE_CLOSE", sizeof("OPCODE_CLOSE") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_OPCODE_CLOSE_name, &const_OPCODE_CLOSE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_OPCODE_CLOSE_name); + + zval const_OPCODE_PING_value; + ZVAL_LONG(&const_OPCODE_PING_value, 0x9); + zend_string *const_OPCODE_PING_name = zend_string_init_interned("OPCODE_PING", sizeof("OPCODE_PING") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_OPCODE_PING_name, &const_OPCODE_PING_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_OPCODE_PING_name); + + zval const_OPCODE_PONG_value; + ZVAL_LONG(&const_OPCODE_PONG_value, 0xa); + zend_string *const_OPCODE_PONG_name = zend_string_init_interned("OPCODE_PONG", sizeof("OPCODE_PONG") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_OPCODE_PONG_name, &const_OPCODE_PONG_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_OPCODE_PONG_name); + + zval const_FLAG_FIN_value; + ZVAL_LONG(&const_FLAG_FIN_value, 1 << 0); + zend_string *const_FLAG_FIN_name = zend_string_init_interned("FLAG_FIN", sizeof("FLAG_FIN") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_FLAG_FIN_name, &const_FLAG_FIN_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_FLAG_FIN_name); + + zval const_FLAG_COMPRESS_value; + ZVAL_LONG(&const_FLAG_COMPRESS_value, 1 << 1); + zend_string *const_FLAG_COMPRESS_name = zend_string_init_interned("FLAG_COMPRESS", sizeof("FLAG_COMPRESS") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_FLAG_COMPRESS_name, &const_FLAG_COMPRESS_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_FLAG_COMPRESS_name); + + zval const_FLAG_RSV1_value; + ZVAL_LONG(&const_FLAG_RSV1_value, 1 << 2); + zend_string *const_FLAG_RSV1_name = zend_string_init_interned("FLAG_RSV1", sizeof("FLAG_RSV1") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_FLAG_RSV1_name, &const_FLAG_RSV1_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_FLAG_RSV1_name); + + zval const_FLAG_RSV2_value; + ZVAL_LONG(&const_FLAG_RSV2_value, 1 << 3); + zend_string *const_FLAG_RSV2_name = zend_string_init_interned("FLAG_RSV2", sizeof("FLAG_RSV2") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_FLAG_RSV2_name, &const_FLAG_RSV2_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_FLAG_RSV2_name); + + zval const_FLAG_RSV3_value; + ZVAL_LONG(&const_FLAG_RSV3_value, 1 << 4); + zend_string *const_FLAG_RSV3_name = zend_string_init_interned("FLAG_RSV3", sizeof("FLAG_RSV3") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_FLAG_RSV3_name, &const_FLAG_RSV3_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_FLAG_RSV3_name); + + zval const_FLAG_MASK_value; + ZVAL_LONG(&const_FLAG_MASK_value, 1 << 5); + zend_string *const_FLAG_MASK_name = zend_string_init_interned("FLAG_MASK", sizeof("FLAG_MASK") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_FLAG_MASK_name, &const_FLAG_MASK_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_FLAG_MASK_name); + + zval const_CLOSE_NORMAL_value; + ZVAL_LONG(&const_CLOSE_NORMAL_value, 1000); + zend_string *const_CLOSE_NORMAL_name = zend_string_init_interned("CLOSE_NORMAL", sizeof("CLOSE_NORMAL") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_NORMAL_name, &const_CLOSE_NORMAL_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_NORMAL_name); + + zval const_CLOSE_GOING_AWAY_value; + ZVAL_LONG(&const_CLOSE_GOING_AWAY_value, 1001); + zend_string *const_CLOSE_GOING_AWAY_name = zend_string_init_interned("CLOSE_GOING_AWAY", sizeof("CLOSE_GOING_AWAY") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_GOING_AWAY_name, &const_CLOSE_GOING_AWAY_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_GOING_AWAY_name); + + zval const_CLOSE_PROTOCOL_ERROR_value; + ZVAL_LONG(&const_CLOSE_PROTOCOL_ERROR_value, 1002); + zend_string *const_CLOSE_PROTOCOL_ERROR_name = zend_string_init_interned("CLOSE_PROTOCOL_ERROR", sizeof("CLOSE_PROTOCOL_ERROR") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_PROTOCOL_ERROR_name, &const_CLOSE_PROTOCOL_ERROR_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_PROTOCOL_ERROR_name); + + zval const_CLOSE_UNSUPPORTED_DATA_value; + ZVAL_LONG(&const_CLOSE_UNSUPPORTED_DATA_value, 1003); + zend_string *const_CLOSE_UNSUPPORTED_DATA_name = zend_string_init_interned("CLOSE_UNSUPPORTED_DATA", sizeof("CLOSE_UNSUPPORTED_DATA") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_UNSUPPORTED_DATA_name, &const_CLOSE_UNSUPPORTED_DATA_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_UNSUPPORTED_DATA_name); + + zval const_CLOSE_NO_STATUS_value; + ZVAL_LONG(&const_CLOSE_NO_STATUS_value, 1005); + zend_string *const_CLOSE_NO_STATUS_name = zend_string_init_interned("CLOSE_NO_STATUS", sizeof("CLOSE_NO_STATUS") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_NO_STATUS_name, &const_CLOSE_NO_STATUS_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_NO_STATUS_name); + + zval const_CLOSE_ABNORMAL_value; + ZVAL_LONG(&const_CLOSE_ABNORMAL_value, 1006); + zend_string *const_CLOSE_ABNORMAL_name = zend_string_init_interned("CLOSE_ABNORMAL", sizeof("CLOSE_ABNORMAL") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_ABNORMAL_name, &const_CLOSE_ABNORMAL_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_ABNORMAL_name); + + zval const_CLOSE_INVALID_PAYLOAD_value; + ZVAL_LONG(&const_CLOSE_INVALID_PAYLOAD_value, 1007); + zend_string *const_CLOSE_INVALID_PAYLOAD_name = zend_string_init_interned("CLOSE_INVALID_PAYLOAD", sizeof("CLOSE_INVALID_PAYLOAD") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_INVALID_PAYLOAD_name, &const_CLOSE_INVALID_PAYLOAD_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_INVALID_PAYLOAD_name); + + zval const_CLOSE_POLICY_VIOLATION_value; + ZVAL_LONG(&const_CLOSE_POLICY_VIOLATION_value, 1008); + zend_string *const_CLOSE_POLICY_VIOLATION_name = zend_string_init_interned("CLOSE_POLICY_VIOLATION", sizeof("CLOSE_POLICY_VIOLATION") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_POLICY_VIOLATION_name, &const_CLOSE_POLICY_VIOLATION_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_POLICY_VIOLATION_name); + + zval const_CLOSE_MESSAGE_TOO_BIG_value; + ZVAL_LONG(&const_CLOSE_MESSAGE_TOO_BIG_value, 1009); + zend_string *const_CLOSE_MESSAGE_TOO_BIG_name = zend_string_init_interned("CLOSE_MESSAGE_TOO_BIG", sizeof("CLOSE_MESSAGE_TOO_BIG") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_MESSAGE_TOO_BIG_name, &const_CLOSE_MESSAGE_TOO_BIG_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_MESSAGE_TOO_BIG_name); + + zval const_CLOSE_EXTENSION_MISSING_value; + ZVAL_LONG(&const_CLOSE_EXTENSION_MISSING_value, 1010); + zend_string *const_CLOSE_EXTENSION_MISSING_name = zend_string_init_interned("CLOSE_EXTENSION_MISSING", sizeof("CLOSE_EXTENSION_MISSING") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_EXTENSION_MISSING_name, &const_CLOSE_EXTENSION_MISSING_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_EXTENSION_MISSING_name); + + zval const_CLOSE_SERVER_ERROR_value; + ZVAL_LONG(&const_CLOSE_SERVER_ERROR_value, 1011); + zend_string *const_CLOSE_SERVER_ERROR_name = zend_string_init_interned("CLOSE_SERVER_ERROR", sizeof("CLOSE_SERVER_ERROR") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_SERVER_ERROR_name, &const_CLOSE_SERVER_ERROR_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_SERVER_ERROR_name); + + zval const_CLOSE_TLS_value; + ZVAL_LONG(&const_CLOSE_TLS_value, 1015); + zend_string *const_CLOSE_TLS_name = zend_string_init_interned("CLOSE_TLS", sizeof("CLOSE_TLS") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_CLOSE_TLS_name, &const_CLOSE_TLS_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_CLOSE_TLS_name); return class_entry; } diff --git a/websocket_connection.c b/websocket_connection.c index cb3c6a9..441fad0 100644 --- a/websocket_connection.c +++ b/websocket_connection.c @@ -56,6 +56,8 @@ static zend_object *websocket_connection_create_object(zend_class_entry *ce) intern->fragmented = false; intern->fragmented_opcode = 0; intern->fragmented_payload = NULL; + intern->accepted_at_usec = 0; + intern->last_activity_usec = 0; intern->std.handlers = &websocket_connection_handlers; @@ -162,6 +164,8 @@ void websocket_connection_close_socket(websocket_connection_object *intern) } intern->fragmented = false; intern->fragmented_opcode = 0; + intern->accepted_at_usec = 0; + intern->last_activity_usec = 0; } bool websocket_connection_has_pending_writes(websocket_connection_object *intern) @@ -319,6 +323,9 @@ bool websocket_connection_send_frame(websocket_connection_object *intern, zend_s } frame = websocket_protocol_pack_payload(payload, opcode, WEBSOCKET_FLAG_FIN); + if (!frame) { + return false; + } if (ZSTR_LEN(frame) > intern->max_queued_bytes) { errno = ENOBUFS; @@ -479,6 +486,10 @@ PHP_METHOD(WebSocket_Connection, send) zend_argument_value_error(1, "control frame payload must be at most 125 bytes"); RETURN_THROWS(); } + if (opcode == WEBSOCKET_OPCODE_TEXT && !websocket_protocol_is_valid_utf8(ZSTR_VAL(payload), ZSTR_LEN(payload))) { + zend_argument_value_error(1, "must be valid UTF-8 for text frames"); + RETURN_THROWS(); + } if (!websocket_connection_send_frame(intern, payload, opcode)) { zend_throw_error(NULL, "Failed to send WebSocket frame"); @@ -499,8 +510,8 @@ PHP_METHOD(WebSocket_Connection, close) Z_PARAM_STR(reason) ZEND_PARSE_PARAMETERS_END(); - if (code < 1000 || code > 4999) { - zend_argument_value_error(1, "must be between 1000 and 4999"); + if (!websocket_protocol_close_code_is_valid(code)) { + zend_argument_value_error(1, "must be a valid WebSocket close code"); RETURN_THROWS(); } } @@ -512,6 +523,10 @@ PHP_METHOD(WebSocket_Connection, close) zend_argument_value_error(2, "must be at most %d bytes", WEBSOCKET_CLOSE_REASON_MAX_LEN); RETURN_THROWS(); } + if (!websocket_protocol_is_valid_utf8(ZSTR_VAL(reason), ZSTR_LEN(reason))) { + zend_argument_value_error(2, "must be valid UTF-8"); + RETURN_THROWS(); + } intern = Z_WEBSOCKET_CONNECTION_P(ZEND_THIS); if (intern->defer_close) { diff --git a/websocket_protocol.c b/websocket_protocol.c index 84f9227..436f2e4 100644 --- a/websocket_protocol.c +++ b/websocket_protocol.c @@ -10,7 +10,14 @@ #include "ext/standard/sha1.h" #include "Zend/zend_enum.h" +#include +#include +#if defined(__linux__) +# include +#endif +#include #include +#include #define WEBSOCKET_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" @@ -25,6 +32,72 @@ static uint32_t websocket_close_frame_prop_reason_num; static uint32_t websocket_close_frame_prop_flags_num; static uint32_t websocket_close_frame_prop_bytes_consumed_num; +static bool websocket_protocol_random_bytes(uint8_t *bytes, const size_t size) +{ +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) + arc4random_buf(bytes, size); + return true; +#elif defined(__linux__) + size_t offset = 0; + + while (offset < size) { + const ssize_t n = getrandom(bytes + offset, size - offset, 0); + + if (n > 0) { + offset += (size_t) n; + continue; + } + if (n < 0 && errno == EINTR) { + continue; + } + if (n < 0 && errno == ENOSYS) { + break; + } + + zend_throw_error(NULL, "Unable to generate WebSocket mask key"); + return false; + } + + if (offset == size) { + return true; + } +#endif + { + int fd; + size_t offset = 0; + + do { + fd = open("/dev/urandom", O_RDONLY); + } while (fd < 0 && errno == EINTR); + + if (fd < 0) { + zend_throw_error(NULL, "Unable to open /dev/urandom for WebSocket mask key"); + return false; + } + + while (offset < size) { + const ssize_t n = read(fd, bytes + offset, size - offset); + + if (n > 0) { + offset += (size_t) n; + continue; + } + if (n < 0 && errno == EINTR) { + continue; + } + + while (close(fd) < 0 && errno == EINTR) { + } + zend_throw_error(NULL, "Unable to read /dev/urandom for WebSocket mask key"); + return false; + } + + while (close(fd) < 0 && errno == EINTR) { + } + return true; + } +} + zend_string *websocket_protocol_accept_key(zend_string *key) { PHP_SHA1_CTX ctx; @@ -69,6 +142,98 @@ bool websocket_protocol_opcode_is_control(zend_long opcode) return opcode >= 0x8 && opcode <= 0xf; } +bool websocket_protocol_close_code_is_valid(const zend_long code) +{ + if (code < 1000 || code > 4999) { + return false; + } + + switch (code) { + case 1004: + case 1005: + case 1006: + case 1015: + return false; + default: + return true; + } +} + +bool websocket_protocol_is_valid_utf8(const char *payload, const size_t payload_len) +{ + const unsigned char *bytes = (const unsigned char *) payload; + size_t i = 0; + + while (i < payload_len) { + unsigned char c = bytes[i]; + + if (c <= 0x7f) { + i++; + continue; + } + + if (c >= 0xc2 && c <= 0xdf) { + if (i + 1 >= payload_len || (bytes[i + 1] & 0xc0) != 0x80) { + return false; + } + i += 2; + continue; + } + + if (c == 0xe0) { + if (i + 2 >= payload_len || bytes[i + 1] < 0xa0 || bytes[i + 1] > 0xbf || (bytes[i + 2] & 0xc0) != 0x80) { + return false; + } + i += 3; + continue; + } + + if ((c >= 0xe1 && c <= 0xec) || (c >= 0xee && c <= 0xef)) { + if (i + 2 >= payload_len || (bytes[i + 1] & 0xc0) != 0x80 || (bytes[i + 2] & 0xc0) != 0x80) { + return false; + } + i += 3; + continue; + } + + if (c == 0xed) { + if (i + 2 >= payload_len || bytes[i + 1] < 0x80 || bytes[i + 1] > 0x9f || (bytes[i + 2] & 0xc0) != 0x80) { + return false; + } + i += 3; + continue; + } + + if (c == 0xf0) { + if (i + 3 >= payload_len || bytes[i + 1] < 0x90 || bytes[i + 1] > 0xbf || (bytes[i + 2] & 0xc0) != 0x80 || (bytes[i + 3] & 0xc0) != 0x80) { + return false; + } + i += 4; + continue; + } + + if (c >= 0xf1 && c <= 0xf3) { + if (i + 3 >= payload_len || (bytes[i + 1] & 0xc0) != 0x80 || (bytes[i + 2] & 0xc0) != 0x80 || (bytes[i + 3] & 0xc0) != 0x80) { + return false; + } + i += 4; + continue; + } + + if (c == 0xf4) { + if (i + 3 >= payload_len || bytes[i + 1] < 0x80 || bytes[i + 1] > 0x8f || (bytes[i + 2] & 0xc0) != 0x80 || (bytes[i + 3] & 0xc0) != 0x80) { + return false; + } + i += 4; + continue; + } + + return false; + } + + return true; +} + uint8_t websocket_protocol_message_type_opcode(zval *type) { zval *case_name_zv; @@ -170,9 +335,13 @@ zend_string *websocket_protocol_pack_payload(zend_string *payload, uint8_t opcod size_t pos = 0; zend_string *frame; unsigned char *out; - uint8_t mask[4] = {0x12, 0x34, 0x56, 0x78}; + uint8_t mask[4]; size_t i; + if (masked && !websocket_protocol_random_bytes(mask, sizeof(mask))) { + return NULL; + } + if (payload_len >= 126 && payload_len <= 0xffff) { header_len += 2; } else if (payload_len > 0xffff) { @@ -285,10 +454,18 @@ zend_string *websocket_protocol_close_payload(zend_long code, zend_string *reaso { zend_string *payload; + if (!websocket_protocol_close_code_is_valid(code)) { + zend_argument_value_error(1, "must be a valid WebSocket close code"); + return NULL; + } if (ZSTR_LEN(reason) > WEBSOCKET_CLOSE_REASON_MAX_LEN) { zend_argument_value_error(2, "must be at most %d bytes", WEBSOCKET_CLOSE_REASON_MAX_LEN); return NULL; } + if (!websocket_protocol_is_valid_utf8(ZSTR_VAL(reason), ZSTR_LEN(reason))) { + zend_argument_value_error(2, "must be valid UTF-8"); + return NULL; + } payload = zend_string_alloc(2 + ZSTR_LEN(reason), 0); ZSTR_VAL(payload)[0] = (char) ((code >> 8) & 0xff); @@ -336,10 +513,18 @@ PHP_METHOD(WebSocket_CloseFrame, __construct) if (!reason) { reason = ZSTR_EMPTY_ALLOC(); } + if (!websocket_protocol_close_code_is_valid(code)) { + zend_argument_value_error(1, "must be a valid WebSocket close code"); + RETURN_THROWS(); + } if (ZSTR_LEN(reason) > WEBSOCKET_CLOSE_REASON_MAX_LEN) { zend_argument_value_error(2, "must be at most %d bytes", WEBSOCKET_CLOSE_REASON_MAX_LEN); RETURN_THROWS(); } + if (!websocket_protocol_is_valid_utf8(ZSTR_VAL(reason), ZSTR_LEN(reason))) { + zend_argument_value_error(2, "must be valid UTF-8"); + RETURN_THROWS(); + } websocket_close_frame_update_properties(ZEND_THIS, code, reason, WEBSOCKET_FLAG_FIN, 0); } @@ -358,6 +543,7 @@ PHP_METHOD(WebSocket_Protocol, acceptKey) PHP_METHOD(WebSocket_Protocol, encode) { zend_string *payload; + zend_string *frame; zval *type = NULL; bool masked = false; uint8_t opcode = WEBSOCKET_OPCODE_TEXT; @@ -376,8 +562,17 @@ PHP_METHOD(WebSocket_Protocol, encode) if (masked) { flags |= WEBSOCKET_FLAG_MASK; } + if (opcode == WEBSOCKET_OPCODE_TEXT && !websocket_protocol_is_valid_utf8(ZSTR_VAL(payload), ZSTR_LEN(payload))) { + zend_argument_value_error(1, "must be valid UTF-8 for text frames"); + RETURN_THROWS(); + } - RETURN_STR(websocket_protocol_pack_payload(payload, opcode, flags)); + frame = websocket_protocol_pack_payload(payload, opcode, flags); + if (!frame) { + RETURN_THROWS(); + } + + RETURN_STR(frame); } PHP_METHOD(WebSocket_Protocol, pack) @@ -458,8 +653,26 @@ PHP_METHOD(WebSocket_Protocol, pack) } RETURN_THROWS(); } + if (opcode == WEBSOCKET_OPCODE_TEXT && (flags & WEBSOCKET_FLAG_FIN) && !websocket_protocol_is_valid_utf8(ZSTR_VAL(payload), ZSTR_LEN(payload))) { + zend_argument_value_error(1, "must be valid UTF-8 for text frames"); + if (tmp_payload) { + zend_string_release(tmp_payload); + } + RETURN_THROWS(); + } + + { + zend_string *frame = websocket_protocol_pack_payload(payload, (uint8_t) opcode, (uint8_t) (flags & WEBSOCKET_FLAGS_ALL)); + + if (!frame) { + if (tmp_payload) { + zend_string_release(tmp_payload); + } + RETURN_THROWS(); + } - RETVAL_STR(websocket_protocol_pack_payload(payload, (uint8_t) opcode, (uint8_t) (flags & WEBSOCKET_FLAGS_ALL))); + RETVAL_STR(frame); + } if (tmp_payload) { zend_string_release(tmp_payload); @@ -575,6 +788,11 @@ static void websocket_protocol_unpack(INTERNAL_FUNCTION_PARAMETERS) unsigned char *close_payload; if (payload_len >= 2) { + if (!websocket_protocol_is_valid_utf8(ZSTR_VAL(payload) + 2, (size_t) payload_len - 2)) { + zend_string_release(payload); + zend_throw_error(NULL, "Invalid WebSocket close reason UTF-8"); + RETURN_THROWS(); + } close_payload = (unsigned char *) ZSTR_VAL(payload); code = ((zend_long) close_payload[0] << 8) | close_payload[1]; reason = zend_string_init(ZSTR_VAL(payload) + 2, (size_t) payload_len - 2, 0); @@ -595,6 +813,11 @@ static void websocket_protocol_unpack(INTERNAL_FUNCTION_PARAMETERS) zend_throw_error(NULL, "Unsupported WebSocket opcode %u", opcode); RETURN_THROWS(); } + if (opcode == WEBSOCKET_OPCODE_TEXT && final && !websocket_protocol_is_valid_utf8(ZSTR_VAL(payload), ZSTR_LEN(payload))) { + zend_string_release(payload); + zend_throw_error(NULL, "Invalid WebSocket text frame UTF-8"); + RETURN_THROWS(); + } object_init_ex(return_value, websocket_frame_ce); diff --git a/websocket_server.c b/websocket_server.c index 0acf1ff..1600d4f 100644 --- a/websocket_server.c +++ b/websocket_server.c @@ -101,11 +101,17 @@ PHP_METHOD(WebSocket_ServerOptions, __construct) { zend_long max_message_size = WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE; zend_long max_queued_bytes = WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES; + zend_long max_connections = WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + zend_long handshake_timeout_ms = WEBSOCKET_DEFAULT_HANDSHAKE_TIMEOUT_MS; + zend_long idle_timeout_ms = WEBSOCKET_DEFAULT_IDLE_TIMEOUT_MS; - ZEND_PARSE_PARAMETERS_START(0, 2) + ZEND_PARSE_PARAMETERS_START(0, 5) Z_PARAM_OPTIONAL Z_PARAM_LONG(max_message_size) Z_PARAM_LONG(max_queued_bytes) + Z_PARAM_LONG(max_connections) + Z_PARAM_LONG(handshake_timeout_ms) + Z_PARAM_LONG(idle_timeout_ms) ZEND_PARSE_PARAMETERS_END(); if (max_message_size < 1) { @@ -116,9 +122,24 @@ PHP_METHOD(WebSocket_ServerOptions, __construct) zend_argument_value_error(2, "must be at least 1"); RETURN_THROWS(); } + if (max_connections < 1) { + zend_argument_value_error(3, "must be at least 1"); + RETURN_THROWS(); + } + if (handshake_timeout_ms < 1) { + zend_argument_value_error(4, "must be at least 1"); + RETURN_THROWS(); + } + if (idle_timeout_ms < 1) { + zend_argument_value_error(5, "must be at least 1"); + RETURN_THROWS(); + } zend_update_property_long(websocket_server_options_ce, Z_OBJ_P(ZEND_THIS), "maxMessageSize", strlen("maxMessageSize"), max_message_size); zend_update_property_long(websocket_server_options_ce, Z_OBJ_P(ZEND_THIS), "maxQueuedBytes", strlen("maxQueuedBytes"), max_queued_bytes); + zend_update_property_long(websocket_server_options_ce, Z_OBJ_P(ZEND_THIS), "maxConnections", strlen("maxConnections"), max_connections); + zend_update_property_long(websocket_server_options_ce, Z_OBJ_P(ZEND_THIS), "handshakeTimeoutMs", strlen("handshakeTimeoutMs"), handshake_timeout_ms); + zend_update_property_long(websocket_server_options_ce, Z_OBJ_P(ZEND_THIS), "idleTimeoutMs", strlen("idleTimeoutMs"), idle_timeout_ms); } PHP_METHOD(WebSocket_Server, listen) diff --git a/websocket_server_runtime.c b/websocket_server_runtime.c index 870b3c0..b9e8f6d 100644 --- a/websocket_server_runtime.c +++ b/websocket_server_runtime.c @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #define WEBSOCKET_LISTEN_BACKLOG 1024 @@ -28,6 +30,7 @@ typedef enum _websocket_server_frame_status { WEBSOCKET_SERVER_FRAME_OK = 1, WEBSOCKET_SERVER_FRAME_PROTOCOL_ERROR = 2, WEBSOCKET_SERVER_FRAME_MESSAGE_TOO_BIG = 3, + WEBSOCKET_SERVER_FRAME_INVALID_PAYLOAD = 4, } websocket_server_frame_status; typedef struct _websocket_server_frame { @@ -37,12 +40,42 @@ typedef struct _websocket_server_frame { zend_string *payload; } websocket_server_frame; +static bool websocket_server_close_with_code(websocket_connection_object *connection_obj, zend_long code, const char *reason); +static uint64_t websocket_server_handshake_timeout_usec(websocket_server_object *intern); +static uint64_t websocket_server_idle_timeout_usec(websocket_server_object *intern); + static const char websocket_bad_request_response[] = "HTTP/1.1 400 Bad Request\r\n" "Connection: close\r\n" "Content-Length: 0\r\n" "\r\n"; +static const char websocket_service_unavailable_response[] = + "HTTP/1.1 503 Service Unavailable\r\n" + "Connection: close\r\n" + "Content-Length: 0\r\n" + "\r\n"; + +static uint64_t websocket_server_now_usec(void) +{ +#ifdef CLOCK_MONOTONIC + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { + return ((uint64_t) ts.tv_sec * 1000000u) + ((uint64_t) ts.tv_nsec / 1000u); + } +#endif + { + struct timeval tv; + + if (gettimeofday(&tv, NULL) == 0) { + return ((uint64_t) tv.tv_sec * 1000000u) + (uint64_t) tv.tv_usec; + } + } + + return 0; +} + static void websocket_server_close_fd(const int fd) { if (fd >= 0) { @@ -286,6 +319,39 @@ static bool websocket_server_notify_connection_closed(websocket_server_object *i return ok; } +static bool websocket_server_connection_expired(websocket_server_object *intern, websocket_connection_object *connection_obj, const uint64_t now_usec) +{ + uint64_t last_activity_usec; + uint64_t timeout_usec; + + if (now_usec == 0) { + return false; + } + + last_activity_usec = connection_obj->last_activity_usec ? connection_obj->last_activity_usec : connection_obj->accepted_at_usec; + if (last_activity_usec == 0 || now_usec < last_activity_usec) { + return false; + } + + if (!connection_obj->upgraded) { + timeout_usec = websocket_server_handshake_timeout_usec(intern); + } else { + timeout_usec = websocket_server_idle_timeout_usec(intern); + } + + return timeout_usec > 0 && now_usec - last_activity_usec >= timeout_usec; +} + +static void websocket_server_close_expired_connection(websocket_connection_object *connection_obj) +{ + if (connection_obj->upgraded) { + (void) websocket_server_close_with_code(connection_obj, WEBSOCKET_CLOSE_NORMAL, "idle timeout"); + return; + } + + websocket_connection_close_socket(connection_obj); +} + static void websocket_server_create_connection_zval(websocket_server_object *intern, zval *connection) { if (!Z_ISUNDEF(intern->reusable_connection)) { @@ -368,54 +434,54 @@ static void websocket_connection_discard_read_bytes(websocket_connection_object connection_obj->read_buffer_len -= bytes; } -static size_t websocket_server_max_message_size(websocket_server_object *intern) +static size_t websocket_server_positive_option(websocket_server_object *intern, const char *name, const size_t name_len, const size_t fallback) { zval *value; zval rv; - zend_long max_message_size; + zend_long option_value; if (Z_TYPE(intern->options) == IS_ARRAY) { - value = zend_hash_str_find(Z_ARRVAL(intern->options), "maxMessageSize", strlen("maxMessageSize")); + value = zend_hash_str_find(Z_ARRVAL(intern->options), name, name_len); if (!value) { - return WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE; + return fallback; } } else if (Z_TYPE(intern->options) == IS_OBJECT && instanceof_function(Z_OBJCE(intern->options), websocket_server_options_ce)) { - value = zend_read_property(websocket_server_options_ce, Z_OBJ(intern->options), "maxMessageSize", strlen("maxMessageSize"), 0, &rv); + value = zend_read_property(websocket_server_options_ce, Z_OBJ(intern->options), name, name_len, 0, &rv); } else { - return WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE; + return fallback; } - max_message_size = zval_get_long(value); - if (max_message_size <= 0) { - return WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE; + option_value = zval_get_long(value); + if (option_value <= 0) { + return fallback; } - return (size_t) max_message_size; + return (size_t) option_value; +} + +static size_t websocket_server_max_message_size(websocket_server_object *intern) +{ + return websocket_server_positive_option(intern, "maxMessageSize", strlen("maxMessageSize"), WEBSOCKET_DEFAULT_MAX_MESSAGE_SIZE); } static size_t websocket_server_max_queued_bytes(websocket_server_object *intern) { - zval *value; - zval rv; - zend_long max_queued_bytes; + return websocket_server_positive_option(intern, "maxQueuedBytes", strlen("maxQueuedBytes"), WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES); +} - if (Z_TYPE(intern->options) == IS_ARRAY) { - value = zend_hash_str_find(Z_ARRVAL(intern->options), "maxQueuedBytes", strlen("maxQueuedBytes")); - if (!value) { - return WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES; - } - } else if (Z_TYPE(intern->options) == IS_OBJECT && instanceof_function(Z_OBJCE(intern->options), websocket_server_options_ce)) { - value = zend_read_property(websocket_server_options_ce, Z_OBJ(intern->options), "maxQueuedBytes", strlen("maxQueuedBytes"), 0, &rv); - } else { - return WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES; - } +static size_t websocket_server_max_connections(websocket_server_object *intern) +{ + return websocket_server_positive_option(intern, "maxConnections", strlen("maxConnections"), WEBSOCKET_DEFAULT_MAX_CONNECTIONS); +} - max_queued_bytes = zval_get_long(value); - if (max_queued_bytes <= 0) { - return WEBSOCKET_DEFAULT_MAX_QUEUED_BYTES; - } +static uint64_t websocket_server_handshake_timeout_usec(websocket_server_object *intern) +{ + return (uint64_t) websocket_server_positive_option(intern, "handshakeTimeoutMs", strlen("handshakeTimeoutMs"), WEBSOCKET_DEFAULT_HANDSHAKE_TIMEOUT_MS) * 1000u; +} - return (size_t) max_queued_bytes; +static uint64_t websocket_server_idle_timeout_usec(websocket_server_object *intern) +{ + return (uint64_t) websocket_server_positive_option(intern, "idleTimeoutMs", strlen("idleTimeoutMs"), WEBSOCKET_DEFAULT_IDLE_TIMEOUT_MS) * 1000u; } static bool websocket_server_close_with_code(websocket_connection_object *connection_obj, const zend_long code, const char *reason) @@ -446,23 +512,6 @@ static zend_always_inline void websocket_server_mask_payload(unsigned char *dst, } } -static bool websocket_server_close_code_is_valid(const zend_long code) -{ - if (code < 1000 || code > 4999) { - return false; - } - - switch (code) { - case 1004: - case 1005: - case 1006: - case 1015: - return false; - default: - return true; - } -} - static websocket_server_frame_status websocket_server_parse_frame(websocket_connection_object *connection_obj, const size_t max_message_size, websocket_server_frame *frame) { const unsigned char *in = (const unsigned char *) connection_obj->read_buffer; @@ -548,7 +597,7 @@ static websocket_server_frame_status websocket_server_parse_frame(websocket_conn if (payload_len >= 2) { const zend_long close_code = ((zend_long) (in[pos] ^ mask[0]) << 8) | (zend_long) (in[pos + 1] ^ mask[1]); - if (!websocket_server_close_code_is_valid(close_code)) { + if (!websocket_protocol_close_code_is_valid(close_code)) { return WEBSOCKET_SERVER_FRAME_PROTOCOL_ERROR; } } @@ -559,6 +608,12 @@ static websocket_server_frame_status websocket_server_parse_frame(websocket_conn ZSTR_VAL(frame->payload)[payload_len] = '\0'; frame->bytes_consumed = pos + (size_t) payload_len; + if (frame->opcode == WEBSOCKET_OPCODE_CLOSE && payload_len >= 2 && !websocket_protocol_is_valid_utf8(ZSTR_VAL(frame->payload) + 2, (size_t) payload_len - 2)) { + zend_string_release(frame->payload); + frame->payload = NULL; + return WEBSOCKET_SERVER_FRAME_INVALID_PAYLOAD; + } + return WEBSOCKET_SERVER_FRAME_OK; } @@ -661,6 +716,11 @@ static bool websocket_server_handle_data_frame(websocket_server_object *intern, return true; } + if (connection_obj->fragmented_opcode == WEBSOCKET_OPCODE_TEXT && !websocket_protocol_is_valid_utf8(ZSTR_VAL(connection_obj->fragmented_payload), ZSTR_LEN(connection_obj->fragmented_payload))) { + websocket_server_clear_fragment(connection_obj); + return websocket_server_close_with_code(connection_obj, WEBSOCKET_CLOSE_INVALID_PAYLOAD, "invalid utf-8"); + } + if (!websocket_server_call_message_handler(intern, connection, connection_obj->fragmented_payload, connection_obj->fragmented_opcode)) { websocket_server_clear_fragment(connection_obj); return false; @@ -676,6 +736,10 @@ static bool websocket_server_handle_data_frame(websocket_server_object *intern, } if (frame->final) { + if (frame->opcode == WEBSOCKET_OPCODE_TEXT && !websocket_protocol_is_valid_utf8(ZSTR_VAL(frame->payload), ZSTR_LEN(frame->payload))) { + return websocket_server_close_with_code(connection_obj, WEBSOCKET_CLOSE_INVALID_PAYLOAD, "invalid utf-8"); + } + return websocket_server_call_message_handler(intern, connection, frame->payload, frame->opcode); } @@ -745,6 +809,12 @@ static bool websocket_server_process_buffered_frames(websocket_server_object *in return true; } + if (status == WEBSOCKET_SERVER_FRAME_INVALID_PAYLOAD) { + websocket_server_clear_fragment(connection_obj); + (void) websocket_server_close_with_code(connection_obj, WEBSOCKET_CLOSE_INVALID_PAYLOAD, "invalid utf-8"); + return true; + } + if (status == WEBSOCKET_SERVER_FRAME_PROTOCOL_ERROR) { websocket_server_clear_fragment(connection_obj); (void) websocket_server_close_with_code(connection_obj, WEBSOCKET_CLOSE_PROTOCOL_ERROR, "protocol error"); @@ -815,6 +885,8 @@ static bool websocket_server_process_handshake(websocket_server_object *intern, size_t bytes_consumed = 0; websocket_http_upgrade_result result; + connection_obj->last_activity_usec = websocket_server_now_usec(); + if (!websocket_connection_ensure_read_capacity(connection_obj, (size_t) bytes_read, WEBSOCKET_HTTP_MAX_REQUEST_SIZE)) { (void) websocket_server_send_bytes(connection_obj->fd, websocket_bad_request_response, sizeof(websocket_bad_request_response) - 1); connection_obj->open = false; @@ -874,6 +946,8 @@ static bool websocket_server_process_frame_reads(websocket_server_object *intern const ssize_t bytes_read = recv(connection_obj->fd, chunk, sizeof(chunk), 0); if (bytes_read > 0) { + connection_obj->last_activity_usec = websocket_server_now_usec(); + if (!websocket_connection_ensure_read_capacity(connection_obj, (size_t) bytes_read, read_limit)) { (void) websocket_server_close_with_code(connection_obj, WEBSOCKET_CLOSE_MESSAGE_TOO_BIG, "message too big"); return true; @@ -922,6 +996,12 @@ static bool websocket_server_accept_connection(websocket_server_object *intern) } errno = 0; + if (intern->connection_count >= websocket_server_max_connections(intern)) { + (void) websocket_server_send_bytes(client_fd, websocket_service_unavailable_response, sizeof(websocket_service_unavailable_response) - 1); + websocket_server_close_fd(client_fd); + return true; + } + if (websocket_server_set_nonblocking(client_fd) == FAILURE) { websocket_server_close_fd(client_fd); zend_throw_error(NULL, "Cannot make accepted WebSocket connection non-blocking: %s", strerror(errno)); @@ -931,6 +1011,8 @@ static bool websocket_server_accept_connection(websocket_server_object *intern) websocket_server_create_connection_zval(intern, &connection); connection_obj = Z_WEBSOCKET_CONNECTION_P(&connection); websocket_connection_open(connection_obj, WEBSOCKET_G(next_connection_id)++, (const struct sockaddr *) &remote_addr, remote_addr_len, client_fd); + connection_obj->accepted_at_usec = websocket_server_now_usec(); + connection_obj->last_activity_usec = connection_obj->accepted_at_usec; connection_obj->max_queued_bytes = websocket_server_max_queued_bytes(intern); if (!websocket_server_ensure_connection_capacity(intern)) { @@ -980,6 +1062,7 @@ static bool websocket_server_accept_pending(websocket_server_object *intern) static bool websocket_server_process_connection_fd(websocket_server_object *intern, const int fd) { size_t i; + const uint64_t now_usec = websocket_server_now_usec(); for (i = 0; i < intern->connection_count; i++) { websocket_connection_object *connection_obj = Z_WEBSOCKET_CONNECTION_P(&intern->connections[i]); @@ -992,6 +1075,11 @@ static bool websocket_server_process_connection_fd(websocket_server_object *inte return true; } + if (websocket_server_connection_expired(intern, connection_obj, now_usec)) { + websocket_server_close_expired_connection(connection_obj); + return true; + } + if (websocket_connection_has_pending_writes(connection_obj) && !websocket_connection_flush(connection_obj)) { return true; } @@ -1013,6 +1101,7 @@ static bool websocket_server_process_connection_fd(websocket_server_object *inte static bool websocket_server_process_connections(websocket_server_object *intern) { size_t i; + const uint64_t now_usec = websocket_server_now_usec(); for (i = 0; i < intern->connection_count; i++) { websocket_connection_object *connection_obj = Z_WEBSOCKET_CONNECTION_P(&intern->connections[i]); @@ -1021,6 +1110,11 @@ static bool websocket_server_process_connections(websocket_server_object *intern continue; } + if (websocket_server_connection_expired(intern, connection_obj, now_usec)) { + websocket_server_close_expired_connection(connection_obj); + continue; + } + if (websocket_connection_has_pending_writes(connection_obj) && !websocket_connection_flush(connection_obj)) { continue; }