diff --git a/.codeclimate.yml b/.codeclimate.yml
index 7e59430..664ee89 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -20,6 +20,10 @@ engines:
enabled: false
CleanCode/StaticAccess:
enabled: false
+ CyclomaticComplexity:
+ enabled: false
+ Design/NpathComplexity:
+ enabled: false
ratings:
paths:
- "**.php.inc"
diff --git a/.editorconfig b/.editorconfig
index df88899..a391069 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,4 +10,6 @@ insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
+charset = utf-8
+end_of_line = lf
trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
index 9f8d40d..a117c44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,66 @@ composer.lock
vendor/*
.DS_Store
._*
+
+# Created by https://www.gitignore.io/api/phpstorm
+
+### PhpStorm ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+### PhpStorm Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+# End of https://www.gitignore.io/api/phpstorm
diff --git a/.idea/PHPWebSockets.iml b/.idea/PHPWebSockets.iml
new file mode 100644
index 0000000..1b45fb7
--- /dev/null
+++ b/.idea/PHPWebSockets.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..99c6453
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
new file mode 100644
index 0000000..4f0611e
--- /dev/null
+++ b/.idea/php.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.styleci.yml b/.styleci.yml
index 8caf0fd..c94349b 100644
--- a/.styleci.yml
+++ b/.styleci.yml
@@ -16,6 +16,7 @@ disabled:
- no_blank_lines_after_class_opening
- single_class_element_per_statement
- simplified_null_return
+ - alpha_ordered_imports
finder:
name:
diff --git a/.travis.yml b/.travis.yml
index 6c904c0..4f29dad 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,8 +6,10 @@ php:
- '7.0'
- '7.1'
env:
- - TESTTYPE=server
- - TESTTYPE=client
+ - TESTTYPE=server BUFFERTPE=memory
+ - TESTTYPE=server BUFFERTPE=tmpfile
+ - TESTTYPE=client BUFFERTPE=memory
+ - TESTTYPE=client BUFFERTPE=tmpfile
before_script:
- sudo apt-get update
- sudo apt-get install python-pip
@@ -15,10 +17,13 @@ before_script:
- sudo -H pip install autobahntestsuite
- phpenv config-rm xdebug.ini
script:
- - php Tests/runner.php $TESTTYPE
+ - php Tests/runner.php $TESTTYPE $BUFFERTPE
notifications:
email:
recipients:
- kevkevm@gmail.com
on_success: change
on_failure: always
+# addons:
+# artifacts:
+# paths: /tmp/reports/
diff --git a/AConnection.php.inc b/AConnection.php.inc
index cd3b7a9..5dd03c6 100644
--- a/AConnection.php.inc
+++ b/AConnection.php.inc
@@ -32,7 +32,7 @@ namespace PHPWebSocket;
abstract class AConnection implements IStreamContainer {
- use \PHPWebSocket\TStreamContainerDefaults;
+ use TStreamContainerDefaults;
/**
* The amount of bytes to read to complete our current frame
@@ -42,11 +42,11 @@ abstract class AConnection implements IStreamContainer {
protected $_currentFrameRemainingBytes = NULL;
/**
- * The opcode of the current partial message
+ * The callable that gets called after the first message part has been read to get the optional stream to write to
*
- * @var int|null
+ * @var callable|null
*/
- protected $_partialMessageOpcode = NULL;
+ protected $_newMessageStreamCallback = NULL;
/**
* If we've initiated the disconnect
@@ -55,6 +55,13 @@ abstract class AConnection implements IStreamContainer {
*/
protected $_weInitiateDisconnect = FALSE;
+ /**
+ * The opcode of the current partial message
+ *
+ * @var int|null
+ */
+ protected $_partialMessageOpcode = NULL;
+
/**
* If we've received the disconnect message from the remote
*
@@ -62,6 +69,13 @@ abstract class AConnection implements IStreamContainer {
*/
protected $_remoteSentDisconnect = FALSE;
+ /**
+ * The stream to write to instead of using _partialMessage
+ *
+ * @var resource|null
+ */
+ protected $_partialMessageStream = NULL;
+
/**
* The priority frames ready to be send (Takes priority over the normal frames buffer)
*
@@ -69,6 +83,13 @@ abstract class AConnection implements IStreamContainer {
*/
protected $_priorityFramesBuffer = [];
+ /**
+ * The current state of the UTF8 validation
+ *
+ * @var int
+ */
+ protected $_utfValidationState = \PHPWebSocket::UTF8_ACCEPT;
+
/**
* The maximum size the handshake can become
*
@@ -90,6 +111,13 @@ abstract class AConnection implements IStreamContainer {
*/
protected $_closeAfterWrite = FALSE;
+ /**
+ * The timestamp since when this connection has been opened
+ *
+ * @var int
+ */
+ protected $_openedTimestamp = NULL;
+
/**
* This array contains as key the index of the RSV bit and as value if it is allowed
*
@@ -173,6 +201,21 @@ abstract class AConnection implements IStreamContainer {
$this->_closeAfterWrite = TRUE;
}
+ /**
+ * Should be called after the path and stream has been set to initialize
+ */
+ protected function _afterOpen() {
+
+ $this->_openedTimestamp = microtime(TRUE);
+ $stream = $this->getStream();
+
+ stream_set_timeout($stream, 15);
+ stream_set_blocking($stream, FALSE);
+ stream_set_read_buffer($stream, 0);
+ stream_set_write_buffer($stream, 0);
+
+ }
+
/**
* In here we attempt to find frames and unmask them, returns finished messages if available
*
@@ -180,7 +223,7 @@ abstract class AConnection implements IStreamContainer {
*
* @throws \UnexpectedValueException
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
protected function _handlePacket(string $newData) : \Generator {
@@ -208,7 +251,7 @@ abstract class AConnection implements IStreamContainer {
if (!$this->_checkRSVBits($headers)) {
- $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR, 'Unexpected RSV bit set');
+ $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR, 'Unexpected RSV bit set');
$this->setCloseAfterWrite();
yield new Update\Error(Update\Error::C_READ_RSVBIT_SET, $this);
@@ -216,7 +259,7 @@ abstract class AConnection implements IStreamContainer {
return;
}
- $frameSize = $headers[Framer::IND_LENGTH] + $headers[Framer::IND_PAYLOADOFFSET];
+ $frameSize = $headers[Framer::IND_LENGTH] + $headers[Framer::IND_PAYLOAD_OFFSET];
if ($numBytes < $frameSize) {
$this->_currentFrameRemainingBytes = $frameSize - $numBytes;
\PHPWebSocket::Log(LOG_DEBUG, 'Setting next read size to ' . $this->_currentFrameRemainingBytes);
@@ -233,7 +276,7 @@ abstract class AConnection implements IStreamContainer {
break; // Frame isn't ready yet
} elseif ($framePayload === FALSE) {
- $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR);
+ $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR);
$this->setCloseAfterWrite();
yield new Update\Error(Update\Error::C_READ_PROTOCOL_ERROR, $this);
@@ -245,9 +288,9 @@ abstract class AConnection implements IStreamContainer {
switch ($opcode) {
case \PHPWebSocket::OPCODE_CONTINUE:
- if ($this->_partialMessage === NULL) {
+ if ($this->_partialMessage === NULL && $this->_partialMessageStream === NULL) {
- $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR, 'Got OPCODE_CONTINUE but no frame that could be continued');
+ $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR, 'Got OPCODE_CONTINUE but no frame that could be continued');
$this->setCloseAfterWrite();
yield new Update\Error(Update\Error::C_READ_PROTOCOL_ERROR, $this);
@@ -259,29 +302,25 @@ abstract class AConnection implements IStreamContainer {
case \PHPWebSocket::OPCODE_FRAME_TEXT:
case \PHPWebSocket::OPCODE_FRAME_BINARY:
- if ($opcode === \PHPWebSocket::OPCODE_CONTINUE) {
- $this->_partialMessage .= $framePayload;
- } elseif ($this->_partialMessage !== NULL) {
+ if (($this->_partialMessageOpcode ?: $opcode) === \PHPWebSocket::OPCODE_FRAME_TEXT) {
- $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOLERROR, 'Got new frame without completing the previous');
- $this->setCloseAfterWrite();
-
- yield new Update\Error(Update\Error::C_READ_INVALID_PAYLOAD, $this);
+ if (!\PHPWebSocket::ValidateUTF8($framePayload, $this->_utfValidationState) || ($headers[Framer::IND_FIN] && $this->_utfValidationState !== \PHPWebSocket::UTF8_ACCEPT)) {
- return;
+ $this->sendDisconnect(\PHPWebSocket::CLOSECODE_INVALID_PAYLOAD, 'Could not decode text frame as UTF-8');
+ $this->setCloseAfterWrite();
- } else {
+ yield new Update\Error(Update\Error::C_READ_INVALID_PAYLOAD, $this);
- $this->_partialMessageOpcode = $opcode;
- $this->_partialMessage = $framePayload;
+ return;
+ }
}
- if ($headers[Framer::IND_FIN]) {
+ if ($opcode !== \PHPWebSocket::OPCODE_CONTINUE) {
- if ($this->_partialMessageOpcode === \PHPWebSocket::OPCODE_FRAME_TEXT && !preg_match('//u', $this->_partialMessage)) {
+ if ($this->_partialMessage !== NULL || $this->_partialMessageStream !== NULL) {
- $this->sendDisconnect(\PHPWebSocket::CLOSECODE_INVALID_PAYLOAD, 'Could not decode text frame as UTF-8');
+ $this->sendDisconnect(\PHPWebSocket::CLOSECODE_PROTOCOL_ERROR, 'Got new frame without completing the previous');
$this->setCloseAfterWrite();
yield new Update\Error(Update\Error::C_READ_INVALID_PAYLOAD, $this);
@@ -289,11 +328,64 @@ abstract class AConnection implements IStreamContainer {
return;
}
- yield new Update\Read(Update\Read::C_READ, $this, $this->_partialMessageOpcode, $this->_partialMessage);
+ $newMessageStream = $this->_getStreamForNewMessage($headers);
+ if ($newMessageStream === FALSE) {
+
+ $this->sendDisconnect(\PHPWebSocket::CLOSECODE_UNSUPPORTED_PAYLOAD);
+ $this->setCloseAfterWrite();
+
+ return;
+ }
+
+ $this->_partialMessageOpcode = $opcode;
+
+ if (is_resource($newMessageStream)) {
+
+ $this->_partialMessageStream = $newMessageStream;
+ $this->_partialMessage = NULL;
+
+ } else {
+
+ $this->_partialMessageStream = NULL;
+ $this->_partialMessage = '';
+
+ }
+
+ }
+
+ if ($this->_partialMessageStream) {
+
+ $payloadLength = strlen($framePayload);
+ $writtenBytes = 0;
+ $res = NULL;
+
+ do {
+
+ $res = @fwrite($this->_partialMessageStream, $framePayload);
+ if ($res !== FALSE) {
+ $writtenBytes += $res;
+ }
+
+ } while ($res !== FALSE && $writtenBytes < $payloadLength);
+
+ if ($res === FALSE) {
+ yield new Update\Error(Update\Error::C_READ_INVALID_TARGET_STREAM, $this);
+ }
+
+ } else {
+ $this->_partialMessage .= $framePayload;
+ }
+
+ if ($headers[Framer::IND_FIN]) {
+
+ yield new Update\Read(Update\Read::C_READ, $this, $this->_partialMessageOpcode, $this->_partialMessage, $this->_partialMessageStream);
$this->_partialMessageOpcode = NULL;
+ $this->_partialMessageStream = NULL;
$this->_partialMessage = NULL;
+ $this->_utfValidationState = \PHPWebSocket::UTF8_ACCEPT;
+
}
break;
@@ -310,12 +402,12 @@ abstract class AConnection implements IStreamContainer {
if (!\PHPWebSocket::IsValidCloseCode($code)) {
$disconnectMessage = 'Invalid close code provided: ' . $code;
- $code = \PHPWebSocket::CLOSECODE_PROTOCOLERROR;
+ $code = \PHPWebSocket::CLOSECODE_PROTOCOL_ERROR;
} elseif (!preg_match('//u', substr($framePayload, 2))) {
$disconnectMessage = 'Received Non-UTF8 close frame payload';
- $code = \PHPWebSocket::CLOSECODE_PROTOCOLERROR;
+ $code = \PHPWebSocket::CLOSECODE_PROTOCOL_ERROR;
} else {
$disconnectMessage = substr($framePayload, 2);
@@ -383,7 +475,7 @@ abstract class AConnection implements IStreamContainer {
/**
* Writes the current buffer to the connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleWrite() : \Generator {
@@ -409,7 +501,7 @@ abstract class AConnection implements IStreamContainer {
\PHPWebSocket::Log(LOG_DEBUG, ' Attempting to write ' . $bytesToWrite . ' bytes');
- $bytesWritten = fwrite($this->_stream, $this->_writeBuffer, min($this->getWriteRate($this), $bytesToWrite));
+ $bytesWritten = @fwrite($this->getStream(), $this->_writeBuffer, min($this->getWriteRate(), $bytesToWrite));
if ($bytesWritten === FALSE) {
\PHPWebSocket::Log(LOG_DEBUG, ' fwrite failed');
yield new Update\Error(Update\Error::C_WRITE, $this);
@@ -425,7 +517,7 @@ abstract class AConnection implements IStreamContainer {
if ($this->_closeAfterWrite && $this->isWriteBufferEmpty()) {
\PHPWebSocket::Log(LOG_DEBUG, ' Close after write');
- $this->close($this);
+ $this->close();
}
}
@@ -459,6 +551,28 @@ abstract class AConnection implements IStreamContainer {
}
+ /**
+ * Returns the stream to write to for this specific message or NULL to use a buffer
+ *
+ * @param array $headers
+ *
+ * @return resource|null|bool
+ */
+ protected function _getStreamForNewMessage(array $headers) {
+
+ $retValue = TRUE;
+ if ($this->_newMessageStreamCallback) {
+
+ $retValue = call_user_func($this->_newMessageStreamCallback, $headers);
+ if (!is_bool($retValue) && !is_resource($retValue)) {
+ throw new \UnexpectedValueException('Got an invalid return value, expected boolean or resource, got ' . gettype($retValue));
+ }
+
+ }
+
+ return $retValue;
+ }
+
/**
* Writes a raw string to the buffer, if priority is set to TRUE it will be send before normal priority messages
*
@@ -529,6 +643,17 @@ abstract class AConnection implements IStreamContainer {
return TRUE;
}
+ /**
+ * Sets the callable that gets called after the first message part has been read to get the optional stream to write to
+ * If no stream but FALSE is returned in this callback the connection will be closed with code CLOSECODE_UNSUPPORTED_PAYLOAD
+ * If TRUE is returned from the callback a memory buffer will be used instead
+ *
+ * @param callable|null $callable
+ */
+ public function setNewMessageStreamCallback(callable $callable = NULL) {
+ $this->_newMessageStreamCallback = $callable;
+ }
+
/**
* Sets if the provided reserved bit is allowed to be set
*
@@ -561,6 +686,15 @@ abstract class AConnection implements IStreamContainer {
return $this->_allowedRSVBits[$bit];
}
+ /**
+ * Returns the timestamp at which the connection was opened
+ *
+ * @return float|null
+ */
+ public function getOpenedTimestamp() {
+ return $this->_openedTimestamp;
+ }
+
/**
* Sets the maximum amount of bytes to write per cycle
*
diff --git a/Client.php.inc b/Client.php.inc
index b1cf98d..151c298 100644
--- a/Client.php.inc
+++ b/Client.php.inc
@@ -30,7 +30,7 @@ declare(strict_types = 1);
namespace PHPWebSocket;
-class Client extends \PHPWebSocket\AConnection {
+class Client extends AConnection {
/**
* The last error code received from the stream
@@ -60,6 +60,11 @@ class Client extends \PHPWebSocket\AConnection {
*/
protected $_resourceIndex = NULL;
+ /**
+ * @var string|null
+ */
+ protected $_userAgent = NULL;
+
/**
* The headers send back from the server when the handshake was accepted
*
@@ -74,6 +79,20 @@ class Client extends \PHPWebSocket\AConnection {
*/
protected $_stream = NULL;
+ /**
+ * The remote host we are connecting to
+ *
+ * @var string|null
+ */
+ protected $_host = NULL;
+
+ /**
+ * The path used in the HTTP request
+ *
+ * @var string
+ */
+ protected $_path = '/';
+
/**
* Connects to the provided resource
*
@@ -88,7 +107,7 @@ class Client extends \PHPWebSocket\AConnection {
public function connectToResource($resource, string $path = '/') {
if (!is_resource($resource)) {
- throw \InvalidArgumentException('Argument is not a resource!');
+ throw new \InvalidArgumentException('Argument is not a resource!');
}
if ($this->isOpen()) {
@@ -96,8 +115,10 @@ class Client extends \PHPWebSocket\AConnection {
}
$this->_stream = $resource;
+ $this->_host = (string) $resource;
+ $this->_path = $path;
- $this->_afterOpen((string) $resource, $path);
+ $this->_afterOpen();
return TRUE;
}
@@ -123,33 +144,30 @@ class Client extends \PHPWebSocket\AConnection {
return FALSE;
}
- $this->_afterOpen($address, $path);
+ $this->_host = $address;
+ $this->_path = $path;
+
+ $this->_afterOpen();
return TRUE;
}
/**
- * Sets the stream settings and appends the HTTP headers to the buffer
- *
- * @param string $address
- * @param string $path
+ * Should be called after the path and stream has been set to initialize
*/
- protected function _afterOpen(string $address, string $path) {
+ protected function _afterOpen() {
- $this->_resourceIndex = (int) $this->getStream();
+ parent::_afterOpen();
- stream_set_timeout($this->_stream, 15);
- stream_set_blocking($this->_stream, FALSE);
- stream_set_read_buffer($this->_stream, 0);
- stream_set_write_buffer($this->_stream, 0);
+ $this->_resourceIndex = (int) $this->getStream();
$headerParts = [
- 'GET ' . $path . ' HTTP/1.1',
- 'Host: ' . $address,
+ 'GET ' . $this->_path . ' HTTP/1.1',
+ 'Host: ' . $this->_host,
'User-Agent: ' . $this->getUserAgent(),
'Upgrade: websocket',
'Connection: Upgrade',
- 'Sec-WebSocket-Key: ' . base64_encode(\PHPWebSocket::RandomString()),
+ 'Sec-WebSocket-Key: ' . base64_encode(\PHPWebSocket::RandomString(16)),
'Sec-WebSocket-Version: 13',
];
@@ -191,7 +209,7 @@ class Client extends \PHPWebSocket\AConnection {
*
* @throws \Exception
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function update(float $timeout = NULL) : \Generator {
yield from \PHPWebSocket::MultiUpdate([$this], $timeout);
@@ -200,13 +218,13 @@ class Client extends \PHPWebSocket\AConnection {
/**
* Attempts to read from our connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleRead() : \Generator {
\PHPWebSocket::Log(LOG_DEBUG, __METHOD__);
- $readRate = $this->getReadRate($this);
+ $readRate = $this->getReadRate();
$newData = fread($this->getStream(), min($this->_currentFrameRemainingBytes ?? $readRate, $readRate));
if ($newData === FALSE) {
yield new Update\Error(Update\Error::C_READ, $this);
@@ -214,7 +232,6 @@ class Client extends \PHPWebSocket\AConnection {
return;
}
- $updates = [];
if (strlen($newData) === 0) {
if ($this->_remoteSentDisconnect && $this->_weSentDisconnect) {
@@ -344,6 +361,15 @@ class Client extends \PHPWebSocket\AConnection {
return is_resource($this->_stream);
}
+ /**
+ * Returns the path send in the HTTP request
+ *
+ * @return string
+ */
+ public function getPath() : string {
+ return $this->_path;
+ }
+
/**
* Simply closes the connection
*/
diff --git a/Framer.php.inc b/Framer.php.inc
index 6230344..c4de560 100644
--- a/Framer.php.inc
+++ b/Framer.php.inc
@@ -48,8 +48,8 @@ final class Framer {
IND_OPCODE = 'opcode',
IND_MASK = 'mask',
IND_LENGTH = 'length',
- IND_MASKINGKEY = 'masking-key',
- IND_PAYLOADOFFSET = 'payload-offset';
+ IND_MASKING_KEY = 'masking-key',
+ IND_PAYLOAD_OFFSET = 'payload-offset';
/**
* Extracts the headers out of the provided frame, returns NULL if the provided frame has invalid headers
@@ -76,8 +76,8 @@ final class Framer {
self::IND_OPCODE => ($byte1 & self::BYTE1_OPCODE),
self::IND_MASK => (bool) ($byte2 & self::BYTE2_MASKED),
self::IND_LENGTH => ($byte2 & self::BYTE2_LENGTH),
- self::IND_MASKINGKEY => NULL,
- self::IND_PAYLOADOFFSET => 2,
+ self::IND_MASKING_KEY => NULL,
+ self::IND_PAYLOAD_OFFSET => 2,
];
if ($headers[self::IND_LENGTH] === 126) { // 16 bits
@@ -87,10 +87,10 @@ final class Framer {
}
$headers[self::IND_LENGTH] = unpack('n', substr($frame, 2, 2))[1];
- $headers[self::IND_PAYLOADOFFSET] += 2;
+ $headers[self::IND_PAYLOAD_OFFSET] += 2;
if ($headers[self::IND_MASK]) {
- $headers[self::IND_MASKINGKEY] = substr($frame, 4, 4);
+ $headers[self::IND_MASKING_KEY] = substr($frame, 4, 4);
}
} elseif ($headers[self::IND_LENGTH] === 127) { // 64 bits
@@ -102,10 +102,10 @@ final class Framer {
$headers[self::IND_LENGTH] = unpack('J', substr($frame, 2, 8))[1];
if ($headers[self::IND_MASK]) {
- $headers[self::IND_MASKINGKEY] = substr($frame, 10, 4);
+ $headers[self::IND_MASKING_KEY] = substr($frame, 10, 4);
}
- $headers[self::IND_PAYLOADOFFSET] += 8;
+ $headers[self::IND_PAYLOAD_OFFSET] += 8;
} elseif ($headers[self::IND_MASK]) { // 7 bits
@@ -113,12 +113,12 @@ final class Framer {
return NULL;
}
- $headers[self::IND_MASKINGKEY] = substr($frame, 2, 4);
+ $headers[self::IND_MASKING_KEY] = substr($frame, 2, 4);
}
if ($headers[self::IND_MASK]) {
- $headers[self::IND_PAYLOADOFFSET] += 4;
+ $headers[self::IND_PAYLOAD_OFFSET] += 4;
}
return $headers;
@@ -140,7 +140,7 @@ final class Framer {
}
$frameLength = strlen($frame);
- if ($frameLength < $headers[self::IND_PAYLOADOFFSET]) { // Frame headers incomplete
+ if ($frameLength < $headers[self::IND_PAYLOAD_OFFSET]) { // Frame headers incomplete
return NULL;
}
@@ -159,13 +159,13 @@ final class Framer {
case \PHPWebSocket::OPCODE_FRAME_TEXT:
case \PHPWebSocket::OPCODE_FRAME_BINARY:
- if ($frameLength < $headers[self::IND_PAYLOADOFFSET] + $headers[self::IND_LENGTH]) {
+ if ($frameLength < $headers[self::IND_PAYLOAD_OFFSET] + $headers[self::IND_LENGTH]) {
return NULL;
}
- $payload = substr($frame, $headers[self::IND_PAYLOADOFFSET], $headers[self::IND_LENGTH]);
+ $payload = substr($frame, $headers[self::IND_PAYLOAD_OFFSET], $headers[self::IND_LENGTH]);
if ($headers[self::IND_MASK]) {
- $payload = self::ApplyMask($payload, $headers[self::IND_MASKINGKEY]);
+ $payload = self::ApplyMask($payload, $headers[self::IND_MASKING_KEY]);
}
return $payload;
@@ -193,7 +193,7 @@ final class Framer {
*
* @return string
*/
- public static function Frame(string $data, bool $mask, int $opcode = \PHPWebSocket::OPCODE_FRAME_TEXT, bool $isFinal = TRUE, bool $rsv1 = FALSE, bool $rsv2 = FALSE, bool $rsv3 = FALSE) {
+ public static function Frame(string $data, bool $mask, int $opcode = \PHPWebSocket::OPCODE_FRAME_TEXT, bool $isFinal = TRUE, bool $rsv1 = FALSE, bool $rsv2 = FALSE, bool $rsv3 = FALSE) : string {
if ($opcode < 0 || $opcode > 15) {
throw new \RangeException('Invalid opcode, opcode should range between 0 and 15');
@@ -259,6 +259,6 @@ final class Framer {
* @return string
*/
public static function ApplyMask(string $payload, string $maskingKey) : string {
- return str_pad('', strlen($payload), $maskingKey) ^ $payload;
+ return (string) (str_pad('', strlen($payload), $maskingKey) ^ $payload);
}
}
diff --git a/IStreamContainer.php.inc b/IStreamContainer.php.inc
index cb2073b..4ad4aa9 100644
--- a/IStreamContainer.php.inc
+++ b/IStreamContainer.php.inc
@@ -34,7 +34,7 @@ interface IStreamContainer {
/**
* Gets called just before stream_select gets called
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function beforeStreamSelect() : \Generator;
@@ -48,21 +48,21 @@ interface IStreamContainer {
/**
* Handles exceptional data reads
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleExceptional() : \Generator;
/**
* Writes the current buffer to the connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleWrite() : \Generator;
/**
* Attempts to read from our connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleRead() : \Generator;
diff --git a/PHPWebSocket.php.inc b/PHPWebSocket.php.inc
index d532d39..60c8ae3 100644
--- a/PHPWebSocket.php.inc
+++ b/PHPWebSocket.php.inc
@@ -50,7 +50,7 @@ final class PHPWebSocket {
const CLOSECODE_NORMAL = 1000,
CLOSECODE_ENDPOINT_CLOSING = 1001,
- CLOSECODE_PROTOCOLERROR = 1002,
+ CLOSECODE_PROTOCOL_ERROR = 1002,
CLOSECODE_UNSUPPORTED_PAYLOAD = 1003,
// CLOSECODE_ = 1004, // Reserved
CLOSECODE_NO_STATUS = 1005,
@@ -62,6 +62,9 @@ final class PHPWebSocket {
CLOSECODE_UNEXPECTED_CONDITION = 1011,
CLOSECODE_TLS_HANDSHAKE_FAILURE = 1015;
+ const UTF8_ACCEPT = 0,
+ UTF8_REJECT = 1;
+
const HTTP_STATUSCODES = [
100 => 'Continue',
101 => 'Switching Protocols',
@@ -159,7 +162,7 @@ final class PHPWebSocket {
*
* @throws \InvalidArgumentException
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public static function MultiUpdate(array $updateObjects, float $timeout = NULL) : \Generator {
@@ -269,13 +272,15 @@ final class PHPWebSocket {
*
* @param int $code
* @param bool $received If the close code is received as reason
+ *
+ * @return bool
*/
public static function IsValidCloseCode(int $code, bool $received = TRUE) : bool {
switch ($code) {
case self::CLOSECODE_NORMAL:
case self::CLOSECODE_ENDPOINT_CLOSING:
- case self::CLOSECODE_PROTOCOLERROR:
+ case self::CLOSECODE_PROTOCOL_ERROR:
case self::CLOSECODE_UNSUPPORTED_PAYLOAD:
case self::CLOSECODE_INVALID_PAYLOAD:
case self::CLOSECODE_POLICY_VIOLATION:
@@ -322,7 +327,7 @@ final class PHPWebSocket {
*
* @return string
*/
- public static function RandomString(int $length = 16) : string {
+ public static function RandomString(int $length) : string {
$key = '';
for ($i = 0; $i < $length; $i++) {
@@ -332,6 +337,51 @@ final class PHPWebSocket {
return $key;
}
+ /**
+ * Validates a string for UTF8 validity, this allows for matching partial UTF8 string by passing the state each time on resume
+ *
+ * @copyright (c) 2008-2009 Bjoern Hoehrmann
+ *
+ * @see http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+ *
+ * @param string $str
+ * @param int &$state
+ *
+ * @return bool
+ */
+ public static function ValidateUTF8(string $str, int &$state = self::UTF8_ACCEPT) : bool {
+
+ $table = [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf
+ 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df
+ 0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef
+ 0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff
+ 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
+ 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
+ 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
+ 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // s7..s8
+ ];
+
+ $len = strlen($str);
+
+ for ($i = 0; $i < $len; $i++) {
+
+ $state = $table[256 + ($state << 4) + $table[ord($str[$i])]];
+ if ($state === self::UTF8_REJECT) {
+ return FALSE;
+ }
+
+ }
+
+ return TRUE;
+ }
+
/**
* Attempts to parse the provided string into key => value pairs based on the HTTP headers syntax
*
@@ -410,7 +460,7 @@ final class PHPWebSocket {
}
if ($logLevel < LOG_DEBUG || self::$_Debug || $forceShow) {
- echo 'PHPWebSocket: ' . $logLevel . ') ' . ((string) $message) . PHP_EOL;
+ echo date(DATE_ATOM) . ' PHPWebSocket: ' . $logLevel . ') ' . ((string) $message) . PHP_EOL;
}
}
diff --git a/Server.php.inc b/Server.php.inc
index 1a14356..b2843a2 100644
--- a/Server.php.inc
+++ b/Server.php.inc
@@ -35,27 +35,6 @@ require_once(__DIR__ . '/Server/Connection.php.inc');
class Server {
- /**
- * The maximum size in bytes for the handshake
- *
- * @var int
- */
- const HANDSHAKE_MAXLENGTH = 8192;
-
- /**
- * The time in seconds in which the stream_socket_accept method has to accept the connection or fail
- *
- * @var float
- */
- const SOCKET_ACCEPT_TIMEOUT = 5.0;
-
- /**
- * The time in seconds in which the client has to send its handshake
- *
- * @var float
- */
- const ACCEPT_TIMEOUT = 5.0;
-
/**
* The counter to provide all websocket servers with an unique ID
*
@@ -70,6 +49,13 @@ class Server {
*/
protected $_cleanupAcceptingConnectionOnClose = TRUE;
+ /**
+ * The time in seconds in which the stream_socket_accept method has to accept the connection or fail
+ *
+ * @var float
+ */
+ protected $_socketAcceptTimeout = 5.0;
+
/**
* The accepting socket connection
*
@@ -209,14 +195,14 @@ class Server {
list($server, $client) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
- $serverConnection = new \PHPWebSocket\Server\Connection($this, $server, '', $this->_connectionIndex);
+ $serverConnection = new Server\Connection($this, $server, '', $this->_connectionIndex);
$this->_connections[$this->_connectionIndex] = $serverConnection;
\PHPWebSocket::Log(LOG_DEBUG, 'Created new connection: ' . $serverConnection);
$this->_connectionIndex++;
- $clientConnection = new \PHPWebSocket\Client();
+ $clientConnection = new Client();
$clientConnection->connectToResource($client);
return [$serverConnection, $clientConnection];
@@ -229,7 +215,7 @@ class Server {
*
* @throws \Exception
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function update(float $timeout = NULL) : \Generator {
yield from \PHPWebSocket::MultiUpdate($this->getConnections(TRUE), $timeout);
@@ -238,7 +224,7 @@ class Server {
/**
* Gets called by the accepting web socket to notify the server that a new connection attempt has occured
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function gotNewConnection() : \Generator {
@@ -256,7 +242,7 @@ class Server {
* @throws \LogicException
* @throws \RuntimeException
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function acceptNewConnection() : \Generator {
@@ -265,12 +251,12 @@ class Server {
}
$peername = '';
- $newStream = stream_socket_accept($this->_acceptingConnection->getStream(), self::SOCKET_ACCEPT_TIMEOUT, $peername);
+ $newStream = stream_socket_accept($this->_acceptingConnection->getStream(), $this->getSocketAcceptTimeout(), $peername);
if (!$newStream) {
throw new \RuntimeException('Unable to accept stream socket!');
}
- $newConnection = new \PHPWebSocket\Server\Connection($this, $newStream, $peername, $this->_connectionIndex);
+ $newConnection = new Server\Connection($this, $newStream, $peername, $this->_connectionIndex);
$this->_connections[$this->_connectionIndex] = $newConnection;
\PHPWebSocket::Log(LOG_DEBUG, 'Got new connection: ' . $newConnection);
@@ -297,7 +283,7 @@ class Server {
'%serverIdentifier%' => $this->getServerIdentifier(),
];
- return str_replace(array_keys($replaceFields), array_values($replaceFields), "HTTP/1.1 %errorCode% %errorString%\r\nServer: %serverIdentifier%\r\n\r\n%errorCode% %errorString%%errorCode% %errorString%
%serverIdentifier%\r\n\r\n");
+ return str_replace(array_keys($replaceFields), array_values($replaceFields), "HTTP/1.1 %errorCode% %errorString%\r\nServer: %serverIdentifier%\r\n\r\n%errorCode% %errorString%%errorCode% %errorString%
%serverIdentifier%
\r\n\r\n");
}
/**
@@ -305,7 +291,7 @@ class Server {
*
* @param resource $stream
*
- * @return \PHPWebSocket\Server\Connection|null
+ * @return Server\Connection|null
*/
public function getConnectionByStream($stream) {
@@ -341,7 +327,9 @@ class Server {
/**
* Returns if the provided connection in owned by this server
*
- * @return \PHPWebSocket\Server\Connection[]
+ * @param \PHPWebSocket\Server\Connection $connection
+ *
+ * @return bool
*/
public function hasConnection(Server\Connection $connection) : bool {
return in_array($connection, $this->_connections, TRUE);
@@ -359,7 +347,9 @@ class Server {
/**
* Returns all connections this server has
*
- * @return \PHPWebSocket\Server\Connection[]
+ * @param bool $includeAccepting
+ *
+ * @return array|\PHPWebSocket\Server\Connection[]
*/
public function getConnections(bool $includeAccepting = FALSE) : array {
@@ -442,6 +432,24 @@ class Server {
}
+ /**
+ * Sets the time in seconds in which the stream_socket_accept method has to accept the connection or fail
+ *
+ * @param float $timeout
+ */
+ public function setSocketAcceptTimeout(float $timeout) {
+ $this->_socketAcceptTimeout = $timeout;
+ }
+
+ /**
+ * Returns the time in seconds in which the stream_socket_accept method has to accept the connection or fail
+ *
+ * @return float
+ */
+ public function getSocketAcceptTimeout() : float {
+ return $this->_socketAcceptTimeout;
+ }
+
/**
* Sets if we should disable the cleanup which happens after forking
*
diff --git a/Server/AcceptingConnection.php.inc b/Server/AcceptingConnection.php.inc
index 0ec7740..eb82d0d 100644
--- a/Server/AcceptingConnection.php.inc
+++ b/Server/AcceptingConnection.php.inc
@@ -30,9 +30,13 @@ declare(strict_types = 1);
namespace PHPWebSocket\Server;
-class AcceptingConnection implements \PHPWebSocket\IStreamContainer {
+use PHPWebSocket\TStreamContainerDefaults;
+use PHPWebSocket\IStreamContainer;
+use PHPWebSocket\Server;
- use \PHPWebSocket\TStreamContainerDefaults;
+class AcceptingConnection implements IStreamContainer {
+
+ use TStreamContainerDefaults;
/**
* The websocket server related to this connection
@@ -48,7 +52,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer {
*/
protected $_stream = NULL;
- public function __construct(\PHPWebSocket\Server $websocket, $stream) {
+ public function __construct(Server $websocket, $stream) {
$this->_server = $websocket;
$this->_stream = $stream;
@@ -63,7 +67,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer {
/**
* Handles exceptional data reads
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleExceptional() : \Generator {
throw new \LogicException('OOB data is not handled for an accepting stream!');
@@ -72,7 +76,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer {
/**
* Writes the current buffer to the connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleWrite() : \Generator {
throw new \LogicException('An accepting socket should never write!');
@@ -81,7 +85,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer {
/**
* Attempts to read from our connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleRead() : \Generator {
yield from $this->_server->gotNewConnection();
@@ -92,7 +96,7 @@ class AcceptingConnection implements \PHPWebSocket\IStreamContainer {
*
* @return \PHPWebSocket\Server
*/
- public function getServer() : \PHPWebSocket\Server {
+ public function getServer() : Server {
return $this->_server;
}
diff --git a/Server/Connection.php.inc b/Server/Connection.php.inc
index 1b5802f..e7657b8 100644
--- a/Server/Connection.php.inc
+++ b/Server/Connection.php.inc
@@ -30,9 +30,11 @@ declare(strict_types = 1);
namespace PHPWebSocket\Server;
+use PHPWebSocket\AConnection;
+use PHPWebSocket\Server;
use PHPWebSocket\Update;
-class Connection extends \PHPWebSocket\AConnection {
+class Connection extends AConnection {
/**
* The stream's resource index
@@ -41,6 +43,13 @@ class Connection extends \PHPWebSocket\AConnection {
*/
protected $_resourceIndex = NULL;
+ /**
+ * The time in seconds in which the client has to send its handshake
+ *
+ * @var float
+ */
+ protected $_acceptTimeout = 5.0;
+
/**
* If we've finished the handshake
*
@@ -97,9 +106,8 @@ class Connection extends \PHPWebSocket\AConnection {
*/
private $_index = NULL;
- public function __construct(\PHPWebSocket\Server $server, $stream, string $streamName, int $index) {
+ public function __construct(Server $server, $stream, string $streamName, int $index) {
- $this->_openedTimestamp = microtime(TRUE);
$this->_server = $server;
$this->_remoteIP = parse_url($streamName, PHP_URL_HOST);
$this->_stream = $stream;
@@ -112,23 +120,20 @@ class Connection extends \PHPWebSocket\AConnection {
stream_socket_enable_crypto($this->_stream, TRUE, STREAM_CRYPTO_METHOD_TLS_SERVER);
}
- stream_set_timeout($this->_stream, 15);
- stream_set_blocking($this->_stream, FALSE);
- stream_set_read_buffer($this->_stream, 0);
- stream_set_write_buffer($this->_stream, 0);
+ $this->_afterOpen();
}
/**
* Attempts to read from our connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleRead() : \Generator {
\PHPWebSocket::Log(LOG_DEBUG, __METHOD__);
- $readRate = $this->getReadRate($this);
+ $readRate = $this->getReadRate();
$newData = fread($this->getStream(), min($this->_currentFrameRemainingBytes ?? $readRate, $readRate));
if ($newData === FALSE) {
yield new Update\Error(Update\Error::C_READ, $this);
@@ -136,7 +141,6 @@ class Connection extends \PHPWebSocket\AConnection {
return;
}
- $updates = [];
if (strlen($newData) === 0) {
if ($this->_remoteSentDisconnect && $this->_weSentDisconnect) {
@@ -217,11 +221,11 @@ class Connection extends \PHPWebSocket\AConnection {
/**
* Gets called just before stream_select gets called
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function beforeStreamSelect() : \Generator {
- if (!$this->isAccepted() && $this->hasHandshake() && $this->getOpenedTimestamp() + \PHPWebSocket\Server::ACCEPT_TIMEOUT < time()) {
+ if (!$this->isAccepted() && $this->hasHandshake() && $this->getOpenedTimestamp() + $this->getAcceptTimeout() < time()) {
yield new Update\Error(Update\Error::C_ACCEPT_TIMEOUT_PASSED, $this);
$this->deny(408); // Request Timeout
@@ -316,6 +320,24 @@ class Connection extends \PHPWebSocket\AConnection {
}
+ /**
+ * Sets the time in seconds in which the client has to send its handshake
+ *
+ * @param float $timeout
+ */
+ public function setAcceptTimeout(float $timeout) {
+ $this->_acceptTimeout = $timeout;
+ }
+
+ /**
+ * Returns the time in seconds in which the client has to send its handshake
+ *
+ * @return float
+ */
+ public function getAcceptTimeout() : float {
+ return $this->_acceptTimeout;
+ }
+
/**
* Returns if the websocket connection has been accepted
*
@@ -330,7 +352,7 @@ class Connection extends \PHPWebSocket\AConnection {
*
* @return \PHPWebSocket\Server
*/
- public function getServer() : \PHPWebSocket\Server {
+ public function getServer() : Server {
return $this->_server;
}
@@ -343,13 +365,6 @@ class Connection extends \PHPWebSocket\AConnection {
return $this->_hasHandshake;
}
- /**
- * Returns the timestamp at which the connection was opened
- */
- public function getOpenedTimestamp() : float {
- return $this->_openedTimestamp;
- }
-
/**
* Returns if the frame we are writing should be masked
*
diff --git a/TStreamContainerDefaults.php.inc b/TStreamContainerDefaults.php.inc
index 0279015..62102a2 100644
--- a/TStreamContainerDefaults.php.inc
+++ b/TStreamContainerDefaults.php.inc
@@ -34,7 +34,7 @@ trait TStreamContainerDefaults {
/**
* Gets called just before stream_select gets called
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function beforeStreamSelect() : \Generator {
if (FALSE) {
@@ -54,7 +54,7 @@ trait TStreamContainerDefaults {
/**
* Handles exceptional data reads
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleExceptional() : \Generator {
if (FALSE) {
@@ -65,7 +65,7 @@ trait TStreamContainerDefaults {
/**
* Writes the current buffer to the connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleWrite() : \Generator {
if (FALSE) {
@@ -76,7 +76,7 @@ trait TStreamContainerDefaults {
/**
* Attempts to read from our connection
*
- * @return \PHPWebSocket\AUpdate[]
+ * @return \Generator
*/
public function handleRead() : \Generator {
if (FALSE) {
diff --git a/Tests/Autobahn/fuzzingclient.json b/Tests/Autobahn/fuzzingclient.json
index de783c9..be52da7 100644
--- a/Tests/Autobahn/fuzzingclient.json
+++ b/Tests/Autobahn/fuzzingclient.json
@@ -1,5 +1,5 @@
{
- "outdir": "/tmp/reports/servers",
+ "outdir": "/tmp/reports/",
"servers": [
{
"url": "ws://127.0.0.1:9001"
diff --git a/Tests/Autobahn/fuzzingserver.json b/Tests/Autobahn/fuzzingserver.json
index 6b8f25c..226528a 100644
--- a/Tests/Autobahn/fuzzingserver.json
+++ b/Tests/Autobahn/fuzzingserver.json
@@ -1,6 +1,6 @@
{
"url": "ws://127.0.0.1:9001",
- "outdir": "/tmp/reports/clients",
+ "outdir": "/tmp/reports/",
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
diff --git a/Tests/client.php.inc b/Tests/client.php.inc
index b83719d..48a78fa 100644
--- a/Tests/client.php.inc
+++ b/Tests/client.php.inc
@@ -38,84 +38,146 @@ $wstestProc = proc_open('wstest -m fuzzingserver -s Autobahn/fuzzingserver.json'
sleep(2);
-$client = new \PHPWebSocket\Client();
-if (!$client->connect($address, '/getCaseCount')) {
- echo 'Unable to connect to server: ' . $client->getLastError() . PHP_EOL;
- exit(1);
-}
+try {
+
+ $client = new \PHPWebSocket\Client();
-$caseCount = NULL;
+ $bufferType = $argv[2] ?? NULL;
+ switch ($bufferType) {
+ case 'memory':
+ // Default, do nothing
+ break;
+ case 'tmpfile':
-while ($client->isOpen()) {
- foreach ($client->update() as $key => $value) {
- if ($value instanceof Read && $value->getCode() === Read::C_READ) {
- $caseCount = (int) $value->getMessage();
+ $client->setNewMessageStreamCallback(function (array $headers) {
+ return tmpfile();
+ });
+
+ break;
+ default:
+ throw new \Exception('Unknown buffer type specified: ' . ($bufferType === NULL ? 'NULL' : $bufferType));
+ }
+
+ if (!$client->connect($address, '/getCaseCount')) {
+ \PHPWebSocket::Log(LOG_ERR, 'Unable to connect to server: ' . $client->getLastError());
+ exit(1);
+ }
+
+ $caseCount = NULL;
+
+ while ($client->isOpen()) {
+ foreach ($client->update() as $key => $value) {
+
+ \PHPWebSocket::Log(LOG_INFO, $value . '');
+
+ if ($value instanceof Read && $value->getCode() === Read::C_READ) {
+
+ $msg = $value->getMessage() ?? NULL;
+ if ($msg === NULL) {
+
+ $stream = $value->getStream();
+ rewind($stream);
+ $msg = stream_get_contents($stream);
+
+ }
+
+ $caseCount = (int) $msg;
+
+ }
}
}
-}
-echo 'Will run ' . $caseCount . ' test cases' . PHP_EOL;
+ if ($caseCount === 0) {
+ throw new \Exception('Unable to get case count from autobahn server!');
+ }
-for ($i = 0; $i < $caseCount; $i++) {
+ \PHPWebSocket::Log(LOG_INFO, 'Will run ' . $caseCount . ' test cases');
- $client = new \PHPWebSocket\Client();
- $client->connect($address, '/runCase?case=' . ($i + 1) . '&agent=' . $client->getUserAgent());
+ for ($i = 0; $i < $caseCount; $i++) {
- while ($client->isOpen()) {
+ $client = new \PHPWebSocket\Client();
+ $client->connect($address, '/runCase?case=' . ($i + 1) . '&agent=' . $client->getUserAgent());
- $updates = $client->update();
- foreach ($updates as $update) {
+ while ($client->isOpen()) {
+
+ $updates = $client->update();
+ foreach ($updates as $update) {
+
+ if ($update instanceof Read && $update->getCode() === Read::C_READ) {
+
+ $message = $update->getMessage() ?? '';
+ if ($message === '') {
+
+ $stream = $update->getStream();
+ if ($stream) {
+
+ rewind($stream);
+ $message = stream_get_contents($stream);
+
+ }
+
+ }
+
+ $client->write($message, $update->getOpcode());
+
+ }
- if ($update instanceof Read && $update->getCode() === Read::C_READ) {
- $client->write($update->getMessage() ?? '', $update->getOpcode());
}
}
}
-}
-
-echo 'All test cases ran, asking for report update' . PHP_EOL;
+ \PHPWebSocket::Log(LOG_INFO, 'All test cases ran, asking for report update');
-$client = new \PHPWebSocket\Client();
-$client->connect($address, '/updateReports?agent=' . $client->getUserAgent());
+ $client = new \PHPWebSocket\Client();
+ $client->connect($address, '/updateReports?agent=' . $client->getUserAgent());
-while ($client->isOpen()) {
- foreach ($client->update() as $key => $value) {
+ while ($client->isOpen()) {
+ foreach ($client->update() as $key => $value) {
+ }
}
-}
-echo 'Reports finished, getting results..' . PHP_EOL;
+ \PHPWebSocket::Log(LOG_INFO, 'Reports finished, getting results..');
-$outputFile = '/tmp/reports/clients/index.json';
-if (!file_exists($outputFile)) {
- echo 'File "' . $outputFile . '" doesn\'t exist!';
- exit(1);
-}
+ $outputFile = '/tmp/reports/index.json';
+ if (!file_exists($outputFile)) {
+ \PHPWebSocket::Log(LOG_ALERT, 'File "' . $outputFile . '" doesn\'t exist!');
+ exit(1);
+ }
-$hasFailures = FALSE;
-$testCases = json_decode(file_get_contents($outputFile), TRUE)[$client->getUserAgent()];
-foreach ($testCases as $case => $data) {
+ $hasFailures = FALSE;
+ $testCases = json_decode(file_get_contents($outputFile), TRUE)[$client->getUserAgent()] ?? NULL;
+ if ($testCases === NULL) {
+ \PHPWebSocket::Log(LOG_ERR, 'Unable to get test case results!');
+ } else {
+
+ foreach ($testCases as $case => $data) {
+
+ \PHPWebSocket::Log(LOG_INFO, $case . ' => ' . $data['behavior']);
+
+ switch ($data['behavior']) {
+ case 'OK':
+ case 'NON-STRICT':
+ case 'INFORMATIONAL':
+ case 'UNIMPLEMENTED':
+ break;
+ default:
+ $hasFailures = TRUE;
+ break;
+ }
- echo $case . ' => ' . $data['behavior'] . PHP_EOL;
+ }
- switch ($data['behavior']) {
- case 'OK':
- case 'NON-STRICT':
- case 'INFORMATIONAL':
- case 'UNIMPLEMENTED':
- break;
- default:
- $hasFailures = TRUE;
- break;
}
+} finally {
+
+ proc_terminate($wstestProc);
+
}
-echo 'Exiting' . PHP_EOL;
+\PHPWebSocket::Log(LOG_INFO, 'Exiting');
exit((int) $hasFailures);
-
-exit();
diff --git a/Tests/runner.php b/Tests/runner.php
index 396cb3d..ac32856 100644
--- a/Tests/runner.php
+++ b/Tests/runner.php
@@ -30,7 +30,7 @@
*/
if (php_sapi_name() !== 'cli') {
- echo 'The tests can only be executed in CLI!' . PHP_EOL;
+ \PHPWebSocket::Log(LOG_ERR, 'The tests can only be executed in CLI!');
exit(1);
}
diff --git a/Tests/server.php.inc b/Tests/server.php.inc
index f50bb86..ec0c947 100644
--- a/Tests/server.php.inc
+++ b/Tests/server.php.inc
@@ -30,82 +30,130 @@ require_once(__DIR__ . '/../PHPWebSocket.php.inc');
use \PHPWebSocket\Update\Read;
-echo 'Starting test' . PHP_EOL . PHP_EOL;
+\PHPWebSocket::Log(LOG_INFO, 'Starting test' . PHP_EOL);
$websocket = new \PHPWebSocket\Server('tcp://0.0.0.0:9001');
+$bufferType = $argv[2] ?? NULL;
$descriptorSpec = [['pipe', 'r'], STDOUT, STDERR];
$wstestProc = proc_open('wstest -m fuzzingclient -s Autobahn/fuzzingclient.json', $descriptorSpec, $pipes, __DIR__);
-while (proc_get_status($wstestProc)['running'] ?? FALSE) {
+try {
- $updates = $websocket->update(0.1);
- foreach ($updates as $update) {
+ while (proc_get_status($wstestProc)['running'] ?? FALSE) {
- if ($update instanceof Read) {
+ $updates = $websocket->update(0.1);
+ foreach ($updates as $update) {
- $sourceObj = $update->getSourceObject();
- $opcode = $update->getCode();
- switch ($opcode) {
- case Read::C_NEWCONNECTION:
- $sourceObj->accept();
- break;
- case Read::C_READ:
+ if ($update instanceof Read) {
- $opcode = $update->getOpcode();
- switch ($opcode) {
- case \PHPWebSocket::OPCODE_CONTINUE:
- case \PHPWebSocket::OPCODE_FRAME_TEXT:
- case \PHPWebSocket::OPCODE_FRAME_BINARY:
+ $sourceObj = $update->getSourceObject();
+ $opcode = $update->getCode();
+ switch ($opcode) {
+ case Read::C_NEWCONNECTION:
- $msg = $update->getMessage();
- if ($msg !== NULL && !$sourceObj->isDisconnecting()) {
- $sourceObj->write($msg, $opcode);
- }
+ $sourceObj->accept();
- break;
- }
+ switch ($bufferType) {
+ case 'memory':
+ // Default, do nothing
+ break;
+ case 'tmpfile':
+
+ $sourceObj->setNewMessageStreamCallback(function (array $headers) {
+ return tmpfile();
+ });
+
+ break;
+ default:
+ throw new \Exception('Unknown buffer type specified: ' . ($bufferType === NULL ? 'NULL' : $bufferType));
+ }
+
+ break;
+ case Read::C_READ:
+
+ $opcode = $update->getOpcode();
+ switch ($opcode) {
+ case \PHPWebSocket::OPCODE_CONTINUE:
+ case \PHPWebSocket::OPCODE_FRAME_TEXT:
+ case \PHPWebSocket::OPCODE_FRAME_BINARY:
+
+ if ($sourceObj->isDisconnecting()) {
+ break;
+ }
+
+ $message = $update->getMessage() ?? '';
+ if ($message === '') {
+
+ $stream = $update->getStream();
+ if ($stream) {
+
+ rewind($stream);
+ $message = stream_get_contents($stream);
+
+ }
+
+ }
+
+ if ($message !== NULL) {
+ $sourceObj->write($message, $opcode);
+ }
+
+ break;
+ }
+
+ break;
+ }
- break;
}
}
}
-}
+ \PHPWebSocket::Log(LOG_INFO, 'Test ended, closing websocket');
-echo 'Test ended, closing websocket' . PHP_EOL;
+ $websocket->close();
-$websocket->close();
+ \PHPWebSocket::Log(LOG_INFO, 'Getting results..');
-echo 'Getting results..' . PHP_EOL;
+ $outputFile = '/tmp/reports/index.json';
+ if (!file_exists($outputFile)) {
+ \PHPWebSocket::Log(LOG_ERR, 'File "' . $outputFile . '" doesn\'t exist!');
+ exit(1);
+ }
-$outputFile = '/tmp/reports/servers/index.json';
-if (!file_exists($outputFile)) {
- echo 'File "' . $outputFile . '" doesn\'t exist!';
- exit(1);
-}
+ $hasFailures = FALSE;
+ $testCases = json_decode(file_get_contents($outputFile), TRUE)[$websocket->getServerIdentifier()] ?? NULL;
+ if ($testCases === NULL) {
+ \PHPWebSocket::Log(LOG_ERR, 'Unable to get test case results!');
+ } else {
+
+ foreach ($testCases as $case => $data) {
+
+ \PHPWebSocket::Log(LOG_INFO, $case . ' => ' . $data['behavior']);
+
+ switch ($data['behavior']) {
+ case 'OK':
+ case 'NON-STRICT':
+ case 'INFORMATIONAL':
+ case 'UNIMPLEMENTED':
+ break;
+ default:
+ $hasFailures = TRUE;
+ break;
+ }
+
+ }
-$hasFailures = FALSE;
-$testCases = json_decode(file_get_contents($outputFile), TRUE)[$websocket->getServerIdentifier()];
-foreach ($testCases as $case => $data) {
-
- echo $case . ' => ' . $data['behavior'] . PHP_EOL;
-
- switch ($data['behavior']) {
- case 'OK':
- case 'NON-STRICT':
- case 'INFORMATIONAL':
- case 'UNIMPLEMENTED':
- break;
- default:
- $hasFailures = TRUE;
- break;
}
+} finally {
+
+ proc_terminate($wstestProc);
+
}
-echo 'Exiting' . PHP_EOL;
+\PHPWebSocket::Log(LOG_INFO, 'Exiting');
exit((int) $hasFailures);
diff --git a/Update/Error.php.inc b/Update/Error.php.inc
index 7f760b2..fc414d8 100644
--- a/Update/Error.php.inc
+++ b/Update/Error.php.inc
@@ -30,7 +30,9 @@ declare(strict_types = 1);
namespace PHPWebSocket\Update;
-class Error extends \PHPWebSocket\AUpdate {
+use PHPWebSocket\AUpdate;
+
+class Error extends AUpdate {
const C_UNKNOWN = 0,
C_SELECT = 1,
@@ -45,7 +47,8 @@ class Error extends \PHPWebSocket\AUpdate {
C_READ_PROTOCOL_ERROR = 10,
C_READ_RSVBIT_SET = 11,
C_WRITE = 12,
- C_ACCEPT_TIMEOUT_PASSED = 13;
+ C_ACCEPT_TIMEOUT_PASSED = 13,
+ C_READ_INVALID_TARGET_STREAM = 14;
const ERROR_STRINGS = [
self::C_UNKNOWN => 'Unknown error',
diff --git a/Update/Read.php.inc b/Update/Read.php.inc
index c7dd7c9..62bc597 100644
--- a/Update/Read.php.inc
+++ b/Update/Read.php.inc
@@ -30,7 +30,9 @@ declare(strict_types = 1);
namespace PHPWebSocket\Update;
-class Read extends \PHPWebSocket\AUpdate {
+use PHPWebSocket\AUpdate;
+
+class Read extends AUpdate {
const C_UNKNOWN = 0,
C_NEWCONNECTION = 1,
@@ -71,12 +73,24 @@ class Read extends \PHPWebSocket\AUpdate {
*/
protected $_opcode = NULL;
- public function __construct(int $code, $sourceObject = NULL, int $opcode = NULL, string $message = NULL) {
+ /**
+ * The resource pointing to the downloaded message
+ *
+ * @var resource|null
+ */
+ protected $_stream = NULL;
+
+ public function __construct(int $code, $sourceObject = NULL, int $opcode = NULL, string $message = NULL, $stream = NULL) {
+
+ if ($stream !== NULL && !is_resource($stream)) {
+ throw new \InvalidArgumentException('The $stream argument has to be NULL or a resource!');
+ }
parent::__construct($code, $sourceObject);
$this->_message = $message;
$this->_opcode = $opcode;
+ $this->_stream = $stream;
}
@@ -88,7 +102,7 @@ class Read extends \PHPWebSocket\AUpdate {
* @return string
*/
public static function StringForCode(int $code) : string {
- return self::ERROR_STRINGS[$code] ?? 'Unknown read code ' . $code;
+ return self::READ_STRINGS[$code] ?? 'Unknown read code ' . $code;
}
/**
@@ -109,7 +123,17 @@ class Read extends \PHPWebSocket\AUpdate {
return $this->_opcode;
}
+ /**
+ * Returns the resource pointing to the downloaded message
+ *
+ * @return resource|null
+ */
+ public function getStream() {
+ return $this->_stream;
+ }
+
public function __toString() {
+
$code = $this->getCode();
return 'Read) ' . self::StringForCode($code) . ' (C: ' . $code . ')';
diff --git a/VERSION b/VERSION
index bd8bf88..27f9cd3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.7.0
+1.8.0