Skip to content

Commit

Permalink
Merge pull request #96 from Textalk/v1.4-master
Browse files Browse the repository at this point in the history
Version 1.4
  • Loading branch information
sirn-se authored Aug 14, 2020
2 parents c93acdf + e56c10a commit ab02241
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 220 deletions.
8 changes: 2 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
language: php
dist: trusty

php:
- 7.4
- 7.3
- 7.2
- 7.1
- 7.0
- 5.6
- 5.5
- 5.4

before_script:
- make install build

script:
- make coverage cs-check
- make coverage
- make cs-check
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cs-check: vendor/bin/phpunit

coverage: vendor/bin/phpunit build
./vendor/bin/phpunit --coverage-clover build/logs/clover.xml
./vendor/bin/coveralls -v
./vendor/bin/php-coveralls -v

composer.phar:
curl -s http://getcomposer.org/installer | php
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Preferred way to install is with [Composer](https://getcomposer.org/).
composer require textalk/websocket
```

Currently support PHP versions `^5.4` and `^7.0`.
Current version support PHP versions `^7.1`.
For PHP `^5.4` and `^7.0` support use version `1.3`.

## Client

Expand All @@ -40,6 +41,7 @@ WebSocket\Client {
public setTimeout(int $seconds) : void
public setFragmentSize(int $fragment_size) : self
public getFragmentSize() : int
public setLogger(Psr\Log\LoggerInterface $logger = null) : void
}
```

Expand Down Expand Up @@ -82,6 +84,7 @@ The `$options` parameter in constructor accepts an associative array of options.
* `fragment_size` - Maximum payload size. Default 4096 chars.
* `context` - A stream context created using [stream_context_create](https://www.php.net/manual/en/function.stream-context-create).
* `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.

```php
Expand Down Expand Up @@ -131,6 +134,7 @@ WebSocket\Server {
public setTimeout(int $seconds) : void
public setFragmentSize(int $fragment_size) : self
public getFragmentSize() : int
public setLogger(Psr\Log\LoggerInterface $logger = null) : void
}
```

Expand Down Expand Up @@ -173,6 +177,7 @@ The `$options` parameter in constructor accepts an associative array of options.
* `timeout` - Time out in seconds. Default 5 seconds.
* `port` - The server port to listen to. Default 8000.
* `fragment_size` - Maximum payload size. Default 4096 chars.
* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger.

```php
$server = new WebSocket\Server([
Expand All @@ -191,6 +196,11 @@ $server = new WebSocket\Server([

## Development and contribution

Requirements on pull requests;
* All tests **MUST** pass.
* Code coverage **MUST** remain on 100%.
* Code **MUST** adhere to PSR-1 and PSR-12 code standards.

Install or update dependencies using [Composer](https://getcomposer.org/).
```
# Install dependencies
Expand Down Expand Up @@ -233,11 +243,19 @@ See [Copying](COPYING).

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.
Ignas Bernotas, Mark Herhold, Andreas Palm, Sören Jensen, pmaasz, Alexey Stavrov,
Michael Slezak.


## Changelog

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)

1.3.1

* Allow control messages without payload (@Logioniz)
Expand Down
4 changes: 1 addition & 3 deletions codestandard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,5 @@
<arg name="colors"/>

<rule ref="PSR1"/>
<rule ref="PSR12">
<exclude name="PSR12.Properties.ConstantVisibility"/>
</rule>
<rule ref="PSR12"/>
</ruleset>
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"type": "library",
"authors": [
{
"name": "Fredrik Liljegren",
"email": "fredrik.liljegren@textalk.se"
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen",
Expand All @@ -24,11 +23,12 @@
}
},
"require": {
"php": "^5.4|^7.0"
"php": "^7.1",
"psr/log": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.1",
"php-coveralls/php-coveralls": "^0.7",
"phpunit/phpunit": "^7.0|^8.0|^9.0",
"php-coveralls/php-coveralls": "^2.0",
"squizlabs/php_codesniffer": "^3.5"
}
}
51 changes: 36 additions & 15 deletions lib/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@

namespace WebSocket;

class Base
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class Base implements LoggerAwareInterface
{
protected $socket;
protected $options = [];
protected $is_closing = false;
protected $last_opcode = null;
protected $close_status = null;
protected $logger;

protected static $opcodes = array(
'continuation' => 0,
Expand Down Expand Up @@ -61,27 +66,37 @@ public function getFragmentSize()
return $this->options['fragment_size'];
}

public function setLogger(LoggerInterface $logger = null)
{
$this->logger = $logger ?: new NullLogger();
}

public function send($payload, $opcode = 'text', $masked = true)
{
if (!$this->isConnected()) {
$this->connect();
}

if (!in_array($opcode, array_keys(self::$opcodes))) {
throw new BadOpcodeException("Bad opcode '$opcode'. Try 'text' or 'binary'.");
$warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'.";
$this->logger->warning($warning);
throw new BadOpcodeException($warning);
}

$payload_chunks = str_split($payload, $this->options['fragment_size']);
$frame_opcode = $opcode;

for ($index = 0; $index < count($payload_chunks); ++$index) {
$chunk = $payload_chunks[$index];
$final = $index == count($payload_chunks) - 1;

$this->sendFragment($final, $chunk, $opcode, $masked);
$this->sendFragment($final, $chunk, $frame_opcode, $masked);

// all fragments after the first will be marked a continuation
$opcode = 'continuation';
$frame_opcode = 'continuation';
}

$this->logger->info("Sent '{$opcode}' message");
}

protected function sendFragment($final, $payload, $opcode, $masked)
Expand Down Expand Up @@ -150,6 +165,7 @@ public function receive()
$payload .= $response[0];
} while (!$response[1]);

$this->logger->info("Received '{$this->last_opcode}' message");
return $payload;
}

Expand All @@ -170,10 +186,9 @@ protected function receiveFragment()
$opcode_int = ord($data[0]) & 31; // Bits 4-7
$opcode_ints = array_flip(self::$opcodes);
if (!array_key_exists($opcode_int, $opcode_ints)) {
throw new ConnectionException(
"Bad opcode in websocket frame: $opcode_int",
ConnectionException::BAD_OPCODE
);
$warning = "Bad opcode in websocket frame: {$opcode_int}";
$this->logger->warning($warning);
throw new ConnectionException($warning, ConnectionException::BAD_OPCODE);
}
$opcode = $opcode_ints[$opcode_int];

Expand Down Expand Up @@ -219,6 +234,7 @@ protected function receiveFragment()

// if we received a ping, send a pong
if ($opcode === 'ping') {
$this->logger->debug("Received 'ping', sending 'pong'.");
$this->send($payload, 'pong', true);
}

Expand All @@ -234,6 +250,8 @@ protected function receiveFragment()
$payload = substr($payload, 2);
}

$this->logger->debug("Received 'close', status: {$this->close_status}.");

if ($this->is_closing) {
$this->is_closing = false; // A close response, all done.
} else {
Expand Down Expand Up @@ -267,25 +285,26 @@ public function close($status = 1000, $message = 'ttfn')
$status_str .= chr(bindec($binstr));
}
$this->send($status_str . $message, 'close', true);
$this->logger->debug("Closing with status: {$status_str}.");

$this->is_closing = true;
$this->receive(); // Receiving a close frame will close the socket now.
}

protected function write($data)
{
$length = strlen($data);
$written = fwrite($this->socket, $data);
if ($written === false) {
$length = strlen($data);
fclose($this->socket);
$this->throwException("Failed to write $length bytes.");
$this->throwException("Failed to write {$length} bytes.");
}

if ($written < strlen($data)) {
$length = strlen($data);
fclose($this->socket);
$this->throwException("Could only write $written out of $length bytes.");
$this->throwException("Could only write {$written} out of {$length} bytes.");
}
$this->logger->debug("Wrote {$written} of {$length} bytes.");
}

protected function read($length)
Expand All @@ -295,7 +314,7 @@ protected function read($length)
$buffer = fread($this->socket, $length - strlen($data));
if ($buffer === false) {
$read = strlen($data);
$this->throwException("Broken frame, read $read of stated $length bytes.");
$this->throwException("Broken frame, read {$read} of stated {$length} bytes.");
}
if ($buffer === '') {
$this->throwException("Empty read; connection dead?");
Expand All @@ -311,12 +330,14 @@ protected function throwException($message, $code = 0)
$json_meta = json_encode($meta);
if (!empty($meta['timed_out'])) {
$code = ConnectionException::TIMED_OUT;
throw new TimeoutException("$message Stream state: $json_meta", $code);
$this->logger->warning("{$message}", (array)$meta);
throw new TimeoutException("{$message} Stream state: {$json_meta}", $code);
}
if (!empty($meta['eof'])) {
$code = ConnectionException::EOF;
}
throw new ConnectionException("$message Stream state: $json_meta", $code);
$this->logger->error("{$message}", (array)$meta);
throw new ConnectionException("{$message} Stream state: {$json_meta}", $code);
}

/**
Expand Down
42 changes: 24 additions & 18 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Client extends Base
'fragment_size' => 4096,
'context' => null,
'headers' => null,
'logger' => null,
'origin' => null, // @deprecated
];

Expand All @@ -36,6 +37,7 @@ public function __construct($uri, $options = array())
{
$this->options = array_merge(self::$default_options, $options);
$this->socket_uri = $uri;
$this->setLogger($this->options['logger']);
}

public function __destruct()
Expand All @@ -53,9 +55,9 @@ protected function connect()
{
$url_parts = parse_url($this->socket_uri);
if (empty($url_parts) || empty($url_parts['scheme']) || empty($url_parts['host'])) {
throw new BadUriException(
"Invalid url '$this->socket_uri' provided."
);
$error = "Invalid url '{$this->socket_uri}' provided.";
$this->logger->error($error);
throw new BadUriException($error);
}
$scheme = $url_parts['scheme'];
$host = $url_parts['host'];
Expand All @@ -75,9 +77,9 @@ protected function connect()
}

if (!in_array($scheme, array('ws', 'wss'))) {
throw new BadUriException(
"Url should have scheme ws or wss, not '$scheme' from URI '$this->socket_uri' ."
);
$error = "Url should have scheme ws or wss, not '{$scheme}' from URI '{$this->socket_uri}'.";
$this->logger->error($error);
throw new BadUriException($error);
}

$host_uri = ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host;
Expand All @@ -88,9 +90,9 @@ protected function connect()
if (@get_resource_type($this->options['context']) === 'stream-context') {
$context = $this->options['context'];
} else {
throw new \InvalidArgumentException(
"Stream context in \$options['context'] isn't a valid context"
);
$error = "Stream context in \$options['context'] isn't a valid context.";
$this->logger->error($error);
throw new \InvalidArgumentException($error);
}
} else {
$context = stream_context_create();
Expand All @@ -110,9 +112,9 @@ protected function connect()
);

if (!$this->isConnected()) {
throw new ConnectionException(
"Could not open socket to \"$host:$port\": $errstr ($errno)."
);
$error = "Could not open socket to \"{$host}:{$port}\": {$errstr} ({$errno}).";
$this->logger->error($error);
throw new ConnectionException($error);
}

// Set timeout on the stream as well.
Expand Down Expand Up @@ -165,22 +167,26 @@ function ($key, $value) {

/// @todo Handle version switching

$address = "{$scheme}://{$host}{$path_with_query}";

// Validate response.
if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
$address = $scheme . '://' . $host . $path_with_query;
throw new ConnectionException(
"Connection to '{$address}' failed: Server sent invalid upgrade response:\n"
. $response
);
$error = "Connection to '{$address}' failed: Server sent invalid upgrade response: {$response}";
$this->logger->error($error);
throw new ConnectionException($error);
}

$keyAccept = trim($matches[1]);
$expectedResonse
= base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));

if ($keyAccept !== $expectedResonse) {
throw new ConnectionException('Server sent bad upgrade response.');
$error = 'Server sent bad upgrade response.';
$this->logger->error($error);
throw new ConnectionException($error);
}

$this->logger->info("Client connected to to {$address}");
}

/**
Expand Down
Loading

0 comments on commit ab02241

Please sign in to comment.