diff --git a/Makefile b/Makefile index c66fb15..ef50184 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ test: vendor/bin/phpunit build ./vendor/bin/phpunit cs-check: vendor/bin/phpunit - ./vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors lib tests + ./vendor/bin/phpcs --standard=codestandard.xml lib tests coverage: vendor/bin/phpunit build ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml diff --git a/README.md b/README.md index a3112b0..c062c90 100644 --- a/README.md +++ b/README.md @@ -236,11 +236,15 @@ Ignas Bernotas, Mark Herhold, Andreas Palm, Sören Jensen, pmaasz, Alexey Stavro ## Changelog +1.3.1 + + * Allow control messages without payload (@Logioniz) + * Error code in ConnectionException (@sirn-se) + 1.3.0 - * Implements ping/pong frames (@pmccarren) - * Correct ping-pong behaviour (@Logioniz) - * Correct close behaviour (@sirn-se) + * Implements ping/pong frames (@pmccarren @Logioniz) + * Close behaviour (@sirn-se) * Various fixes concerning connection handling (@sirn-se) * Overhaul of Composer, Travis and Coveralls setup, PSR code standard and unit tests (@sirn-se) diff --git a/codestandard.xml b/codestandard.xml new file mode 100644 index 0000000..a354b81 --- /dev/null +++ b/codestandard.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/Base.php b/lib/Base.php index fd9b8c4..a179e9c 100644 --- a/lib/Base.php +++ b/lib/Base.php @@ -170,7 +170,10 @@ protected function receiveFragment() $opcode_int = ord($data[0]) & 31; // Bits 4-7 $opcode_ints = array_flip(self::$opcodes); if (!array_key_exists($opcode_int, $opcode_ints)) { - throw new ConnectionException("Bad opcode in websocket frame: $opcode_int"); + throw new ConnectionException( + "Bad opcode in websocket frame: $opcode_int", + ConnectionException::BAD_OPCODE + ); } $opcode = $opcode_ints[$opcode_int]; @@ -272,11 +275,14 @@ public function close($status = 1000, $message = 'ttfn') protected function write($data) { $written = fwrite($this->socket, $data); + if ($written === false) { + $length = strlen($data); + $this->throwException("Failed to write $length bytes."); + } if ($written < strlen($data)) { - throw new ConnectionException( - "Could only write $written out of " . strlen($data) . " bytes." - ); + $length = strlen($data); + $this->throwException("Could only write $written out of $length bytes."); } } @@ -286,24 +292,29 @@ protected function read($length) while (strlen($data) < $length) { $buffer = fread($this->socket, $length - strlen($data)); if ($buffer === false) { - $metadata = stream_get_meta_data($this->socket); - throw new ConnectionException( - 'Broken frame, read ' . strlen($data) . ' of stated ' - . $length . ' bytes. Stream state: ' - . json_encode($metadata) - ); + $read = strlen($data); + $this->throwException("Broken frame, read $read of stated $length bytes."); } if ($buffer === '') { - $metadata = stream_get_meta_data($this->socket); - throw new ConnectionException( - 'Empty read; connection dead? Stream state: ' . json_encode($metadata) - ); + $this->throwException("Empty read; connection dead?"); } $data .= $buffer; } return $data; } + protected function throwException($message, $code = 0) + { + $meta = stream_get_meta_data($this->socket); + if (!empty($meta['timed_out'])) { + $code = ConnectionException::TIMED_OUT; + } + if (!empty($meta['eof'])) { + $code = ConnectionException::EOF; + } + $json_meta = json_encode($meta); + throw new ConnectionException("$message Stream state: $json_meta", $code); + } /** * Helper to convert a binary to a string of '0' and '1'. diff --git a/lib/ConnectionException.php b/lib/ConnectionException.php index ec06d07..e8c2d03 100644 --- a/lib/ConnectionException.php +++ b/lib/ConnectionException.php @@ -4,4 +4,8 @@ class ConnectionException extends Exception { + // Native codes in interval 0-106 + const TIMED_OUT = 1024; + const EOF = 1025; + const BAD_OPCODE = 1026; } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index bbcb055..17b5e25 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -261,6 +261,7 @@ public function testBadStreamContext() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage Could not open socket to "localhost:8000" */ public function testFailedConnection() @@ -272,6 +273,7 @@ public function testFailedConnection() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage Connection to 'ws://localhost/my/mock/path' failed */ public function testInvalidUpgrade() @@ -283,6 +285,7 @@ public function testInvalidUpgrade() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage Server sent bad upgrade response */ public function testInvalidKey() @@ -308,6 +311,7 @@ public function testSendBadOpcode() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1026 * @expectedExceptionMessage Bad opcode in websocket frame: 12 */ public function testRecieveBadOpcode() @@ -321,6 +325,7 @@ public function testRecieveBadOpcode() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1025 * @expectedExceptionMessage Could only write 18 out of 22 bytes. */ public function testBrokenWrite() @@ -334,6 +339,21 @@ public function testBrokenWrite() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1024 + * @expectedExceptionMessage Failed to write 22 bytes. + */ + public function testFailedWrite() + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('send-failed-write', $this); + $client->send('Failing to write'); + } + + /** + * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1025 * @expectedExceptionMessage Broken frame, read 0 of stated 2 bytes. */ public function testBrokenRead() @@ -347,6 +367,7 @@ public function testBrokenRead() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1024 * @expectedExceptionMessage Empty read; connection dead? */ public function testEmptyRead() diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 9edc011..6a83649 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -220,6 +220,7 @@ public function testSetTimeout() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage Could not open listening socket: */ public function testFailedSocketServer() @@ -230,6 +231,7 @@ public function testFailedSocketServer() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage Server failed to connect */ public function testFailedConnect() @@ -244,6 +246,7 @@ public function testFailedConnect() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage Server failed to connect */ public function testFailedConnectTimeout() @@ -258,6 +261,7 @@ public function testFailedConnectTimeout() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage No GET in request */ public function testFailedHttp() @@ -271,6 +275,7 @@ public function testFailedHttp() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 0 * @expectedExceptionMessage Client had no Key in upgrade request */ public function testFailedWsKey() @@ -284,6 +289,7 @@ public function testFailedWsKey() /** * @expectedException WebSocket\BadOpcodeException + * @expectedExceptionCode 0 * @expectedExceptionMessage Bad opcode 'bad'. Try 'text' or 'binary'. */ public function testSendBadOpcode() @@ -298,6 +304,7 @@ public function testSendBadOpcode() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1026 * @expectedExceptionMessage Bad opcode in websocket frame: 12 */ public function testRecieveBadOpcode() @@ -313,6 +320,7 @@ public function testRecieveBadOpcode() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1025 * @expectedExceptionMessage Could only write 18 out of 22 bytes. */ public function testBrokenWrite() @@ -328,6 +336,23 @@ public function testBrokenWrite() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1024 + * @expectedExceptionMessage Failed to write 22 bytes. + */ + public function testFailedWrite() + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('send-failed-write', $this); + $server->send('Failing to write'); + } + + /** + * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1025 * @expectedExceptionMessage Broken frame, read 0 of stated 2 bytes. */ public function testBrokenRead() @@ -343,6 +368,7 @@ public function testBrokenRead() /** * @expectedException WebSocket\ConnectionException + * @expectedExceptionCode 1024 * @expectedExceptionMessage Empty read; connection dead? */ public function testEmptyRead() diff --git a/tests/scripts/receive-broken-read.json b/tests/scripts/receive-broken-read.json index 3fb87ab..e23fa72 100644 --- a/tests/scripts/receive-broken-read.json +++ b/tests/scripts/receive-broken-read.json @@ -19,7 +19,7 @@ "return": { "timed_out": false, "blocked": true, - "eof": false, + "eof": true, "stream_type": "tcp_socket\/ssl", "mode": "r+", "unread_bytes": 2, diff --git a/tests/scripts/receive-empty-read.json b/tests/scripts/receive-empty-read.json index 2de01d2..534589c 100644 --- a/tests/scripts/receive-empty-read.json +++ b/tests/scripts/receive-empty-read.json @@ -17,7 +17,7 @@ "@mock-stream" ], "return": { - "timed_out": false, + "timed_out": true, "blocked": true, "eof": false, "stream_type": "tcp_socket\/ssl", diff --git a/tests/scripts/send-broken-write.json b/tests/scripts/send-broken-write.json index d910fc3..4e2a4aa 100644 --- a/tests/scripts/send-broken-write.json +++ b/tests/scripts/send-broken-write.json @@ -12,5 +12,20 @@ "@mock-stream" ], "return": 18 + }, + { + "function": "stream_get_meta_data", + "params": [ + "@mock-stream" + ], + "return": { + "timed_out": false, + "blocked": true, + "eof": true, + "stream_type": "tcp_socket\/ssl", + "mode": "r+", + "unread_bytes": 2, + "seekable": false + } } ] \ No newline at end of file diff --git a/tests/scripts/send-failed-write.json b/tests/scripts/send-failed-write.json new file mode 100644 index 0000000..325e0fb --- /dev/null +++ b/tests/scripts/send-failed-write.json @@ -0,0 +1,31 @@ +[ + { + "function": "get_resource_type", + "params": [ + "@mock-stream" + ], + "return": "stream" + }, + { + "function": "fwrite", + "params": [ + "@mock-stream" + ], + "return": false + }, + { + "function": "stream_get_meta_data", + "params": [ + "@mock-stream" + ], + "return": { + "timed_out": true, + "blocked": true, + "eof": false, + "stream_type": "tcp_socket\/ssl", + "mode": "r+", + "unread_bytes": 2, + "seekable": false + } + } +] \ No newline at end of file