From 0ff39c83d38ce538a9f5dba53eca0fa9cb16d9e6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 14:45:49 +0200 Subject: Adding upstream version 0.10.2+dfsg1. Signed-off-by: Daniel Baumann --- .../websocket/.github/ISSUE_TEMPLATE/bug_report.md | 21 + .../.github/ISSUE_TEMPLATE/feature_request.md | 14 + .../.github/ISSUE_TEMPLATE/other-issue.md | 10 + .../websocket/.github/workflows/acceptance.yml | 97 ++ vendor/textalk/websocket/.gitignore | 6 + vendor/textalk/websocket/COPYING.md | 16 + vendor/textalk/websocket/Makefile | 32 + vendor/textalk/websocket/README.md | 67 + vendor/textalk/websocket/codestandard.xml | 10 + vendor/textalk/websocket/composer.json | 34 + vendor/textalk/websocket/docs/Changelog.md | 130 ++ vendor/textalk/websocket/docs/Client.md | 137 ++ vendor/textalk/websocket/docs/Contributing.md | 44 + vendor/textalk/websocket/docs/Examples.md | 98 ++ vendor/textalk/websocket/docs/Message.md | 60 + vendor/textalk/websocket/docs/Server.md | 136 ++ vendor/textalk/websocket/examples/echoserver.php | 87 + .../textalk/websocket/examples/random_client.php | 94 ++ .../textalk/websocket/examples/random_server.php | 93 ++ vendor/textalk/websocket/examples/send.php | 51 + .../textalk/websocket/lib/BadOpcodeException.php | 7 + vendor/textalk/websocket/lib/BadUriException.php | 7 + vendor/textalk/websocket/lib/Base.php | 486 ++++++ vendor/textalk/websocket/lib/Client.php | 216 +++ .../textalk/websocket/lib/ConnectionException.php | 26 + vendor/textalk/websocket/lib/Exception.php | 7 + vendor/textalk/websocket/lib/Message/Binary.php | 8 + vendor/textalk/websocket/lib/Message/Close.php | 8 + vendor/textalk/websocket/lib/Message/Factory.php | 25 + vendor/textalk/websocket/lib/Message/Message.php | 53 + vendor/textalk/websocket/lib/Message/Ping.php | 8 + vendor/textalk/websocket/lib/Message/Pong.php | 8 + vendor/textalk/websocket/lib/Message/Text.php | 8 + vendor/textalk/websocket/lib/Server.php | 176 ++ vendor/textalk/websocket/lib/TimeoutException.php | 7 + vendor/textalk/websocket/phpunit.xml.dist | 14 + vendor/textalk/websocket/tests/ClientTest.php | 449 ++++++ vendor/textalk/websocket/tests/ExceptionTest.php | 52 + vendor/textalk/websocket/tests/MessageTest.php | 61 + vendor/textalk/websocket/tests/README.md | 29 + vendor/textalk/websocket/tests/ServerTest.php | 448 ++++++ vendor/textalk/websocket/tests/bootstrap.php | 6 + vendor/textalk/websocket/tests/mock/EchoLog.php | 34 + vendor/textalk/websocket/tests/mock/MockSocket.php | 79 + .../textalk/websocket/tests/mock/mock-socket.php | 78 + .../textalk/websocket/tests/mock/payload.128.txt | 5 + .../textalk/websocket/tests/mock/payload.65536.txt | 1682 ++++++++++++++++++++ .../websocket/tests/scripts/client.close.json | 76 + .../tests/scripts/client.connect-authed.json | 59 + .../tests/scripts/client.connect-bad-context.json | 7 + .../tests/scripts/client.connect-context.json | 59 + .../tests/scripts/client.connect-error.json | 23 + .../tests/scripts/client.connect-extended.json | 59 + .../tests/scripts/client.connect-failed.json | 19 + .../tests/scripts/client.connect-headers.json | 59 + .../tests/scripts/client.connect-invalid-key.json | 50 + .../scripts/client.connect-invalid-upgrade.json | 50 + .../tests/scripts/client.connect-persistent.json | 95 ++ .../tests/scripts/client.connect-timeout.json | 59 + .../websocket/tests/scripts/client.connect.json | 59 + .../websocket/tests/scripts/client.destruct.json | 23 + .../websocket/tests/scripts/client.reconnect.json | 100 ++ .../websocket/tests/scripts/close-remote.json | 55 + .../websocket/tests/scripts/config-timeout.json | 24 + .../textalk/websocket/tests/scripts/ping-pong.json | 150 ++ .../tests/scripts/receive-bad-opcode.json | 18 + .../tests/scripts/receive-broken-read.json | 58 + .../tests/scripts/receive-client-timeout.json | 50 + .../tests/scripts/receive-empty-read.json | 58 + .../tests/scripts/receive-fragmentation.json | 126 ++ .../websocket/tests/scripts/send-bad-opcode.json | 9 + .../websocket/tests/scripts/send-broken-write.json | 43 + .../websocket/tests/scripts/send-convenicance.json | 86 + .../websocket/tests/scripts/send-failed-write.json | 43 + .../websocket/tests/scripts/send-receive-128.json | 50 + .../tests/scripts/send-receive-65536.json | 113 ++ .../tests/scripts/send-receive-multi-fragment.json | 112 ++ .../websocket/tests/scripts/send-receive.json | 50 + .../tests/scripts/server.accept-destruct.json | 315 ++++ .../tests/scripts/server.accept-error-connect.json | 18 + .../scripts/server.accept-failed-connect.json | 14 + .../tests/scripts/server.accept-failed-http.json | 265 +++ .../tests/scripts/server.accept-failed-ws-key.json | 265 +++ .../tests/scripts/server.accept-timeout.json | 289 ++++ .../websocket/tests/scripts/server.accept.json | 287 ++++ .../websocket/tests/scripts/server.close.json | 70 + .../server.construct-error-socket-server.json | 28 + .../server.construct-failed-socket-server.json | 20 + .../websocket/tests/scripts/server.construct.json | 11 + 89 files changed, 8648 insertions(+) create mode 100644 vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md create mode 100644 vendor/textalk/websocket/.github/workflows/acceptance.yml create mode 100644 vendor/textalk/websocket/.gitignore create mode 100644 vendor/textalk/websocket/COPYING.md create mode 100644 vendor/textalk/websocket/Makefile create mode 100644 vendor/textalk/websocket/README.md create mode 100644 vendor/textalk/websocket/codestandard.xml create mode 100644 vendor/textalk/websocket/composer.json create mode 100644 vendor/textalk/websocket/docs/Changelog.md create mode 100644 vendor/textalk/websocket/docs/Client.md create mode 100644 vendor/textalk/websocket/docs/Contributing.md create mode 100644 vendor/textalk/websocket/docs/Examples.md create mode 100644 vendor/textalk/websocket/docs/Message.md create mode 100644 vendor/textalk/websocket/docs/Server.md create mode 100644 vendor/textalk/websocket/examples/echoserver.php create mode 100644 vendor/textalk/websocket/examples/random_client.php create mode 100644 vendor/textalk/websocket/examples/random_server.php create mode 100644 vendor/textalk/websocket/examples/send.php create mode 100644 vendor/textalk/websocket/lib/BadOpcodeException.php create mode 100644 vendor/textalk/websocket/lib/BadUriException.php create mode 100644 vendor/textalk/websocket/lib/Base.php create mode 100644 vendor/textalk/websocket/lib/Client.php create mode 100644 vendor/textalk/websocket/lib/ConnectionException.php create mode 100644 vendor/textalk/websocket/lib/Exception.php create mode 100644 vendor/textalk/websocket/lib/Message/Binary.php create mode 100644 vendor/textalk/websocket/lib/Message/Close.php create mode 100644 vendor/textalk/websocket/lib/Message/Factory.php create mode 100644 vendor/textalk/websocket/lib/Message/Message.php create mode 100644 vendor/textalk/websocket/lib/Message/Ping.php create mode 100644 vendor/textalk/websocket/lib/Message/Pong.php create mode 100644 vendor/textalk/websocket/lib/Message/Text.php create mode 100644 vendor/textalk/websocket/lib/Server.php create mode 100644 vendor/textalk/websocket/lib/TimeoutException.php create mode 100644 vendor/textalk/websocket/phpunit.xml.dist create mode 100644 vendor/textalk/websocket/tests/ClientTest.php create mode 100644 vendor/textalk/websocket/tests/ExceptionTest.php create mode 100644 vendor/textalk/websocket/tests/MessageTest.php create mode 100644 vendor/textalk/websocket/tests/README.md create mode 100644 vendor/textalk/websocket/tests/ServerTest.php create mode 100644 vendor/textalk/websocket/tests/bootstrap.php create mode 100644 vendor/textalk/websocket/tests/mock/EchoLog.php create mode 100644 vendor/textalk/websocket/tests/mock/MockSocket.php create mode 100644 vendor/textalk/websocket/tests/mock/mock-socket.php create mode 100644 vendor/textalk/websocket/tests/mock/payload.128.txt create mode 100644 vendor/textalk/websocket/tests/mock/payload.65536.txt create mode 100644 vendor/textalk/websocket/tests/scripts/client.close.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-authed.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-bad-context.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-context.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-error.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-extended.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-failed.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-headers.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-invalid-key.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-invalid-upgrade.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-persistent.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect-timeout.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.connect.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.destruct.json create mode 100644 vendor/textalk/websocket/tests/scripts/client.reconnect.json create mode 100644 vendor/textalk/websocket/tests/scripts/close-remote.json create mode 100644 vendor/textalk/websocket/tests/scripts/config-timeout.json create mode 100644 vendor/textalk/websocket/tests/scripts/ping-pong.json create mode 100644 vendor/textalk/websocket/tests/scripts/receive-bad-opcode.json create mode 100644 vendor/textalk/websocket/tests/scripts/receive-broken-read.json create mode 100644 vendor/textalk/websocket/tests/scripts/receive-client-timeout.json create mode 100644 vendor/textalk/websocket/tests/scripts/receive-empty-read.json create mode 100644 vendor/textalk/websocket/tests/scripts/receive-fragmentation.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-bad-opcode.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-broken-write.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-convenicance.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-failed-write.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-receive-128.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-receive-65536.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-receive-multi-fragment.json create mode 100644 vendor/textalk/websocket/tests/scripts/send-receive.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.accept-destruct.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.accept-error-connect.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.accept-failed-connect.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.accept-failed-http.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.accept-failed-ws-key.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.accept-timeout.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.accept.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.close.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.construct-error-socket-server.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.construct-failed-socket-server.json create mode 100644 vendor/textalk/websocket/tests/scripts/server.construct.json (limited to 'vendor/textalk/websocket') diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d402046 --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Use this if you believe there is a bug in this repo +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +Please provide a clear and concise description of the suspected issue. + +**How to reproduce** +If possible, provide information - possibly including code snippets - on how to reproduce the issue. + +**Logs** +If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#echo-logger on how to use the EchoLog. + +**Versions** +* Version of this library +* PHP version diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ce777f6 --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this library +title: '' +labels: feature request +assignees: '' + +--- + +**Is it within the scope of this library?** +Consider and describe why the feature would be beneficial in this library, and not implemented as a separate project using this as a dependency. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md new file mode 100644 index 0000000..fe5cc8d --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md @@ -0,0 +1,10 @@ +--- +name: Other issue +about: Use this for other issues +title: '' +labels: '' +assignees: '' + +--- + +**Describe your issue** diff --git a/vendor/textalk/websocket/.github/workflows/acceptance.yml b/vendor/textalk/websocket/.github/workflows/acceptance.yml new file mode 100644 index 0000000..7bab97c --- /dev/null +++ b/vendor/textalk/websocket/.github/workflows/acceptance.yml @@ -0,0 +1,97 @@ +name: Acceptance + +on: [push, pull_request] + +jobs: + test-7-2: + runs-on: ubuntu-latest + name: Test PHP 7.2 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 7.2 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.2' + - name: Composer + run: make install + - name: Test + run: make test + + test-7-3: + runs-on: ubuntu-latest + name: Test PHP 7.3 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 7.3 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.3' + - name: Composer + run: make install + - name: Test + run: make test + + test-7-4: + runs-on: ubuntu-latest + name: Test PHP 7.4 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 7.4 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + - name: Composer + run: make install + - name: Test + run: make test + + test-8-0: + runs-on: ubuntu-latest + name: Test PHP 8.0 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + - name: Composer + run: make install + - name: Test + run: make test + + cs-check: + runs-on: ubuntu-latest + name: Code standard + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + - name: Composer + run: make install + - name: Code standard + run: make cs-check + + coverage: + runs-on: ubuntu-latest + name: Code coverage + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + extensions: xdebug + - name: Composer + run: make install + - name: Code coverage + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: make coverage diff --git a/vendor/textalk/websocket/.gitignore b/vendor/textalk/websocket/.gitignore new file mode 100644 index 0000000..379ab4b --- /dev/null +++ b/vendor/textalk/websocket/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.phpunit.result.cache +build/ +composer.lock +composer.phar +vendor/ \ No newline at end of file diff --git a/vendor/textalk/websocket/COPYING.md b/vendor/textalk/websocket/COPYING.md new file mode 100644 index 0000000..ba96480 --- /dev/null +++ b/vendor/textalk/websocket/COPYING.md @@ -0,0 +1,16 @@ +# Websocket: License + +Websocket PHP is free software released under the following license: + +[ISC License](http://en.wikipedia.org/wiki/ISC_license) + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/vendor/textalk/websocket/Makefile b/vendor/textalk/websocket/Makefile new file mode 100644 index 0000000..930a9ed --- /dev/null +++ b/vendor/textalk/websocket/Makefile @@ -0,0 +1,32 @@ +install: composer.phar + ./composer.phar install + +update: composer.phar + ./composer.phar self-update + ./composer.phar update + +test: composer.lock + ./vendor/bin/phpunit + +cs-check: composer.lock + ./vendor/bin/phpcs --standard=codestandard.xml lib tests examples + +coverage: composer.lock build + XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml + ./vendor/bin/php-coveralls -v + +composer.phar: + curl -s http://getcomposer.org/installer | php + +composer.lock: composer.phar + ./composer.phar --no-interaction install + +vendor/bin/phpunit: install + +build: + mkdir build + +clean: + rm composer.phar + rm -r vendor + rm -r build diff --git a/vendor/textalk/websocket/README.md b/vendor/textalk/websocket/README.md new file mode 100644 index 0000000..921efef --- /dev/null +++ b/vendor/textalk/websocket/README.md @@ -0,0 +1,67 @@ +# Websocket Client and Server for PHP + +[![Build Status](https://github.com/Textalk/websocket-php/actions/workflows/acceptance.yml/badge.svg)](https://github.com/Textalk/websocket-php/actions) +[![Coverage Status](https://coveralls.io/repos/github/Textalk/websocket-php/badge.svg?branch=master)](https://coveralls.io/github/Textalk/websocket-php) + +This library contains WebSocket client and server for PHP. + +The client and server provides methods for reading and writing to WebSocket streams. +It does not include convenience operations such as listeners and implicit error handling. + +## Documentation + +- [Client](docs/Client.md) +- [Server](docs/Server.md) +- [Message](docs/Message.md) +- [Examples](docs/Examples.md) +- [Changelog](docs/Changelog.md) +- [Contributing](docs/Contributing.md) + +## Installing + +Preferred way to install is with [Composer](https://getcomposer.org/). +``` +composer require textalk/websocket +``` + +* Current version support PHP versions `^7.2|8.0`. +* For PHP `7.1` support use version `1.4`. +* For PHP `^5.4` and `7.0` support use version `1.3`. + +## Client + +The [client](docs/Client.md) can read and write on a WebSocket stream. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +$client->text("Hello WebSocket.org!"); +echo $client->receive(); +$client->close(); +``` + +## Server + +The library contains a rudimentary single stream/single thread [server](docs/Server.md). +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +Note that it does **not** support threading or automatic association ot continuous client requests. +If you require this kind of server behavior, you need to build it on top of provided server implementation. + +```php +$server = new WebSocket\Server(); +$server->accept(); +$message = $server->receive(); +$server->text($message); +$server->close(); +``` + +### License and Contributors + +[ISC License](COPYING.md) + +Fredrik Liljegren, Armen Baghumian Sankbarani, Ruslan Bekenev, +Joshua Thijssen, Simon Lipp, Quentin Bellus, Patrick McCarren, swmcdonnell, +Ignas Bernotas, Mark Herhold, Andreas Palm, Sören Jensen, pmaasz, Alexey Stavrov, +Michael Slezak, Pierre Seznec, rmeisler, Nickolay V. Shmyrev, Christoph Kempen, +Marc Roberts, Antonio Mora, Simon Podlipsky. diff --git a/vendor/textalk/websocket/codestandard.xml b/vendor/textalk/websocket/codestandard.xml new file mode 100644 index 0000000..bb1cd26 --- /dev/null +++ b/vendor/textalk/websocket/codestandard.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vendor/textalk/websocket/composer.json b/vendor/textalk/websocket/composer.json new file mode 100644 index 0000000..9bc0dcc --- /dev/null +++ b/vendor/textalk/websocket/composer.json @@ -0,0 +1,34 @@ +{ + "name": "textalk/websocket", + "description": "WebSocket client and server", + "license": "ISC", + "type": "library", + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "soren@abicart.se" + } + ], + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "autoload-dev": { + "psr-4": { + "WebSocket\\": "tests/mock" + } + }, + "require": { + "php": "^7.2 | ^8.0", + "psr/log": "^1 | ^2 | ^3" + }, + "require-dev": { + "phpunit/phpunit": "^8.0|^9.0", + "php-coveralls/php-coveralls": "^2.0", + "squizlabs/php_codesniffer": "^3.5" + } +} diff --git a/vendor/textalk/websocket/docs/Changelog.md b/vendor/textalk/websocket/docs/Changelog.md new file mode 100644 index 0000000..6a45453 --- /dev/null +++ b/vendor/textalk/websocket/docs/Changelog.md @@ -0,0 +1,130 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • Changelog • [Contributing](Contributing.md) + +# Websocket: Changelog + +## `v1.5` + + > PHP version `^7.2|^8.0` + +### `1.5.5` + + * Support for psr/log v2 and v3 (@simPod) + * GitHub Actions replaces Travis (@sirn-se) + +### `1.5.4` + + * Keep open connection on read timeout (@marcroberts) + +### `1.5.3` + + * Fix for persistent connection (@sirn-se) + +### `1.5.2` + + * Fix for getName() method (@sirn-se) + +### `1.5.1` + + * Fix for persistent connections (@rmeisler) + +### `1.5.0` + + * Convenience send methods; text(), binary(), ping(), pong() (@sirn-se) + * Optional Message instance as receive() method return (@sirn-se) + * Opcode filter for receive() method (@sirn-se) + * Added PHP `8.0` support (@webpatser) + * Dropped PHP `7.1` support (@sirn-se) + * Fix for unordered fragmented messages (@sirn-se) + * Improved error handling on stream calls (@sirn-se) + * Various code re-write (@sirn-se) + +## `v1.4` + + > PHP version `^7.1` + +#### `1.4.3` + + * Solve stream closure/get meta conflict (@sirn-se) + * Examples and documentation overhaul (@sirn-se) + +#### `1.4.2` + + * Force stream close on read error (@sirn-se) + * Authorization headers line feed (@sirn-se) + * Documentation (@matias-pool, @sirn-se) + +#### `1.4.1` + + * Ping/Pong, handled internally to avoid breaking fragmented messages (@nshmyrev, @sirn-se) + * Fix for persistent connections (@rmeisler) + * Fix opcode bitmask (@peterjah) + +#### `1.4.0` + + * Dropped support of old PHP versions (@sirn-se) + * Added PSR-3 Logging support (@sirn-se) + * Persistent connection option (@slezakattack) + * TimeoutException on connection time out (@slezakattack) + +## `v1.3` + + > PHP version `^5.4` and `^7.0` + +#### `1.3.1` + + * Allow control messages without payload (@Logioniz) + * Error code in ConnectionException (@sirn-se) + +#### `1.3.0` + + * 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) + +## `v1.2` + + > PHP version `^5.4` and `^7.0` + +#### `1.2.0` + + * Adding stream context options (to set e.g. SSL `allow_self_signed`). + +## `v1.1` + + > PHP version `^5.4` and `^7.0` + +#### `1.1.2` + + * Fixed error message on broken frame. + +#### `1.1.1` + + * Adding license information. + +#### `1.1.0` + + * Supporting huge payloads. + +## `v1.0` + + > PHP version `^5.4` and `^7.0` + +#### `1.0.3` + + * Bugfix: Correcting address in error-message + +#### `1.0.2` + + * Bugfix: Add port in request-header. + +#### `1.0.1` + + * Fixing a bug from empty payloads. + +#### `1.0.0` + + * Release as production ready. + * Adding option to set/override headers. + * Supporting basic authentication from user:pass in URL. + diff --git a/vendor/textalk/websocket/docs/Client.md b/vendor/textalk/websocket/docs/Client.md new file mode 100644 index 0000000..e6154b6 --- /dev/null +++ b/vendor/textalk/websocket/docs/Client.md @@ -0,0 +1,137 @@ +Client • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Client + +The client can read and write on a WebSocket stream. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +## Class synopsis + +```php +WebSocket\Client { + + public __construct(string $uri, array $options = []) + public __destruct() + public __toString() : string + + public text(string $payload) : void + public binary(string $payload) : void + public ping(string $payload = '') : void + public pong(string $payload = '') : void + public send(mixed $payload, string $opcode = 'text', bool $masked = true) : void + public receive() : mixed + public close(int $status = 1000, mixed $message = 'ttfn') : mixed + + public getName() : string|null + public getPier() : string|null + public getLastOpcode() : string + public getCloseStatus() : int + public isConnected() : bool + public setTimeout(int $seconds) : void + public setFragmentSize(int $fragment_size) : self + public getFragmentSize() : int + public setLogger(Psr\Log\LoggerInterface $logger = null) : void +} +``` + +## Examples + +### Simple send-receive operation + +This example send a single message to a server, and output the response. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +$client->text("Hello WebSocket.org!"); +echo $client->receive(); +$client->close(); +``` + +### Listening to a server + +To continuously listen to incoming messages, you need to put the receive operation within a loop. +Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out. +By consuming exceptions, the code will re-connect the socket in next loop iteration. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +while (true) { + try { + $message = $client->receive(); + // Act on received message + // Break while loop to stop listening + } catch (\WebSocket\ConnectionException $e) { + // Possibly log errors + } +} +$client->close(); +``` + +### Filtering received messages + +By default the `receive()` method return messages of 'text' and 'binary' opcode. +The filter option allows you to specify which message types to return. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text']]); +$client->receive(); // Only return 'text' messages + +$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text', 'binary', 'ping', 'pong', 'close']]); +$client->receive(); // Return all messages +``` + +### Sending messages + +There are convenience methods to send messages with different opcodes. +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); + +// Convenience methods +$client->text('A plain text message'); // Send an opcode=text message +$client->binary($binary_string); // Send an opcode=binary message +$client->ping(); // Send an opcode=ping frame +$client->pong(); // Send an unsolicited opcode=pong frame + +// Generic send method +$client->send($payload); // Sent as masked opcode=text +$client->send($payload, 'binary'); // Sent as masked opcode=binary +$client->send($payload, 'binary', false); // Sent as unmasked opcode=binary +``` + +## Constructor options + +The `$options` parameter in constructor accepts an associative array of options. + +* `context` - A stream context created using [stream_context_create](https://www.php.net/manual/en/function.stream-context-create). +* `filter` - Array of opcodes to return on receive, default `['text', 'binary']` +* `fragment_size` - Maximum payload size. Default 4096 chars. +* `headers` - Additional headers as associative array name => content. +* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger. +* `persistent` - Connection is re-used between requests until time out is reached. Default false. +* `return_obj` - Return a [Message](Message.md) instance on receive, default false +* `timeout` - Time out in seconds. Default 5 seconds. + +```php +$context = stream_context_create(); +stream_context_set_option($context, 'ssl', 'verify_peer', false); +stream_context_set_option($context, 'ssl', 'verify_peer_name', false); + +$client = new WebSocket\Client("ws://echo.websocket.org/", [ + 'context' => $context, // Attach stream context created above + 'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return + 'headers' => [ // Additional headers, used to specify subprotocol + 'Sec-WebSocket-Protocol' => 'soap', + 'origin' => 'localhost', + ], + 'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger + 'return_obj' => true, // Return Message instance rather than just text + 'timeout' => 60, // 1 minute time out +]); +``` + +## Exceptions + +* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid. +* `WebSocket\BadUriException` - Thrown if provided URI is invalid. +* `WebSocket\ConnectionException` - Thrown on any socket I/O failure. +* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out. diff --git a/vendor/textalk/websocket/docs/Contributing.md b/vendor/textalk/websocket/docs/Contributing.md new file mode 100644 index 0000000..263d868 --- /dev/null +++ b/vendor/textalk/websocket/docs/Contributing.md @@ -0,0 +1,44 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • Contributing + +# Websocket: Contributing + +Everyone is welcome to help out! +But to keep this project sustainable, please ensure your contribution respects the requirements below. + +## PR Requirements + +Requirements on pull requests; +* All tests **MUST** pass. +* Code coverage **MUST** remain at 100%. +* Code **MUST** adhere to PSR-1 and PSR-12 code standards. + +## Dependency management + +Install or update dependencies using [Composer](https://getcomposer.org/). + +``` +# Install dependencies +make install + +# Update dependencies +make update +``` + +## Code standard + +This project uses [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/) code standards. +``` +# Check code standard adherence +make cs-check +``` + +## Unit testing + +Unit tests with [PHPUnit](https://phpunit.readthedocs.io/), coverage with [Coveralls](https://github.com/php-coveralls/php-coveralls) +``` +# Run unit tests +make test + +# Create coverage +make coverage +``` diff --git a/vendor/textalk/websocket/docs/Examples.md b/vendor/textalk/websocket/docs/Examples.md new file mode 100644 index 0000000..7dd4e0c --- /dev/null +++ b/vendor/textalk/websocket/docs/Examples.md @@ -0,0 +1,98 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • Examples • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Examples + +Here are some examples on how to use the WebSocket library. + +## Echo logger + +In dev environment (as in having run composer to include dev dependencies) you have +access to a simple echo logger that print out information synchronously. + +This is usable for debugging. For production, use a proper logger. + +```php +namespace WebSocket; + +$logger = new EchoLogger(); + +$client = new Client('ws://echo.websocket.org/'); +$client->setLogger($logger); + +$server = new Server(); +$server->setLogger($logger); +``` + +An example of server output; +``` +info | Server listening to port 8000 [] +debug | Wrote 129 of 129 bytes. [] +info | Server connected to port 8000 [] +info | Received 'text' message [] +debug | Wrote 9 of 9 bytes. [] +info | Sent 'text' message [] +debug | Received 'close', status: 1000. [] +debug | Wrote 32 of 32 bytes. [] +info | Sent 'close' message [] +info | Received 'close' message [] +``` + +## The `send` client + +Source: [examples/send.php](../examples/send.php) + +A simple, single send/receive client. + +Example use: +``` +php examples/send.php --opcode text "A text message" // Send a text message to localhost +php examples/send.php --opcode ping "ping it" // Send a ping message to localhost +php examples/send.php --uri ws://echo.websocket.org "A text message" // Send a text message to echo.websocket.org +php examples/send.php --opcode text --debug "A text message" // Use runtime debugging +``` + +## The `echoserver` server + +Source: [examples/echoserver.php](../examples/echoserver.php) + +A simple server that responds to recevied commands. + +Example use: +``` +php examples/echoserver.php // Run with default settings +php examples/echoserver.php --port 8080 // Listen on port 8080 +php examples/echoserver.php --debug // Use runtime debugging +``` + +These strings can be sent as message to trigger server to perform actions; +* `exit` - Server will initiate close procedure +* `ping` - Server will send a ping message +* `headers` - Server will respond with all headers provided by client +* `auth` - Server will respond with auth header if provided by client +* For other sent strings, server will respond with the same strings + +## The `random` client + +Source: [examples/random_client.php](../examples/random_client.php) + +The random client will use random options and continuously send/receive random messages. + +Example use: +``` +php examples/random_client.php --uri ws://echo.websocket.org // Connect to echo.websocket.org +php examples/random_client.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_client.php --debug // Use runtime debugging +``` + +## The `random` server + +Source: [examples/random_server.php](../examples/random_server.php) + +The random server will use random options and continuously send/receive random messages. + +Example use: +``` +php examples/random_server.php --port 8080 // // Listen on port 8080 +php examples/random_server.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_server.php --debug // Use runtime debugging +``` diff --git a/vendor/textalk/websocket/docs/Message.md b/vendor/textalk/websocket/docs/Message.md new file mode 100644 index 0000000..9bd0f2b --- /dev/null +++ b/vendor/textalk/websocket/docs/Message.md @@ -0,0 +1,60 @@ +[Client](Client.md) • [Server](Server.md) • Message • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Messages + +If option `return_obj` is set to `true` on [client](Client.md) or [server](Server.md), +the `receive()` method will return a Message instance instead of a string. + +Available classes correspond to opcode; +* WebSocket\Message\Text +* WebSocket\Message\Binary +* WebSocket\Message\Ping +* WebSocket\Message\Pong +* WebSocket\Message\Close + +Additionally; +* WebSocket\Message\Message - abstract base class for all messages above +* WebSocket\Message\Factory - Factory class to create Msssage instances + +## Message abstract class synopsis + +```php +WebSocket\Message\Message { + + public __construct(string $payload = '') + public __toString() : string + + public getOpcode() : string + public getLength() : int + public getTimestamp() : DateTime + public getContent() : string + public setContent(string $payload = '') : void + public hasContent() : bool +} +``` + +## Factory class synopsis + +```php +WebSocket\Message\Factory { + + public create(string $opcode, string $payload = '') : Message +} +``` + +## Example + +Receving a Message and echo some methods. + +```php +$client = new WebSocket\Client('ws://echo.websocket.org/', ['return_obj' => true]); +$client->text('Hello WebSocket.org!'); +// Echo return same message as sent +$message = $client->receive(); +echo $message->getOpcode(); // -> "text" +echo $message->getLength(); // -> 20 +echo $message->getContent(); // -> "Hello WebSocket.org!" +echo $message->hasContent(); // -> true +echo $message->getTimestamp()->format('H:i:s'); // -> 19:37:18 +$client->close(); +``` diff --git a/vendor/textalk/websocket/docs/Server.md b/vendor/textalk/websocket/docs/Server.md new file mode 100644 index 0000000..7d01a41 --- /dev/null +++ b/vendor/textalk/websocket/docs/Server.md @@ -0,0 +1,136 @@ +[Client](Client.md) • Server • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Server + +The library contains a rudimentary single stream/single thread server. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +Note that it does **not** support threading or automatic association ot continuous client requests. +If you require this kind of server behavior, you need to build it on top of provided server implementation. + +## Class synopsis + +```php +WebSocket\Server { + + public __construct(array $options = []) + public __destruct() + public __toString() : string + + public accept() : bool + public text(string $payload) : void + public binary(string $payload) : void + public ping(string $payload = '') : void + public pong(string $payload = '') : void + public send(mixed $payload, string $opcode = 'text', bool $masked = true) : void + public receive() : mixed + public close(int $status = 1000, mixed $message = 'ttfn') : mixed + + public getPort() : int + public getPath() : string + public getRequest() : array + public getHeader(string $header_name) : string|null + + public getName() : string|null + public getPier() : string|null + public getLastOpcode() : string + public getCloseStatus() : int + public isConnected() : bool + public setTimeout(int $seconds) : void + public setFragmentSize(int $fragment_size) : self + public getFragmentSize() : int + public setLogger(Psr\Log\LoggerInterface $logger = null) : void +} +``` + +## Examples + +### Simple receive-send operation + +This example reads a single message from a client, and respond with the same message. + +```php +$server = new WebSocket\Server(); +$server->accept(); +$message = $server->receive(); +$server->text($message); +$server->close(); +``` + +### Listening to clients + +To continuously listen to incoming messages, you need to put the receive operation within a loop. +Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out. +By consuming exceptions, the code will re-connect the socket in next loop iteration. + +```php +$server = new WebSocket\Server(); +while ($server->accept()) { + try { + $message = $server->receive(); + // Act on received message + // Break while loop to stop listening + } catch (\WebSocket\ConnectionException $e) { + // Possibly log errors + } +} +$server->close(); +``` + +### Filtering received messages + +By default the `receive()` method return messages of 'text' and 'binary' opcode. +The filter option allows you to specify which message types to return. + +```php +$server = new WebSocket\Server(['filter' => ['text']]); +$server->receive(); // only return 'text' messages + +$server = new WebSocket\Server(['filter' => ['text', 'binary', 'ping', 'pong', 'close']]); +$server->receive(); // return all messages +``` + +### Sending messages + +There are convenience methods to send messages with different opcodes. +```php +$server = new WebSocket\Server(); + +// Convenience methods +$server->text('A plain text message'); // Send an opcode=text message +$server->binary($binary_string); // Send an opcode=binary message +$server->ping(); // Send an opcode=ping frame +$server->pong(); // Send an unsolicited opcode=pong frame + +// Generic send method +$server->send($payload); // Sent as masked opcode=text +$server->send($payload, 'binary'); // Sent as masked opcode=binary +$server->send($payload, 'binary', false); // Sent as unmasked opcode=binary +``` + +## Constructor options + +The `$options` parameter in constructor accepts an associative array of options. + +* `filter` - Array of opcodes to return on receive, default `['text', 'binary']` +* `fragment_size` - Maximum payload size. Default 4096 chars. +* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger. +* `port` - The server port to listen to. Default 8000. +* `return_obj` - Return a [Message](Message.md) instance on receive, default false +* `timeout` - Time out in seconds. Default 5 seconds. + +```php +$server = new WebSocket\Server([ + 'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return + 'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger + 'port' => 9000, // Listening port + 'return_obj' => true, // Return Message insatnce rather than just text + 'timeout' => 60, // 1 minute time out +]); +``` + +## Exceptions + +* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid. +* `WebSocket\ConnectionException` - Thrown on any socket I/O failure. +* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out. diff --git a/vendor/textalk/websocket/examples/echoserver.php b/vendor/textalk/websocket/examples/echoserver.php new file mode 100644 index 0000000..231c4c9 --- /dev/null +++ b/vendor/textalk/websocket/examples/echoserver.php @@ -0,0 +1,87 @@ + : The port to listen to, default 8000 + * --timeout : Timeout in seconds, default 200 seconds + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +echo "> Random server\n"; + +// Server options specified or random +$options = array_merge([ + 'port' => 8000, + 'timeout' => 200, + 'filter' => ['text', 'binary', 'ping', 'pong'], +], getopt('', ['port:', 'timeout:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Setting timeout to 200 seconds to make time for all tests and manual runs. +try { + $server = new Server($options); +} catch (ConnectionException $e) { + echo "> ERROR: {$e->getMessage()}\n"; + die(); +} + +echo "> Listening to port {$server->getPort()}\n"; + +// Force quit to close server +while (true) { + try { + while ($server->accept()) { + echo "> Accepted on port {$server->getPort()}\n"; + while (true) { + $message = $server->receive(); + $opcode = $server->getLastOpcode(); + if (is_null($message)) { + echo "> Closing connection\n"; + continue 2; + } + echo "> Got '{$message}' [opcode: {$opcode}]\n"; + if (in_array($opcode, ['ping', 'pong'])) { + $server->send($message); + continue; + } + // Allow certain string to trigger server action + switch ($message) { + case 'exit': + echo "> Client told me to quit. Bye bye.\n"; + $server->close(); + echo "> Close status: {$server->getCloseStatus()}\n"; + exit; + case 'headers': + $server->text(implode("\r\n", $server->getRequest())); + break; + case 'ping': + $server->ping($message); + break; + case 'auth': + $auth = $server->getHeader('Authorization'); + $server->text("{$auth} - {$message}"); + break; + default: + $server->text($message); + } + } + } + } catch (ConnectionException $e) { + echo "> ERROR: {$e->getMessage()}\n"; + } +} diff --git a/vendor/textalk/websocket/examples/random_client.php b/vendor/textalk/websocket/examples/random_client.php new file mode 100644 index 0000000..b23bd6b --- /dev/null +++ b/vendor/textalk/websocket/examples/random_client.php @@ -0,0 +1,94 @@ + : The URI to connect to, default ws://localhost:8000 + * --timeout : Timeout in seconds, random default + * --fragment_size : Fragment size as bytes, random default + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +$randStr = function (int $maxlength = 4096) { + $string = ''; + $length = rand(1, $maxlength); + for ($i = 0; $i < $length; $i++) { + $string .= chr(rand(33, 126)); + } + return $string; +}; + +echo "> Random client\n"; + +// Server options specified or random +$options = array_merge([ + 'uri' => 'ws://localhost:8000', + 'timeout' => rand(1, 60), + 'fragment_size' => rand(1, 4096) * 8, +], getopt('', ['uri:', 'timeout:', 'fragment_size:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Main loop +while (true) { + try { + $client = new Client($options['uri'], $options); + $info = json_encode([ + 'uri' => $options['uri'], + 'timeout' => $options['timeout'], + 'framgemt_size' => $client->getFragmentSize(), + ]); + echo "> Creating client {$info}\n"; + + try { + while (true) { + // Random actions + switch (rand(1, 10)) { + case 1: + echo "> Sending text\n"; + $client->text("Text message {$randStr()}"); + break; + case 2: + echo "> Sending binary\n"; + $client->binary("Binary message {$randStr()}"); + break; + case 3: + echo "> Sending close\n"; + $client->close(rand(1000, 2000), "Close message {$randStr(8)}"); + break; + case 4: + echo "> Sending ping\n"; + $client->ping("Ping message {$randStr(8)}"); + break; + case 5: + echo "> Sending pong\n"; + $client->pong("Pong message {$randStr(8)}"); + break; + default: + echo "> Receiving\n"; + $received = $client->receive(); + echo "> Received {$client->getLastOpcode()}: {$received}\n"; + } + sleep(rand(1, 5)); + } + } catch (\Throwable $e) { + echo "ERROR I/O: {$e->getMessage()} [{$e->getCode()}]\n"; + } + } catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; + } + sleep(rand(1, 5)); +} diff --git a/vendor/textalk/websocket/examples/random_server.php b/vendor/textalk/websocket/examples/random_server.php new file mode 100644 index 0000000..0b0849c --- /dev/null +++ b/vendor/textalk/websocket/examples/random_server.php @@ -0,0 +1,93 @@ + : The port to listen to, default 8000 + * --timeout : Timeout in seconds, random default + * --fragment_size : Fragment size as bytes, random default + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +$randStr = function (int $maxlength = 4096) { + $string = ''; + $length = rand(1, $maxlength); + for ($i = 0; $i < $length; $i++) { + $string .= chr(rand(33, 126)); + } + return $string; +}; + +echo "> Random server\n"; + +// Server options specified or random +$options = array_merge([ + 'port' => 8000, + 'timeout' => rand(1, 60), + 'fragment_size' => rand(1, 4096) * 8, +], getopt('', ['port:', 'timeout:', 'fragment_size:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Force quit to close server +while (true) { + try { + // Setup server + $server = new Server($options); + $info = json_encode([ + 'port' => $server->getPort(), + 'timeout' => $options['timeout'], + 'framgemt_size' => $server->getFragmentSize(), + ]); + echo "> Creating server {$info}\n"; + + while ($server->accept()) { + while (true) { + // Random actions + switch (rand(1, 10)) { + case 1: + echo "> Sending text\n"; + $server->text("Text message {$randStr()}"); + break; + case 2: + echo "> Sending binary\n"; + $server->binary("Binary message {$randStr()}"); + break; + case 3: + echo "> Sending close\n"; + $server->close(rand(1000, 2000), "Close message {$randStr(8)}"); + break; + case 4: + echo "> Sending ping\n"; + $server->ping("Ping message {$randStr(8)}"); + break; + case 5: + echo "> Sending pong\n"; + $server->pong("Pong message {$randStr(8)}"); + break; + default: + echo "> Receiving\n"; + $received = $server->receive(); + echo "> Received {$server->getLastOpcode()}: {$received}\n"; + } + sleep(rand(1, 5)); + } + } + } catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; + } + sleep(rand(1, 5)); +} diff --git a/vendor/textalk/websocket/examples/send.php b/vendor/textalk/websocket/examples/send.php new file mode 100644 index 0000000..30e48e0 --- /dev/null +++ b/vendor/textalk/websocket/examples/send.php @@ -0,0 +1,51 @@ + + * + * Console options: + * --uri : The URI to connect to, default ws://localhost:8000 + * --opcode : Opcode to send, default 'text' + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +echo "> Send client\n"; + +// Server options specified or random +$options = array_merge([ + 'uri' => 'ws://localhost:8000', + 'opcode' => 'text', +], getopt('', ['uri:', 'opcode:', 'debug'])); +$message = array_pop($argv); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +try { + // Create client, send and recevie + $client = new Client($options['uri'], $options); + $client->send($message, $options['opcode']); + echo "> Sent '{$message}' [opcode: {$options['opcode']}]\n"; + if (in_array($options['opcode'], ['text', 'binary'])) { + $message = $client->receive(); + $opcode = $client->getLastOpcode(); + if (!is_null($message)) { + echo "> Got '{$message}' [opcode: {$opcode}]\n"; + } + } + $client->close(); + echo "> Closing client\n"; +} catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; +} diff --git a/vendor/textalk/websocket/lib/BadOpcodeException.php b/vendor/textalk/websocket/lib/BadOpcodeException.php new file mode 100644 index 0000000..a518715 --- /dev/null +++ b/vendor/textalk/websocket/lib/BadOpcodeException.php @@ -0,0 +1,7 @@ + 0, + 'text' => 1, + 'binary' => 2, + 'close' => 8, + 'ping' => 9, + 'pong' => 10, + ]; + + public function getLastOpcode(): ?string + { + return $this->last_opcode; + } + + public function getCloseStatus(): ?int + { + return $this->close_status; + } + + public function isConnected(): bool + { + return $this->socket && + (get_resource_type($this->socket) == 'stream' || + get_resource_type($this->socket) == 'persistent stream'); + } + + public function setTimeout(int $timeout): void + { + $this->options['timeout'] = $timeout; + + if ($this->isConnected()) { + stream_set_timeout($this->socket, $timeout); + } + } + + public function setFragmentSize(int $fragment_size): self + { + $this->options['fragment_size'] = $fragment_size; + return $this; + } + + public function getFragmentSize(): int + { + return $this->options['fragment_size']; + } + + public function setLogger(LoggerInterface $logger = null): void + { + $this->logger = $logger ?: new NullLogger(); + } + + public function send(string $payload, string $opcode = 'text', bool $masked = true): void + { + if (!$this->isConnected()) { + $this->connect(); + } + + if (!in_array($opcode, array_keys(self::$opcodes))) { + $warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'."; + $this->logger->warning($warning); + throw new BadOpcodeException($warning); + } + + $payload_chunks = str_split($payload, $this->options['fragment_size']); + $frame_opcode = $opcode; + + for ($index = 0; $index < count($payload_chunks); ++$index) { + $chunk = $payload_chunks[$index]; + $final = $index == count($payload_chunks) - 1; + + $this->sendFragment($final, $chunk, $frame_opcode, $masked); + + // all fragments after the first will be marked a continuation + $frame_opcode = 'continuation'; + } + + $this->logger->info("Sent '{$opcode}' message", [ + 'opcode' => $opcode, + 'content-length' => strlen($payload), + 'frames' => count($payload_chunks), + ]); + } + + /** + * Convenience method to send text message + * @param string $payload Content as string + */ + public function text(string $payload): void + { + $this->send($payload); + } + + /** + * Convenience method to send binary message + * @param string $payload Content as binary string + */ + public function binary(string $payload): void + { + $this->send($payload, 'binary'); + } + + /** + * Convenience method to send ping + * @param string $payload Optional text as string + */ + public function ping(string $payload = ''): void + { + $this->send($payload, 'ping'); + } + + /** + * Convenience method to send unsolicited pong + * @param string $payload Optional text as string + */ + public function pong(string $payload = ''): void + { + $this->send($payload, 'pong'); + } + + /** + * Get name of local socket, or null if not connected + * @return string|null + */ + public function getName(): ?string + { + return $this->isConnected() ? stream_socket_get_name($this->socket, false) : null; + } + + /** + * Get name of remote socket, or null if not connected + * @return string|null + */ + public function getPier(): ?string + { + return $this->isConnected() ? stream_socket_get_name($this->socket, true) : null; + } + + /** + * Get string representation of instance + * @return string String representation + */ + public function __toString(): string + { + return sprintf( + "%s(%s)", + get_class($this), + $this->getName() ?: 'closed' + ); + } + + /** + * Receive one message. + * Will continue reading until read message match filter settings. + * Return Message instance or string according to settings. + */ + protected function sendFragment(bool $final, string $payload, string $opcode, bool $masked): void + { + $data = ''; + + $byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker. + $byte_1 |= self::$opcodes[$opcode]; // Set opcode. + $data .= pack('C', $byte_1); + + $byte_2 = $masked ? 0b10000000 : 0b00000000; // Masking bit marker. + + // 7 bits of payload length... + $payload_length = strlen($payload); + if ($payload_length > 65535) { + $data .= pack('C', $byte_2 | 0b01111111); + $data .= pack('J', $payload_length); + } elseif ($payload_length > 125) { + $data .= pack('C', $byte_2 | 0b01111110); + $data .= pack('n', $payload_length); + } else { + $data .= pack('C', $byte_2 | $payload_length); + } + + // Handle masking + if ($masked) { + // generate a random mask: + $mask = ''; + for ($i = 0; $i < 4; $i++) { + $mask .= chr(rand(0, 255)); + } + $data .= $mask; + + // Append payload to frame: + for ($i = 0; $i < $payload_length; $i++) { + $data .= $payload[$i] ^ $mask[$i % 4]; + } + } else { + $data .= $payload; + } + + $this->write($data); + $this->logger->debug("Sent '{$opcode}' frame", [ + 'opcode' => $opcode, + 'final' => $final, + 'content-length' => strlen($payload), + ]); + } + + public function receive() + { + $filter = $this->options['filter']; + if (!$this->isConnected()) { + $this->connect(); + } + + do { + $response = $this->receiveFragment(); + list ($payload, $final, $opcode) = $response; + + // Continuation and factual opcode + $continuation = ($opcode == 'continuation'); + $payload_opcode = $continuation ? $this->read_buffer['opcode'] : $opcode; + + // Filter frames + if (!in_array($payload_opcode, $filter)) { + if ($payload_opcode == 'close') { + return null; // Always abort receive on close + } + $final = false; + continue; // Continue reading + } + + // First continuation frame, create buffer + if (!$final && !$continuation) { + $this->read_buffer = ['opcode' => $opcode, 'payload' => $payload, 'frames' => 1]; + continue; // Continue reading + } + + // Subsequent continuation frames, add to buffer + if ($continuation) { + $this->read_buffer['payload'] .= $payload; + $this->read_buffer['frames']++; + } + } while (!$final); + + // Final, return payload + $frames = 1; + if ($continuation) { + $payload = $this->read_buffer['payload']; + $frames = $this->read_buffer['frames']; + $this->read_buffer = null; + } + $this->logger->info("Received '{opcode}' message", [ + 'opcode' => $payload_opcode, + 'content-length' => strlen($payload), + 'frames' => $frames, + ]); + + $this->last_opcode = $payload_opcode; + $factory = new Factory(); + return $this->options['return_obj'] + ? $factory->create($payload_opcode, $payload) + : $payload; + } + + protected function receiveFragment(): array + { + // Read the fragment "header" first, two bytes. + $data = $this->read(2); + list ($byte_1, $byte_2) = array_values(unpack('C*', $data)); + + $final = (bool)($byte_1 & 0b10000000); // Final fragment marker. + $rsv = $byte_1 & 0b01110000; // Unused bits, ignore + + // Parse opcode + $opcode_int = $byte_1 & 0b00001111; + $opcode_ints = array_flip(self::$opcodes); + if (!array_key_exists($opcode_int, $opcode_ints)) { + $warning = "Bad opcode in websocket frame: {$opcode_int}"; + $this->logger->warning($warning); + throw new ConnectionException($warning, ConnectionException::BAD_OPCODE); + } + $opcode = $opcode_ints[$opcode_int]; + + // Masking bit + $mask = (bool)($byte_2 & 0b10000000); + + $payload = ''; + + // Payload length + $payload_length = $byte_2 & 0b01111111; + + if ($payload_length > 125) { + if ($payload_length === 126) { + $data = $this->read(2); // 126: Payload is a 16-bit unsigned int + $payload_length = current(unpack('n', $data)); + } else { + $data = $this->read(8); // 127: Payload is a 64-bit unsigned int + $payload_length = current(unpack('J', $data)); + } + } + + // Get masking key. + if ($mask) { + $masking_key = $this->read(4); + } + + // Get the actual payload, if any (might not be for e.g. close frames. + if ($payload_length > 0) { + $data = $this->read($payload_length); + + if ($mask) { + // Unmask payload. + for ($i = 0; $i < $payload_length; $i++) { + $payload .= ($data[$i] ^ $masking_key[$i % 4]); + } + } else { + $payload = $data; + } + } + + $this->logger->debug("Read '{opcode}' frame", [ + 'opcode' => $opcode, + 'final' => $final, + 'content-length' => strlen($payload), + ]); + + // if we received a ping, send a pong and wait for the next message + if ($opcode === 'ping') { + $this->logger->debug("Received 'ping', sending 'pong'."); + $this->send($payload, 'pong', true); + return [$payload, true, $opcode]; + } + + // if we received a pong, wait for the next message + if ($opcode === 'pong') { + $this->logger->debug("Received 'pong'."); + return [$payload, true, $opcode]; + } + + if ($opcode === 'close') { + $status_bin = ''; + $status = ''; + // Get the close status. + $status_bin = ''; + $status = ''; + if ($payload_length > 0) { + $status_bin = $payload[0] . $payload[1]; + $status = current(unpack('n', $payload)); + $this->close_status = $status; + } + // Get additional close message + if ($payload_length >= 2) { + $payload = substr($payload, 2); + } + + $this->logger->debug("Received 'close', status: {$this->close_status}."); + + if ($this->is_closing) { + $this->is_closing = false; // A close response, all done. + } else { + $this->send($status_bin . 'Close acknowledged: ' . $status, 'close', true); // Respond. + } + + // Close the socket. + fclose($this->socket); + + // Closing should not return message. + return [$payload, true, $opcode]; + } + + return [$payload, $final, $opcode]; + } + + /** + * Tell the socket to close. + * + * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4 + * @param string $message A closing message, max 125 bytes. + */ + public function close(int $status = 1000, string $message = 'ttfn'): void + { + if (!$this->isConnected()) { + return; + } + $status_binstr = sprintf('%016b', $status); + $status_str = ''; + foreach (str_split($status_binstr, 8) as $binstr) { + $status_str .= chr(bindec($binstr)); + } + $this->send($status_str . $message, 'close', true); + $this->logger->debug("Closing with status: {$status_str}."); + + $this->is_closing = true; + $this->receive(); // Receiving a close frame will close the socket now. + } + + /** + * Disconnect from client/server. + */ + public function disconnect(): void + { + if ($this->isConnected()) { + fclose($this->socket); + $this->socket = null; + } + } + + protected function write(string $data): void + { + $length = strlen($data); + $written = @fwrite($this->socket, $data); + if ($written === false) { + $this->throwException("Failed to write {$length} bytes."); + } + if ($written < strlen($data)) { + $this->throwException("Could only write {$written} out of {$length} bytes."); + } + $this->logger->debug("Wrote {$written} of {$length} bytes."); + } + + protected function read(string $length): string + { + $data = ''; + while (strlen($data) < $length) { + $buffer = @fread($this->socket, $length - strlen($data)); + + if (!$buffer) { + $meta = stream_get_meta_data($this->socket); + if (!empty($meta['timed_out'])) { + $message = 'Client read timeout'; + $this->logger->error($message, $meta); + throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta); + } + } + if ($buffer === false) { + $read = strlen($data); + $this->throwException("Broken frame, read {$read} of stated {$length} bytes."); + } + if ($buffer === '') { + $this->throwException("Empty read; connection dead?"); + } + $data .= $buffer; + $read = strlen($data); + $this->logger->debug("Read {$read} of {$length} bytes."); + } + return $data; + } + + protected function throwException(string $message, int $code = 0): void + { + $meta = ['closed' => true]; + if ($this->isConnected()) { + $meta = stream_get_meta_data($this->socket); + fclose($this->socket); + $this->socket = null; + } + if (!empty($meta['timed_out'])) { + $this->logger->error($message, $meta); + throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta); + } + if (!empty($meta['eof'])) { + $code = ConnectionException::EOF; + } + $this->logger->error($message, $meta); + throw new ConnectionException($message, $code, $meta); + } +} diff --git a/vendor/textalk/websocket/lib/Client.php b/vendor/textalk/websocket/lib/Client.php new file mode 100644 index 0000000..8cefaaa --- /dev/null +++ b/vendor/textalk/websocket/lib/Client.php @@ -0,0 +1,216 @@ + null, + 'filter' => ['text', 'binary'], + 'fragment_size' => 4096, + 'headers' => null, + 'logger' => null, + 'origin' => null, // @deprecated + 'persistent' => false, + 'return_obj' => false, + 'timeout' => 5, + ]; + + protected $socket_uri; + + /** + * @param string $uri A ws/wss-URI + * @param array $options + * Associative array containing: + * - context: Set the stream context. Default: empty context + * - timeout: Set the socket timeout in seconds. Default: 5 + * - fragment_size: Set framgemnt size. Default: 4096 + * - headers: Associative array of headers to set/override. + */ + public function __construct(string $uri, array $options = []) + { + $this->options = array_merge(self::$default_options, $options); + $this->socket_uri = $uri; + $this->setLogger($this->options['logger']); + } + + public function __destruct() + { + if ($this->isConnected() && get_resource_type($this->socket) !== 'persistent stream') { + fclose($this->socket); + } + $this->socket = null; + } + + /** + * Perform WebSocket handshake + */ + protected function connect(): void + { + $url_parts = parse_url($this->socket_uri); + if (empty($url_parts) || empty($url_parts['scheme']) || empty($url_parts['host'])) { + $error = "Invalid url '{$this->socket_uri}' provided."; + $this->logger->error($error); + throw new BadUriException($error); + } + $scheme = $url_parts['scheme']; + $host = $url_parts['host']; + $user = isset($url_parts['user']) ? $url_parts['user'] : ''; + $pass = isset($url_parts['pass']) ? $url_parts['pass'] : ''; + $port = isset($url_parts['port']) ? $url_parts['port'] : ($scheme === 'wss' ? 443 : 80); + $path = isset($url_parts['path']) ? $url_parts['path'] : '/'; + $query = isset($url_parts['query']) ? $url_parts['query'] : ''; + $fragment = isset($url_parts['fragment']) ? $url_parts['fragment'] : ''; + + $path_with_query = $path; + if (!empty($query)) { + $path_with_query .= '?' . $query; + } + if (!empty($fragment)) { + $path_with_query .= '#' . $fragment; + } + + if (!in_array($scheme, ['ws', 'wss'])) { + $error = "Url should have scheme ws or wss, not '{$scheme}' from URI '{$this->socket_uri}'."; + $this->logger->error($error); + throw new BadUriException($error); + } + + $host_uri = ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host; + + // Set the stream context options if they're already set in the config + if (isset($this->options['context'])) { + // Suppress the error since we'll catch it below + if (@get_resource_type($this->options['context']) === 'stream-context') { + $context = $this->options['context']; + } else { + $error = "Stream context in \$options['context'] isn't a valid context."; + $this->logger->error($error); + throw new \InvalidArgumentException($error); + } + } else { + $context = stream_context_create(); + } + + $persistent = $this->options['persistent'] === true; + $flags = STREAM_CLIENT_CONNECT; + $flags = $persistent ? $flags | STREAM_CLIENT_PERSISTENT : $flags; + + $error = $errno = $errstr = null; + set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) { + $this->logger->warning($message, ['severity' => $severity]); + $error = $message; + }, E_ALL); + + // Open the socket. + $this->socket = stream_socket_client( + "{$host_uri}:{$port}", + $errno, + $errstr, + $this->options['timeout'], + $flags, + $context + ); + + restore_error_handler(); + + if (!$this->isConnected()) { + $error = "Could not open socket to \"{$host}:{$port}\": {$errstr} ({$errno}) {$error}."; + $this->logger->error($error); + throw new ConnectionException($error); + } + + $address = "{$scheme}://{$host}{$path_with_query}"; + + if (!$persistent || ftell($this->socket) == 0) { + // Set timeout on the stream as well. + stream_set_timeout($this->socket, $this->options['timeout']); + + // Generate the WebSocket key. + $key = self::generateKey(); + + // Default headers + $headers = [ + 'Host' => $host . ":" . $port, + 'User-Agent' => 'websocket-client-php', + 'Connection' => 'Upgrade', + 'Upgrade' => 'websocket', + 'Sec-WebSocket-Key' => $key, + 'Sec-WebSocket-Version' => '13', + ]; + + // Handle basic authentication. + if ($user || $pass) { + $headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass); + } + + // Deprecated way of adding origin (use headers instead). + if (isset($this->options['origin'])) { + $headers['origin'] = $this->options['origin']; + } + + // Add and override with headers from options. + if (isset($this->options['headers'])) { + $headers = array_merge($headers, $this->options['headers']); + } + + $header = "GET " . $path_with_query . " HTTP/1.1\r\n" . implode( + "\r\n", + array_map( + function ($key, $value) { + return "$key: $value"; + }, + array_keys($headers), + $headers + ) + ) . "\r\n\r\n"; + + // Send headers. + $this->write($header); + + // Get server response header (terminated with double CR+LF). + $response = stream_get_line($this->socket, 1024, "\r\n\r\n"); + + // Validate response. + if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) { + $error = "Connection to '{$address}' failed: Server sent invalid upgrade response: {$response}"; + $this->logger->error($error); + throw new ConnectionException($error); + } + + $keyAccept = trim($matches[1]); + $expectedResonse + = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); + + if ($keyAccept !== $expectedResonse) { + $error = 'Server sent bad upgrade response.'; + $this->logger->error($error); + throw new ConnectionException($error); + } + } + + $this->logger->info("Client connected to {$address}"); + } + + /** + * Generate a random string for WebSocket key. + * + * @return string Random string + */ + protected static function generateKey(): string + { + $key = ''; + for ($i = 0; $i < 16; $i++) { + $key .= chr(rand(33, 126)); + } + return base64_encode($key); + } +} diff --git a/vendor/textalk/websocket/lib/ConnectionException.php b/vendor/textalk/websocket/lib/ConnectionException.php new file mode 100644 index 0000000..7e1ecbf --- /dev/null +++ b/vendor/textalk/websocket/lib/ConnectionException.php @@ -0,0 +1,26 @@ +data = $data; + } + + public function getData(): array + { + return $this->data; + } +} diff --git a/vendor/textalk/websocket/lib/Exception.php b/vendor/textalk/websocket/lib/Exception.php new file mode 100644 index 0000000..6482b7e --- /dev/null +++ b/vendor/textalk/websocket/lib/Exception.php @@ -0,0 +1,7 @@ +payload = $payload; + $this->timestamp = new DateTime(); + } + + public function getOpcode(): string + { + return $this->opcode; + } + + public function getLength(): int + { + return strlen($this->payload); + } + + public function getTimestamp(): DateTime + { + return $this->timestamp; + } + + public function getContent(): string + { + return $this->payload; + } + + public function setContent(string $payload = ''): void + { + $this->payload = $payload; + } + + public function hasContent(): bool + { + return $this->payload != ''; + } + + public function __toString(): string + { + return get_class($this); + } +} diff --git a/vendor/textalk/websocket/lib/Message/Ping.php b/vendor/textalk/websocket/lib/Message/Ping.php new file mode 100644 index 0000000..908d233 --- /dev/null +++ b/vendor/textalk/websocket/lib/Message/Ping.php @@ -0,0 +1,8 @@ + ['text', 'binary'], + 'fragment_size' => 4096, + 'logger' => null, + 'port' => 8000, + 'return_obj' => false, + 'timeout' => null, + ]; + + protected $addr; + protected $port; + protected $listening; + protected $request; + protected $request_path; + + /** + * @param array $options + * Associative array containing: + * - timeout: Set the socket timeout in seconds. + * - fragment_size: Set framgemnt size. Default: 4096 + * - port: Chose port for listening. Default 8000. + */ + public function __construct(array $options = []) + { + $this->options = array_merge(self::$default_options, $options); + $this->port = $this->options['port']; + $this->setLogger($this->options['logger']); + + $error = $errno = $errstr = null; + set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) { + $this->logger->warning($message, ['severity' => $severity]); + $error = $message; + }, E_ALL); + + do { + $this->listening = stream_socket_server("tcp://0.0.0.0:$this->port", $errno, $errstr); + } while ($this->listening === false && $this->port++ < 10000); + + restore_error_handler(); + + if (!$this->listening) { + $error = "Could not open listening socket: {$errstr} ({$errno}) {$error}"; + $this->logger->error($error); + throw new ConnectionException($error, (int)$errno); + } + + $this->logger->info("Server listening to port {$this->port}"); + } + + public function __destruct() + { + if ($this->isConnected()) { + fclose($this->socket); + } + $this->socket = null; + } + + public function getPort(): int + { + return $this->port; + } + + public function getPath(): string + { + return $this->request_path; + } + + public function getRequest(): array + { + return $this->request; + } + + public function getHeader($header): ?string + { + foreach ($this->request as $row) { + if (stripos($row, $header) !== false) { + list($headername, $headervalue) = explode(":", $row); + return trim($headervalue); + } + } + return null; + } + + public function accept(): bool + { + $this->socket = null; + return (bool)$this->listening; + } + + protected function connect(): void + { + + $error = null; + set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) { + $this->logger->warning($message, ['severity' => $severity]); + $error = $message; + }, E_ALL); + + if (isset($this->options['timeout'])) { + $this->socket = stream_socket_accept($this->listening, $this->options['timeout']); + } else { + $this->socket = stream_socket_accept($this->listening); + } + + restore_error_handler(); + + if (!$this->socket) { + $this->throwException("Server failed to connect. {$error}"); + } + if (isset($this->options['timeout'])) { + stream_set_timeout($this->socket, $this->options['timeout']); + } + + $this->logger->info("Client has connected to port {port}", [ + 'port' => $this->port, + 'pier' => stream_socket_get_name($this->socket, true), + ]); + $this->performHandshake(); + } + + protected function performHandshake(): void + { + $request = ''; + do { + $buffer = stream_get_line($this->socket, 1024, "\r\n"); + $request .= $buffer . "\n"; + $metadata = stream_get_meta_data($this->socket); + } while (!feof($this->socket) && $metadata['unread_bytes'] > 0); + + if (!preg_match('/GET (.*) HTTP\//mUi', $request, $matches)) { + $error = "No GET in request: {$request}"; + $this->logger->error($error); + throw new ConnectionException($error); + } + $get_uri = trim($matches[1]); + $uri_parts = parse_url($get_uri); + + $this->request = explode("\n", $request); + $this->request_path = $uri_parts['path']; + /// @todo Get query and fragment as well. + + if (!preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $request, $matches)) { + $error = "Client had no Key in upgrade request: {$request}"; + $this->logger->error($error); + throw new ConnectionException($error); + } + + $key = trim($matches[1]); + + /// @todo Validate key length and base 64... + $response_key = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); + + $header = "HTTP/1.1 101 Switching Protocols\r\n" + . "Upgrade: websocket\r\n" + . "Connection: Upgrade\r\n" + . "Sec-WebSocket-Accept: $response_key\r\n" + . "\r\n"; + + $this->write($header); + $this->logger->debug("Handshake on {$get_uri}"); + } +} diff --git a/vendor/textalk/websocket/lib/TimeoutException.php b/vendor/textalk/websocket/lib/TimeoutException.php new file mode 100644 index 0000000..d20e622 --- /dev/null +++ b/vendor/textalk/websocket/lib/TimeoutException.php @@ -0,0 +1,7 @@ + + + + + + tests + + + + + lib/ + + + diff --git a/vendor/textalk/websocket/tests/ClientTest.php b/vendor/textalk/websocket/tests/ClientTest.php new file mode 100644 index 0000000..c7bdc16 --- /dev/null +++ b/vendor/textalk/websocket/tests/ClientTest.php @@ -0,0 +1,449 @@ +send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals(4096, $client->getFragmentSize()); + + MockSocket::initialize('send-receive', $this); + $client->send('Sending a message'); + $message = $client->receive(); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals('text', $client->getLastOpcode()); + + MockSocket::initialize('client.close', $this); + $this->assertTrue($client->isConnected()); + $this->assertNull($client->getCloseStatus()); + + $client->close(); + $this->assertFalse($client->isConnected()); + $this->assertEquals(1000, $client->getCloseStatus()); + + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testDestruct(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('client.destruct', $this); + } + + public function testClienExtendedUrl(): void + { + MockSocket::initialize('client.connect-extended', $this); + $client = new Client('ws://localhost:8000/my/mock/path?my_query=yes#my_fragment'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientWithTimeout(): void + { + MockSocket::initialize('client.connect-timeout', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['timeout' => 300]); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientWithContext(): void + { + MockSocket::initialize('client.connect-context', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['context' => '@mock-stream-context']); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientAuthed(): void + { + MockSocket::initialize('client.connect-authed', $this); + $client = new Client('wss://usename:password@localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testWithHeaders(): void + { + MockSocket::initialize('client.connect-headers', $this); + $client = new Client('ws://localhost:8000/my/mock/path', [ + 'origin' => 'Origin header', + 'headers' => ['Generic header' => 'Generic content'], + ]); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload128(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.128.txt'); + + MockSocket::initialize('send-receive-128', $this); + $client->send($payload, 'text', false); + $message = $client->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload65536(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt'); + $client->setFragmentSize(65540); + + MockSocket::initialize('send-receive-65536', $this); + $client->send($payload, 'text', false); + $message = $client->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals(65540, $client->getFragmentSize()); + } + + public function testMultiFragment(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('send-receive-multi-fragment', $this); + $client->setFragmentSize(8); + $client->send('Multi fragment test'); + $message = $client->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals(8, $client->getFragmentSize()); + } + + public function testPingPong(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('ping-pong', $this); + $client->send('Server ping', 'ping'); + $client->send('', 'ping'); + $message = $client->receive(); + $this->assertEquals('Receiving a message', $message); + $this->assertEquals('text', $client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testRemoteClose(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('close-remote', $this); + + $message = $client->receive(); + $this->assertNull($message); + + $this->assertFalse($client->isConnected()); + $this->assertEquals(17260, $client->getCloseStatus()); + $this->assertNull($client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testSetTimeout(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('config-timeout', $this); + $client->setTimeout(300); + $this->assertTrue($client->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testReconnect(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('client.close', $this); + $this->assertTrue($client->isConnected()); + $this->assertNull($client->getCloseStatus()); + $client->close(); + $this->assertFalse($client->isConnected()); + $this->assertEquals(1000, $client->getCloseStatus()); + $this->assertNull($client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('client.reconnect', $this); + $message = $client->receive(); + $this->assertTrue($client->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPersistentConnection(): void + { + MockSocket::initialize('client.connect-persistent', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]); + $client->send('Connect'); + $client->disconnect(); + $this->assertFalse($client->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testBadScheme(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('bad://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\BadUriException'); + $this->expectExceptionMessage('Url should have scheme ws or wss'); + $client->send('Connect'); + } + + public function testBadUrl(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('this is not an url'); + $this->expectException('WebSocket\BadUriException'); + $this->expectExceptionMessage('Invalid url \'this is not an url\' provided.'); + $client->send('Connect'); + } + + public function testBadStreamContext(): void + { + MockSocket::initialize('client.connect-bad-context', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['context' => 'BAD']); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Stream context in $options[\'context\'] isn\'t a valid context'); + $client->send('Connect'); + } + + public function testFailedConnection(): void + { + MockSocket::initialize('client.connect-failed', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open socket to "localhost:8000"'); + $client->send('Connect'); + } + + public function testFailedConnectionWithError(): void + { + MockSocket::initialize('client.connect-error', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open socket to "localhost:8000"'); + $client->send('Connect'); + } + + public function testInvalidUpgrade(): void + { + MockSocket::initialize('client.connect-invalid-upgrade', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Connection to \'ws://localhost/my/mock/path\' failed'); + $client->send('Connect'); + } + + public function testInvalidKey(): void + { + MockSocket::initialize('client.connect-invalid-key', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server sent bad upgrade response'); + $client->send('Connect'); + } + + public function testSendBadOpcode(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + + MockSocket::initialize('send-bad-opcode', $this); + $this->expectException('WebSocket\BadOpcodeException'); + $this->expectExceptionMessage('Bad opcode \'bad\'. Try \'text\' or \'binary\'.'); + $client->send('Bad Opcode', 'bad'); + } + + public function testRecieveBadOpcode(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-bad-opcode', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1026); + $this->expectExceptionMessage('Bad opcode in websocket frame: 12'); + $message = $client->receive(); + } + + public function testBrokenWrite(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('send-broken-write', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Could only write 18 out of 22 bytes.'); + $client->send('Failing to write'); + } + + public function testFailedWrite(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('send-failed-write', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Failed to write 22 bytes.'); + $client->send('Failing to write'); + } + + public function testBrokenRead(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-broken-read', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.'); + $client->receive(); + } + + public function testReadTimeout(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-client-timeout', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Client read timeout'); + $client->receive(); + } + + public function testEmptyRead(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-empty-read', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Empty read; connection dead?'); + $client->receive(); + } + + public function testFrameFragmentation(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client( + 'ws://localhost:8000/my/mock/path', + ['filter' => ['text', 'binary', 'pong', 'close']] + ); + $client->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $client->receive(); + $this->assertEquals('Server ping', $message); + $this->assertEquals('pong', $client->getLastOpcode()); + $message = $client->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertEquals('text', $client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $client->receive(); + $this->assertEquals('Closing', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertFalse($client->isConnected()); + $this->assertEquals(17260, $client->getCloseStatus()); + $this->assertEquals('close', $client->getLastOpcode()); + } + + public function testMessageFragmentation(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client( + 'ws://localhost:8000/my/mock/path', + ['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true] + ); + $client->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $client->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Pong', $message); + $this->assertEquals('Server ping', $message->getContent()); + $this->assertEquals('pong', $message->getOpcode()); + $message = $client->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $this->assertEquals('Multi fragment test', $message->getContent()); + $this->assertEquals('text', $message->getOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $client->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Close', $message); + $this->assertEquals('Closing', $message->getContent()); + $this->assertEquals('close', $message->getOpcode()); + } + + public function testConvenicanceMethods(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->assertNull($client->getName()); + $this->assertNull($client->getPier()); + $this->assertEquals('WebSocket\Client(closed)', "{$client}"); + $client->text('Connect'); + MockSocket::initialize('send-convenicance', $this); + $client->binary(base64_encode('Binary content')); + $client->ping(); + $client->pong(); + $this->assertEquals('127.0.0.1:12345', $client->getName()); + $this->assertEquals('127.0.0.1:8000', $client->getPier()); + $this->assertEquals('WebSocket\Client(127.0.0.1:12345)', "{$client}"); + } +} diff --git a/vendor/textalk/websocket/tests/ExceptionTest.php b/vendor/textalk/websocket/tests/ExceptionTest.php new file mode 100644 index 0000000..84b939c --- /dev/null +++ b/vendor/textalk/websocket/tests/ExceptionTest.php @@ -0,0 +1,52 @@ + 'with data'], + new TimeoutException( + 'Nested exception', + ConnectionException::TIMED_OUT + ) + ); + } catch (Throwable $e) { + } + + $this->assertInstanceOf('WebSocket\ConnectionException', $e); + $this->assertInstanceOf('WebSocket\Exception', $e); + $this->assertInstanceOf('Exception', $e); + $this->assertInstanceOf('Throwable', $e); + $this->assertEquals('An error message', $e->getMessage()); + $this->assertEquals(1025, $e->getCode()); + $this->assertEquals(['test' => 'with data'], $e->getData()); + + $p = $e->getPrevious(); + $this->assertInstanceOf('WebSocket\TimeoutException', $p); + $this->assertInstanceOf('WebSocket\ConnectionException', $p); + $this->assertEquals('Nested exception', $p->getMessage()); + $this->assertEquals(1024, $p->getCode()); + $this->assertEquals([], $p->getData()); + } +} diff --git a/vendor/textalk/websocket/tests/MessageTest.php b/vendor/textalk/websocket/tests/MessageTest.php new file mode 100644 index 0000000..2d06ab7 --- /dev/null +++ b/vendor/textalk/websocket/tests/MessageTest.php @@ -0,0 +1,61 @@ +create('text', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $message = $factory->create('binary', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Binary', $message); + $message = $factory->create('ping', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Ping', $message); + $message = $factory->create('pong', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Pong', $message); + $message = $factory->create('close', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Close', $message); + } + + public function testMessage() + { + $message = new Text('Some content'); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $this->assertEquals('Some content', $message->getContent()); + $this->assertEquals('text', $message->getOpcode()); + $this->assertEquals(12, $message->getLength()); + $this->assertTrue($message->hasContent()); + $this->assertInstanceOf('DateTime', $message->getTimestamp()); + $message->setContent(''); + $this->assertEquals(0, $message->getLength()); + $this->assertFalse($message->hasContent()); + $this->assertEquals('WebSocket\Message\Text', "{$message}"); + } + + public function testBadOpcode() + { + $factory = new Factory(); + $this->expectException('WebSocket\BadOpcodeException'); + $this->expectExceptionMessage("Invalid opcode 'invalid' provided"); + $message = $factory->create('invalid', 'Some content'); + } +} diff --git a/vendor/textalk/websocket/tests/README.md b/vendor/textalk/websocket/tests/README.md new file mode 100644 index 0000000..b710a7e --- /dev/null +++ b/vendor/textalk/websocket/tests/README.md @@ -0,0 +1,29 @@ +# Testing + +Unit tests with [PHPUnit](https://phpunit.readthedocs.io/). + + +## How to run + +To run all test, run in console. + +``` +make test +``` + + +## Continuous integration + +GitHub Actions are run on PHP versions +`7.2`, `7.3`, `7.4` and `8.0`. + +Code coverage by [Coveralls](https://coveralls.io/github/Textalk/websocket-php). + + +## Test strategy + +Test set up overloads various stream and socket functions, +and use "scripts" to define and mock input/output of these functions. + +This set up negates the dependency on running servers, +and allow testing various errors that might occur. diff --git a/vendor/textalk/websocket/tests/ServerTest.php b/vendor/textalk/websocket/tests/ServerTest.php new file mode 100644 index 0000000..8294236 --- /dev/null +++ b/vendor/textalk/websocket/tests/ServerTest.php @@ -0,0 +1,448 @@ +assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertEquals(8000, $server->getPort()); + $this->assertEquals('/my/mock/path', $server->getPath()); + $this->assertTrue($server->isConnected()); + $this->assertEquals(4096, $server->getFragmentSize()); + $this->assertNull($server->getCloseStatus()); + $this->assertEquals([ + 'GET /my/mock/path HTTP/1.1', + 'host: localhost:8000', + 'user-agent: websocket-client-php', + 'connection: Upgrade', + 'upgrade: websocket', + 'sec-websocket-key: cktLWXhUdDQ2OXF0ZCFqOQ==', + 'sec-websocket-version: 13', + '', + '', + ], $server->getRequest()); + $this->assertEquals('websocket-client-php', $server->getHeader('USER-AGENT')); + $this->assertNull($server->getHeader('no such header')); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('send-receive', $this); + $server->send('Sending a message'); + $message = $server->receive(); + $this->assertEquals('Receiving a message', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertNull($server->getCloseStatus()); + $this->assertEquals('text', $server->getLastOpcode()); + + MockSocket::initialize('server.close', $this); + $server->close(); + $this->assertFalse($server->isConnected()); + $this->assertEquals(1000, $server->getCloseStatus()); + $this->assertTrue(MockSocket::isEmpty()); + + $server->close(); // Already closed + } + + public function testDestruct(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + + MockSocket::initialize('server.accept-destruct', $this); + $server->accept(); + $message = $server->receive(); + } + + public function testServerWithTimeout(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['timeout' => 300]); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept-timeout', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload128(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.128.txt'); + + MockSocket::initialize('send-receive-128', $this); + $server->send($payload, 'text', false); + $message = $server->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload65536(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt'); + $server->setFragmentSize(65540); + + MockSocket::initialize('send-receive-65536', $this); + $server->send($payload, 'text', false); + $message = $server->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testMultiFragment(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('send-receive-multi-fragment', $this); + $server->setFragmentSize(8); + $server->send('Multi fragment test'); + $message = $server->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPingPong(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('ping-pong', $this); + $server->send('Server ping', 'ping'); + $server->send('', 'ping'); + $message = $server->receive(); + $this->assertEquals('Receiving a message', $message); + $this->assertEquals('text', $server->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testRemoteClose(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('close-remote', $this); + + $message = $server->receive(); + $this->assertEquals('', $message); + + $this->assertTrue(MockSocket::isEmpty()); + $this->assertFalse($server->isConnected()); + $this->assertEquals(17260, $server->getCloseStatus()); + $this->assertNull($server->getLastOpcode()); + } + + public function testSetTimeout(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('config-timeout', $this); + $server->setTimeout(300); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testFailedSocketServer(): void + { + MockSocket::initialize('server.construct-failed-socket-server', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open listening socket:'); + $server = new Server(['port' => 9999]); + } + + public function testFailedSocketServerWithError(): void + { + MockSocket::initialize('server.construct-error-socket-server', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open listening socket:'); + $server = new Server(['port' => 9999]); + } + + public function testFailedConnect(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + + MockSocket::initialize('server.accept-failed-connect', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server failed to connect'); + $server->send('Connect'); + } + + public function testFailedConnectWithError(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + + MockSocket::initialize('server.accept-error-connect', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server failed to connect'); + $server->send('Connect'); + } + + public function testFailedConnectTimeout(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['timeout' => 300]); + + MockSocket::initialize('server.accept-failed-connect', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server failed to connect'); + $server->send('Connect'); + } + + public function testFailedHttp(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept-failed-http', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('No GET in request'); + $server->send('Connect'); + } + + public function testFailedWsKey(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept-failed-ws-key', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Client had no Key in upgrade request'); + $server->send('Connect'); + } + + public function testSendBadOpcode(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->expectException('WebSocket\BadOpcodeException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Bad opcode \'bad\'. Try \'text\' or \'binary\'.'); + $server->send('Bad Opcode', 'bad'); + } + + public function testRecieveBadOpcode(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-bad-opcode', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1026); + $this->expectExceptionMessage('Bad opcode in websocket frame: 12'); + $message = $server->receive(); + } + + public function testBrokenWrite(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('send-broken-write', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Could only write 18 out of 22 bytes.'); + $server->send('Failing to write'); + } + + public function testFailedWrite(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('send-failed-write', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Failed to write 22 bytes.'); + $server->send('Failing to write'); + } + + public function testBrokenRead(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-broken-read', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.'); + $server->receive(); + } + + public function testEmptyRead(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-empty-read', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Empty read; connection dead?'); + $server->receive(); + } + + public function testFrameFragmentation(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['filter' => ['text', 'binary', 'pong', 'close']]); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $server->receive(); + $this->assertEquals('Server ping', $message); + $this->assertEquals('pong', $server->getLastOpcode()); + $message = $server->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertEquals('text', $server->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $server->receive(); + $this->assertEquals('Closing', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertFalse($server->isConnected()); + $this->assertEquals(17260, $server->getCloseStatus()); + $this->assertEquals('close', $server->getLastOpcode()); + } + + public function testMessageFragmentation(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true]); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $server->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Pong', $message); + $this->assertEquals('Server ping', $message->getContent()); + $this->assertEquals('pong', $message->getOpcode()); + $message = $server->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $this->assertEquals('Multi fragment test', $message->getContent()); + $this->assertEquals('text', $message->getOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $server->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Close', $message); + $this->assertEquals('Closing', $message->getContent()); + $this->assertEquals('close', $message->getOpcode()); + } + + public function testConvenicanceMethods(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertNull($server->getName()); + $this->assertNull($server->getPier()); + $this->assertEquals('WebSocket\Server(closed)', "{$server}"); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->text('Connect'); + MockSocket::initialize('send-convenicance', $this); + $server->binary(base64_encode('Binary content')); + $server->ping(); + $server->pong(); + $this->assertEquals('127.0.0.1:12345', $server->getName()); + $this->assertEquals('127.0.0.1:8000', $server->getPier()); + $this->assertEquals('WebSocket\Server(127.0.0.1:12345)', "{$server}"); + $this->assertTrue(MockSocket::isEmpty()); + } +} diff --git a/vendor/textalk/websocket/tests/bootstrap.php b/vendor/textalk/websocket/tests/bootstrap.php new file mode 100644 index 0000000..5d6bdd0 --- /dev/null +++ b/vendor/textalk/websocket/tests/bootstrap.php @@ -0,0 +1,6 @@ +interpolate($message, $context); + $context_string = empty($context) ? '' : json_encode($context); + echo str_pad($level, 8) . " | {$message} {$context_string}\n"; + } + + public function interpolate($message, array $context = []) + { + // Build a replacement array with braces around the context keys + $replace = []; + foreach ($context as $key => $val) { + // Check that the value can be cast to string + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + + // Interpolate replacement values into the message and return + return strtr($message, $replace); + } +} diff --git a/vendor/textalk/websocket/tests/mock/MockSocket.php b/vendor/textalk/websocket/tests/mock/MockSocket.php new file mode 100644 index 0000000..e12d6ed --- /dev/null +++ b/vendor/textalk/websocket/tests/mock/MockSocket.php @@ -0,0 +1,79 @@ +assertEquals($current['function'], $function); + foreach ($current['params'] as $index => $param) { + self::$asserter->assertEquals($param, $params[$index], json_encode([$current, $params])); + } + if (isset($current['error'])) { + $map = array_merge(['msg' => 'Error', 'type' => E_USER_NOTICE], (array)$current['error']); + trigger_error($map['msg'], $map['type']); + } + if (isset($current['return-op'])) { + return self::op($current['return-op'], $params, $current['return']); + } + if (isset($current['return'])) { + return $current['return']; + } + return call_user_func_array($function, $params); + } + + // Check if all expected calls are performed + public static function isEmpty(): bool + { + return empty(self::$queue); + } + + // Initialize call queue + public static function initialize($op_file, $asserter): void + { + $file = dirname(__DIR__) . "/scripts/{$op_file}.json"; + self::$queue = json_decode(file_get_contents($file), true); + self::$asserter = $asserter; + } + + // Special output handling + private static function op($op, $params, $data) + { + switch ($op) { + case 'chr-array': + // Convert int array to string + $out = ''; + foreach ($data as $val) { + $out .= chr($val); + } + return $out; + case 'file': + $content = file_get_contents(__DIR__ . "/{$data[0]}"); + return substr($content, $data[1], $data[2]); + case 'key-save': + preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $params[1], $matches); + self::$stored['sec-websocket-key'] = trim($matches[1]); + return $data; + case 'key-respond': + $key = self::$stored['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; + $encoded = base64_encode(pack('H*', sha1($key))); + return str_replace('{key}', $encoded, $data); + } + return $data; + } +} diff --git a/vendor/textalk/websocket/tests/mock/mock-socket.php b/vendor/textalk/websocket/tests/mock/mock-socket.php new file mode 100644 index 0000000..1d6889a --- /dev/null +++ b/vendor/textalk/websocket/tests/mock/mock-socket.php @@ -0,0 +1,78 @@ +