This commit is contained in:
Enoch
2024-11-10 21:16:01 +08:00
commit eec520f969
157 changed files with 15607 additions and 0 deletions
@@ -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
@@ -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.
@@ -0,0 +1,10 @@
---
name: Other issue
about: Use this for other issues
title: ''
labels: ''
assignees: ''
---
**Describe your issue**
@@ -0,0 +1,97 @@
name: Acceptance
on: [push, pull_request]
jobs:
test-7-4:
runs-on: ubuntu-latest
name: Test PHP 7.4
steps:
- name: Checkout
uses: actions/checkout@v3
- 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@v3
- 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
test-8-1:
runs-on: ubuntu-latest
name: Test PHP 8.1
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 8.1
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Composer
run: make install
- name: Test
run: make test
test-8-2:
runs-on: ubuntu-latest
name: Test PHP 8.2
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 8.2
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- 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@v3
- 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@v3
- 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
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
.phpunit.result.cache
build/
composer.lock
composer.phar
vendor/
+16
View File
@@ -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.
+32
View File
@@ -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=PSR1,PSR12 --encoding=UTF-8 --report=full --colors 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
+76
View File
@@ -0,0 +1,76 @@
# 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)
## Archived project
This project has been archived and is no longer maintained. No bug fix and no additional features will be added.<br>
You won't be able to submit new issues or pull requests, and no additional features will be added
This library has been replaced by [sirn-se/websocket-php](https://github.com/sirn-se/websocket-php)
## Websocket Client and Server for 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)
- [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.4|^8.0`.
* For PHP `7.2` and `7.3` support use version [`1.5`](https://github.com/Textalk/websocket-php/tree/1.5.0).
* For PHP `7.1` support use version [`1.4`](https://github.com/Textalk/websocket-php/tree/1.4.0).
* For PHP `^5.4` and `7.0` support use version [`1.3`](https://github.com/Textalk/websocket-php/tree/1.3.0).
## 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, etrinh.
+36
View File
@@ -0,0 +1,36 @@
{
"name": "textalk/websocket",
"description": "WebSocket client and server",
"license": "ISC",
"type": "library",
"authors": [
{
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen"
}
],
"autoload": {
"psr-4": {
"WebSocket\\": "lib"
}
},
"autoload-dev": {
"psr-4": {
"WebSocket\\": "tests/mock"
}
},
"require": {
"php": "^7.4 | ^8.0",
"phrity/net-uri": "^1.0",
"phrity/util-errorhandler": "^1.0",
"psr/log": "^1.0 | ^2.0 | ^3.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"php-coveralls/php-coveralls": "^2.0",
"squizlabs/php_codesniffer": "^3.5"
}
}
+167
View File
@@ -0,0 +1,167 @@
[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • Changelog • [Contributing](Contributing.md)
# Websocket: Changelog
## `v1.6`
> PHP version `^7.4|^8.0`
### `1.6.3`
* Fix issue with implicit default ports (@etrinh, @sirn-se)
### `1.6.2`
* Fix issue where port was missing in socket uri (@sirn-se)
### `1.6.1`
* Fix client path for http request (@simPod, @sirn-se)
### `1.6.0`
* Connection separate from Client and Server (@sirn-se)
* getPier() deprecated, replaced by getRemoteName() (@sirn-se)
* Client accepts `Psr\Http\Message\UriInterface` as input for URI:s (@sirn-se)
* Bad URI throws exception when Client is instanciated, previously when used (@sirn-se)
* Preparations for multiple conection and listeners (@sirn-se)
* Major internal refactoring (@sirn-se)
## `v1.5`
> PHP version `^7.2|^8.0`
### `1.5.8`
* Handle read error during handshake (@sirn-se)
### `1.5.7`
* Large header block fix (@sirn-se)
### `1.5.6`
* Add test for PHP 8.1 (@sirn-se)
* Code standard (@sirn-se)
### `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.
+137
View File
@@ -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(UriInterface|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(Message|string $payload, string $opcode = 'text', bool $masked = true) : void;
public close(int $status = 1000, mixed $message = 'ttfn') : void;
public receive() : Message|string|null;
public getName() : string|null;
public getRemoteName() : 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.
+51
View File
@@ -0,0 +1,51 @@
[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.
Base your patch on corresponding version branch, and target that version branch in your pull request.
* `v1.6-master` current version
* `v1.5-master` previous version, bug fixes only
* Older versions should not be target of pull requests
## 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
```
+101
View File
@@ -0,0 +1,101 @@
[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;
* `auth` - Server will respond with auth header if provided by client
* `close` - Server will close current connection
* `exit` - Server will close all active connections
* `headers` - Server will respond with all headers provided by client
* `ping` - Server will send a ping message
* `pong` - Server will send a pong message
* `stop` - Server will stop listening
* 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
```
+60
View File
@@ -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 Message 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();
```
+136
View File
@@ -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(Message|string $payload, string $opcode = 'text', bool $masked = true) : void;
public close(int $status = 1000, mixed $message = 'ttfn') : void;
public receive() : Message|string|null;
public getPort() : int;
public getPath() : string;
public getRequest() : array;
public getHeader(string $header_name) : string|null;
public getName() : string|null;
public getRemoteName() : 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 instance 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.
+87
View File
@@ -0,0 +1,87 @@
<?php
/**
* This file is used for the tests, but can also serve as an example of a WebSocket\Server.
* Run in console: php examples/echoserver.php
*
* Console options:
* --port <int> : The port to listen to, default 8000
* --timeout <int> : 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 "> Echo server\n";
// Server options specified or random
$options = array_merge([
'port' => 8000,
'timeout' => 200,
'filter' => ['text', 'binary', 'ping', 'pong', 'close'],
], 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";
}
// Initiate server.
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";
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
/**
* Websocket client that read/write random data.
* Run in console: php examples/random_client.php
*
* Console options:
* --uri <uri> : The URI to connect to, default ws://localhost:8000
* --timeout <int> : Timeout in seconds, random default
* --fragment_size <int> : 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));
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/**
* Websocket server that read/write random data.
* Run in console: php examples/random_server.php
*
* Console options:
* --port <int> : The port to listen to, default 8000
* --timeout <int> : Timeout in seconds, random default
* --fragment_size <int> : 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));
}
+51
View File
@@ -0,0 +1,51 @@
<?php
/**
* Simple send & receive client for test purpose.
* Run in console: php examples/send.php <options> <message>
*
* Console options:
* --uri <uri> : The URI to connect to, default ws://localhost:8000
* --opcode <string> : 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";
}
+14
View File
@@ -0,0 +1,14 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
class BadOpcodeException extends Exception
{
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace WebSocket;
class BadUriException extends Exception
{
}
+490
View File
@@ -0,0 +1,490 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use ErrorException;
use InvalidArgumentException;
use Phrity\Net\Uri;
use Phrity\Util\ErrorHandler;
use Psr\Http\Message\UriInterface;
use Psr\Log\{
LoggerAwareInterface,
LoggerAwareTrait,
LoggerInterface,
NullLogger
};
use WebSocket\Message\Factory;
class Client implements LoggerAwareInterface
{
use LoggerAwareTrait; // provides setLogger(LoggerInterface $logger)
use OpcodeTrait;
// Default options
protected static $default_options = [
'context' => null,
'filter' => ['text', 'binary'],
'fragment_size' => 4096,
'headers' => null,
'logger' => null,
'origin' => null, // @deprecated
'persistent' => false,
'return_obj' => false,
'timeout' => 5,
];
private $socket_uri;
private $connection;
private $options = [];
private $listen = false;
private $last_opcode = null;
/* ---------- Magic methods ------------------------------------------------------ */
/**
* @param UriInterface|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($uri, array $options = [])
{
$this->socket_uri = $this->parseUri($uri);
$this->options = array_merge(self::$default_options, [
'logger' => new NullLogger(),
], $options);
$this->setLogger($this->options['logger']);
}
/**
* Get string representation of instance.
* @return string String representation.
*/
public function __toString(): string
{
return sprintf(
"%s(%s)",
get_class($this),
$this->getName() ?: 'closed'
);
}
/* ---------- Client option functions -------------------------------------------- */
/**
* Set timeout.
* @param int $timeout Timeout in seconds.
*/
public function setTimeout(int $timeout): void
{
$this->options['timeout'] = $timeout;
if (!$this->isConnected()) {
return;
}
$this->connection->setTimeout($timeout);
$this->connection->setOptions($this->options);
}
/**
* Set fragmentation size.
* @param int $fragment_size Fragment size in bytes.
* @return self.
*/
public function setFragmentSize(int $fragment_size): self
{
$this->options['fragment_size'] = $fragment_size;
$this->connection->setOptions($this->options);
return $this;
}
/**
* Get fragmentation size.
* @return int $fragment_size Fragment size in bytes.
*/
public function getFragmentSize(): int
{
return $this->options['fragment_size'];
}
/* ---------- Connection operations ---------------------------------------------- */
/**
* Send text message.
* @param string $payload Content as string.
*/
public function text(string $payload): void
{
$this->send($payload);
}
/**
* Send binary message.
* @param string $payload Content as binary string.
*/
public function binary(string $payload): void
{
$this->send($payload, 'binary');
}
/**
* Send ping.
* @param string $payload Optional text as string.
*/
public function ping(string $payload = ''): void
{
$this->send($payload, 'ping');
}
/**
* Send unsolicited pong.
* @param string $payload Optional text as string.
*/
public function pong(string $payload = ''): void
{
$this->send($payload, 'pong');
}
/**
* Send message.
* @param string $payload Message to send.
* @param string $opcode Opcode to use, default: 'text'.
* @param bool $masked If message should be masked default: true.
*/
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);
}
$factory = new Factory();
$message = $factory->create($opcode, $payload);
$this->connection->pushMessage($message, $masked);
}
/**
* 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;
}
$this->connection->close($status, $message);
}
/**
* Disconnect from server.
*/
public function disconnect(): void
{
if ($this->isConnected()) {
$this->connection->disconnect();
}
}
/**
* Receive message.
* Note that this operation will block reading.
* @return mixed Message, text or null depending on settings.
*/
public function receive()
{
$filter = $this->options['filter'];
$return_obj = $this->options['return_obj'];
if (!$this->isConnected()) {
$this->connect();
}
while (true) {
$message = $this->connection->pullMessage();
$opcode = $message->getOpcode();
if (in_array($opcode, $filter)) {
$this->last_opcode = $opcode;
$return = $return_obj ? $message : $message->getContent();
break;
} elseif ($opcode == 'close') {
$this->last_opcode = null;
$return = $return_obj ? $message : null;
break;
}
}
return $return;
}
/* ---------- Connection functions ----------------------------------------------- */
/**
* Get last received opcode.
* @return string|null Opcode.
*/
public function getLastOpcode(): ?string
{
return $this->last_opcode;
}
/**
* Get close status on connection.
* @return int|null Close status.
*/
public function getCloseStatus(): ?int
{
return $this->connection ? $this->connection->getCloseStatus() : null;
}
/**
* If Client has active connection.
* @return bool True if active connection.
*/
public function isConnected(): bool
{
return $this->connection && $this->connection->isConnected();
}
/**
* Get name of local socket, or null if not connected.
* @return string|null
*/
public function getName(): ?string
{
return $this->isConnected() ? $this->connection->getName() : null;
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
*/
public function getRemoteName(): ?string
{
return $this->isConnected() ? $this->connection->getRemoteName() : null;
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
* @deprecated Will be removed in future version, use getPeer() instead.
*/
public function getPier(): ?string
{
trigger_error(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
E_USER_DEPRECATED
);
return $this->getRemoteName();
}
/* ---------- Helper functions --------------------------------------------------- */
/**
* Perform WebSocket handshake
*/
protected function connect(): void
{
$this->connection = null;
$host_uri = $this->socket_uri
->withScheme($this->socket_uri->getScheme() == 'wss' ? 'ssl' : 'tcp')
->withPort($this->socket_uri->getPort() ?? ($this->socket_uri->getScheme() == 'wss' ? 443 : 80))
->withPath('')
->withQuery('')
->withFragment('')
->withUserInfo('');
// Path must be absolute
$http_path = $this->socket_uri->getPath();
if ($http_path === '' || $http_path[0] !== '/') {
$http_path = "/{$http_path}";
}
$http_uri = (new Uri())
->withPath($http_path)
->withQuery($this->socket_uri->getQuery());
// 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;
$socket = null;
try {
$handler = new ErrorHandler();
$socket = $handler->with(function () use ($host_uri, $flags, $context) {
$error = $errno = $errstr = null;
// Open the socket.
return stream_socket_client(
$host_uri,
$errno,
$errstr,
$this->options['timeout'],
$flags,
$context
);
});
if (!$socket) {
throw new ErrorException('No socket');
}
} catch (ErrorException $e) {
$error = "Could not open socket to \"{$host_uri->getAuthority()}\": {$e->getMessage()} ({$e->getCode()}).";
$this->logger->error($error, ['severity' => $e->getSeverity()]);
throw new ConnectionException($error, 0, [], $e);
}
$this->connection = new Connection($socket, $this->options);
$this->connection->setLogger($this->logger);
if (!$this->isConnected()) {
$error = "Invalid stream on \"{$host_uri->getAuthority()}\".";
$this->logger->error($error);
throw new ConnectionException($error);
}
if (!$persistent || $this->connection->tell() == 0) {
// Set timeout on the stream as well.
$this->connection->setTimeout($this->options['timeout']);
// Generate the WebSocket key.
$key = self::generateKey();
// Default headers
$headers = [
'Host' => $host_uri->getAuthority(),
'User-Agent' => 'websocket-client-php',
'Connection' => 'Upgrade',
'Upgrade' => 'websocket',
'Sec-WebSocket-Key' => $key,
'Sec-WebSocket-Version' => '13',
];
// Handle basic authentication.
if ($userinfo = $this->socket_uri->getUserInfo()) {
$headers['authorization'] = 'Basic ' . base64_encode($userinfo);
}
// 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 {$http_uri} 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->connection->write($header);
// Get server response header (terminated with double CR+LF).
$response = '';
try {
do {
$buffer = $this->connection->gets(1024);
$response .= $buffer;
} while (substr_count($response, "\r\n\r\n") == 0);
} catch (Exception $e) {
throw new ConnectionException('Client handshake error', $e->getCode(), $e->getData(), $e);
}
// Validate response.
if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
$error = sprintf(
"Connection to '%s' failed: Server sent invalid upgrade response: %s",
(string)$this->socket_uri,
(string)$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 {$this->socket_uri}");
}
/**
* 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);
}
protected function parseUri($uri): UriInterface
{
if ($uri instanceof UriInterface) {
$uri = $uri;
} elseif (is_string($uri)) {
try {
$uri = new Uri($uri);
} catch (InvalidArgumentException $e) {
throw new BadUriException("Invalid URI '{$uri}' provided.", 0, $e);
}
} else {
throw new BadUriException("Provided URI must be a UriInterface or string.");
}
if (!in_array($uri->getScheme(), ['ws', 'wss'])) {
throw new BadUriException("Invalid URI scheme, must be 'ws' or 'wss'.");
}
return $uri;
}
}
+518
View File
@@ -0,0 +1,518 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use Psr\Log\{
LoggerAwareInterface,
LoggerAwareTrait,
LoggerInterface, NullLogger
};
use WebSocket\Message\{
Factory,
Message
};
class Connection implements LoggerAwareInterface
{
use LoggerAwareTrait;
use OpcodeTrait;
private $stream;
private $read_buffer;
private $msg_factory;
private $options = [];
protected $is_closing = false;
protected $close_status = null;
private $uid;
/* ---------- Construct & Destruct ----------------------------------------------- */
public function __construct($stream, array $options = [])
{
$this->stream = $stream;
$this->setOptions($options);
$this->setLogger(new NullLogger());
$this->msg_factory = new Factory();
}
public function __destruct()
{
if ($this->getType() === 'stream') {
fclose($this->stream);
}
}
public function setOptions(array $options = []): void
{
$this->options = array_merge($this->options, $options);
}
public function getCloseStatus(): ?int
{
return $this->close_status;
}
/**
* 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));
}
$message = $this->msg_factory->create('close', $status_str . $message);
$this->pushMessage($message, true);
$this->logger->debug("Closing with status: {$status}.");
$this->is_closing = true;
while (true) {
$message = $this->pullMessage();
if ($message->getOpcode() == 'close') {
break;
}
}
}
/* ---------- Message methods ---------------------------------------------------- */
// Push a message to stream
public function pushMessage(Message $message, bool $masked = true): void
{
$frames = $message->getFrames($masked, $this->options['fragment_size']);
foreach ($frames as $frame) {
$this->pushFrame($frame);
}
$this->logger->info("[connection] Pushed {$message}", [
'opcode' => $message->getOpcode(),
'content-length' => $message->getLength(),
'frames' => count($frames),
]);
}
// Pull a message from stream
public function pullMessage(): Message
{
do {
$frame = $this->pullFrame();
$frame = $this->autoRespond($frame);
list ($final, $payload, $opcode, $masked) = $frame;
if ($opcode == 'close') {
$this->close();
}
// Continuation and factual opcode
$continuation = $opcode == 'continuation';
$payload_opcode = $continuation ? $this->read_buffer['opcode'] : $opcode;
// 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;
}
$message = $this->msg_factory->create($payload_opcode, $payload);
$this->logger->info("[connection] Pulled {$message}", [
'opcode' => $payload_opcode,
'content-length' => strlen($payload),
'frames' => $frames,
]);
return $message;
}
/* ---------- Frame I/O methods -------------------------------------------------- */
// Pull frame from stream
private function pullFrame(): 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
$masked = (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 ($masked) {
$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 ($masked) {
// Unmask payload.
for ($i = 0; $i < $payload_length; $i++) {
$payload .= ($data[$i] ^ $masking_key[$i % 4]);
}
} else {
$payload = $data;
}
}
$this->logger->debug("[connection] Pulled '{opcode}' frame", [
'opcode' => $opcode,
'final' => $final,
'content-length' => strlen($payload),
]);
return [$final, $payload, $opcode, $masked];
}
// Push frame to stream
private function pushFrame(array $frame): void
{
list ($final, $payload, $opcode, $masked) = $frame;
$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("[connection] Pushed '{$opcode}' frame", [
'opcode' => $opcode,
'final' => $final,
'content-length' => strlen($payload),
]);
}
// Trigger auto response for frame
private function autoRespond(array $frame)
{
list ($final, $payload, $opcode, $masked) = $frame;
$payload_length = strlen($payload);
switch ($opcode) {
case 'ping':
// If we received a ping, respond with a pong
$this->logger->debug("[connection] Received 'ping', sending 'pong'.");
$message = $this->msg_factory->create('pong', $payload);
$this->pushMessage($message, $masked);
return [$final, $payload, $opcode, $masked];
case 'close':
// If we received close, possibly acknowledge and close connection
$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("[connection] Received 'close', status: {$status}.");
if (!$this->is_closing) {
$ack = "{$status_bin}Close acknowledged: {$status}";
$message = $this->msg_factory->create('close', $ack);
$this->pushMessage($message, $masked);
} else {
$this->is_closing = false; // A close response, all done.
}
$this->disconnect();
return [$final, $payload, $opcode, $masked];
default:
return [$final, $payload, $opcode, $masked];
}
}
/* ---------- Stream I/O methods ------------------------------------------------- */
/**
* Close connection stream.
* @return bool
*/
public function disconnect(): bool
{
$this->logger->debug('Closing connection');
return fclose($this->stream);
}
/**
* If connected to stream.
* @return bool
*/
public function isConnected(): bool
{
return in_array($this->getType(), ['stream', 'persistent stream']);
}
/**
* Return type of connection.
* @return string|null Type of connection or null if invalid type.
*/
public function getType(): ?string
{
return get_resource_type($this->stream);
}
/**
* Get name of local socket, or null if not connected.
* @return string|null
*/
public function getName(): ?string
{
return stream_socket_get_name($this->stream, false);
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
*/
public function getRemoteName(): ?string
{
return stream_socket_get_name($this->stream, true);
}
/**
* Get meta data for connection.
* @return array
*/
public function getMeta(): array
{
return stream_get_meta_data($this->stream);
}
/**
* Returns current position of stream pointer.
* @return int
* @throws ConnectionException
*/
public function tell(): int
{
$tell = ftell($this->stream);
if ($tell === false) {
$this->throwException('Could not resolve stream pointer position');
}
return $tell;
}
/**
* If stream pointer is at end of file.
* @return bool
*/
public function eof(): int
{
return feof($this->stream);
}
/* ---------- Stream option methods ---------------------------------------------- */
/**
* Set time out on connection.
* @param int $seconds Timeout part in seconds
* @param int $microseconds Timeout part in microseconds
* @return bool
*/
public function setTimeout(int $seconds, int $microseconds = 0): bool
{
$this->logger->debug("Setting timeout {$seconds}:{$microseconds} seconds");
return stream_set_timeout($this->stream, $seconds, $microseconds);
}
/* ---------- Stream read/write methods ------------------------------------------ */
/**
* Read line from stream.
* @param int $length Maximum number of bytes to read
* @param string $ending Line delimiter
* @return string Read data
*/
public function getLine(int $length, string $ending): string
{
$line = stream_get_line($this->stream, $length, $ending);
if ($line === false) {
$this->throwException('Could not read from stream');
}
$read = strlen($line);
$this->logger->debug("Read {$read} bytes of line.");
return $line;
}
/**
* Read line from stream.
* @param int $length Maximum number of bytes to read
* @return string Read data
*/
public function gets(int $length): string
{
$line = fgets($this->stream, $length);
if ($line === false) {
$this->throwException('Could not read from stream');
}
$read = strlen($line);
$this->logger->debug("Read {$read} bytes of line.");
return $line;
}
/**
* Read characters from stream.
* @param int $length Maximum number of bytes to read
* @return string Read data
*/
public function read(string $length): string
{
$data = '';
while (strlen($data) < $length) {
$buffer = fread($this->stream, $length - strlen($data));
if (!$buffer) {
$meta = stream_get_meta_data($this->stream);
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;
}
/**
* Write characters to stream.
* @param string $data Data to read
*/
public function write(string $data): void
{
$length = strlen($data);
$written = fwrite($this->stream, $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.");
}
/* ---------- Internal helper methods -------------------------------------------- */
private function throwException(string $message, int $code = 0): void
{
$meta = ['closed' => true];
if ($this->isConnected()) {
$meta = $this->getMeta();
$this->disconnect();
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);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use Throwable;
class ConnectionException extends Exception
{
// Native codes in interval 0-106
public const TIMED_OUT = 1024;
public const EOF = 1025;
public const BAD_OPCODE = 1026;
private $data;
public function __construct(string $message, int $code = 0, array $data = [], Throwable $prev = null)
{
parent::__construct($message, $code, $prev);
$this->data = $data;
}
public function getData(): array
{
return $this->data;
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace WebSocket;
class Exception extends \Exception
{
}
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Binary extends Message
{
protected $opcode = 'binary';
}
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Close extends Message
{
protected $opcode = 'close';
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
use WebSocket\BadOpcodeException;
class Factory
{
public function create(string $opcode, string $payload = ''): Message
{
switch ($opcode) {
case 'text':
return new Text($payload);
case 'binary':
return new Binary($payload);
case 'ping':
return new Ping($payload);
case 'pong':
return new Pong($payload);
case 'close':
return new Close($payload);
}
throw new BadOpcodeException("Invalid opcode '{$opcode}' provided");
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
use DateTime;
abstract class Message
{
protected $opcode;
protected $payload;
protected $timestamp;
public function __construct(string $payload = '')
{
$this->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);
}
// Split messages into frames
public function getFrames(bool $masked = true, int $framesize = 4096): array
{
$frames = [];
$split = str_split($this->getContent(), $framesize) ?: [''];
foreach ($split as $payload) {
$frames[] = [false, $payload, 'continuation', $masked];
}
$frames[0][2] = $this->opcode;
$frames[array_key_last($frames)][0] = true;
return $frames;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Ping extends Message
{
protected $opcode = 'ping';
}
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Pong extends Message
{
protected $opcode = 'pong';
}
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Text extends Message
{
protected $opcode = 'text';
}
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
trait OpcodeTrait
{
private static $opcodes = [
'continuation' => 0,
'text' => 1,
'binary' => 2,
'close' => 8,
'ping' => 9,
'pong' => 10,
];
}
+470
View File
@@ -0,0 +1,470 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use Closure;
use ErrorException;
use Phrity\Util\ErrorHandler;
use Psr\Log\{
LoggerAwareInterface,
LoggerAwareTrait,
LoggerInterface,
NullLogger
};
use Throwable;
use WebSocket\Message\Factory;
class Server implements LoggerAwareInterface
{
use LoggerAwareTrait; // Provides setLogger(LoggerInterface $logger)
use OpcodeTrait;
// Default options
protected static $default_options = [
'filter' => ['text', 'binary'],
'fragment_size' => 4096,
'logger' => null,
'port' => 8000,
'return_obj' => false,
'timeout' => null,
];
protected $port;
protected $listening;
protected $request;
protected $request_path;
private $connections = [];
private $options = [];
private $listen = false;
private $last_opcode;
/* ---------- Magic methods ------------------------------------------------------ */
/**
* @param array $options
* Associative array containing:
* - filter: Array of opcodes to handle. Default: ['text', 'binary'].
* - fragment_size: Set framgemnt size. Default: 4096
* - logger: PSR-3 compatible logger. Default NullLogger.
* - port: Chose port for listening. Default 8000.
* - return_obj: If receive() function return Message instance. Default false.
* - timeout: Set the socket timeout in seconds.
*/
public function __construct(array $options = [])
{
$this->options = array_merge(self::$default_options, [
'logger' => new NullLogger(),
], $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}");
}
/**
* Get string representation of instance.
* @return string String representation.
*/
public function __toString(): string
{
return sprintf(
"%s(%s)",
get_class($this),
$this->getName() ?: 'closed'
);
}
/* ---------- Server operations -------------------------------------------------- */
/**
* Accept a single incoming request.
* Note that this operation will block accepting additional requests.
* @return bool True if listening.
*/
public function accept(): bool
{
$this->disconnect();
return (bool)$this->listening;
}
/* ---------- Server option functions -------------------------------------------- */
/**
* Get current port.
* @return int port.
*/
public function getPort(): int
{
return $this->port;
}
/**
* Set timeout.
* @param int $timeout Timeout in seconds.
*/
public function setTimeout(int $timeout): void
{
$this->options['timeout'] = $timeout;
if (!$this->isConnected()) {
return;
}
foreach ($this->connections as $connection) {
$connection->setTimeout($timeout);
$connection->setOptions($this->options);
}
}
/**
* Set fragmentation size.
* @param int $fragment_size Fragment size in bytes.
* @return self.
*/
public function setFragmentSize(int $fragment_size): self
{
$this->options['fragment_size'] = $fragment_size;
foreach ($this->connections as $connection) {
$connection->setOptions($this->options);
}
return $this;
}
/**
* Get fragmentation size.
* @return int $fragment_size Fragment size in bytes.
*/
public function getFragmentSize(): int
{
return $this->options['fragment_size'];
}
/* ---------- Connection broadcast operations ------------------------------------ */
/**
* Broadcast text message to all conenctions.
* @param string $payload Content as string.
*/
public function text(string $payload): void
{
$this->send($payload);
}
/**
* Broadcast binary message to all conenctions.
* @param string $payload Content as binary string.
*/
public function binary(string $payload): void
{
$this->send($payload, 'binary');
}
/**
* Broadcast ping message to all conenctions.
* @param string $payload Optional text as string.
*/
public function ping(string $payload = ''): void
{
$this->send($payload, 'ping');
}
/**
* Broadcast pong message to all conenctions.
* @param string $payload Optional text as string.
*/
public function pong(string $payload = ''): void
{
$this->send($payload, 'pong');
}
/**
* Send message on all connections.
* @param string $payload Message to send.
* @param string $opcode Opcode to use, default: 'text'.
* @param bool $masked If message should be masked default: true.
*/
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);
}
$factory = new Factory();
$message = $factory->create($opcode, $payload);
foreach ($this->connections as $connection) {
$connection->pushMessage($message, $masked);
}
}
/**
* Close all connections.
* @param int $status Close status, default: 1000.
* @param string $message Close message, default: 'ttfn'.
*/
public function close(int $status = 1000, string $message = 'ttfn'): void
{
foreach ($this->connections as $connection) {
if ($connection->isConnected()) {
$connection->close($status, $message);
}
}
}
/**
* Disconnect all connections.
*/
public function disconnect(): void
{
foreach ($this->connections as $connection) {
if ($connection->isConnected()) {
$connection->disconnect();
}
}
$this->connections = [];
}
/**
* Receive message from single connection.
* Note that this operation will block reading and only read from first available connection.
* @return mixed Message, text or null depending on settings.
*/
public function receive()
{
$filter = $this->options['filter'];
$return_obj = $this->options['return_obj'];
if (!$this->isConnected()) {
$this->connect();
}
$connection = current($this->connections);
while (true) {
$message = $connection->pullMessage();
$opcode = $message->getOpcode();
if (in_array($opcode, $filter)) {
$this->last_opcode = $opcode;
$return = $return_obj ? $message : $message->getContent();
break;
} elseif ($opcode == 'close') {
$this->last_opcode = null;
$return = $return_obj ? $message : null;
break;
}
}
return $return;
}
/* ---------- Connection functions ----------------------------------------------- */
/**
* Get requested path from last connection.
* @return string Path.
*/
public function getPath(): string
{
return $this->request_path;
}
/**
* Get request from last connection.
* @return array Request.
*/
public function getRequest(): array
{
return $this->request;
}
/**
* Get headers from last connection.
* @return string|null Headers.
*/
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;
}
/**
* Get last received opcode.
* @return string|null Opcode.
*/
public function getLastOpcode(): ?string
{
return $this->last_opcode;
}
/**
* Get close status from single connection.
* @return int|null Close status.
*/
public function getCloseStatus(): ?int
{
return $this->connections ? current($this->connections)->getCloseStatus() : null;
}
/**
* If Server has active connections.
* @return bool True if active connection.
*/
public function isConnected(): bool
{
foreach ($this->connections as $connection) {
if ($connection->isConnected()) {
return true;
}
}
return false;
}
/**
* Get name of local socket from single connection.
* @return string|null Name of local socket.
*/
public function getName(): ?string
{
return $this->isConnected() ? current($this->connections)->getName() : null;
}
/**
* Get name of remote socket from single connection.
* @return string|null Name of remote socket.
*/
public function getRemoteName(): ?string
{
return $this->isConnected() ? current($this->connections)->getRemoteName() : null;
}
/**
* @deprecated Will be removed in future version.
*/
public function getPier(): ?string
{
trigger_error(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
E_USER_DEPRECATED
);
return $this->getRemoteName();
}
/* ---------- Helper functions --------------------------------------------------- */
// Connect when read/write operation is performed.
private function connect(): void
{
try {
$handler = new ErrorHandler();
$socket = $handler->with(function () {
if (isset($this->options['timeout'])) {
$socket = stream_socket_accept($this->listening, $this->options['timeout']);
} else {
$socket = stream_socket_accept($this->listening);
}
if (!$socket) {
throw new ErrorException('No socket');
}
return $socket;
});
} catch (ErrorException $e) {
$error = "Server failed to connect. {$e->getMessage()}";
$this->logger->error($error, ['severity' => $e->getSeverity()]);
throw new ConnectionException($error, 0, [], $e);
}
$connection = new Connection($socket, $this->options);
$connection->setLogger($this->logger);
if (isset($this->options['timeout'])) {
$connection->setTimeout($this->options['timeout']);
}
$this->logger->info("Client has connected to port {port}", [
'port' => $this->port,
'peer' => $connection->getRemoteName(),
]);
$this->performHandshake($connection);
$this->connections = ['*' => $connection];
}
// Perform upgrade handshake on new connections.
private function performHandshake(Connection $connection): void
{
$request = '';
do {
$buffer = $connection->getLine(1024, "\r\n");
$request .= $buffer . "\n";
$metadata = $connection->getMeta();
} while (!$connection->eof() && $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";
$connection->write($header);
$this->logger->debug("Handshake on {$get_uri}");
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
class TimeoutException extends ConnectionException
{
}
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="tests/bootstrap.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">lib/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Unit tests">
<directory suffix=".php">tests</directory>
</testsuite>
</testsuites>
</phpunit>
+568
View File
@@ -0,0 +1,568 @@
<?php
/**
* Test case for Client.
* Note that this test is performed by mocking socket/stream calls.
*/
declare(strict_types=1);
namespace WebSocket;
use ErrorException;
use Phrity\Net\Uri;
use Phrity\Util\ErrorHandler;
use PHPUnit\Framework\TestCase;
class ClientTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testClientMasked(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->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 testClientNoPath(): void
{
MockSocket::initialize('client.connect-root', $this);
$client = new Client('ws://localhost:8000');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientRelativePath(): void
{
MockSocket::initialize('client.connect', $this);
$uri = new Uri('ws://localhost:8000');
$uri = $uri->withPath('my/mock/path');
$client = new Client($uri);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientWsDefaultPort(): void
{
MockSocket::initialize('client.connect-default-port-ws', $this);
$uri = new Uri('ws://localhost');
$uri = $uri->withPath('my/mock/path');
$client = new Client($uri);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientWssDefaultPort(): void
{
MockSocket::initialize('client.connect-default-port-wss', $this);
$uri = new Uri('wss://localhost');
$uri = $uri->withPath('my/mock/path');
$client = new Client($uri);
$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 testFailedPersistentConnection(): void
{
MockSocket::initialize('client.connect-persistent-failure', $this);
$client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionMessage('Could not resolve stream pointer position');
$client->send('Connect');
}
public function testBadScheme(): void
{
MockSocket::initialize('client.connect', $this);
$this->expectException('WebSocket\BadUriException');
$this->expectExceptionMessage("Invalid URI scheme, must be 'ws' or 'wss'.");
$client = new Client('bad://localhost:8000/my/mock/path');
}
public function testBadUri(): void
{
MockSocket::initialize('client.connect', $this);
$this->expectException('WebSocket\BadUriException');
$this->expectExceptionMessage("Invalid URI '--:this is not an uri:--' provided.");
$client = new Client('--:this is not an uri:--');
}
public function testInvalidUriType(): void
{
MockSocket::initialize('client.connect', $this);
$this->expectException('WebSocket\BadUriException');
$this->expectExceptionMessage("Provided URI must be a UriInterface or string.");
$client = new Client([]);
}
public function testUriInterface(): void
{
MockSocket::initialize('client.connect', $this);
$uri = new Uri('ws://localhost:8000/my/mock/path');
$client = new Client($uri);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
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 testBadStreamConnection(): void
{
MockSocket::initialize('client.connect-bad-stream', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Invalid stream on "localhost:8000"');
$client->send('Connect');
}
public function testHandshakeFailure(): void
{
MockSocket::initialize('client.connect-handshake-failure', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Client handshake error');
$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:8000/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 testHandshakeError(): void
{
MockSocket::initialize('client.connect-handshake-error', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1024);
$this->expectExceptionMessage('Client handshake error');
$client->send('Connect');
}
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->getRemoteName());
$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->getRemoteName());
$this->assertEquals('WebSocket\Client(127.0.0.1:12345)', "{$client}");
}
public function testUnconnectedClient(): void
{
$client = new Client('ws://localhost:8000/my/mock/path');
$this->assertFalse($client->isConnected());
$client->setTimeout(30);
$client->close();
$this->assertFalse($client->isConnected());
$this->assertNull($client->getName());
$this->assertNull($client->getRemoteName());
$this->assertNull($client->getCloseStatus());
}
public function testDeprecated(): void
{
$client = new Client('ws://localhost:8000/my/mock/path');
(new ErrorHandler())->withAll(function () use ($client) {
$this->assertNull($client->getPier());
}, function ($exceptions, $result) {
$this->assertEquals(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
$exceptions[0]->getMessage()
);
}, E_USER_DEPRECATED);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
/**
* Test case for Exceptions.
*/
declare(strict_types=1);
namespace WebSocket;
use PHPUnit\Framework\TestCase;
use Throwable;
class ExceptionTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testConnectionException(): void
{
try {
throw new ConnectionException(
'An error message',
ConnectionException::EOF,
['test' => '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());
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
/**
* Test case for Message subsection.
*/
declare(strict_types=1);
namespace WebSocket;
use PHPUnit\Framework\TestCase;
use WebSocket\Message\Factory;
use WebSocket\Message\Text;
class MessageTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testFactory(): void
{
$factory = new Factory();
$message = $factory->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');
}
}
+28
View File
@@ -0,0 +1,28 @@
# 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.4`, `8.0`, `8.1` and `8.2`.
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.
+511
View File
@@ -0,0 +1,511 @@
<?php
/**
* Test case for Server.
* Note that this test is performed by mocking socket/stream calls.
*/
declare(strict_types=1);
namespace WebSocket;
use ErrorException;
use Phrity\Util\ErrorHandler;
use PHPUnit\Framework\TestCase;
class ServerTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testServerMasked(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->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->getRemoteName());
$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->getRemoteName());
$this->assertEquals('WebSocket\Server(127.0.0.1:12345)', "{$server}");
$this->assertTrue(MockSocket::isEmpty());
}
public function testUnconnectedServer(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertFalse($server->isConnected());
$server->setTimeout(30);
$server->close();
$this->assertFalse($server->isConnected());
$this->assertNull($server->getName());
$this->assertNull($server->getRemoteName());
$this->assertNull($server->getCloseStatus());
$this->assertTrue(MockSocket::isEmpty());
}
public function testFailedHandshake(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept-failed-handshake', $this);
$server->accept();
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Could not read from stream');
$server->send('Connect');
$this->assertFalse($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testServerDisconnect(): 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('server.disconnect', $this);
$server->disconnect();
$this->assertFalse($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testDeprecated(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
(new ErrorHandler())->withAll(function () use ($server) {
$this->assertNull($server->getPier());
}, function ($exceptions, $result) {
$this->assertEquals(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
$exceptions[0]->getMessage()
);
}, E_USER_DEPRECATED);
}
}
+6
View File
@@ -0,0 +1,6 @@
<?php
namespace WebSocket;
require dirname(__DIR__) . '/vendor/autoload.php';
require __DIR__ . '/mock/mock-socket.php';
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* Simple echo logger (only available when running in dev environment)
*/
namespace WebSocket;
class EchoLog implements \Psr\Log\LoggerInterface
{
use \Psr\Log\LoggerTrait;
public function log($level, $message, array $context = [])
{
$message = $this->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);
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* This class is used by tests to mock and track various socket/stream calls.
*/
namespace WebSocket;
class MockSocket
{
private static $queue = [];
private static $stored = [];
private static $asserter;
// Handler called by function overloads in mock-socket.php
public static function handle($function, $params = [])
{
$current = array_shift(self::$queue);
if ($function == 'get_resource_type' && is_null($current)) {
return null; // Catch destructors
}
self::$asserter->assertEquals($current['function'], $function);
foreach ($current['params'] as $index => $param) {
if (isset($current['input-op'])) {
$param = self::op($current['input-op'], $params, $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 str_replace('{key}', self::$stored['sec-websocket-key'], $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;
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
/**
* This file is used by tests to overload and mock various socket/stream calls.
*/
namespace WebSocket;
function stream_socket_server($local_socket, &$errno, &$errstr)
{
$args = [$local_socket, $errno, $errstr];
return MockSocket::handle('stream_socket_server', $args);
}
function stream_socket_accept()
{
$args = func_get_args();
return MockSocket::handle('stream_socket_accept', $args);
}
function stream_set_timeout()
{
$args = func_get_args();
return MockSocket::handle('stream_set_timeout', $args);
}
function stream_get_line()
{
$args = func_get_args();
return MockSocket::handle('stream_get_line', $args);
}
function stream_get_meta_data()
{
$args = func_get_args();
return MockSocket::handle('stream_get_meta_data', $args);
}
function feof()
{
$args = func_get_args();
return MockSocket::handle('feof', $args);
}
function ftell()
{
$args = func_get_args();
return MockSocket::handle('ftell', $args);
}
function fclose()
{
$args = func_get_args();
return MockSocket::handle('fclose', $args);
}
function fwrite()
{
$args = func_get_args();
return MockSocket::handle('fwrite', $args);
}
function fread()
{
$args = func_get_args();
return MockSocket::handle('fread', $args);
}
function fgets()
{
$args = func_get_args();
return MockSocket::handle('fgets', $args);
}
function stream_context_create()
{
$args = func_get_args();
return MockSocket::handle('stream_context_create', $args);
}
function stream_socket_client($remote_socket, &$errno, &$errstr, $timeout, $flags, $context)
{
$args = [$remote_socket, $errno, $errstr, $timeout, $flags, $context];
return MockSocket::handle('stream_socket_client', $args);
}
function get_resource_type()
{
$args = func_get_args();
return MockSocket::handle('get_resource_type', $args);
}
function stream_socket_get_name()
{
$args = func_get_args();
return MockSocket::handle('stream_socket_get_name', $args);
}
+5
View File
@@ -0,0 +1,5 @@
128-chars
abscdefgheijklmnopqrstuvwxyz0123456789
abscdefgheijklmnopqrstuvwxyz0123456789
abscdefgheijklmnopqrstuvwxyz0123456789
a
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [],
"return": 12
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return":[136, 154]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return":[98, 250, 210, 113]
},
{
"function": "fread",
"params": [
"@mock-stream",
26
],
"return-op": "chr-array",
"return": [97, 18, 145, 29, 13, 137, 183, 81, 3, 153, 185, 31, 13, 141, 190, 20, 6, 157, 183, 21, 88, 218, 227, 65, 82, 202]
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return":true
}
]
@@ -0,0 +1,58 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"ssl:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 248
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,7 @@
[
{
"function": "get_resource_type",
"params": [],
"return": "@mock-bad-context"
}
]
@@ -0,0 +1,26 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "bad stream"
}
]
@@ -0,0 +1,58 @@
[
{
"function": "get_resource_type",
"params": [],
"return": "stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,59 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:80",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream",
"GET /my/mock/path HTTP/1.1\r\nHost: localhost:80\r\nUser-Agent: websocket-client-php\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
],
"input-op": "key-save",
"return": 224
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,59 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"ssl:\/\/localhost:443",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream",
"GET /my/mock/path HTTP/1.1\r\nHost: localhost:443\r\nUser-Agent: websocket-client-php\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
],
"input-op": "key-save",
"return": 224
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,23 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"error": {
"msg": "A PHP error",
"type": 512
},
"return": false
}
]
@@ -0,0 +1,59 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream",
"GET /my/mock/path?my_query=yes HTTP/1.1\r\nHost: localhost:8000\r\nUser-Agent: websocket-client-php\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
],
"input-op": "key-save",
"return": 224
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,19 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": false
}
]
@@ -0,0 +1,86 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return": false
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": true,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return": true
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": ""
}
]
@@ -0,0 +1,50 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return": false
}
]
@@ -0,0 +1,67 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 255
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\nX-Very-Long_Header: This is added to provoke split reads of headers in client 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456\r\n"
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "Next234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,49 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: BAD\r\n\r\n"
}
]
@@ -0,0 +1,49 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return": "Invalid upgrade response\r\n\r\n"
}
]
@@ -0,0 +1,34 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
5,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "persistent stream"
},
{
"function": "ftell",
"params": [
"@mock-stream"
],
"return": false
}
]
@@ -0,0 +1,80 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
5,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "persistent stream"
},
{
"function": "ftell",
"params": [
"@mock-stream"
],
"return": 0
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 248
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "persistent stream"
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return":true
}
]
@@ -0,0 +1,59 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream",
"GET / HTTP/1.1\r\nHost: localhost:8000\r\nUser-Agent: websocket-client-php\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
],
"input-op": "key-save",
"return": 224
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,58 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
300,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
300
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,60 @@
[
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"regexp": true,
"params": [
"@mock-stream",
"GET /my/mock/path HTTP/1.1\r\nHost: localhost:8000\r\nUser-Agent: websocket-client-php\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
],
"input-op": "key-save",
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,16 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return": true
}
]
@@ -0,0 +1,99 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "unknown"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "unknown"
},
{
"function": "stream_context_create",
"params": [],
"return": "@mock-stream-context"
},
{
"function": "stream_socket_client",
"params": [
"tcp:\/\/localhost:8000",
null,
null,
5,
4,
"@mock-stream-context"
],
"return": "@mock-stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
5
],
"return": true
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return-op": "key-save",
"return": 199
},
{
"function": "fgets",
"params": [
"@mock-stream",
1024
],
"return-op": "key-respond",
"return": "HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [129, 147]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [33, 111, 149, 174]
},
{
"function": "fread",
"params": [
"@mock-stream",
19
],
"return-op": "chr-array",
"return": [115, 10, 246, 203, 72, 25, 252, 192, 70, 79, 244, 142, 76, 10, 230, 221, 64, 8, 240]
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
}
]
@@ -0,0 +1,48 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [136, 137]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [54, 79, 233, 244]
},
{
"function": "fread",
"params": [
"@mock-stream",
9
],
"return-op": "chr-array",
"return": [117, 35, 170, 152, 89, 60, 128, 154, 81]
},
{
"function": "fwrite",
"params": [],
"return": 33
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return": true
}
]
@@ -0,0 +1,24 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
300
],
"return": true
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
}
]
+143
View File
@@ -0,0 +1,143 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 17
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 6
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [138, 139]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [1, 1, 1, 1]
},
{
"function": "fread",
"params": [
"@mock-stream",
11
],
"return-op": "chr-array",
"return": [82, 100, 115, 119, 100, 115, 33, 113, 104, 111, 102]
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [138, 128]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [1, 1, 1, 1]
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [137, 139]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [180, 77, 192, 201]
},
{
"function": "fread",
"params": [
"@mock-stream",
11
],
"return-op": "chr-array",
"return": [247, 33, 169, 172, 218, 57, 224, 185, 221, 35, 167]
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 17
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [129, 147]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [33, 111, 149, 174]
},
{
"function": "fread",
"params": [
"@mock-stream",
19
],
"return-op": "chr-array",
"return": [115, 10, 246, 203, 72, 25, 252, 192, 70, 79, 244, 142, 76, 10, 230, 221, 64, 8, 240]
}
]
@@ -0,0 +1,18 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [140, 115]
}
]
@@ -0,0 +1,58 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [],
"return": false
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": true,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": true,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return":true
}
]
@@ -0,0 +1,43 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [],
"return": false
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": true,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return":true
}
]
@@ -0,0 +1,58 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [],
"return": ""
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": true,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return":true
}
]
@@ -0,0 +1,126 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [1, 136]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [105, 29, 187, 18]
},
{
"function": "fread",
"params": [
"@mock-stream",
8
],
"return-op": "chr-array",
"return": [36, 104, 215, 102, 0, 61, 221, 96]
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [138, 139]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [1, 1, 1, 1]
},
{
"function": "fread",
"params": [
"@mock-stream",
11
],
"return-op": "chr-array",
"return": [82, 100, 115, 119, 100, 115, 33, 113, 104, 111, 102]
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [0, 136]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [221, 240, 46, 69]
},
{
"function": "fread",
"params": [
"@mock-stream",
8
],
"return-op": "chr-array",
"return": [188, 151, 67, 32, 179, 132, 14, 49]
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [128, 131]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [9, 60, 117, 193]
},
{
"function": "fread",
"params": [
"@mock-stream",
3
],
"return-op": "chr-array",
"return": [108, 79, 1]
}
]
@@ -0,0 +1,9 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
}
]
@@ -0,0 +1,43 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 18
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": true,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "fclose",
"params": [],
"return": true
}
]
@@ -0,0 +1,86 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 26
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 6
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 6
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:8000"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
}
]
@@ -0,0 +1,43 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": true,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "fclose",
"params": [],
"return": true
}
]
@@ -0,0 +1,50 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 132
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [129, 126]
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [0, 128]
},
{
"function": "fread",
"params": [
"@mock-stream",
128
],
"return-op": "file",
"return": ["payload.128.txt", 0, 132]
}
]
@@ -0,0 +1,113 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 65546
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [129, 127]
},
{
"function": "fread",
"params": [
"@mock-stream",
8
],
"return-op": "chr-array",
"return": [0, 0, 0, 0, 0, 1, 0, 0]
},
{
"function": "fread",
"params": [
"@mock-stream",
65536
],
"return-op": "file",
"return": ["payload.65536.txt", 0, 16374]
},
{
"function": "fread",
"params": [
"@mock-stream",
49162
],
"return-op": "file",
"return": ["payload.65536.txt", 16374, 8192]
},
{
"function": "fread",
"params": [
"@mock-stream",
40970
],
"return-op": "file",
"return": ["payload.65536.txt", 24566, 8192]
},
{
"function": "fread",
"params": [
"@mock-stream",
32778
],
"return-op": "file",
"return": ["payload.65536.txt", 32758, 8192]
},
{
"function": "fread",
"params": [
"@mock-stream",
24586
],
"return-op": "file",
"return": ["payload.65536.txt", 40950, 8192]
},
{
"function": "fread",
"params": [
"@mock-stream",
16394
],
"return-op": "file",
"return": ["payload.65536.txt", 49142, 8192]
},
{
"function": "fread",
"params": [
"@mock-stream",
8202
],
"return-op": "file",
"return": ["payload.65536.txt", 57334, 8192]
},
{
"function": "fread",
"params": [
"@mock-stream",
10
],
"return-op": "file",
"return": ["payload.65536.txt", 65526, 10]
}
]
@@ -0,0 +1,112 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [],
"return": 14
},
{
"function": "fwrite",
"params": [],
"return": 14
},
{
"function": "fwrite",
"params": [],
"return": 9
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [1, 136]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [105, 29, 187, 18]
},
{
"function": "fread",
"params": [
"@mock-stream",
8
],
"return-op": "chr-array",
"return": [36, 104, 215, 102, 0, 61, 221, 96]
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [0, 136]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [221, 240, 46, 69]
},
{
"function": "fread",
"params": [
"@mock-stream",
8
],
"return-op": "chr-array",
"return": [188, 151, 67, 32, 179, 132, 14, 49]
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [128, 131]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [9, 60, 117, 193]
},
{
"function": "fread",
"params": [
"@mock-stream",
3
],
"return-op": "chr-array",
"return": [108, 79, 1]
}
]
@@ -0,0 +1,50 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 23
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [129, 147]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [33, 111, 149, 174]
},
{
"function": "fread",
"params": [
"@mock-stream",
19
],
"return-op": "chr-array",
"return": [115, 10, 246, 203, 72, 25, 252, 192, 70, 79, 244, 142, 76, 10, 230, 221, 64, 8, 240]
}
]
@@ -0,0 +1,315 @@
[
{
"function": "stream_socket_accept",
"params": [
"@mock-socket"
],
"return": "@mock-stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "GET \/my\/mock\/path HTTP\/1.1"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 171,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "host: localhost:8000"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 149,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "user-agent: websocket-client-php"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 115,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "connection: Upgrade"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 94,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "upgrade: websocket"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 74,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-key: cktLWXhUdDQ2OXF0ZCFqOQ=="
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 29,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-version: 13"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
}
,
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": ""
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "fwrite",
"params": [
"@mock-stream",
"HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: YmysboNHNoWzWVeQpduY7xELjgU=\r\n\r\n"
],
"return": 129
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [129, 147]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [33, 111, 149, 174]
},
{
"function": "fread",
"params": [
"@mock-stream",
19
],
"return-op": "chr-array",
"return": [115, 10, 246, 203, 72, 25, 252, 192, 70, 79, 244, 142, 76, 10, 230, 221, 64, 8, 240]
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return": true
}
]
@@ -0,0 +1,18 @@
[
{
"function": "stream_socket_accept",
"params": [],
"error": {
"msg": "A PHP error",
"type": 512
},
"return": false
},
{
"function": "fclose",
"params": [
false
],
"return": true
}
]
@@ -0,0 +1,14 @@
[
{
"function": "stream_socket_accept",
"params": [],
"return": false
},
{
"function": "fclose",
"params": [
false
],
"return": true
}
]
@@ -0,0 +1,32 @@
[
{
"function": "stream_socket_accept",
"params": [
"@mock-socket"
],
"return": "@mock-stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": false
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": ""
}
]
@@ -0,0 +1,265 @@
[
{
"function": "stream_socket_accept",
"params": [
"@mock-socket"
],
"return": "@mock-stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "missing http header"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 171,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "host: localhost:8000"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 149,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "user-agent: websocket-client-php"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 115,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "connection: Upgrade"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 94,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "upgrade: websocket"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 74,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "no key in upgrade request"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 29,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-version: 13"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
}
,
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": ""
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
}
]
@@ -0,0 +1,265 @@
[
{
"function": "stream_socket_accept",
"params": [
"@mock-socket"
],
"return": "@mock-stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "GET \/my\/mock\/path HTTP\/1.1"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 171,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "host: localhost:8000"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 149,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "user-agent: websocket-client-php"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 115,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "connection: Upgrade"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 94,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "upgrade: websocket"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 74,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "no key in upgrade request"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 29,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-version: 13"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
}
,
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": ""
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
}
]
@@ -0,0 +1,289 @@
[
{
"function": "stream_socket_accept",
"params": [
"@mock-socket",
300
],
"return": "@mock-stream"
},
{
"function": "stream_set_timeout",
"params": [
"@mock-stream",
300
],
"return": true
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "GET \/my\/mock\/path HTTP\/1.1"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 171,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "host: localhost:8000"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 149,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "user-agent: websocket-client-php"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 115,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "connection: Upgrade"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 94,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "upgrade: websocket"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 74,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-key: cktLWXhUdDQ2OXF0ZCFqOQ=="
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 29,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-version: 13"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
}
,
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": ""
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "fwrite",
"params": [
"@mock-stream",
"HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: YmysboNHNoWzWVeQpduY7xELjgU=\r\n\r\n"
],
"return": 129
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
}
]
@@ -0,0 +1,287 @@
[
{
"function": "stream_socket_accept",
"params": [
"@mock-socket"
],
"return": "@mock-stream"
},
{
"function": "stream_socket_get_name",
"params": [
"@mock-stream"
],
"return": "127.0.0.1:12345"
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "GET \/my\/mock\/path HTTP\/1.1"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 171,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "host: localhost:8000"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 149,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "user-agent: websocket-client-php"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 115,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "connection: Upgrade"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 94,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "upgrade: websocket"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 74,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-key: cktLWXhUdDQ2OXF0ZCFqOQ=="
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 29,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": "sec-websocket-version: 13"
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 2,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
}
,
{
"function": "stream_get_line",
"params": [
"@mock-stream",
1024,
"\r\n"
],
"return": ""
},
{
"function": "stream_get_meta_data",
"params": [
"@mock-stream"
],
"return": {
"timed_out": false,
"blocked": true,
"eof": false,
"stream_type": "tcp_socket\/ssl",
"mode": "r+",
"unread_bytes": 0,
"seekable": false
}
},
{
"function": "feof",
"params": [
"@mock-stream"
],
"return": false
},
{
"function": "fwrite",
"params": [
"@mock-stream",
"HTTP\/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: YmysboNHNoWzWVeQpduY7xELjgU=\r\n\r\n"
],
"return": 129
},
{
"function": "fwrite",
"params": [
"@mock-stream"
],
"return": 13
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
}
]
@@ -0,0 +1,56 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fwrite",
"params": [],
"return":12
},
{
"function": "fread",
"params": [
"@mock-stream",
2
],
"return-op": "chr-array",
"return": [136,154]
},
{
"function": "fread",
"params": [
"@mock-stream",
4
],
"return-op": "chr-array",
"return": [245,55,62,8]
},
{
"function": "fread",
"params": [
"@mock-stream",
26
],
"return-op": "chr-array",
"return": [246,223,125,100,154,68,91,40,148,84,85,102,154,64,82,109,145,80,91,108,207,23,15,56,197,7]
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return": true
}
]
@@ -0,0 +1,28 @@
[
{
"function": "stream_socket_server",
"params": [
"tcp://0.0.0.0:9999",
null,
null
],
"error": {
"msg": "A PHP error",
"type": 512
},
"return": false
},
{
"function": "stream_socket_server",
"params": [
"tcp://0.0.0.0:10000",
null,
null
],
"error": {
"msg": "A PHP error",
"type": 512
},
"return": false
}
]
@@ -0,0 +1,20 @@
[
{
"function": "stream_socket_server",
"params": [
"tcp://0.0.0.0:9999",
null,
null
],
"return": false
},
{
"function": "stream_socket_server",
"params": [
"tcp://0.0.0.0:10000",
null,
null
],
"return": false
}
]
@@ -0,0 +1,11 @@
[
{
"function": "stream_socket_server",
"params": [
"tcp://0.0.0.0:8000",
null,
null
],
"return": "@mock-socket"
}
]
@@ -0,0 +1,24 @@
[
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": "stream"
},
{
"function": "fclose",
"params": [
"@mock-stream"
],
"return": true
},
{
"function": "get_resource_type",
"params": [
"@mock-stream"
],
"return": ""
}
]