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"]
+ }
+}