diff --git a/.travis.yml b/.travis.yml index 03889a2..5650d7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,5 +13,5 @@ notifications: email: recipients: - kevkevm@gmail.com - on_success: always + on_success: change on_failure: always diff --git a/AConnection.php.inc b/AConnection.php.inc index 9d1b31e..49b0a94 100644 --- a/AConnection.php.inc +++ b/AConnection.php.inc @@ -1,4 +1,5 @@ _maxHandshakeLength = $maxLength; @@ -143,7 +162,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns the maximum size for the handshake in bytes - * @return integer + * + * @return int */ public function getMaxHandshakeLength() : int { return $this->_maxHandshakeLength; @@ -151,7 +171,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns if we have (partial)frames ready to be send - * @return boolean + * + * @return bool */ public function isWriteBufferEmpty() : bool { return empty($this->_priorityFramesBuffer) && empty($this->_framesBuffer) && empty($this->_writeBuffer); @@ -166,7 +187,9 @@ abstract class AConnection implements IStreamContainer { /** * In here we attempt to find frames and unmask them, returns finished messages if available + * * @param string $newData + * * @return \PHPWebSocket\AUpdate[] */ protected function _handlePacket(string $newData) : \Generator { @@ -369,6 +392,7 @@ abstract class AConnection implements IStreamContainer { /** * Writes the current buffer to the connection + * * @return \PHPWebSocket\AUpdate[] */ public function handleWrite() : \Generator { @@ -418,9 +442,11 @@ abstract class AConnection implements IStreamContainer { /** * Splits the provided data into frames of the specified size and sends them to the remote - * @param string $data - * @param integer $opcode - * @param integer $frameSize + * + * @param string $data + * @param int $opcode + * @param int $frameSize + * * @throws \Exception */ public function writeMultiFramed(string $data, int $opcode = \PHPWebSocket::OPCODE_FRAME_TEXT, int $frameSize = 65535) { @@ -444,8 +470,9 @@ abstract class AConnection implements IStreamContainer { /** * Writes a raw string to the buffer, if priority is set to TRUE it will be send before normal priority messages - * @param string $data - * @param boolean $priority + * + * @param string $data + * @param bool $priority */ public function writeRaw(string $data, bool $priority) { @@ -459,9 +486,10 @@ abstract class AConnection implements IStreamContainer { /** * Queues a string to be written to the remote - * @param string $data - * @param integer $opcode - * @param boolean $isFinal + * + * @param string $data + * @param int $opcode + * @param bool $isFinal */ public function write(string $data, int $opcode = \PHPWebSocket::OPCODE_FRAME_TEXT, bool $isFinal = TRUE) { $this->writeRaw(Framer::Frame($data, $this->_shouldMask(), $opcode, $isFinal), \PHPWebSocket::IsPriorityOpcode($opcode)); @@ -469,8 +497,9 @@ abstract class AConnection implements IStreamContainer { /** * Sends a disconnect message to the remote, this causes the connection to be closed once they responds with its disconnect message - * @param integer $code - * @param string $reason + * + * @param int $code + * @param string $reason */ public function sendDisconnect(int $code, string $reason = '') { @@ -486,7 +515,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns TRUE if we are disconnecting - * @return boolean + * + * @return bool */ public function isDisconnecting() : bool { return $this->_weSentDisconnect || $this->_remoteSentDisconnect; @@ -494,8 +524,10 @@ abstract class AConnection implements IStreamContainer { /** * Checks if the remote is in error by sending us one of the RSV bits - * @param array $headers - * @return boolean + * + * @param array $headers + * + * @return bool */ protected function _checkRSVBits(array $headers) : bool { @@ -508,7 +540,8 @@ abstract class AConnection implements IStreamContainer { /** * Sets if we allow the RSV1 bit to be set - * @param boolean $allow + * + * @param bool $allow */ public function setAllowRSV1(bool $allow) { $this->_allowRSV1 = $allow; @@ -516,7 +549,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns if we allow the RSV1 bit to be set - * @return boolean + * + * @return bool */ public function getAllowRSV1() : bool { return $this->_allowRSV1; @@ -524,7 +558,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns if we allow the RSV2 bit to be set - * @param boolean $allow + * + * @param bool $allow */ public function setAllowRSV2(bool $allow) { $this->_allowRSV2 = $allow; @@ -532,7 +567,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns if we allow the RSV2 bit to be set - * @return boolean + * + * @return bool */ public function getAllowRSV2() : bool { return $this->_allowRSV2; @@ -540,7 +576,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns if we allow the RSV3 bit to be set - * @param boolean $allow + * + * @param bool $allow */ public function setAllowRSV3(bool $allow) { $this->_allowRSV3 = $allow; @@ -548,7 +585,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns if we allow the RSV3 bit to be set - * @return boolean + * + * @return bool */ public function getAllowRSV3() : bool { return $this->_allowRSV3; @@ -556,7 +594,8 @@ abstract class AConnection implements IStreamContainer { /** * Sets the maximum amount of bytes to write per cycle - * @param integer $rate + * + * @param int $rate */ public function setWriteRate(int $rate) { $this->_writeRate = $rate; @@ -564,7 +603,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns the maximum amount of bytes to write per cycle - * @return integer + * + * @return int */ public function getWriteRate() : int { return $this->_writeRate; @@ -572,7 +612,8 @@ abstract class AConnection implements IStreamContainer { /** * Sets the maximum amount of bytes to read per cycle - * @param integer $rate + * + * @param int $rate */ public function setReadRate(int $rate) { $this->_readRate = $rate; @@ -580,7 +621,8 @@ abstract class AConnection implements IStreamContainer { /** * Returns the maximum amount of bytes to read per cycle - * @return integer + * + * @return int */ public function getReadRate() : int { return $this->_readRate; @@ -588,13 +630,15 @@ abstract class AConnection implements IStreamContainer { /** * Returns if the frame we are writing should be masked - * @return boolean + * + * @return bool */ abstract protected function _shouldMask() : bool; /** * Returns TRUE if our connection is open - * @return boolean + * + * @return bool */ abstract public function isOpen() : bool; @@ -610,5 +654,4 @@ abstract class AConnection implements IStreamContainer { } } - } diff --git a/AUpdate.php.inc b/AUpdate.php.inc index 34a5dd6..e3c1091 100644 --- a/AUpdate.php.inc +++ b/AUpdate.php.inc @@ -1,4 +1,5 @@ _sourceObject; @@ -61,7 +65,8 @@ abstract class AUpdate { /** * Returns the code for this update - * @return integer + * + * @return int */ public function getCode() : int { return $this->_code; @@ -70,5 +75,4 @@ abstract class AUpdate { public function __toString() { return 'AUpdate) (C: ' . $code . ')'; } - } diff --git a/Client.php.inc b/Client.php.inc index 3f08b4e..ee8bb2f 100644 --- a/Client.php.inc +++ b/Client.php.inc @@ -1,4 +1,5 @@ writeRaw(implode("\r\n", $headerParts) . "\r\n\r\n", FALSE); @@ -142,7 +156,8 @@ class Client extends \PHPWebSocket\AConnection { /** * Returns the code of the last error that occured - * @return integer|NULL + * + * @return int|null */ public function getLastErrorCode() { return $this->_streamLastErrorCode; @@ -150,7 +165,8 @@ class Client extends \PHPWebSocket\AConnection { /** * Returns the last error that occured - * @return string|NULL + * + * @return string|null */ public function getLastError() { return $this->_streamLastError; @@ -158,7 +174,8 @@ class Client extends \PHPWebSocket\AConnection { /** * Returns the stream resource for this client - * @return resource|NULL + * + * @return resource|null */ public function getStream() { return $this->_stream; @@ -166,9 +183,12 @@ class Client extends \PHPWebSocket\AConnection { /** * Checks for updates for this client - * @param float|NULL $timeout The amount of seconds to wait for updates, setting this value to NULL causes this function to block indefinitely until there is an update - * @return \PHPWebSocket\AUpdate[] + * + * @param float|null $timeout The amount of seconds to wait for updates, setting this value to NULL causes this function to block indefinitely until there is an update + * * @throws \Exception + * + * @return \PHPWebSocket\AUpdate[] */ public function update(float $timeout = NULL) : \Generator { yield from \PHPWebSocket::MultiUpdate([$this], $timeout); @@ -176,6 +196,7 @@ class Client extends \PHPWebSocket\AConnection { /** * Attempts to read from our connection + * * @return \PHPWebSocket\AUpdate[] */ public function handleRead() : \Generator { @@ -186,6 +207,7 @@ class Client extends \PHPWebSocket\AConnection { $newData = fread($this->getStream(), min($this->_currentFrameRemainingBytes ?? $readRate, $readRate)); if ($newData === FALSE) { yield new Update\Error(Update\Error::C_READ, $this); + return; } @@ -267,7 +289,8 @@ class Client extends \PHPWebSocket\AConnection { /** * Sets that we should close the connection after all our writes have finished - * @return boolean + * + * @return bool */ public function handshakeAccepted() : bool { return $this->_handshakeAccepted; @@ -275,6 +298,7 @@ class Client extends \PHPWebSocket\AConnection { /** * Returns the user agent string that is reported to the server that we are connecting to + * * @return string */ public function getUserAgent() : string { @@ -283,7 +307,8 @@ class Client extends \PHPWebSocket\AConnection { /** * Returns if the frame we are writing should be masked - * @return boolean + * + * @return bool */ protected function _shouldMask() : bool { return TRUE; @@ -291,6 +316,7 @@ class Client extends \PHPWebSocket\AConnection { /** * Returns the headers set during the http request + * * @return array */ public function getHeaders() : array { @@ -299,7 +325,8 @@ class Client extends \PHPWebSocket\AConnection { /** * Returns if our connection is open - * @return boolean + * + * @return bool */ public function isOpen() : bool { return is_resource($this->_stream); @@ -320,5 +347,4 @@ class Client extends \PHPWebSocket\AConnection { public function __toString() { return 'WSClient #' . $this->_resourceIndex; } - } diff --git a/Framer.php.inc b/Framer.php.inc index 64dc544..36759cf 100644 --- a/Framer.php.inc +++ b/Framer.php.inc @@ -1,4 +1,5 @@ (bool) ($byte1 & self::BYTE1_RSV1), self::IND_RSV2 => (bool) ($byte1 & self::BYTE1_RSV2), self::IND_RSV3 => (bool) ($byte1 & self::BYTE1_RSV3), - self::IND_OPCODE => ($byte1 & self::BYTE1_OPCODE), + self::IND_OPCODE => ($byte1 & self::BYTE1_OPCODE), self::IND_MASK => (bool) ($byte2 & self::BYTE2_MASKED), - self::IND_LENGTH => ($byte2 & self::BYTE2_LENGTH), + self::IND_LENGTH => ($byte2 & self::BYTE2_LENGTH), self::IND_MASKINGKEY => NULL, - self::IND_PAYLOADOFFSET => 2 + self::IND_PAYLOADOFFSET => 2, ]; if ($headers[self::IND_LENGTH] === 126) { // 16 bits @@ -121,9 +124,11 @@ final class Framer { /** * Returns the payload from the frame, returns NULL for incomplete frames and FALSE for protocol error - * @param string $frame - * @param array|NULL $headers - * @return string|boolean|NULL + * + * @param string $frame + * @param array|null $headers + * + * @return string|bool|null */ public static function GetFramePayload(string $frame, array $headers = NULL) { @@ -143,7 +148,7 @@ final class Framer { case \PHPWebSocket::OPCODE_PING: case \PHPWebSocket::OPCODE_PONG: - if ($headers[self::IND_LENGTH] === 1 || $headers[self::IND_LENGTH] > 125 || !$headers[Framer::IND_FIN]) { + if ($headers[self::IND_LENGTH] === 1 || $headers[self::IND_LENGTH] > 125 || !$headers[self::IND_FIN]) { return FALSE; } @@ -164,6 +169,7 @@ final class Framer { return $payload; default: \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_WARNING, 'Encountered unknown opcode: ' . $opcode); + return FALSE; // Failure, unknown action } @@ -171,16 +177,19 @@ final class Framer { /** * Frames a message - * @param string $data - * @param bool $mask - * @param integer $opcode - * @param boolean $isFinal - * @param boolean $rsv1 - * @param boolean $rsv2 - * @param boolean $rsv3 - * @return string + * + * @param string $data + * @param bool $mask + * @param int $opcode + * @param bool $isFinal + * @param bool $rsv1 + * @param bool $rsv2 + * @param bool $rsv3 + * * @throws \RangeException * @throws \LogicException + * + * @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) { @@ -241,12 +250,13 @@ final class Framer { /** * Applies the mask to the provided payload - * @param array $headers - * @param string $payload + * + * @param array $headers + * @param string $payload + * * @return string */ public static function ApplyMask(string $payload, string $maskingKey) : string { return str_pad('', strlen($payload), $maskingKey) ^ $payload; } - } diff --git a/IStreamContainer.php.inc b/IStreamContainer.php.inc index 93a24d5..ad7ceda 100644 --- a/IStreamContainer.php.inc +++ b/IStreamContainer.php.inc @@ -1,70 +1,75 @@ - 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', - - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', - 208 => 'Already Reported', - 226 => 'IM Used', - - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 306 => 'Switch Proxy', - 307 => 'Temporary Redirect', - 308 => 'Permanent Redirect', - - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Payload Too Large', - 414 => 'URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Range Not Satisfiable', - 417 => 'Expectation Failed', - 418 => 'I\'m a teapot', - 421 => 'Misdirected Request', - 422 => 'Unprocessable Entity', - 423 => 'Locked', - 424 => 'Failed Dependency', - 426 => 'Upgrade Required', - 428 => 'Precondition Required', - 429 => 'Too Many Requests', - 431 => 'Request Header Fields Too Large', - 451 => 'Unavailable For Legal Reasons', - - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - 506 => 'Variant Also Negotiates', - 507 => 'Insufficient Storage', - 508 => 'Loop Detected', - 510 => 'Not Extended', - 511 => 'Network Authentication Required' - ]; - - /** - * The log handler function - * @var callable|NULL - */ - private static $_LogHandler = NULL; - - /** - * The current version of PHPWebSocket - * @var string|NULL - */ - private static $_Version = NULL; - - /** - * If we're currently debugging - * @var boolean - */ - private static $_Debug = FALSE; - - /** - * Checks for updates for the provided IStreamContainer objects - * @param \PHPWebSocket\IStreamContainer $objects - * @param float|NULL $timeout The amount of seconds to wait for updates, setting this value to NULL causes this function to block indefinitely until there is an update - * @return \PHPWebSocket\AUpdate[] - * @throws \InvalidArgumentException - */ - public static function MultiUpdate(array $updateObjects, float $timeout = NULL) : \Generator { - - $timeInt = NULL; - $timePart = 0; - - if ($timeout !== NULL) { - - $timeInt = (int) floor($timeout); - $timePart = (int) (fmod($timeout, 1) * 1000000); - - } - - $objectStreamMap = []; - $exceptional = []; - $write = []; - $read = []; - - foreach ($updateObjects as $object) { - - if (!$object instanceof \PHPWebSocket\IStreamContainer) { - throw new \InvalidArgumentException('Got invalid object, all provided objects should implement of \PHPWebSocket\IStreamContainer'); - } - - yield from $object->beforeStreamSelect(); - - $stream = $object->getStream(); - - $objectStreamMap[(int) $object->getStream()] = $object; - $exceptional[] = $stream; - if (!$object->isWriteBufferEmpty()) { - $write[] = $stream; - } - $read[] = $stream; - - } - - if (!empty($read) || !empty($write) || !empty($exceptional)) { - - $streams = @stream_select($read, $write, $exceptional, $timeInt, $timePart); // Stream select filters everything out of the arrays - if ($streams === FALSE) { - yield new \PHPWebSocket\Update\Error(\PHPWebSocket\Update\Error::C_SELECT); - return; - } else { - - if (!empty($read) || !empty($write) || !empty($exceptional)) { - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Read: ' . count($read) . ' Write: ' . count($write) . ' Except: ' . count($exceptional)); - } - - foreach ($read as $stream) { - - $object = $objectStreamMap[(int) $stream]; - if ($object === NULL) { - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Unable to find stream container related to stream during read!'); - continue; - } - - yield from $object->handleRead(); - - } - - foreach ($write as $stream) { - - $object = $objectStreamMap[(int) $stream]; - if ($object === NULL) { - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Unable to find stream container related to stream during write!'); - continue; - } - - if (!is_resource($object->getStream())) { // Check if it is still open, it could have closed - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_WARNING, 'Unable to complete write for stream container ' . $object . ', connection was closed'); - continue; - } - - yield from $object->handleWrite(); - - } - - foreach ($exceptional as $stream) { - - $object = $objectStreamMap[(int) $stream]; - if ($object === NULL) { - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Unable to find stream container related to stream during exceptional read!'); - continue; - } - - if (!is_resource($object->getStream())) { // Check if it is still open, it could have closed - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_WARNING, 'Unable to complete exceptional read for stream container, connection was closed'); - continue; - } - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Got exceptional for ' . $object); - - yield from $object->handleExceptional(); - - } - - } - - } - - } - - /** - * Returns TRUE if the specified code is valid - * @param integer $code - * @param boolean $received If the close code is received as reason - */ - 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_UNSUPPORTED_PAYLOAD: - case self::CLOSECODE_INVALID_PAYLOAD: - case self::CLOSECODE_POLICY_VIOLATION: - case self::CLOSECODE_PAYLOAD_TO_LARGE: - case self::CLOSECODE_EXTENSION_NEGOTIATION_FAILURE: - case self::CLOSECODE_UNEXPECTED_CONDITION: - return TRUE; - case self::CLOSECODE_NO_STATUS: - case self::CLOSECODE_ABNORMAL_DISCONNECT: - case self::CLOSECODE_TLS_HANDSHAKE_FAILURE: - return !$received; - default: - return $code >= 3000 && $code <= 4999; - } - - } - - /** - * Returns if the message with the provided opcode should be send with priority - * @param integer $opcode - * @return boolean - */ - public static function IsPriorityOpcode(int $opcode) : bool { - return self::IsControlOpcode($opcode); - } - - /** - * Returns if the provided opcode is a control opcode - * @param integer $opcode - * @return boolean - */ - public static function IsControlOpcode(int $opcode) : bool { - return $opcode >= 8 && $opcode <= 15; - } - - /** - * Generates a random string - * @param integer $length - * @return string - */ - public static function RandomString(int $length = 16) : string { - - $key = ''; - for ($i = 0; $i < $length; $i++) { - $key .= chr(mt_rand(32, 93)); - } - - return $key; - } - - /** - * Attempts to parse the provided string into key => value pairs based on the HTTP headers syntax - * @param string $rawHeaders - * @return array - */ - public static function ParseHTTPHeaders(string $rawHeaders) : array { - - $headers = []; - - $lines = explode("\n", $rawHeaders); - foreach ($lines as $line) { - - if (strpos($line, ':') !== FALSE) { - - $header = explode(':', $line, 2); - $headers[strtolower(trim($header[0]))] = trim($header[1]); - - } elseif (stripos($line, 'get ') !== FALSE) { - - preg_match('/GET (.*) HTTP/i', $line, $reqResource); - $headers['get'] = trim($reqResource[1]); - - } elseif (preg_match('#HTTP/\d+\.\d+ (\d+)#', $line)) { - - $pieces = explode(' ', $line, 3); - $headers['status-code'] = intval($pieces[1]); - $headers['status-text'] = $pieces[2]; - - } - - } - - return $headers; - } - - /** - * Returns the text related to the provided error code, returns NULL if the code is unknown - * @param integer $errorCode - * @return string|NULL - */ - public static function GetStringForErrorCode(int $errorCode) { - return self::HTTP_ERRORCODES[$errorCode] ?? NULL; - } - - /** - * Returns the version of PHPWebSocket - * @return string - */ - public static function Version() : string { - - if (self::$_Version !== NULL) { - return self::$_Version; - } - - return (self::$_Version = trim(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'VERSION'))); - } - - /** - * Logs a message - * @param integer $logLevel The loglevel, @see \PHPWebSocket::LOGLEVEL_* - * @param string $message The message to log - * @param boolean $forceShow If the message should be shown regardless of the loglevel - */ - public static function Log(int $logLevel, $message, bool $forceShow = FALSE) { - - if (self::$_LogHandler !== NULL) { - call_user_func_array(self::$_LogHandler, func_get_args()); - return; - } - - if ($logLevel > self::LOGLEVEL_DEBUG || self::$_Debug || $forceShow) { - echo('PHPWebSocket: ' . $logLevel . ') ' . ((string) $message) . PHP_EOL); - } - - } - - /** - * Installs the log handler, the callable should accept the same arguments as \PHPWebSocket::Log - * @param callable $logHandler - */ - public static function SetLogHandler(callable $logHandler = NULL) { - self::$_LogHandler = $logHandler; - } - -} + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Switch Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ]; + + /** + * The log handler function + * + * @var callable|null + */ + private static $_LogHandler = NULL; + + /** + * The current version of PHPWebSocket + * + * @var string|null + */ + private static $_Version = NULL; + + /** + * If we're currently debugging + * + * @var bool + */ + private static $_Debug = FALSE; + + /** + * Checks for updates for the provided IStreamContainer objects + * + * @param \PHPWebSocket\IStreamContainer $objects + * @param float|null $timeout The amount of seconds to wait for updates, setting this value to NULL causes this function to block indefinitely until there is an update + * + * @throws \InvalidArgumentException + * + * @return \PHPWebSocket\AUpdate[] + */ + public static function MultiUpdate(array $updateObjects, float $timeout = NULL) : \Generator { + + $timeInt = NULL; + $timePart = 0; + + if ($timeout !== NULL) { + + $timeInt = (int) floor($timeout); + $timePart = (int) (fmod($timeout, 1) * 1000000); + + } + + $objectStreamMap = []; + $exceptional = []; + $write = []; + $read = []; + + foreach ($updateObjects as $object) { + + if (!$object instanceof \PHPWebSocket\IStreamContainer) { + throw new \InvalidArgumentException('Got invalid object, all provided objects should implement of \PHPWebSocket\IStreamContainer'); + } + + yield from $object->beforeStreamSelect(); + + $stream = $object->getStream(); + + $objectStreamMap[(int) $object->getStream()] = $object; + $exceptional[] = $stream; + if (!$object->isWriteBufferEmpty()) { + $write[] = $stream; + } + $read[] = $stream; + + } + + if (!empty($read) || !empty($write) || !empty($exceptional)) { + + $streams = @stream_select($read, $write, $exceptional, $timeInt, $timePart); // Stream select filters everything out of the arrays + if ($streams === FALSE) { + yield new \PHPWebSocket\Update\Error(\PHPWebSocket\Update\Error::C_SELECT); + + return; + } else { + + if (!empty($read) || !empty($write) || !empty($exceptional)) { + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Read: ' . count($read) . ' Write: ' . count($write) . ' Except: ' . count($exceptional)); + } + + foreach ($read as $stream) { + + $object = $objectStreamMap[(int) $stream]; + if ($object === NULL) { + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Unable to find stream container related to stream during read!'); + continue; + } + + yield from $object->handleRead(); + + } + + foreach ($write as $stream) { + + $object = $objectStreamMap[(int) $stream]; + if ($object === NULL) { + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Unable to find stream container related to stream during write!'); + continue; + } + + if (!is_resource($object->getStream())) { // Check if it is still open, it could have closed + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_WARNING, 'Unable to complete write for stream container ' . $object . ', connection was closed'); + continue; + } + + yield from $object->handleWrite(); + + } + + foreach ($exceptional as $stream) { + + $object = $objectStreamMap[(int) $stream]; + if ($object === NULL) { + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Unable to find stream container related to stream during exceptional read!'); + continue; + } + + if (!is_resource($object->getStream())) { // Check if it is still open, it could have closed + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_WARNING, 'Unable to complete exceptional read for stream container, connection was closed'); + continue; + } + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_ERROR, 'Got exceptional for ' . $object); + + yield from $object->handleExceptional(); + + } + + } + + } + + } + + /** + * Returns TRUE if the specified code is valid + * + * @param int $code + * @param bool $received If the close code is received as reason + */ + 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_UNSUPPORTED_PAYLOAD: + case self::CLOSECODE_INVALID_PAYLOAD: + case self::CLOSECODE_POLICY_VIOLATION: + case self::CLOSECODE_PAYLOAD_TO_LARGE: + case self::CLOSECODE_EXTENSION_NEGOTIATION_FAILURE: + case self::CLOSECODE_UNEXPECTED_CONDITION: + return TRUE; + case self::CLOSECODE_NO_STATUS: + case self::CLOSECODE_ABNORMAL_DISCONNECT: + case self::CLOSECODE_TLS_HANDSHAKE_FAILURE: + return !$received; + default: + return $code >= 3000 && $code <= 4999; + } + + } + + /** + * Returns if the message with the provided opcode should be send with priority + * + * @param int $opcode + * + * @return bool + */ + public static function IsPriorityOpcode(int $opcode) : bool { + return self::IsControlOpcode($opcode); + } + + /** + * Returns if the provided opcode is a control opcode + * + * @param int $opcode + * + * @return bool + */ + public static function IsControlOpcode(int $opcode) : bool { + return $opcode >= 8 && $opcode <= 15; + } + + /** + * Generates a random string + * + * @param int $length + * + * @return string + */ + public static function RandomString(int $length = 16) : string { + + $key = ''; + for ($i = 0; $i < $length; $i++) { + $key .= chr(mt_rand(32, 93)); + } + + return $key; + } + + /** + * Attempts to parse the provided string into key => value pairs based on the HTTP headers syntax + * + * @param string $rawHeaders + * + * @return array + */ + public static function ParseHTTPHeaders(string $rawHeaders) : array { + + $headers = []; + + $lines = explode("\n", $rawHeaders); + foreach ($lines as $line) { + + if (strpos($line, ':') !== FALSE) { + + $header = explode(':', $line, 2); + $headers[strtolower(trim($header[0]))] = trim($header[1]); + + } elseif (stripos($line, 'get ') !== FALSE) { + + preg_match('/GET (.*) HTTP/i', $line, $reqResource); + $headers['get'] = trim($reqResource[1]); + + } elseif (preg_match('#HTTP/\d+\.\d+ (\d+)#', $line)) { + + $pieces = explode(' ', $line, 3); + $headers['status-code'] = intval($pieces[1]); + $headers['status-text'] = $pieces[2]; + + } + + } + + return $headers; + } + + /** + * Returns the text related to the provided error code, returns NULL if the code is unknown + * + * @param int $errorCode + * + * @return string|null + */ + public static function GetStringForErrorCode(int $errorCode) { + return self::HTTP_ERRORCODES[$errorCode] ?? NULL; + } + + /** + * Returns the version of PHPWebSocket + * + * @return string + */ + public static function Version() : string { + + if (self::$_Version !== NULL) { + return self::$_Version; + } + + return self::$_Version = trim(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'VERSION')); + } + + /** + * Logs a message + * + * @param int $logLevel The loglevel, @see \PHPWebSocket::LOGLEVEL_* + * @param string $message The message to log + * @param bool $forceShow If the message should be shown regardless of the loglevel + */ + public static function Log(int $logLevel, $message, bool $forceShow = FALSE) { + + if (self::$_LogHandler !== NULL) { + call_user_func_array(self::$_LogHandler, func_get_args()); + + return; + } + + if ($logLevel > self::LOGLEVEL_DEBUG || self::$_Debug || $forceShow) { + echo 'PHPWebSocket: ' . $logLevel . ') ' . ((string) $message) . PHP_EOL; + } + + } + + /** + * Installs the log handler, the callable should accept the same arguments as \PHPWebSocket::Log + * + * @param callable $logHandler + */ + public static function SetLogHandler(callable $logHandler = NULL) { + self::$_LogHandler = $logHandler; + } +} diff --git a/README.md b/README.md index 7fcc4df..04d2d74 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A basic websocket echo server would be: ```php require('PHPWebSocket/PHPWebSocket.php.inc'); -$websocket = new \PHPWebSocket\Server('0.0.0.0', 9001); +$websocket = new \PHPWebSocket\Server('tcp://0.0.0.0:9001'); while (TRUE) { @@ -50,7 +50,7 @@ A basic websocket echo client would be: require('PHPWebSocket/PHPWebSocket.php.inc'); $client = new \PHPWebSocket\Client(); -if (!$client->connect('localhost', 9001, '/webSocket')) { +if (!$client->connect('tcp://localhost:9001/webSocket')) { die('Unable to connect to server: ' . $client->getLastError() . PHP_EOL); } diff --git a/Server.php.inc b/Server.php.inc index 5e8f8f2..6228ed8 100644 --- a/Server.php.inc +++ b/Server.php.inc @@ -1,476 +1,515 @@ -_serverIndex = self::$_ServerCounter; - $this->_address = $address; - - self::$_ServerCounter++; - - $options = []; - if ($this->usesSSL()) { - - $options['ssl'] = [ - 'allow_self_signed' => TRUE, - 'local_cert', $sslCert - ]; - - } - - if ($this->_address !== NULL) { - - $pos = strpos($this->_address, '://'); - if ($pos !== FALSE) { - - $protocol = substr($this->_address, 0, $pos); - switch ($protocol) { - case 'unix': - case 'udg': - - $path = substr($this->_address, $pos + 3); - if (file_exists($path)) { - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_WARNING, 'Unix socket "' . $path . '" still exists, unlinking!'); - if (!unlink($path)) { - throw new \Exception('Unable to unlink file "' . $path . '"'); - } - - } else { - - $dir = pathinfo($path, PATHINFO_DIRNAME); - if (!is_dir($dir)) { - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Directory "' . $dir . '" does not exist, creating..'); - mkdir($dir, 0770, TRUE); - - } - - } - - break; - } - - } - - $errCode = NULL; - $errString = NULL; - $acceptingSocket = @stream_socket_server($this->_address, $errCode, $errString, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, stream_context_create($options)); - if (!$acceptingSocket) { - throw new \Exception('Unable to create webserver: ' . $errString, $errCode); - } - - $this->_acceptingConnection = new Server\AcceptingConnection($this, $acceptingSocket); - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_NORMAL, 'Opened websocket on ' . $this->_address, TRUE); - - } - - $this->_open = TRUE; - - } - - /** - * Creates a new client/connection pair to be used in fork communication - * @return array - */ - public function createServerClientPair() : array { - - list($server, $client) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); - - $serverConnection = new \PHPWebSocket\Server\Connection($this, $server, '', $this->_connectionIndex); - $this->_connections[$this->_connectionIndex] = $serverConnection; - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Created new connection: ' . $serverConnection); - - $this->_connectionIndex++; - - $clientConnection = new \PHPWebSocket\Client(); - $clientConnection->connectToResource($client); - - return [$serverConnection, $clientConnection]; - } - - /** - * Checks for updates - * @param float|NULL $timeout The amount of seconds to wait for updates, setting this value to NULL causes this function to block indefinitely until there is an update - * @return \PHPWebSocket\AUpdate[] - * @throws \Exception - */ - public function update(float $timeout = NULL) : \Generator { - yield from \PHPWebSocket::MultiUpdate($this->getConnections(TRUE), $timeout); - } - - /** - * Gets called by the accepting web socket to notify the server that a new connection attempt has occured - * @return \PHPWebSocket\AUpdate[] - */ - public function gotNewConnection() : \Generator { - - if (!$this->_autoAccept) { - yield new Update\Read(Update\Read::C_NEW_TCP_CONNECTION_AVAILABLE, $this->_acceptingConnection); - } else { - yield from $this->acceptNewConnection(); - } - - } - - /** - * Accepts a new connection from the accepting socket - * @return \PHPWebSocket\AUpdate[] - * @throws \Exception - */ - public function acceptNewConnection() : \Generator { - - if ($this->_acceptingConnection === NULL) { - throw new \Exception('This server has no accepting connection, unable to accept a new connection!'); - } - - $peername = ''; - $newStream = stream_socket_accept($this->_acceptingConnection->getStream(), self::SOCKET_ACCEPT_TIMEOUT, $peername); - if (!$newStream) { - throw new \Exception('Unable to accept stream socket!'); - } - - $newConnection = new \PHPWebSocket\Server\Connection($this, $newStream, $peername, $this->_connectionIndex); - $this->_connections[$this->_connectionIndex] = $newConnection; - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Got new connection: ' . $newConnection); - - $this->_connectionIndex++; - - yield new Update\Read(Update\Read::C_NEW_TCP_CONNECTION, $this->_acceptingConnection); - - } - - /** - * Generates a error response for the provided code - * @param integer $errorCode - * @param string $fallbackErrorString - * @return string - */ - public function getErrorPageForCode(int $errorCode, string $fallbackErrorString = 'Unknown error code') : string { - - $replaceFields = [ - '%errorCode%' => (string) $errorCode, - '%errorString%' => \PHPWebSocket::GetStringForErrorCode($errorCode) ?: $fallbackErrorString, - '%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"); - } - - /** - * Attempts to return the connection object related to the provided stream - * @param resource $stream - * @return \PHPWebSocket\Server\Connection|NULL - */ - public function getConnectionByStream($stream) { - - foreach ($this->_connections as $connection) { - - if ($stream === $connection->getStream()) { - return $connection; - } - - } - - return NULL; - } - - /** - * Returns the server identifier string reported to clients - * @return string - */ - public function getServerIdentifier() : string { - return 'PHPWebSocket/' . \PHPWebSocket::Version(); - } - - /** - * Returns if the provided connection in owned by this server - * @return \PHPWebSocket\Server\Connection[] - */ - public function hasConnection(Server\Connection $connection) : bool { - return in_array($connection, $this->_connections, TRUE); - } - - /** - * Returns the accepting connection - * @return \PHPWebSocket\Server\AcceptingConnection|NULL - */ - public function getAcceptingConnection() { - return $this->_acceptingConnection; - } - - /** - * Returns all connections this server has - * @return \PHPWebSocket\Server\Connection[] - */ - public function getConnections(bool $includeAccepting = FALSE) : array { - - $ret = $this->_connections; - if ($includeAccepting) { - - $acceptingConnection = $this->getAcceptingConnection(); - if ($acceptingConnection !== NULL && $acceptingConnection->isOpen()) { - array_unshift($ret, $acceptingConnection); // Insert the accepting connection on the first index - } - - } - - return $ret; - } - - /** - * Sends a disconnect message to all clients - * @param integer $closeCode - * @param string $reason - */ - public function disconnectAll(int $closeCode, string $reason = '') { - - foreach ($this->getConnections() as $connection) { - $connection->sendDisconnect($closeCode, $reason); - } - - } - - /** - * Returns the bind address for this websocket - * @return string - */ - public function getAddress() : string { - return $this->_address; - } - - /** - * This should be called after a process has been fork with the PID returned from pcntl_fork, this ensures that the connection is closed in the new fork without interupting the main process - * @param integer $pid - */ - public function processDidFork(int $pid) { - - if ($this->_disableForkCleanup) { - return; - } - - if ($pid === 0) { // We are in the new fork - - $this->_cleanupAcceptingConnectionOnClose = FALSE; - $this->close(); - - } - - } - - /** - * Removes the specified connection from the connections array and closes it if open - * @param \PHPWebSocket\Server\Connection $connection - */ - public function removeConnection(Server\Connection $connection) { - - if ($connection->getServer() !== $this) { - throw new \LogicException('Unable to remove connection ' . $connection . ', this is not our connection!'); - } - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Removing ' . $connection); - - if ($connection->isOpen()) { - $connection->close(); - } - - unset($this->_connections[$connection->getIndex()]); - - } - - /** - * Sets if we should disable the cleanup which happens after forking - * @param boolean $disableForkCleanup - */ - public function setDisableForkCleanup(bool $disableForkCleanup) { - $this->_disableForkCleanup = $disableForkCleanup; - } - - /** - * Returns if we should disable the cleanup which happens after forking - * @return boolean - */ - public function getDisableForkCleanup() : bool { - return $this->_disableForkCleanup; - } - - /** - * Sets if we should automatically accept the TCP connection - * @param boolean $autoAccept - */ - public function setAutoAccept(bool $autoAccept) { - $this->_autoAccept = $autoAccept; - } - - /** - * Returns if we accept the TCP connection automatically - * @return boolean - */ - public function getAutoAccept() : bool { - return $this->_autoAccept; - } - - /** - * Returns if this webserver uses SSL - * @return boolean - */ - public function usesSSL() : bool { - return $this->_usesSSL; - } - - /** - * Closes the webserver, note: clients should be notified beforehand that we are disconnecting, calling close while having connected clients will result in an improper disconnect - */ - public function close() { - - foreach ($this->_connections as $connection) { - $connection->close(); - } - - if ($this->_acceptingConnection !== NULL) { - - if ($this->_acceptingConnection->isOpen()) { - $this->_acceptingConnection->close($this->_cleanupAcceptingConnectionOnClose); - } - - $this->_acceptingConnection = NULL; - - } - - $this->_open = FALSE; - - } - - public function __destruct() { - $this->close(); - } - - public function __toString() { - return 'WSServer ' . $this->_serverIndex; - } - -} +_serverIndex = self::$_ServerCounter; + $this->_address = $address; + + self::$_ServerCounter++; + + $options = []; + if ($this->usesSSL()) { + + $options['ssl'] = [ + 'allow_self_signed' => TRUE, + 'local_cert', $sslCert, + ]; + + } + + if ($this->_address !== NULL) { + + $pos = strpos($this->_address, '://'); + if ($pos !== FALSE) { + + $protocol = substr($this->_address, 0, $pos); + switch ($protocol) { + case 'unix': + case 'udg': + + $path = substr($this->_address, $pos + 3); + if (file_exists($path)) { + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_WARNING, 'Unix socket "' . $path . '" still exists, unlinking!'); + if (!unlink($path)) { + throw new \Exception('Unable to unlink file "' . $path . '"'); + } + + } else { + + $dir = pathinfo($path, PATHINFO_DIRNAME); + if (!is_dir($dir)) { + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Directory "' . $dir . '" does not exist, creating..'); + mkdir($dir, 0770, TRUE); + + } + + } + + break; + } + + } + + $errCode = NULL; + $errString = NULL; + $acceptingSocket = @stream_socket_server($this->_address, $errCode, $errString, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, stream_context_create($options)); + if (!$acceptingSocket) { + throw new \Exception('Unable to create webserver: ' . $errString, $errCode); + } + + $this->_acceptingConnection = new Server\AcceptingConnection($this, $acceptingSocket); + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_NORMAL, 'Opened websocket on ' . $this->_address, TRUE); + + } + + $this->_open = TRUE; + + } + + /** + * Creates a new client/connection pair to be used in fork communication + * + * @return array + */ + public function createServerClientPair() : array { + + list($server, $client) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + + $serverConnection = new \PHPWebSocket\Server\Connection($this, $server, '', $this->_connectionIndex); + $this->_connections[$this->_connectionIndex] = $serverConnection; + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Created new connection: ' . $serverConnection); + + $this->_connectionIndex++; + + $clientConnection = new \PHPWebSocket\Client(); + $clientConnection->connectToResource($client); + + return [$serverConnection, $clientConnection]; + } + + /** + * Checks for updates + * + * @param float|null $timeout The amount of seconds to wait for updates, setting this value to NULL causes this function to block indefinitely until there is an update + * + * @throws \Exception + * + * @return \PHPWebSocket\AUpdate[] + */ + public function update(float $timeout = NULL) : \Generator { + yield from \PHPWebSocket::MultiUpdate($this->getConnections(TRUE), $timeout); + } + + /** + * Gets called by the accepting web socket to notify the server that a new connection attempt has occured + * + * @return \PHPWebSocket\AUpdate[] + */ + public function gotNewConnection() : \Generator { + + if (!$this->_autoAccept) { + yield new Update\Read(Update\Read::C_NEW_TCP_CONNECTION_AVAILABLE, $this->_acceptingConnection); + } else { + yield from $this->acceptNewConnection(); + } + + } + + /** + * Accepts a new connection from the accepting socket + * + * @throws \Exception + * + * @return \PHPWebSocket\AUpdate[] + */ + public function acceptNewConnection() : \Generator { + + if ($this->_acceptingConnection === NULL) { + throw new \Exception('This server has no accepting connection, unable to accept a new connection!'); + } + + $peername = ''; + $newStream = stream_socket_accept($this->_acceptingConnection->getStream(), self::SOCKET_ACCEPT_TIMEOUT, $peername); + if (!$newStream) { + throw new \Exception('Unable to accept stream socket!'); + } + + $newConnection = new \PHPWebSocket\Server\Connection($this, $newStream, $peername, $this->_connectionIndex); + $this->_connections[$this->_connectionIndex] = $newConnection; + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Got new connection: ' . $newConnection); + + $this->_connectionIndex++; + + yield new Update\Read(Update\Read::C_NEW_TCP_CONNECTION, $this->_acceptingConnection); + + } + + /** + * Generates a error response for the provided code + * + * @param int $errorCode + * @param string $fallbackErrorString + * + * @return string + */ + public function getErrorPageForCode(int $errorCode, string $fallbackErrorString = 'Unknown error code') : string { + + $replaceFields = [ + '%errorCode%' => (string) $errorCode, + '%errorString%' => \PHPWebSocket::GetStringForErrorCode($errorCode) ?: $fallbackErrorString, + '%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"); + } + + /** + * Attempts to return the connection object related to the provided stream + * + * @param resource $stream + * + * @return \PHPWebSocket\Server\Connection|null + */ + public function getConnectionByStream($stream) { + + foreach ($this->_connections as $connection) { + + if ($stream === $connection->getStream()) { + return $connection; + } + + } + + return NULL; + } + + /** + * Returns the server identifier string reported to clients + * + * @return string + */ + public function getServerIdentifier() : string { + return 'PHPWebSocket/' . \PHPWebSocket::Version(); + } + + /** + * Returns if the provided connection in owned by this server + * + * @return \PHPWebSocket\Server\Connection[] + */ + public function hasConnection(Server\Connection $connection) : bool { + return in_array($connection, $this->_connections, TRUE); + } + + /** + * Returns the accepting connection + * + * @return \PHPWebSocket\Server\AcceptingConnection|null + */ + public function getAcceptingConnection() { + return $this->_acceptingConnection; + } + + /** + * Returns all connections this server has + * + * @return \PHPWebSocket\Server\Connection[] + */ + public function getConnections(bool $includeAccepting = FALSE) : array { + + $ret = $this->_connections; + if ($includeAccepting) { + + $acceptingConnection = $this->getAcceptingConnection(); + if ($acceptingConnection !== NULL && $acceptingConnection->isOpen()) { + array_unshift($ret, $acceptingConnection); // Insert the accepting connection on the first index + } + + } + + return $ret; + } + + /** + * Sends a disconnect message to all clients + * + * @param int $closeCode + * @param string $reason + */ + public function disconnectAll(int $closeCode, string $reason = '') { + + foreach ($this->getConnections() as $connection) { + $connection->sendDisconnect($closeCode, $reason); + } + + } + + /** + * Returns the bind address for this websocket + * + * @return string + */ + public function getAddress() : string { + return $this->_address; + } + + /** + * This should be called after a process has been fork with the PID returned from pcntl_fork, this ensures that the connection is closed in the new fork without interupting the main process + * + * @param int $pid + */ + public function processDidFork(int $pid) { + + if ($this->_disableForkCleanup) { + return; + } + + if ($pid === 0) { // We are in the new fork + + $this->_cleanupAcceptingConnectionOnClose = FALSE; + $this->close(); + + } + + } + + /** + * Removes the specified connection from the connections array and closes it if open + * + * @param \PHPWebSocket\Server\Connection $connection + */ + public function removeConnection(Server\Connection $connection) { + + if ($connection->getServer() !== $this) { + throw new \LogicException('Unable to remove connection ' . $connection . ', this is not our connection!'); + } + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Removing ' . $connection); + + if ($connection->isOpen()) { + $connection->close(); + } + + unset($this->_connections[$connection->getIndex()]); + + } + + /** + * Sets if we should disable the cleanup which happens after forking + * + * @param bool $disableForkCleanup + */ + public function setDisableForkCleanup(bool $disableForkCleanup) { + $this->_disableForkCleanup = $disableForkCleanup; + } + + /** + * Returns if we should disable the cleanup which happens after forking + * + * @return bool + */ + public function getDisableForkCleanup() : bool { + return $this->_disableForkCleanup; + } + + /** + * Sets if we should automatically accept the TCP connection + * + * @param bool $autoAccept + */ + public function setAutoAccept(bool $autoAccept) { + $this->_autoAccept = $autoAccept; + } + + /** + * Returns if we accept the TCP connection automatically + * + * @return bool + */ + public function getAutoAccept() : bool { + return $this->_autoAccept; + } + + /** + * Returns if this webserver uses SSL + * + * @return bool + */ + public function usesSSL() : bool { + return $this->_usesSSL; + } + + /** + * Closes the webserver, note: clients should be notified beforehand that we are disconnecting, calling close while having connected clients will result in an improper disconnect + */ + public function close() { + + foreach ($this->_connections as $connection) { + $connection->close(); + } + + if ($this->_acceptingConnection !== NULL) { + + if ($this->_acceptingConnection->isOpen()) { + $this->_acceptingConnection->close($this->_cleanupAcceptingConnectionOnClose); + } + + $this->_acceptingConnection = NULL; + + } + + $this->_open = FALSE; + + } + + public function __destruct() { + $this->close(); + } + + public function __toString() { + return 'WSServer ' . $this->_serverIndex; + } +} diff --git a/Server/AcceptingConnection.php.inc b/Server/AcceptingConnection.php.inc index a46fcd1..3c01a85 100644 --- a/Server/AcceptingConnection.php.inc +++ b/Server/AcceptingConnection.php.inc @@ -1,165 +1,173 @@ -_server = $websocket; - $this->_stream = $stream; - - stream_set_timeout($this->_stream, 1); - stream_set_blocking($this->_stream, FALSE); - stream_set_read_buffer($this->_stream, 0); - stream_set_write_buffer($this->_stream, 0); - - } - - /** - * Handles exceptional data reads - * @return \PHPWebSocket\AUpdate[] - */ - public function handleExceptional() : \Generator { - throw new \LogicException('OOB data is not handled for an accepting stream!'); - } - - /** - * Writes the current buffer to the connection - * @return \PHPWebSocket\AUpdate[] - */ - public function handleWrite() : \Generator { - throw new \LogicException('An accepting socket should never write!'); - } - - /** - * Attempts to read from our connection - * @return \PHPWebSocket\AUpdate[] - */ - public function handleRead() : \Generator { - yield from $this->_server->gotNewConnection(); - } - - /** - * Returns the related websocket server - * @return \PHPWebSocket\Server - */ - public function getServer() : \PHPWebSocket\Server { - return $this->_server; - } - - /** - * Returns the stream object for this connection - * @return resource - */ - public function getStream() { - return $this->_stream; - } - - /** - * Returns if our connection is open - * @return boolean - */ - public function isOpen() : bool { - return is_resource($this->_stream); - } - - /** - * Closes the stream - * @param boolean $cleanup If we should remove our unix socket if we used one - */ - public function close(bool $cleanup = TRUE) { - - if (is_resource($this->_stream)) { - fclose($this->_stream); - $this->_stream = NULL; - } - - $address = $this->_server->getAddress(); - $pos = strpos($address, '://'); - if ($pos !== FALSE) { - - $protocol = substr($address, 0, $pos); - switch ($protocol) { - case 'unix': - case 'udg': - - if (!$cleanup) { - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Not cleaning up'); - break; - } - - $path = substr($address, $pos + 3); - if (file_exists($path)) { - - if (!$cleanup) { - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Not cleaning up ' . $path); - } else { - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Unlinking: ' . $path); - unlink($path); - - } - - } - - break; - } - - } - - } - - public function __destruct() { - - if ($this->isOpen()) { - $this->close(); - } - - } - - public function __toString() { - return 'AWSConnection #' . (int) $this->getStream() . ' @ ' . $this->_server; - } - -} +_server = $websocket; + $this->_stream = $stream; + + stream_set_timeout($this->_stream, 1); + stream_set_blocking($this->_stream, FALSE); + stream_set_read_buffer($this->_stream, 0); + stream_set_write_buffer($this->_stream, 0); + + } + + /** + * Handles exceptional data reads + * + * @return \PHPWebSocket\AUpdate[] + */ + public function handleExceptional() : \Generator { + throw new \LogicException('OOB data is not handled for an accepting stream!'); + } + + /** + * Writes the current buffer to the connection + * + * @return \PHPWebSocket\AUpdate[] + */ + public function handleWrite() : \Generator { + throw new \LogicException('An accepting socket should never write!'); + } + + /** + * Attempts to read from our connection + * + * @return \PHPWebSocket\AUpdate[] + */ + public function handleRead() : \Generator { + yield from $this->_server->gotNewConnection(); + } + + /** + * Returns the related websocket server + * + * @return \PHPWebSocket\Server + */ + public function getServer() : \PHPWebSocket\Server { + return $this->_server; + } + + /** + * Returns the stream object for this connection + * + * @return resource + */ + public function getStream() { + return $this->_stream; + } + + /** + * Returns if our connection is open + * + * @return bool + */ + public function isOpen() : bool { + return is_resource($this->_stream); + } + + /** + * Closes the stream + * + * @param bool $cleanup If we should remove our unix socket if we used one + */ + public function close(bool $cleanup = TRUE) { + + if (is_resource($this->_stream)) { + fclose($this->_stream); + $this->_stream = NULL; + } + + $address = $this->_server->getAddress(); + $pos = strpos($address, '://'); + if ($pos !== FALSE) { + + $protocol = substr($address, 0, $pos); + switch ($protocol) { + case 'unix': + case 'udg': + + if (!$cleanup) { + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Not cleaning up'); + break; + } + + $path = substr($address, $pos + 3); + if (file_exists($path)) { + + if (!$cleanup) { + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Not cleaning up ' . $path); + } else { + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Unlinking: ' . $path); + unlink($path); + + } + + } + + break; + } + + } + + } + + public function __destruct() { + + if ($this->isOpen()) { + $this->close(); + } + + } + + public function __toString() { + return 'AWSConnection #' . (int) $this->getStream() . ' @ ' . $this->_server; + } +} diff --git a/Server/Connection.php.inc b/Server/Connection.php.inc index 2c6e9a6..ed77f09 100644 --- a/Server/Connection.php.inc +++ b/Server/Connection.php.inc @@ -1,392 +1,419 @@ -_openedTimestamp = microtime(TRUE); - $this->_server = $server; - $this->_remoteIP = parse_url($streamName, PHP_URL_HOST); - $this->_stream = $stream; - $this->_index = $index; - - if ($this->_server->usesSSL()) { - stream_socket_enable_crypto($this->_stream, TRUE, STREAM_CRYPTO_METHOD_TLS_SERVER); - } - - $this->_resourceIndex = (int) $this->_stream; - - 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); - - } - - /** - * Attempts to read from our connection - * @return \PHPWebSocket\AUpdate[] - */ - public function handleRead() : \Generator { - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, __METHOD__); - - $readRate = $this->getReadRate($this); - $newData = fread($this->getStream(), min($this->_currentFrameRemainingBytes ?? $readRate, $readRate)); - if ($newData === FALSE) { - yield new Update\Error(Update\Error::C_READ, $this); - return; - } - - $updates = []; - if (strlen($newData) === 0) { - - if ($this->_remoteSentDisconnect && $this->_weSentDisconnect) { - yield new Update\Read(Update\Read::C_SOCK_DISCONNECT, $this); - } else { - yield new Update\Error(Update\Error::C_READ_UNEXPECTED_DISCONNECT, $this); - } - - $this->close(); - - return; - - } else { - - $hasHandshake = $this->hasHandshake(); - if (!$hasHandshake) { - - $headersEnd = strpos($newData, "\r\n\r\n"); - if ($headersEnd === FALSE) { - - \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Handshake data hasn\'t finished yet, waiting..'); - - if ($this->_readBuffer === NULL) { - $this->_readBuffer = ''; - } - - $this->_readBuffer .= $newData; - - if (strlen($this->_readBuffer) > $this->getMaxHandshakeLength()) { - - yield new Update\Error(Update\Error::C_READ_HANDSHAKETOLARGE, $this); - $this->close(); - - } - - return; // Still waiting for headers - } - - if ($this->_readBuffer !== NULL) { - - $newData = $this->_readBuffer . $newData; - $this->_readBuffer = NULL; - - } - - $rawHandshake = substr($newData, 0, $headersEnd); - - if (strlen($newData) > strlen($rawHandshake)) { - $newData = substr($newData, $headersEnd + 4); - } - - $responseCode = 0; - if ($this->_doHandshake($rawHandshake, $responseCode)) { - yield new Update\Read(Update\Read::C_NEWCONNECTION, $this); - } else { - - $this->writeRaw($this->_server->getErrorPageForCode($responseCode), FALSE); - $this->setCloseAfterWrite(); - - yield new Update\Error(Update\Error::C_READ_HANDSHAKEFAILURE, $this); - - } - - $hasHandshake = $this->hasHandshake(); - - } - - if ($hasHandshake) { - yield from $this->_handlePacket($newData); - } - - } - - } - - /** - * Gets called just before stream_select gets called - * @return \PHPWebSocket\AUpdate[] - */ - public function beforeStreamSelect() : \Generator { - - if (!$this->isAccepted() && $this->hasHandshake() && $this->getOpenedTimestamp() + \PHPWebSocket\Server::ACCEPT_TIMEOUT < time()) { - - yield new Update\Error(Update\Error::C_ACCEPT_TIMEOUT_PASSED, $this); - $this->deny(408); - - } - - } - - /** - * Handles the handshake from the client and returns if the handshake was valid - * @param string $rawHandshake - * @return boolean - */ - protected function _doHandshake(string $rawHandshake, int &$responseCode) : bool { - - $headers = \PHPWebSocket::ParseHTTPHeaders($rawHandshake); - - $responseCode = 101; - if (!isset($headers['get'])) { - $responseCode = 405; - } else if (!isset($headers['host'])) { - $responseCode = 400; - } elseif (!isset($headers['upgrade']) || strtolower($headers['upgrade']) !== 'websocket') { - $responseCode = 400; - } elseif (!isset($headers['connection']) || strpos(strtolower($headers['connection']), 'upgrade') === FALSE) { - $responseCode = 400; - } elseif (!isset($headers['sec-websocket-key'])) { - $responseCode = 400; - } elseif (!isset($headers['sec-websocket-version']) || intval($headers['sec-websocket-version']) !== 13) { - $responseCode = 426; - } - - $this->_headers = $headers; - - if ($responseCode >= 300) { - return FALSE; - } - - $this->_hasHandshake = TRUE; - - $hash = sha1($headers['sec-websocket-key'] . \PHPWebSocket::WEBSOCKET_GUID); - $this->_rawToken = ''; - for ($i = 0; $i < 20; $i++) { - $this->_rawToken .= chr(hexdec(substr($hash, $i * 2, 2))); - } - - return TRUE; - } - - /** - * Accepts the connection - * @param string|NULL $protocol The accepted protocol - * @throws \Exception - */ - public function accept(string $protocol = NULL) { - - if ($this->isAccepted()) { - throw new \Exception('Connection has already been accepted!'); - } - - $misc = ''; - if ($protocol !== NULL) { - $misc .= 'Sec-WebSocket-Protocol ' . $protocol . "\r\n"; - } - - $this->writeRaw('HTTP/1.1 101 ' . \PHPWebSocket::GetStringForErrorCode(101) . "\r\nServer: " . $this->_server->getServerIdentifier() . "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " . base64_encode($this->_rawToken) . "\r\n" . $misc . "\r\n", FALSE); - - $this->_accepted = TRUE; - - } - - /** - * Denies the websocket connection, a HTTP error code has to be provided indicating what went wrong - * @param integer $errCode - */ - public function deny(int $errCode) { - - if ($this->isAccepted()) { - throw new \Exception('Connection has already been accepted!'); - } - - $this->writeRaw('HTTP/1.1 ' . $errCode . ' ' . \PHPWebSocket::GetStringForErrorCode($errCode) . "\r\nServer: PHPWebSocket/" . \PHPWebSocket::Version() . "\r\n\r\n", FALSE); - $this->setCloseAfterWrite(); - - } - - /** - * Returns if the websocket connection has been accepted - * @return boolean - */ - public function isAccepted() : bool { - return $this->_accepted; - } - - /** - * Returns the related websocket server - * @return \PHPWebSocket\Server - */ - public function getServer() : \PHPWebSocket\Server { - return $this->_server; - } - - /** - * Returns if we've received the handshake - * @return boolean - */ - public function hasHandshake() : bool { - 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 - * @return boolean - */ - protected function _shouldMask() : bool { - return FALSE; - } - - /** - * Returns the headers set during the http request - * @return array - */ - public function getHeaders() : array { - return $this->_headers; - } - - /** - * Returns the remote IP address of the client - * @return string|NULL - */ - public function getRemoteIP() { - return $this->_remoteIP; - } - - /** - * Returns the stream object for this connection - * @return resource - */ - public function getStream() { - return $this->_stream; - } - - /** - * Returns the index for this connection - * @return integer - */ - public function getIndex() : int { - return $this->_index; - } - - /** - * Returns if our connection is open - * @return boolean - */ - public function isOpen() : bool { - return is_resource($this->_stream); - } - - /** - * Simply closes the connection - */ - public function close() { - - if (is_resource($this->_stream)) { - fclose($this->_stream); - $this->_stream = NULL; - } - - $this->_server->removeConnection($this); - - } - - public function __toString() { - - $remoteIP = $this->getRemoteIP(); - - return 'WSConnection #' . $this->_resourceIndex . ($remoteIP ? ' => ' . $remoteIP : '') . ' @ ' . $this->_server; - } - -} +_openedTimestamp = microtime(TRUE); + $this->_server = $server; + $this->_remoteIP = parse_url($streamName, PHP_URL_HOST); + $this->_stream = $stream; + $this->_index = $index; + + if ($this->_server->usesSSL()) { + stream_socket_enable_crypto($this->_stream, TRUE, STREAM_CRYPTO_METHOD_TLS_SERVER); + } + + $this->_resourceIndex = (int) $this->_stream; + + 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); + + } + + /** + * Attempts to read from our connection + * + * @return \PHPWebSocket\AUpdate[] + */ + public function handleRead() : \Generator { + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, __METHOD__); + + $readRate = $this->getReadRate($this); + $newData = fread($this->getStream(), min($this->_currentFrameRemainingBytes ?? $readRate, $readRate)); + if ($newData === FALSE) { + yield new Update\Error(Update\Error::C_READ, $this); + + return; + } + + $updates = []; + if (strlen($newData) === 0) { + + if ($this->_remoteSentDisconnect && $this->_weSentDisconnect) { + yield new Update\Read(Update\Read::C_SOCK_DISCONNECT, $this); + } else { + yield new Update\Error(Update\Error::C_READ_UNEXPECTED_DISCONNECT, $this); + } + + $this->close(); + + return; + + } else { + + $hasHandshake = $this->hasHandshake(); + if (!$hasHandshake) { + + $headersEnd = strpos($newData, "\r\n\r\n"); + if ($headersEnd === FALSE) { + + \PHPWebSocket::Log(\PHPWebSocket::LOGLEVEL_DEBUG, 'Handshake data hasn\'t finished yet, waiting..'); + + if ($this->_readBuffer === NULL) { + $this->_readBuffer = ''; + } + + $this->_readBuffer .= $newData; + + if (strlen($this->_readBuffer) > $this->getMaxHandshakeLength()) { + + yield new Update\Error(Update\Error::C_READ_HANDSHAKETOLARGE, $this); + $this->close(); + + } + + return; // Still waiting for headers + } + + if ($this->_readBuffer !== NULL) { + + $newData = $this->_readBuffer . $newData; + $this->_readBuffer = NULL; + + } + + $rawHandshake = substr($newData, 0, $headersEnd); + + if (strlen($newData) > strlen($rawHandshake)) { + $newData = substr($newData, $headersEnd + 4); + } + + $responseCode = 0; + if ($this->_doHandshake($rawHandshake, $responseCode)) { + yield new Update\Read(Update\Read::C_NEWCONNECTION, $this); + } else { + + $this->writeRaw($this->_server->getErrorPageForCode($responseCode), FALSE); + $this->setCloseAfterWrite(); + + yield new Update\Error(Update\Error::C_READ_HANDSHAKEFAILURE, $this); + + } + + $hasHandshake = $this->hasHandshake(); + + } + + if ($hasHandshake) { + yield from $this->_handlePacket($newData); + } + + } + + } + + /** + * Gets called just before stream_select gets called + * + * @return \PHPWebSocket\AUpdate[] + */ + public function beforeStreamSelect() : \Generator { + + if (!$this->isAccepted() && $this->hasHandshake() && $this->getOpenedTimestamp() + \PHPWebSocket\Server::ACCEPT_TIMEOUT < time()) { + + yield new Update\Error(Update\Error::C_ACCEPT_TIMEOUT_PASSED, $this); + $this->deny(408); + + } + + } + + /** + * Handles the handshake from the client and returns if the handshake was valid + * + * @param string $rawHandshake + * + * @return bool + */ + protected function _doHandshake(string $rawHandshake, int &$responseCode) : bool { + + $headers = \PHPWebSocket::ParseHTTPHeaders($rawHandshake); + + $responseCode = 101; + if (!isset($headers['get'])) { + $responseCode = 405; + } elseif (!isset($headers['host'])) { + $responseCode = 400; + } elseif (!isset($headers['upgrade']) || strtolower($headers['upgrade']) !== 'websocket') { + $responseCode = 400; + } elseif (!isset($headers['connection']) || strpos(strtolower($headers['connection']), 'upgrade') === FALSE) { + $responseCode = 400; + } elseif (!isset($headers['sec-websocket-key'])) { + $responseCode = 400; + } elseif (!isset($headers['sec-websocket-version']) || intval($headers['sec-websocket-version']) !== 13) { + $responseCode = 426; + } + + $this->_headers = $headers; + + if ($responseCode >= 300) { + return FALSE; + } + + $this->_hasHandshake = TRUE; + + $hash = sha1($headers['sec-websocket-key'] . \PHPWebSocket::WEBSOCKET_GUID); + $this->_rawToken = ''; + for ($i = 0; $i < 20; $i++) { + $this->_rawToken .= chr(hexdec(substr($hash, $i * 2, 2))); + } + + return TRUE; + } + + /** + * Accepts the connection + * + * @param string|null $protocol The accepted protocol + * + * @throws \Exception + */ + public function accept(string $protocol = NULL) { + + if ($this->isAccepted()) { + throw new \Exception('Connection has already been accepted!'); + } + + $misc = ''; + if ($protocol !== NULL) { + $misc .= 'Sec-WebSocket-Protocol ' . $protocol . "\r\n"; + } + + $this->writeRaw('HTTP/1.1 101 ' . \PHPWebSocket::GetStringForErrorCode(101) . "\r\nServer: " . $this->_server->getServerIdentifier() . "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " . base64_encode($this->_rawToken) . "\r\n" . $misc . "\r\n", FALSE); + + $this->_accepted = TRUE; + + } + + /** + * Denies the websocket connection, a HTTP error code has to be provided indicating what went wrong + * + * @param int $errCode + */ + public function deny(int $errCode) { + + if ($this->isAccepted()) { + throw new \Exception('Connection has already been accepted!'); + } + + $this->writeRaw('HTTP/1.1 ' . $errCode . ' ' . \PHPWebSocket::GetStringForErrorCode($errCode) . "\r\nServer: PHPWebSocket/" . \PHPWebSocket::Version() . "\r\n\r\n", FALSE); + $this->setCloseAfterWrite(); + + } + + /** + * Returns if the websocket connection has been accepted + * + * @return bool + */ + public function isAccepted() : bool { + return $this->_accepted; + } + + /** + * Returns the related websocket server + * + * @return \PHPWebSocket\Server + */ + public function getServer() : \PHPWebSocket\Server { + return $this->_server; + } + + /** + * Returns if we've received the handshake + * + * @return bool + */ + public function hasHandshake() : bool { + 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 + * + * @return bool + */ + protected function _shouldMask() : bool { + return FALSE; + } + + /** + * Returns the headers set during the http request + * + * @return array + */ + public function getHeaders() : array { + return $this->_headers; + } + + /** + * Returns the remote IP address of the client + * + * @return string|null + */ + public function getRemoteIP() { + return $this->_remoteIP; + } + + /** + * Returns the stream object for this connection + * + * @return resource + */ + public function getStream() { + return $this->_stream; + } + + /** + * Returns the index for this connection + * + * @return int + */ + public function getIndex() : int { + return $this->_index; + } + + /** + * Returns if our connection is open + * + * @return bool + */ + public function isOpen() : bool { + return is_resource($this->_stream); + } + + /** + * Simply closes the connection + */ + public function close() { + + if (is_resource($this->_stream)) { + fclose($this->_stream); + $this->_stream = NULL; + } + + $this->_server->removeConnection($this); + + } + + public function __toString() { + + $remoteIP = $this->getRemoteIP(); + + return 'WSConnection #' . $this->_resourceIndex . ($remoteIP ? ' => ' . $remoteIP : '') . ' @ ' . $this->_server; + } +} diff --git a/TStreamContainerDefaults.php.inc b/TStreamContainerDefaults.php.inc index 0a7e507..eac828a 100644 --- a/TStreamContainerDefaults.php.inc +++ b/TStreamContainerDefaults.php.inc @@ -1,4 +1,5 @@ connect($address, '/getCaseCount')) { + echo 'Unable to connect to server: ' . $client->getLastError() . PHP_EOL; + exit(1); +} + +$caseCount = NULL; + +while ($client->isOpen()) { + foreach ($client->update() as $key => $value) { + if ($value instanceof Read && $value->getCode() === Read::C_READ) { + $caseCount = (int) $value->getMessage(); + } + } +} + +echo 'Will run ' . $caseCount . ' test cases' . PHP_EOL; + +for ($i = 0; $i < $caseCount; $i++) { + + $client = new \PHPWebSocket\Client(); + $client->connect($address, '/runCase?case=' . ($i + 1) . '&agent=' . $client->getUserAgent()); + + while ($client->isOpen()) { + + $updates = $client->update(); + foreach ($updates as $update) { + + 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; + +$client = new \PHPWebSocket\Client(); +$client->connect($address, '/updateReports?agent=' . $client->getUserAgent()); + +while ($client->isOpen()) { + foreach ($client->update() as $key => $value) { + + } +} + +echo 'Reports finished, getting results..' . PHP_EOL; + +$outputFile = '/tmp/reports/clients/index.json'; +if (!file_exists($outputFile)) { + echo 'File "' . $outputFile . '" doesn\'t exist!'; + exit(1); +} + +$hasFailures = FALSE; +$testCases = json_decode(file_get_contents($outputFile), TRUE)[$client->getUserAgent()]; +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; + } + +} + +echo 'Exiting' . PHP_EOL; + +exit((int) $hasFailures); exit(); diff --git a/Tests/server.php b/Tests/server.php index 476b497..c0f69f4 100644 --- a/Tests/server.php +++ b/Tests/server.php @@ -2,6 +2,84 @@ require_once(__DIR__ . '/../PHPWebSocket.php.inc'); -// Todo: server test using autobahn +use \PHPWebSocket\Update\Read; -exit(); +echo 'Starting test' . PHP_EOL . PHP_EOL; + +$websocket = new \PHPWebSocket\Server('tcp://0.0.0.0:9001'); + +$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) { + + $updates = $websocket->update(0.1); + foreach ($updates as $update) { + + if ($update instanceof Read) { + + $sourceObj = $update->getSourceObject(); + $opcode = $update->getCode(); + switch ($opcode) { + case Read::C_NEWCONNECTION: + $sourceObj->accept(); + break; + case Read::C_READ: + + $opcode = $update->getOpcode(); + switch ($opcode) { + case \PHPWebSocket::OPCODE_CONTINUE: + case \PHPWebSocket::OPCODE_FRAME_TEXT: + case \PHPWebSocket::OPCODE_FRAME_BINARY: + + $msg = $update->getMessage(); + if ($msg !== NULL && !$sourceObj->isDisconnecting()) { + $sourceObj->write($msg, $opcode); + } + + break; + } + + break; + } + + } + + } + +} + +echo 'Test ended, closing websocket' . PHP_EOL; + +$websocket->close(); + +echo 'Getting results..' . PHP_EOL; + +$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()]; +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; + } + +} + +echo 'Exiting' . PHP_EOL; + +exit((int) $hasFailures); diff --git a/Update/Error.php.inc b/Update/Error.php.inc index 01534ac..98a0de0 100644 --- a/Update/Error.php.inc +++ b/Update/Error.php.inc @@ -1,4 +1,5 @@ getCode(); + return 'Error) ' . self::StringForErrorCode($code) . ' (C: ' . $code . ')'; } - } diff --git a/Update/Read.php.inc b/Update/Read.php.inc index d228c51..c172899 100644 --- a/Update/Read.php.inc +++ b/Update/Read.php.inc @@ -1,4 +1,5 @@ _message; @@ -106,7 +112,8 @@ class Read extends \PHPWebSocket\AUpdate { /** * Returns the opcode for this message - * @return integer|NULL + * + * @return int|null */ public function getOpcode() { return $this->_opcode; @@ -114,7 +121,7 @@ class Read extends \PHPWebSocket\AUpdate { public function __toString() { $code = $this->getCode(); + return 'Read) ' . self::StringForErrorCode($code) . ' (C: ' . $code . ')'; } - } diff --git a/VERSION b/VERSION index 6085e94..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.1 +1.3.0 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d934bd9 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "WarriorXK/PHPWebSockets", + "type": "library", + "description": "A websocket library for PHP 7.0+ with support for IPC using socket pairs", + "keywords": ["websocket", "php", "phpwebsockets", "ipc", "socket", "client", "server"], + "homepage": "https://github.com/WarriorXK/PHPWebSockets", + "license": "MIT", + "authors": [ + { + "name": "Kevin Meijer", + "email": "admin@kevinmeijer.nl", + "homepage": "https://kevinmeijer.nl", + "role": "Developer" + } + ], + "require": { + "php": ">=7.0.0", + "ext-sockets": "*" + }, + "autoload": { + "files": ["PHPWebSocket.php.inc"] + } +}