diff --git a/.codeclimate.yml b/.codeclimate.yml index 7e59430..664ee89 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -20,6 +20,10 @@ engines: enabled: false CleanCode/StaticAccess: enabled: false + CyclomaticComplexity: + enabled: false + Design/NpathComplexity: + enabled: false ratings: paths: - "**.php.inc" diff --git a/.editorconfig b/.editorconfig index df88899..a391069 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,4 +10,6 @@ insert_final_newline = true trim_trailing_whitespace = true [*.md] +charset = utf-8 +end_of_line = lf trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 9f8d40d..a117c44 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,66 @@ composer.lock vendor/* .DS_Store ._* + +# Created by https://www.gitignore.io/api/phpstorm + +### PhpStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PhpStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +# End of https://www.gitignore.io/api/phpstorm diff --git a/.idea/PHPWebSockets.iml b/.idea/PHPWebSockets.iml new file mode 100644 index 0000000..1b45fb7 --- /dev/null +++ b/.idea/PHPWebSockets.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..99c6453 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..4f0611e --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml index 8caf0fd..c94349b 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -16,6 +16,7 @@ disabled: - no_blank_lines_after_class_opening - single_class_element_per_statement - simplified_null_return + - alpha_ordered_imports finder: name: diff --git a/.travis.yml b/.travis.yml index 6c904c0..4f29dad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,10 @@ php: - '7.0' - '7.1' env: - - TESTTYPE=server - - TESTTYPE=client + - TESTTYPE=server BUFFERTPE=memory + - TESTTYPE=server BUFFERTPE=tmpfile + - TESTTYPE=client BUFFERTPE=memory + - TESTTYPE=client BUFFERTPE=tmpfile before_script: - sudo apt-get update - sudo apt-get install python-pip @@ -15,10 +17,13 @@ before_script: - sudo -H pip install autobahntestsuite - phpenv config-rm xdebug.ini script: - - php Tests/runner.php $TESTTYPE + - php Tests/runner.php $TESTTYPE $BUFFERTPE notifications: email: recipients: - kevkevm@gmail.com on_success: change on_failure: always +# addons: +# artifacts: +# paths: /tmp/reports/ diff --git a/AConnection.php.inc b/AConnection.php.inc index cd3b7a9..5dd03c6 100644 --- a/AConnection.php.inc +++ b/AConnection.php.inc @@ -32,7 +32,7 @@ namespace PHPWebSocket; abstract class AConnection implements IStreamContainer { - use \PHPWebSocket\TStreamContainerDefaults; + use TStreamContainerDefaults; /** * The amount of bytes to read to complete our current frame @@ -42,11 +42,11 @@ abstract class AConnection implements IStreamContainer { protected $_currentFrameRemainingBytes = NULL; /** - * The opcode of the current partial message + * The callable that gets called after the first message part has been read to get the optional stream to write to * - * @var int|null + * @var callable|null */ - protected $_partialMessageOpcode = NULL; + protected $_newMessageStreamCallback = NULL; /** * If we've initiated the disconnect @@ -55,6 +55,13 @@ abstract class AConnection implements IStreamContainer { */ protected $_weInitiateDisconnect = FALSE; + /** + * The opcode of the current partial message + * + * @var int|null + */ + protected $_partialMessageOpcode = NULL; + /** * If we've received the disconnect message from the remote * @@ -62,6 +69,13 @@ abstract class AConnection implements IStreamContainer { */ protected $_remoteSentDisconnect = FALSE; + /** + * The stream to write to instead of using _partialMessage + * + * @var resource|null + */ + protected $_partialMessageStream = NULL; + /** * The priority frames ready to be send (Takes priority over the normal frames buffer) * @@ -69,6 +83,13 @@ abstract class AConnection implements IStreamContainer { */ protected $_priorityFramesBuffer = []; + /** + * The current state of the UTF8 validation + * + * @var int + */ + protected $_utfValidationState = \PHPWebSocket::UTF8_ACCEPT; + /** * The maximum size the handshake can become * @@ -90,6 +111,13 @@ abstract class AConnection implements IStreamContainer { */ protected $_closeAfterWrite = FALSE; + /** + * The timestamp since when this connection has been opened + * + * @var int + */ + protected $_openedTimestamp = NULL; + /** * This array contains as key the index of the RSV bit and as value if it is allowed * @@ -173,6 +201,21 @@ abstract class AConnection implements IStreamContainer { $this->_closeAfterWrite = TRUE; } + /** + * Should be called after the path and stream has been set to initialize + */ + protected function _afterOpen() { + + $this->_openedTimestamp = microtime(TRUE); + $stream = $this->getStream(); + + stream_set_timeout($stream, 15); + stream_set_blocking($stream, FALSE); + stream_set_read_buffer($stream, 0); + stream_set_write_buffer($stream, 0); + + } + /** * In here we attempt to find frames and unmask them, returns finished messages if available * @@ -180,7 +223,7 @@ abstract class AConnection implements IStreamContainer { * * @throws \UnexpectedValueException * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ protected function _handlePacket(string $newData) : \Generator { @@ -208,7 +251,7 @@ abstract class AConnection implements IStreamContainer { if (!$this->_checkRSVBits($headers)) { - $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR, 'Unexpected RSV bit set'); + $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR, 'Unexpected RSV bit set'); $this->setCloseAfterWrite(); yield new Update\Error(Update\Error::C_READ_RSVBIT_SET, $this); @@ -216,7 +259,7 @@ abstract class AConnection implements IStreamContainer { return; } - $frameSize = $headers[Framer::IND_LENGTH] + $headers[Framer::IND_PAYLOADOFFSET]; + $frameSize = $headers[Framer::IND_LENGTH] + $headers[Framer::IND_PAYLOAD_OFFSET]; if ($numBytes < $frameSize) { $this->_currentFrameRemainingBytes = $frameSize - $numBytes; \PHPWebSocket::Log(LOG_DEBUG, 'Setting next read size to ' . $this->_currentFrameRemainingBytes); @@ -233,7 +276,7 @@ abstract class AConnection implements IStreamContainer { break; // Frame isn't ready yet } elseif ($framePayload === FALSE) { - $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR); + $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR); $this->setCloseAfterWrite(); yield new Update\Error(Update\Error::C_READ_PROTOCOL_ERROR, $this); @@ -245,9 +288,9 @@ abstract class AConnection implements IStreamContainer { switch ($opcode) { case \PHPWebSocket::OPCODE_CONTINUE: - if ($this->_partialMessage === NULL) { + if ($this->_partialMessage === NULL && $this->_partialMessageStream === NULL) { - $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR, 'Got OPCODE_CONTINUE but no frame that could be continued'); + $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR, 'Got OPCODE_CONTINUE but no frame that could be continued'); $this->setCloseAfterWrite(); yield new Update\Error(Update\Error::C_READ_PROTOCOL_ERROR, $this); @@ -259,29 +302,25 @@ abstract class AConnection implements IStreamContainer { case \PHPWebSocket::OPCODE_FRAME_TEXT: case \PHPWebSocket::OPCODE_FRAME_BINARY: - if ($opcode === \PHPWebSocket::OPCODE_CONTINUE) { - $this->_partialMessage .= $framePayload; - } elseif ($this->_partialMessage !== NULL) { + if (($this->_partialMessageOpcode ?: $opcode) === \PHPWebSocket::OPCODE_FRAME_TEXT) { - $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR, 'Got new frame without completing the previous'); - $this->setCloseAfterWrite(); - - yield new Update\Error(Update\Error::C_READ_INVALID_PAYLOAD, $this); + if (!\PHPWebSocket::ValidateUTF8($framePayload, $this->_utfValidationState) || ($headers[Framer::IND_FIN] && $this->_utfValidationState !== \PHPWebSocket::UTF8_ACCEPT)) { - return; + $this->sendDisconnect(\PHPWebSocket::CLOSECODE_INVALID_PAYLOAD, 'Could not decode text frame as UTF-8'); + $this->setCloseAfterWrite(); - } else { + yield new Update\Error(Update\Error::C_READ_INVALID_PAYLOAD, $this); - $this->_partialMessageOpcode = $opcode; - $this->_partialMessage = $framePayload; + return; + } } - if ($headers[Framer::IND_FIN]) { + if ($opcode !== \PHPWebSocket::OPCODE_CONTINUE) { - if ($this->_partialMessageOpcode === \PHPWebSocket::OPCODE_FRAME_TEXT && !preg_match('//u', $this->_partialMessage)) { + if ($this->_partialMessage !== NULL || $this->_partialMessageStream !== NULL) { - $this->sendDisconnect(\PHPWebSocket::CLOSECODE_INVALID_PAYLOAD, 'Could not decode text frame as UTF-8'); + $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR, 'Got new frame without completing the previous'); $this->setCloseAfterWrite(); yield new Update\Error(Update\Error::C_READ_INVALID_PAYLOAD, $this); @@ -289,11 +328,64 @@ abstract class AConnection implements IStreamContainer { return; } - yield new Update\Read(Update\Read::C_READ, $this, $this->_partialMessageOpcode, $this->_partialMessage); + $newMessageStream = $this->_getStreamForNewMessage($headers); + if ($newMessageStream === FALSE) { + + $this->sendDisconnect(\PHPWebSocket::CLOSECODE_UNSUPPORTED_PAYLOAD); + $this->setCloseAfterWrite(); + + return; + } + + $this->_partialMessageOpcode = $opcode; + + if (is_resource($newMessageStream)) { + + $this->_partialMessageStream = $newMessageStream; + $this->_partialMessage = NULL; + + } else { + + $this->_partialMessageStream = NULL; + $this->_partialMessage = ''; + + } + + } + + if ($this->_partialMessageStream) { + + $payloadLength = strlen($framePayload); + $writtenBytes = 0; + $res = NULL; + + do { + + $res = @fwrite($this->_partialMessageStream, $framePayload); + if ($res !== FALSE) { + $writtenBytes += $res; + } + + } while ($res !== FALSE && $writtenBytes < $payloadLength); + + if ($res === FALSE) { + yield new Update\Error(Update\Error::C_READ_INVALID_TARGET_STREAM, $this); + } + + } else { + $this->_partialMessage .= $framePayload; + } + + if ($headers[Framer::IND_FIN]) { + + yield new Update\Read(Update\Read::C_READ, $this, $this->_partialMessageOpcode, $this->_partialMessage, $this->_partialMessageStream); $this->_partialMessageOpcode = NULL; + $this->_partialMessageStream = NULL; $this->_partialMessage = NULL; + $this->_utfValidationState = \PHPWebSocket::UTF8_ACCEPT; + } break; @@ -310,12 +402,12 @@ abstract class AConnection implements IStreamContainer { if (!\PHPWebSocket::IsValidCloseCode($code)) { $disconnectMessage = 'Invalid close code provided: ' . $code; - $code = \PHPWebSocket::CLOSECODE_PROTOCOLERROR; + $code = \PHPWebSocket::CLOSECODE_PROTOCOL_ERROR; } elseif (!preg_match('//u', substr($framePayload, 2))) { $disconnectMessage = 'Received Non-UTF8 close frame payload'; - $code = \PHPWebSocket::CLOSECODE_PROTOCOLERROR; + $code = \PHPWebSocket::CLOSECODE_PROTOCOL_ERROR; } else { $disconnectMessage = substr($framePayload, 2); @@ -383,7 +475,7 @@ abstract class AConnection implements IStreamContainer { /** * Writes the current buffer to the connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleWrite() : \Generator { @@ -409,7 +501,7 @@ abstract class AConnection implements IStreamContainer { \PHPWebSocket::Log(LOG_DEBUG, ' Attempting to write ' . $bytesToWrite . ' bytes'); - $bytesWritten = fwrite($this->_stream, $this->_writeBuffer, min($this->getWriteRate($this), $bytesToWrite)); + $bytesWritten = @fwrite($this->getStream(), $this->_writeBuffer, min($this->getWriteRate(), $bytesToWrite)); if ($bytesWritten === FALSE) { \PHPWebSocket::Log(LOG_DEBUG, ' fwrite failed'); yield new Update\Error(Update\Error::C_WRITE, $this); @@ -425,7 +517,7 @@ abstract class AConnection implements IStreamContainer { if ($this->_closeAfterWrite && $this->isWriteBufferEmpty()) { \PHPWebSocket::Log(LOG_DEBUG, ' Close after write'); - $this->close($this); + $this->close(); } } @@ -459,6 +551,28 @@ abstract class AConnection implements IStreamContainer { } + /** + * Returns the stream to write to for this specific message or NULL to use a buffer + * + * @param array $headers + * + * @return resource|null|bool + */ + protected function _getStreamForNewMessage(array $headers) { + + $retValue = TRUE; + if ($this->_newMessageStreamCallback) { + + $retValue = call_user_func($this->_newMessageStreamCallback, $headers); + if (!is_bool($retValue) && !is_resource($retValue)) { + throw new \UnexpectedValueException('Got an invalid return value, expected boolean or resource, got ' . gettype($retValue)); + } + + } + + return $retValue; + } + /** * Writes a raw string to the buffer, if priority is set to TRUE it will be send before normal priority messages * @@ -529,6 +643,17 @@ abstract class AConnection implements IStreamContainer { return TRUE; } + /** + * Sets the callable that gets called after the first message part has been read to get the optional stream to write to + * If no stream but FALSE is returned in this callback the connection will be closed with code CLOSECODE_UNSUPPORTED_PAYLOAD + * If TRUE is returned from the callback a memory buffer will be used instead + * + * @param callable|null $callable + */ + public function setNewMessageStreamCallback(callable $callable = NULL) { + $this->_newMessageStreamCallback = $callable; + } + /** * Sets if the provided reserved bit is allowed to be set * @@ -561,6 +686,15 @@ abstract class AConnection implements IStreamContainer { return $this->_allowedRSVBits[$bit]; } + /** + * Returns the timestamp at which the connection was opened + * + * @return float|null + */ + public function getOpenedTimestamp() { + return $this->_openedTimestamp; + } + /** * Sets the maximum amount of bytes to write per cycle * diff --git a/Client.php.inc b/Client.php.inc index b1cf98d..151c298 100644 --- a/Client.php.inc +++ b/Client.php.inc @@ -30,7 +30,7 @@ declare(strict_types = 1); namespace PHPWebSocket; -class Client extends \PHPWebSocket\AConnection { +class Client extends AConnection { /** * The last error code received from the stream @@ -60,6 +60,11 @@ class Client extends \PHPWebSocket\AConnection { */ protected $_resourceIndex = NULL; + /** + * @var string|null + */ + protected $_userAgent = NULL; + /** * The headers send back from the server when the handshake was accepted * @@ -74,6 +79,20 @@ class Client extends \PHPWebSocket\AConnection { */ protected $_stream = NULL; + /** + * The remote host we are connecting to + * + * @var string|null + */ + protected $_host = NULL; + + /** + * The path used in the HTTP request + * + * @var string + */ + protected $_path = '/'; + /** * Connects to the provided resource * @@ -88,7 +107,7 @@ class Client extends \PHPWebSocket\AConnection { public function connectToResource($resource, string $path = '/') { if (!is_resource($resource)) { - throw \InvalidArgumentException('Argument is not a resource!'); + throw new \InvalidArgumentException('Argument is not a resource!'); } if ($this->isOpen()) { @@ -96,8 +115,10 @@ class Client extends \PHPWebSocket\AConnection { } $this->_stream = $resource; + $this->_host = (string) $resource; + $this->_path = $path; - $this->_afterOpen((string) $resource, $path); + $this->_afterOpen(); return TRUE; } @@ -123,33 +144,30 @@ class Client extends \PHPWebSocket\AConnection { return FALSE; } - $this->_afterOpen($address, $path); + $this->_host = $address; + $this->_path = $path; + + $this->_afterOpen(); return TRUE; } /** - * Sets the stream settings and appends the HTTP headers to the buffer - * - * @param string $address - * @param string $path + * Should be called after the path and stream has been set to initialize */ - protected function _afterOpen(string $address, string $path) { + protected function _afterOpen() { - $this->_resourceIndex = (int) $this->getStream(); + parent::_afterOpen(); - stream_set_timeout($this->_stream, 15); - stream_set_blocking($this->_stream, FALSE); - stream_set_read_buffer($this->_stream, 0); - stream_set_write_buffer($this->_stream, 0); + $this->_resourceIndex = (int) $this->getStream(); $headerParts = [ - 'GET ' . $path . ' HTTP/1.1', - 'Host: ' . $address, + 'GET ' . $this->_path . ' HTTP/1.1', + 'Host: ' . $this->_host, 'User-Agent: ' . $this->getUserAgent(), 'Upgrade: websocket', 'Connection: Upgrade', - 'Sec-WebSocket-Key: ' . base64_encode(\PHPWebSocket::RandomString()), + 'Sec-WebSocket-Key: ' . base64_encode(\PHPWebSocket::RandomString(16)), 'Sec-WebSocket-Version: 13', ]; @@ -191,7 +209,7 @@ class Client extends \PHPWebSocket\AConnection { * * @throws \Exception * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function update(float $timeout = NULL) : \Generator { yield from \PHPWebSocket::MultiUpdate([$this], $timeout); @@ -200,13 +218,13 @@ class Client extends \PHPWebSocket\AConnection { /** * Attempts to read from our connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleRead() : \Generator { \PHPWebSocket::Log(LOG_DEBUG, __METHOD__); - $readRate = $this->getReadRate($this); + $readRate = $this->getReadRate(); $newData = fread($this->getStream(), min($this->_currentFrameRemainingBytes ?? $readRate, $readRate)); if ($newData === FALSE) { yield new Update\Error(Update\Error::C_READ, $this); @@ -214,7 +232,6 @@ class Client extends \PHPWebSocket\AConnection { return; } - $updates = []; if (strlen($newData) === 0) { if ($this->_remoteSentDisconnect && $this->_weSentDisconnect) { @@ -344,6 +361,15 @@ class Client extends \PHPWebSocket\AConnection { return is_resource($this->_stream); } + /** + * Returns the path send in the HTTP request + * + * @return string + */ + public function getPath() : string { + return $this->_path; + } + /** * Simply closes the connection */ diff --git a/Framer.php.inc b/Framer.php.inc index 6230344..c4de560 100644 --- a/Framer.php.inc +++ b/Framer.php.inc @@ -48,8 +48,8 @@ final class Framer { IND_OPCODE = 'opcode', IND_MASK = 'mask', IND_LENGTH = 'length', - IND_MASKINGKEY = 'masking-key', - IND_PAYLOADOFFSET = 'payload-offset'; + IND_MASKING_KEY = 'masking-key', + IND_PAYLOAD_OFFSET = 'payload-offset'; /** * Extracts the headers out of the provided frame, returns NULL if the provided frame has invalid headers @@ -76,8 +76,8 @@ final class Framer { self::IND_OPCODE => ($byte1 & self::BYTE1_OPCODE), self::IND_MASK => (bool) ($byte2 & self::BYTE2_MASKED), self::IND_LENGTH => ($byte2 & self::BYTE2_LENGTH), - self::IND_MASKINGKEY => NULL, - self::IND_PAYLOADOFFSET => 2, + self::IND_MASKING_KEY => NULL, + self::IND_PAYLOAD_OFFSET => 2, ]; if ($headers[self::IND_LENGTH] === 126) { // 16 bits @@ -87,10 +87,10 @@ final class Framer { } $headers[self::IND_LENGTH] = unpack('n', substr($frame, 2, 2))[1]; - $headers[self::IND_PAYLOADOFFSET] += 2; + $headers[self::IND_PAYLOAD_OFFSET] += 2; if ($headers[self::IND_MASK]) { - $headers[self::IND_MASKINGKEY] = substr($frame, 4, 4); + $headers[self::IND_MASKING_KEY] = substr($frame, 4, 4); } } elseif ($headers[self::IND_LENGTH] === 127) { // 64 bits @@ -102,10 +102,10 @@ final class Framer { $headers[self::IND_LENGTH] = unpack('J', substr($frame, 2, 8))[1]; if ($headers[self::IND_MASK]) { - $headers[self::IND_MASKINGKEY] = substr($frame, 10, 4); + $headers[self::IND_MASKING_KEY] = substr($frame, 10, 4); } - $headers[self::IND_PAYLOADOFFSET] += 8; + $headers[self::IND_PAYLOAD_OFFSET] += 8; } elseif ($headers[self::IND_MASK]) { // 7 bits @@ -113,12 +113,12 @@ final class Framer { return NULL; } - $headers[self::IND_MASKINGKEY] = substr($frame, 2, 4); + $headers[self::IND_MASKING_KEY] = substr($frame, 2, 4); } if ($headers[self::IND_MASK]) { - $headers[self::IND_PAYLOADOFFSET] += 4; + $headers[self::IND_PAYLOAD_OFFSET] += 4; } return $headers; @@ -140,7 +140,7 @@ final class Framer { } $frameLength = strlen($frame); - if ($frameLength < $headers[self::IND_PAYLOADOFFSET]) { // Frame headers incomplete + if ($frameLength < $headers[self::IND_PAYLOAD_OFFSET]) { // Frame headers incomplete return NULL; } @@ -159,13 +159,13 @@ final class Framer { case \PHPWebSocket::OPCODE_FRAME_TEXT: case \PHPWebSocket::OPCODE_FRAME_BINARY: - if ($frameLength < $headers[self::IND_PAYLOADOFFSET] + $headers[self::IND_LENGTH]) { + if ($frameLength < $headers[self::IND_PAYLOAD_OFFSET] + $headers[self::IND_LENGTH]) { return NULL; } - $payload = substr($frame, $headers[self::IND_PAYLOADOFFSET], $headers[self::IND_LENGTH]); + $payload = substr($frame, $headers[self::IND_PAYLOAD_OFFSET], $headers[self::IND_LENGTH]); if ($headers[self::IND_MASK]) { - $payload = self::ApplyMask($payload, $headers[self::IND_MASKINGKEY]); + $payload = self::ApplyMask($payload, $headers[self::IND_MASKING_KEY]); } return $payload; @@ -193,7 +193,7 @@ final class Framer { * * @return string */ - public static function Frame(string $data, bool $mask, int $opcode = \PHPWebSocket::OPCODE_FRAME_TEXT, bool $isFinal = TRUE, bool $rsv1 = FALSE, bool $rsv2 = FALSE, bool $rsv3 = FALSE) { + public static function Frame(string $data, bool $mask, int $opcode = \PHPWebSocket::OPCODE_FRAME_TEXT, bool $isFinal = TRUE, bool $rsv1 = FALSE, bool $rsv2 = FALSE, bool $rsv3 = FALSE) : string { if ($opcode < 0 || $opcode > 15) { throw new \RangeException('Invalid opcode, opcode should range between 0 and 15'); @@ -259,6 +259,6 @@ final class Framer { * @return string */ public static function ApplyMask(string $payload, string $maskingKey) : string { - return str_pad('', strlen($payload), $maskingKey) ^ $payload; + return (string) (str_pad('', strlen($payload), $maskingKey) ^ $payload); } } diff --git a/IStreamContainer.php.inc b/IStreamContainer.php.inc index cb2073b..4ad4aa9 100644 --- a/IStreamContainer.php.inc +++ b/IStreamContainer.php.inc @@ -34,7 +34,7 @@ interface IStreamContainer { /** * Gets called just before stream_select gets called * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function beforeStreamSelect() : \Generator; @@ -48,21 +48,21 @@ interface IStreamContainer { /** * Handles exceptional data reads * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleExceptional() : \Generator; /** * Writes the current buffer to the connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleWrite() : \Generator; /** * Attempts to read from our connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleRead() : \Generator; diff --git a/PHPWebSocket.php.inc b/PHPWebSocket.php.inc index d532d39..60c8ae3 100644 --- a/PHPWebSocket.php.inc +++ b/PHPWebSocket.php.inc @@ -50,7 +50,7 @@ final class PHPWebSocket { const CLOSECODE_NORMAL = 1000, CLOSECODE_ENDPOINT_CLOSING = 1001, - CLOSECODE_PROTOCOLERROR = 1002, + CLOSECODE_PROTOCOL_ERROR = 1002, CLOSECODE_UNSUPPORTED_PAYLOAD = 1003, // CLOSECODE_ = 1004, // Reserved CLOSECODE_NO_STATUS = 1005, @@ -62,6 +62,9 @@ final class PHPWebSocket { CLOSECODE_UNEXPECTED_CONDITION = 1011, CLOSECODE_TLS_HANDSHAKE_FAILURE = 1015; + const UTF8_ACCEPT = 0, + UTF8_REJECT = 1; + const HTTP_STATUSCODES = [ 100 => 'Continue', 101 => 'Switching Protocols', @@ -159,7 +162,7 @@ final class PHPWebSocket { * * @throws \InvalidArgumentException * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public static function MultiUpdate(array $updateObjects, float $timeout = NULL) : \Generator { @@ -269,13 +272,15 @@ final class PHPWebSocket { * * @param int $code * @param bool $received If the close code is received as reason + * + * @return bool */ public static function IsValidCloseCode(int $code, bool $received = TRUE) : bool { switch ($code) { case self::CLOSECODE_NORMAL: case self::CLOSECODE_ENDPOINT_CLOSING: - case self::CLOSECODE_PROTOCOLERROR: + case self::CLOSECODE_PROTOCOL_ERROR: case self::CLOSECODE_UNSUPPORTED_PAYLOAD: case self::CLOSECODE_INVALID_PAYLOAD: case self::CLOSECODE_POLICY_VIOLATION: @@ -322,7 +327,7 @@ final class PHPWebSocket { * * @return string */ - public static function RandomString(int $length = 16) : string { + public static function RandomString(int $length) : string { $key = ''; for ($i = 0; $i < $length; $i++) { @@ -332,6 +337,51 @@ final class PHPWebSocket { return $key; } + /** + * Validates a string for UTF8 validity, this allows for matching partial UTF8 string by passing the state each time on resume + * + * @copyright (c) 2008-2009 Bjoern Hoehrmann + * + * @see http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * @param string $str + * @param int &$state + * + * @return bool + */ + public static function ValidateUTF8(string $str, int &$state = self::UTF8_ACCEPT) : bool { + + $table = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df + 0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef + 0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff + 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2 + 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4 + 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6 + 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // s7..s8 + ]; + + $len = strlen($str); + + for ($i = 0; $i < $len; $i++) { + + $state = $table[256 + ($state << 4) + $table[ord($str[$i])]]; + if ($state === self::UTF8_REJECT) { + return FALSE; + } + + } + + return TRUE; + } + /** * Attempts to parse the provided string into key => value pairs based on the HTTP headers syntax * @@ -410,7 +460,7 @@ final class PHPWebSocket { } if ($logLevel < LOG_DEBUG || self::$_Debug || $forceShow) { - echo 'PHPWebSocket: ' . $logLevel . ') ' . ((string) $message) . PHP_EOL; + echo date(DATE_ATOM) . ' PHPWebSocket: ' . $logLevel . ') ' . ((string) $message) . PHP_EOL; } } diff --git a/Server.php.inc b/Server.php.inc index 1a14356..b2843a2 100644 --- a/Server.php.inc +++ b/Server.php.inc @@ -35,27 +35,6 @@ require_once(__DIR__ . '/Server/Connection.php.inc'); class Server { - /** - * The maximum size in bytes for the handshake - * - * @var int - */ - const HANDSHAKE_MAXLENGTH = 8192; - - /** - * The time in seconds in which the stream_socket_accept method has to accept the connection or fail - * - * @var float - */ - const SOCKET_ACCEPT_TIMEOUT = 5.0; - - /** - * The time in seconds in which the client has to send its handshake - * - * @var float - */ - const ACCEPT_TIMEOUT = 5.0; - /** * The counter to provide all websocket servers with an unique ID * @@ -70,6 +49,13 @@ class Server { */ protected $_cleanupAcceptingConnectionOnClose = TRUE; + /** + * The time in seconds in which the stream_socket_accept method has to accept the connection or fail + * + * @var float + */ + protected $_socketAcceptTimeout = 5.0; + /** * The accepting socket connection * @@ -209,14 +195,14 @@ class Server { list($server, $client) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); - $serverConnection = new \PHPWebSocket\Server\Connection($this, $server, '', $this->_connectionIndex); + $serverConnection = new Server\Connection($this, $server, '', $this->_connectionIndex); $this->_connections[$this->_connectionIndex] = $serverConnection; \PHPWebSocket::Log(LOG_DEBUG, 'Created new connection: ' . $serverConnection); $this->_connectionIndex++; - $clientConnection = new \PHPWebSocket\Client(); + $clientConnection = new Client(); $clientConnection->connectToResource($client); return [$serverConnection, $clientConnection]; @@ -229,7 +215,7 @@ class Server { * * @throws \Exception * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function update(float $timeout = NULL) : \Generator { yield from \PHPWebSocket::MultiUpdate($this->getConnections(TRUE), $timeout); @@ -238,7 +224,7 @@ class Server { /** * Gets called by the accepting web socket to notify the server that a new connection attempt has occured * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function gotNewConnection() : \Generator { @@ -256,7 +242,7 @@ class Server { * @throws \LogicException * @throws \RuntimeException * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function acceptNewConnection() : \Generator { @@ -265,12 +251,12 @@ class Server { } $peername = ''; - $newStream = stream_socket_accept($this->_acceptingConnection->getStream(), self::SOCKET_ACCEPT_TIMEOUT, $peername); + $newStream = stream_socket_accept($this->_acceptingConnection->getStream(), $this->getSocketAcceptTimeout(), $peername); if (!$newStream) { throw new \RuntimeException('Unable to accept stream socket!'); } - $newConnection = new \PHPWebSocket\Server\Connection($this, $newStream, $peername, $this->_connectionIndex); + $newConnection = new Server\Connection($this, $newStream, $peername, $this->_connectionIndex); $this->_connections[$this->_connectionIndex] = $newConnection; \PHPWebSocket::Log(LOG_DEBUG, 'Got new connection: ' . $newConnection); @@ -297,7 +283,7 @@ class Server { '%serverIdentifier%' => $this->getServerIdentifier(), ]; - return str_replace(array_keys($replaceFields), array_values($replaceFields), "HTTP/1.1 %errorCode% %errorString%\r\nServer: %serverIdentifier%\r\n\r\n%errorCode% %errorString%

%errorCode% %errorString%


%serverIdentifier%
\r\n\r\n"); + return str_replace(array_keys($replaceFields), array_values($replaceFields), "HTTP/1.1 %errorCode% %errorString%\r\nServer: %serverIdentifier%\r\n\r\n%errorCode% %errorString%

%errorCode% %errorString%


%serverIdentifier%
\r\n\r\n"); } /** @@ -305,7 +291,7 @@ class Server { * * @param resource $stream * - * @return \PHPWebSocket\Server\Connection|null + * @return Server\Connection|null */ public function getConnectionByStream($stream) { @@ -341,7 +327,9 @@ class Server { /** * Returns if the provided connection in owned by this server * - * @return \PHPWebSocket\Server\Connection[] + * @param \PHPWebSocket\Server\Connection $connection + * + * @return bool */ public function hasConnection(Server\Connection $connection) : bool { return in_array($connection, $this->_connections, TRUE); @@ -359,7 +347,9 @@ class Server { /** * Returns all connections this server has * - * @return \PHPWebSocket\Server\Connection[] + * @param bool $includeAccepting + * + * @return array|\PHPWebSocket\Server\Connection[] */ public function getConnections(bool $includeAccepting = FALSE) : array { @@ -442,6 +432,24 @@ class Server { } + /** + * Sets the time in seconds in which the stream_socket_accept method has to accept the connection or fail + * + * @param float $timeout + */ + public function setSocketAcceptTimeout(float $timeout) { + $this->_socketAcceptTimeout = $timeout; + } + + /** + * Returns the time in seconds in which the stream_socket_accept method has to accept the connection or fail + * + * @return float + */ + public function getSocketAcceptTimeout() : float { + return $this->_socketAcceptTimeout; + } + /** * Sets if we should disable the cleanup which happens after forking * diff --git a/Server/AcceptingConnection.php.inc b/Server/AcceptingConnection.php.inc index 0ec7740..eb82d0d 100644 --- a/Server/AcceptingConnection.php.inc +++ b/Server/AcceptingConnection.php.inc @@ -30,9 +30,13 @@ declare(strict_types = 1); namespace PHPWebSocket\Server; -class AcceptingConnection implements \PHPWebSocket\IStreamContainer { +use PHPWebSocket\TStreamContainerDefaults; +use PHPWebSocket\IStreamContainer; +use PHPWebSocket\Server; - use \PHPWebSocket\TStreamContainerDefaults; +class AcceptingConnection implements IStreamContainer { + + use TStreamContainerDefaults; /** * The websocket server related to this connection @@ -48,7 +52,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer { */ protected $_stream = NULL; - public function __construct(\PHPWebSocket\Server $websocket, $stream) { + public function __construct(Server $websocket, $stream) { $this->_server = $websocket; $this->_stream = $stream; @@ -63,7 +67,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer { /** * Handles exceptional data reads * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleExceptional() : \Generator { throw new \LogicException('OOB data is not handled for an accepting stream!'); @@ -72,7 +76,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer { /** * Writes the current buffer to the connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleWrite() : \Generator { throw new \LogicException('An accepting socket should never write!'); @@ -81,7 +85,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer { /** * Attempts to read from our connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleRead() : \Generator { yield from $this->_server->gotNewConnection(); @@ -92,7 +96,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer { * * @return \PHPWebSocket\Server */ - public function getServer() : \PHPWebSocket\Server { + public function getServer() : Server { return $this->_server; } diff --git a/Server/Connection.php.inc b/Server/Connection.php.inc index 1b5802f..e7657b8 100644 --- a/Server/Connection.php.inc +++ b/Server/Connection.php.inc @@ -30,9 +30,11 @@ declare(strict_types = 1); namespace PHPWebSocket\Server; +use PHPWebSocket\AConnection; +use PHPWebSocket\Server; use PHPWebSocket\Update; -class Connection extends \PHPWebSocket\AConnection { +class Connection extends AConnection { /** * The stream's resource index @@ -41,6 +43,13 @@ class Connection extends \PHPWebSocket\AConnection { */ protected $_resourceIndex = NULL; + /** + * The time in seconds in which the client has to send its handshake + * + * @var float + */ + protected $_acceptTimeout = 5.0; + /** * If we've finished the handshake * @@ -97,9 +106,8 @@ class Connection extends \PHPWebSocket\AConnection { */ private $_index = NULL; - public function __construct(\PHPWebSocket\Server $server, $stream, string $streamName, int $index) { + public function __construct(Server $server, $stream, string $streamName, int $index) { - $this->_openedTimestamp = microtime(TRUE); $this->_server = $server; $this->_remoteIP = parse_url($streamName, PHP_URL_HOST); $this->_stream = $stream; @@ -112,23 +120,20 @@ class Connection extends \PHPWebSocket\AConnection { stream_socket_enable_crypto($this->_stream, TRUE, STREAM_CRYPTO_METHOD_TLS_SERVER); } - stream_set_timeout($this->_stream, 15); - stream_set_blocking($this->_stream, FALSE); - stream_set_read_buffer($this->_stream, 0); - stream_set_write_buffer($this->_stream, 0); + $this->_afterOpen(); } /** * Attempts to read from our connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleRead() : \Generator { \PHPWebSocket::Log(LOG_DEBUG, __METHOD__); - $readRate = $this->getReadRate($this); + $readRate = $this->getReadRate(); $newData = fread($this->getStream(), min($this->_currentFrameRemainingBytes ?? $readRate, $readRate)); if ($newData === FALSE) { yield new Update\Error(Update\Error::C_READ, $this); @@ -136,7 +141,6 @@ class Connection extends \PHPWebSocket\AConnection { return; } - $updates = []; if (strlen($newData) === 0) { if ($this->_remoteSentDisconnect && $this->_weSentDisconnect) { @@ -217,11 +221,11 @@ class Connection extends \PHPWebSocket\AConnection { /** * Gets called just before stream_select gets called * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function beforeStreamSelect() : \Generator { - if (!$this->isAccepted() && $this->hasHandshake() && $this->getOpenedTimestamp() + \PHPWebSocket\Server::ACCEPT_TIMEOUT < time()) { + if (!$this->isAccepted() && $this->hasHandshake() && $this->getOpenedTimestamp() + $this->getAcceptTimeout() < time()) { yield new Update\Error(Update\Error::C_ACCEPT_TIMEOUT_PASSED, $this); $this->deny(408); // Request Timeout @@ -316,6 +320,24 @@ class Connection extends \PHPWebSocket\AConnection { } + /** + * Sets the time in seconds in which the client has to send its handshake + * + * @param float $timeout + */ + public function setAcceptTimeout(float $timeout) { + $this->_acceptTimeout = $timeout; + } + + /** + * Returns the time in seconds in which the client has to send its handshake + * + * @return float + */ + public function getAcceptTimeout() : float { + return $this->_acceptTimeout; + } + /** * Returns if the websocket connection has been accepted * @@ -330,7 +352,7 @@ class Connection extends \PHPWebSocket\AConnection { * * @return \PHPWebSocket\Server */ - public function getServer() : \PHPWebSocket\Server { + public function getServer() : Server { return $this->_server; } @@ -343,13 +365,6 @@ class Connection extends \PHPWebSocket\AConnection { return $this->_hasHandshake; } - /** - * Returns the timestamp at which the connection was opened - */ - public function getOpenedTimestamp() : float { - return $this->_openedTimestamp; - } - /** * Returns if the frame we are writing should be masked * diff --git a/TStreamContainerDefaults.php.inc b/TStreamContainerDefaults.php.inc index 0279015..62102a2 100644 --- a/TStreamContainerDefaults.php.inc +++ b/TStreamContainerDefaults.php.inc @@ -34,7 +34,7 @@ trait TStreamContainerDefaults { /** * Gets called just before stream_select gets called * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function beforeStreamSelect() : \Generator { if (FALSE) { @@ -54,7 +54,7 @@ trait TStreamContainerDefaults { /** * Handles exceptional data reads * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleExceptional() : \Generator { if (FALSE) { @@ -65,7 +65,7 @@ trait TStreamContainerDefaults { /** * Writes the current buffer to the connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleWrite() : \Generator { if (FALSE) { @@ -76,7 +76,7 @@ trait TStreamContainerDefaults { /** * Attempts to read from our connection * - * @return \PHPWebSocket\AUpdate[] + * @return \Generator */ public function handleRead() : \Generator { if (FALSE) { diff --git a/Tests/Autobahn/fuzzingclient.json b/Tests/Autobahn/fuzzingclient.json index de783c9..be52da7 100644 --- a/Tests/Autobahn/fuzzingclient.json +++ b/Tests/Autobahn/fuzzingclient.json @@ -1,5 +1,5 @@ { - "outdir": "/tmp/reports/servers", + "outdir": "/tmp/reports/", "servers": [ { "url": "ws://127.0.0.1:9001" diff --git a/Tests/Autobahn/fuzzingserver.json b/Tests/Autobahn/fuzzingserver.json index 6b8f25c..226528a 100644 --- a/Tests/Autobahn/fuzzingserver.json +++ b/Tests/Autobahn/fuzzingserver.json @@ -1,6 +1,6 @@ { "url": "ws://127.0.0.1:9001", - "outdir": "/tmp/reports/clients", + "outdir": "/tmp/reports/", "cases": ["*"], "exclude-cases": [], "exclude-agent-cases": {} diff --git a/Tests/client.php.inc b/Tests/client.php.inc index b83719d..48a78fa 100644 --- a/Tests/client.php.inc +++ b/Tests/client.php.inc @@ -38,84 +38,146 @@ $wstestProc = proc_open('wstest -m fuzzingserver -s Autobahn/fuzzingserver.json' sleep(2); -$client = new \PHPWebSocket\Client(); -if (!$client->connect($address, '/getCaseCount')) { - echo 'Unable to connect to server: ' . $client->getLastError() . PHP_EOL; - exit(1); -} +try { + + $client = new \PHPWebSocket\Client(); -$caseCount = NULL; + $bufferType = $argv[2] ?? NULL; + switch ($bufferType) { + case 'memory': + // Default, do nothing + break; + case 'tmpfile': -while ($client->isOpen()) { - foreach ($client->update() as $key => $value) { - if ($value instanceof Read && $value->getCode() === Read::C_READ) { - $caseCount = (int) $value->getMessage(); + $client->setNewMessageStreamCallback(function (array $headers) { + return tmpfile(); + }); + + break; + default: + throw new \Exception('Unknown buffer type specified: ' . ($bufferType === NULL ? 'NULL' : $bufferType)); + } + + if (!$client->connect($address, '/getCaseCount')) { + \PHPWebSocket::Log(LOG_ERR, 'Unable to connect to server: ' . $client->getLastError()); + exit(1); + } + + $caseCount = NULL; + + while ($client->isOpen()) { + foreach ($client->update() as $key => $value) { + + \PHPWebSocket::Log(LOG_INFO, $value . ''); + + if ($value instanceof Read && $value->getCode() === Read::C_READ) { + + $msg = $value->getMessage() ?? NULL; + if ($msg === NULL) { + + $stream = $value->getStream(); + rewind($stream); + $msg = stream_get_contents($stream); + + } + + $caseCount = (int) $msg; + + } } } -} -echo 'Will run ' . $caseCount . ' test cases' . PHP_EOL; + if ($caseCount === 0) { + throw new \Exception('Unable to get case count from autobahn server!'); + } -for ($i = 0; $i < $caseCount; $i++) { + \PHPWebSocket::Log(LOG_INFO, 'Will run ' . $caseCount . ' test cases'); - $client = new \PHPWebSocket\Client(); - $client->connect($address, '/runCase?case=' . ($i + 1) . '&agent=' . $client->getUserAgent()); + for ($i = 0; $i < $caseCount; $i++) { - while ($client->isOpen()) { + $client = new \PHPWebSocket\Client(); + $client->connect($address, '/runCase?case=' . ($i + 1) . '&agent=' . $client->getUserAgent()); - $updates = $client->update(); - foreach ($updates as $update) { + while ($client->isOpen()) { + + $updates = $client->update(); + foreach ($updates as $update) { + + if ($update instanceof Read && $update->getCode() === Read::C_READ) { + + $message = $update->getMessage() ?? ''; + if ($message === '') { + + $stream = $update->getStream(); + if ($stream) { + + rewind($stream); + $message = stream_get_contents($stream); + + } + + } + + $client->write($message, $update->getOpcode()); + + } - if ($update instanceof Read && $update->getCode() === Read::C_READ) { - $client->write($update->getMessage() ?? '', $update->getOpcode()); } } } -} - -echo 'All test cases ran, asking for report update' . PHP_EOL; + \PHPWebSocket::Log(LOG_INFO, 'All test cases ran, asking for report update'); -$client = new \PHPWebSocket\Client(); -$client->connect($address, '/updateReports?agent=' . $client->getUserAgent()); + $client = new \PHPWebSocket\Client(); + $client->connect($address, '/updateReports?agent=' . $client->getUserAgent()); -while ($client->isOpen()) { - foreach ($client->update() as $key => $value) { + while ($client->isOpen()) { + foreach ($client->update() as $key => $value) { + } } -} -echo 'Reports finished, getting results..' . PHP_EOL; + \PHPWebSocket::Log(LOG_INFO, 'Reports finished, getting results..'); -$outputFile = '/tmp/reports/clients/index.json'; -if (!file_exists($outputFile)) { - echo 'File "' . $outputFile . '" doesn\'t exist!'; - exit(1); -} + $outputFile = '/tmp/reports/index.json'; + if (!file_exists($outputFile)) { + \PHPWebSocket::Log(LOG_ALERT, 'File "' . $outputFile . '" doesn\'t exist!'); + exit(1); + } -$hasFailures = FALSE; -$testCases = json_decode(file_get_contents($outputFile), TRUE)[$client->getUserAgent()]; -foreach ($testCases as $case => $data) { + $hasFailures = FALSE; + $testCases = json_decode(file_get_contents($outputFile), TRUE)[$client->getUserAgent()] ?? NULL; + if ($testCases === NULL) { + \PHPWebSocket::Log(LOG_ERR, 'Unable to get test case results!'); + } else { + + foreach ($testCases as $case => $data) { + + \PHPWebSocket::Log(LOG_INFO, $case . ' => ' . $data['behavior']); + + switch ($data['behavior']) { + case 'OK': + case 'NON-STRICT': + case 'INFORMATIONAL': + case 'UNIMPLEMENTED': + break; + default: + $hasFailures = TRUE; + break; + } - echo $case . ' => ' . $data['behavior'] . PHP_EOL; + } - switch ($data['behavior']) { - case 'OK': - case 'NON-STRICT': - case 'INFORMATIONAL': - case 'UNIMPLEMENTED': - break; - default: - $hasFailures = TRUE; - break; } +} finally { + + proc_terminate($wstestProc); + } -echo 'Exiting' . PHP_EOL; +\PHPWebSocket::Log(LOG_INFO, 'Exiting'); exit((int) $hasFailures); - -exit(); diff --git a/Tests/runner.php b/Tests/runner.php index 396cb3d..ac32856 100644 --- a/Tests/runner.php +++ b/Tests/runner.php @@ -30,7 +30,7 @@ */ if (php_sapi_name() !== 'cli') { - echo 'The tests can only be executed in CLI!' . PHP_EOL; + \PHPWebSocket::Log(LOG_ERR, 'The tests can only be executed in CLI!'); exit(1); } diff --git a/Tests/server.php.inc b/Tests/server.php.inc index f50bb86..ec0c947 100644 --- a/Tests/server.php.inc +++ b/Tests/server.php.inc @@ -30,82 +30,130 @@ require_once(__DIR__ . '/../PHPWebSocket.php.inc'); use \PHPWebSocket\Update\Read; -echo 'Starting test' . PHP_EOL . PHP_EOL; +\PHPWebSocket::Log(LOG_INFO, 'Starting test' . PHP_EOL); $websocket = new \PHPWebSocket\Server('tcp://0.0.0.0:9001'); +$bufferType = $argv[2] ?? NULL; $descriptorSpec = [['pipe', 'r'], STDOUT, STDERR]; $wstestProc = proc_open('wstest -m fuzzingclient -s Autobahn/fuzzingclient.json', $descriptorSpec, $pipes, __DIR__); -while (proc_get_status($wstestProc)['running'] ?? FALSE) { +try { - $updates = $websocket->update(0.1); - foreach ($updates as $update) { + while (proc_get_status($wstestProc)['running'] ?? FALSE) { - if ($update instanceof Read) { + $updates = $websocket->update(0.1); + foreach ($updates as $update) { - $sourceObj = $update->getSourceObject(); - $opcode = $update->getCode(); - switch ($opcode) { - case Read::C_NEWCONNECTION: - $sourceObj->accept(); - break; - case Read::C_READ: + if ($update instanceof Read) { - $opcode = $update->getOpcode(); - switch ($opcode) { - case \PHPWebSocket::OPCODE_CONTINUE: - case \PHPWebSocket::OPCODE_FRAME_TEXT: - case \PHPWebSocket::OPCODE_FRAME_BINARY: + $sourceObj = $update->getSourceObject(); + $opcode = $update->getCode(); + switch ($opcode) { + case Read::C_NEWCONNECTION: - $msg = $update->getMessage(); - if ($msg !== NULL && !$sourceObj->isDisconnecting()) { - $sourceObj->write($msg, $opcode); - } + $sourceObj->accept(); - break; - } + switch ($bufferType) { + case 'memory': + // Default, do nothing + break; + case 'tmpfile': + + $sourceObj->setNewMessageStreamCallback(function (array $headers) { + return tmpfile(); + }); + + break; + default: + throw new \Exception('Unknown buffer type specified: ' . ($bufferType === NULL ? 'NULL' : $bufferType)); + } + + break; + case Read::C_READ: + + $opcode = $update->getOpcode(); + switch ($opcode) { + case \PHPWebSocket::OPCODE_CONTINUE: + case \PHPWebSocket::OPCODE_FRAME_TEXT: + case \PHPWebSocket::OPCODE_FRAME_BINARY: + + if ($sourceObj->isDisconnecting()) { + break; + } + + $message = $update->getMessage() ?? ''; + if ($message === '') { + + $stream = $update->getStream(); + if ($stream) { + + rewind($stream); + $message = stream_get_contents($stream); + + } + + } + + if ($message !== NULL) { + $sourceObj->write($message, $opcode); + } + + break; + } + + break; + } - break; } } } -} + \PHPWebSocket::Log(LOG_INFO, 'Test ended, closing websocket'); -echo 'Test ended, closing websocket' . PHP_EOL; + $websocket->close(); -$websocket->close(); + \PHPWebSocket::Log(LOG_INFO, 'Getting results..'); -echo 'Getting results..' . PHP_EOL; + $outputFile = '/tmp/reports/index.json'; + if (!file_exists($outputFile)) { + \PHPWebSocket::Log(LOG_ERR, 'File "' . $outputFile . '" doesn\'t exist!'); + exit(1); + } -$outputFile = '/tmp/reports/servers/index.json'; -if (!file_exists($outputFile)) { - echo 'File "' . $outputFile . '" doesn\'t exist!'; - exit(1); -} + $hasFailures = FALSE; + $testCases = json_decode(file_get_contents($outputFile), TRUE)[$websocket->getServerIdentifier()] ?? NULL; + if ($testCases === NULL) { + \PHPWebSocket::Log(LOG_ERR, 'Unable to get test case results!'); + } else { + + foreach ($testCases as $case => $data) { + + \PHPWebSocket::Log(LOG_INFO, $case . ' => ' . $data['behavior']); + + switch ($data['behavior']) { + case 'OK': + case 'NON-STRICT': + case 'INFORMATIONAL': + case 'UNIMPLEMENTED': + break; + default: + $hasFailures = TRUE; + break; + } + + } -$hasFailures = FALSE; -$testCases = json_decode(file_get_contents($outputFile), TRUE)[$websocket->getServerIdentifier()]; -foreach ($testCases as $case => $data) { - - echo $case . ' => ' . $data['behavior'] . PHP_EOL; - - switch ($data['behavior']) { - case 'OK': - case 'NON-STRICT': - case 'INFORMATIONAL': - case 'UNIMPLEMENTED': - break; - default: - $hasFailures = TRUE; - break; } +} finally { + + proc_terminate($wstestProc); + } -echo 'Exiting' . PHP_EOL; +\PHPWebSocket::Log(LOG_INFO, 'Exiting'); exit((int) $hasFailures); diff --git a/Update/Error.php.inc b/Update/Error.php.inc index 7f760b2..fc414d8 100644 --- a/Update/Error.php.inc +++ b/Update/Error.php.inc @@ -30,7 +30,9 @@ declare(strict_types = 1); namespace PHPWebSocket\Update; -class Error extends \PHPWebSocket\AUpdate { +use PHPWebSocket\AUpdate; + +class Error extends AUpdate { const C_UNKNOWN = 0, C_SELECT = 1, @@ -45,7 +47,8 @@ class Error extends \PHPWebSocket\AUpdate { C_READ_PROTOCOL_ERROR = 10, C_READ_RSVBIT_SET = 11, C_WRITE = 12, - C_ACCEPT_TIMEOUT_PASSED = 13; + C_ACCEPT_TIMEOUT_PASSED = 13, + C_READ_INVALID_TARGET_STREAM = 14; const ERROR_STRINGS = [ self::C_UNKNOWN => 'Unknown error', diff --git a/Update/Read.php.inc b/Update/Read.php.inc index c7dd7c9..62bc597 100644 --- a/Update/Read.php.inc +++ b/Update/Read.php.inc @@ -30,7 +30,9 @@ declare(strict_types = 1); namespace PHPWebSocket\Update; -class Read extends \PHPWebSocket\AUpdate { +use PHPWebSocket\AUpdate; + +class Read extends AUpdate { const C_UNKNOWN = 0, C_NEWCONNECTION = 1, @@ -71,12 +73,24 @@ class Read extends \PHPWebSocket\AUpdate { */ protected $_opcode = NULL; - public function __construct(int $code, $sourceObject = NULL, int $opcode = NULL, string $message = NULL) { + /** + * The resource pointing to the downloaded message + * + * @var resource|null + */ + protected $_stream = NULL; + + public function __construct(int $code, $sourceObject = NULL, int $opcode = NULL, string $message = NULL, $stream = NULL) { + + if ($stream !== NULL && !is_resource($stream)) { + throw new \InvalidArgumentException('The $stream argument has to be NULL or a resource!'); + } parent::__construct($code, $sourceObject); $this->_message = $message; $this->_opcode = $opcode; + $this->_stream = $stream; } @@ -88,7 +102,7 @@ class Read extends \PHPWebSocket\AUpdate { * @return string */ public static function StringForCode(int $code) : string { - return self::ERROR_STRINGS[$code] ?? 'Unknown read code ' . $code; + return self::READ_STRINGS[$code] ?? 'Unknown read code ' . $code; } /** @@ -109,7 +123,17 @@ class Read extends \PHPWebSocket\AUpdate { return $this->_opcode; } + /** + * Returns the resource pointing to the downloaded message + * + * @return resource|null + */ + public function getStream() { + return $this->_stream; + } + public function __toString() { + $code = $this->getCode(); return 'Read) ' . self::StringForCode($code) . ' (C: ' . $code . ')'; diff --git a/VERSION b/VERSION index bd8bf88..27f9cd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.7.0 +1.8.0