代码同步

This commit is contained in:
Ghost
2025-03-24 14:59:19 +08:00
parent f0fa19f89f
commit bd2e1e5386
716 changed files with 90318 additions and 26155 deletions

View File

@@ -0,0 +1,7 @@
<?php
namespace WebSocket;
class BadOpcodeException extends Exception
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace WebSocket;
class BadUriException extends Exception
{
}

View File

@@ -0,0 +1,486 @@
<?php
/**
* Copyright (C) 2014-2020 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, LoggerInterface, NullLogger};
use WebSocket\Message\Factory;
class Base implements LoggerAwareInterface
{
protected $socket;
protected $options = [];
protected $is_closing = false;
protected $last_opcode = null;
protected $close_status = null;
protected $logger;
private $read_buffer;
protected static $opcodes = [
'continuation' => 0,
'text' => 1,
'binary' => 2,
'close' => 8,
'ping' => 9,
'pong' => 10,
];
public function getLastOpcode(): ?string
{
return $this->last_opcode;
}
public function getCloseStatus(): ?int
{
return $this->close_status;
}
public function isConnected(): bool
{
return $this->socket &&
(get_resource_type($this->socket) == 'stream' ||
get_resource_type($this->socket) == 'persistent stream');
}
public function setTimeout(int $timeout): void
{
$this->options['timeout'] = $timeout;
if ($this->isConnected()) {
stream_set_timeout($this->socket, $timeout);
}
}
public function setFragmentSize(int $fragment_size): self
{
$this->options['fragment_size'] = $fragment_size;
return $this;
}
public function getFragmentSize(): int
{
return $this->options['fragment_size'];
}
public function setLogger(LoggerInterface $logger = null): void
{
$this->logger = $logger ?: new NullLogger();
}
public function send(string $payload, string $opcode = 'text', bool $masked = true): void
{
if (!$this->isConnected()) {
$this->connect();
}
if (!in_array($opcode, array_keys(self::$opcodes))) {
$warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'.";
$this->logger->warning($warning);
throw new BadOpcodeException($warning);
}
$payload_chunks = str_split($payload, $this->options['fragment_size']);
$frame_opcode = $opcode;
for ($index = 0; $index < count($payload_chunks); ++$index) {
$chunk = $payload_chunks[$index];
$final = $index == count($payload_chunks) - 1;
$this->sendFragment($final, $chunk, $frame_opcode, $masked);
// all fragments after the first will be marked a continuation
$frame_opcode = 'continuation';
}
$this->logger->info("Sent '{$opcode}' message", [
'opcode' => $opcode,
'content-length' => strlen($payload),
'frames' => count($payload_chunks),
]);
}
/**
* Convenience method to send text message
* @param string $payload Content as string
*/
public function text(string $payload): void
{
$this->send($payload);
}
/**
* Convenience method to send binary message
* @param string $payload Content as binary string
*/
public function binary(string $payload): void
{
$this->send($payload, 'binary');
}
/**
* Convenience method to send ping
* @param string $payload Optional text as string
*/
public function ping(string $payload = ''): void
{
$this->send($payload, 'ping');
}
/**
* Convenience method to send unsolicited pong
* @param string $payload Optional text as string
*/
public function pong(string $payload = ''): void
{
$this->send($payload, 'pong');
}
/**
* Get name of local socket, or null if not connected
* @return string|null
*/
public function getName(): ?string
{
return $this->isConnected() ? stream_socket_get_name($this->socket, false) : null;
}
/**
* Get name of remote socket, or null if not connected
* @return string|null
*/
public function getPier(): ?string
{
return $this->isConnected() ? stream_socket_get_name($this->socket, true) : null;
}
/**
* Get string representation of instance
* @return string String representation
*/
public function __toString(): string
{
return sprintf(
"%s(%s)",
get_class($this),
$this->getName() ?: 'closed'
);
}
/**
* Receive one message.
* Will continue reading until read message match filter settings.
* Return Message instance or string according to settings.
*/
protected function sendFragment(bool $final, string $payload, string $opcode, bool $masked): void
{
$data = '';
$byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
$byte_1 |= self::$opcodes[$opcode]; // Set opcode.
$data .= pack('C', $byte_1);
$byte_2 = $masked ? 0b10000000 : 0b00000000; // Masking bit marker.
// 7 bits of payload length...
$payload_length = strlen($payload);
if ($payload_length > 65535) {
$data .= pack('C', $byte_2 | 0b01111111);
$data .= pack('J', $payload_length);
} elseif ($payload_length > 125) {
$data .= pack('C', $byte_2 | 0b01111110);
$data .= pack('n', $payload_length);
} else {
$data .= pack('C', $byte_2 | $payload_length);
}
// Handle masking
if ($masked) {
// generate a random mask:
$mask = '';
for ($i = 0; $i < 4; $i++) {
$mask .= chr(rand(0, 255));
}
$data .= $mask;
// Append payload to frame:
for ($i = 0; $i < $payload_length; $i++) {
$data .= $payload[$i] ^ $mask[$i % 4];
}
} else {
$data .= $payload;
}
$this->write($data);
$this->logger->debug("Sent '{$opcode}' frame", [
'opcode' => $opcode,
'final' => $final,
'content-length' => strlen($payload),
]);
}
public function receive()
{
$filter = $this->options['filter'];
if (!$this->isConnected()) {
$this->connect();
}
do {
$response = $this->receiveFragment();
list ($payload, $final, $opcode) = $response;
// Continuation and factual opcode
$continuation = ($opcode == 'continuation');
$payload_opcode = $continuation ? $this->read_buffer['opcode'] : $opcode;
// Filter frames
if (!in_array($payload_opcode, $filter)) {
if ($payload_opcode == 'close') {
return null; // Always abort receive on close
}
$final = false;
continue; // Continue reading
}
// First continuation frame, create buffer
if (!$final && !$continuation) {
$this->read_buffer = ['opcode' => $opcode, 'payload' => $payload, 'frames' => 1];
continue; // Continue reading
}
// Subsequent continuation frames, add to buffer
if ($continuation) {
$this->read_buffer['payload'] .= $payload;
$this->read_buffer['frames']++;
}
} while (!$final);
// Final, return payload
$frames = 1;
if ($continuation) {
$payload = $this->read_buffer['payload'];
$frames = $this->read_buffer['frames'];
$this->read_buffer = null;
}
$this->logger->info("Received '{opcode}' message", [
'opcode' => $payload_opcode,
'content-length' => strlen($payload),
'frames' => $frames,
]);
$this->last_opcode = $payload_opcode;
$factory = new Factory();
return $this->options['return_obj']
? $factory->create($payload_opcode, $payload)
: $payload;
}
protected function receiveFragment(): array
{
// Read the fragment "header" first, two bytes.
$data = $this->read(2);
list ($byte_1, $byte_2) = array_values(unpack('C*', $data));
$final = (bool)($byte_1 & 0b10000000); // Final fragment marker.
$rsv = $byte_1 & 0b01110000; // Unused bits, ignore
// Parse opcode
$opcode_int = $byte_1 & 0b00001111;
$opcode_ints = array_flip(self::$opcodes);
if (!array_key_exists($opcode_int, $opcode_ints)) {
$warning = "Bad opcode in websocket frame: {$opcode_int}";
$this->logger->warning($warning);
throw new ConnectionException($warning, ConnectionException::BAD_OPCODE);
}
$opcode = $opcode_ints[$opcode_int];
// Masking bit
$mask = (bool)($byte_2 & 0b10000000);
$payload = '';
// Payload length
$payload_length = $byte_2 & 0b01111111;
if ($payload_length > 125) {
if ($payload_length === 126) {
$data = $this->read(2); // 126: Payload is a 16-bit unsigned int
$payload_length = current(unpack('n', $data));
} else {
$data = $this->read(8); // 127: Payload is a 64-bit unsigned int
$payload_length = current(unpack('J', $data));
}
}
// Get masking key.
if ($mask) {
$masking_key = $this->read(4);
}
// Get the actual payload, if any (might not be for e.g. close frames.
if ($payload_length > 0) {
$data = $this->read($payload_length);
if ($mask) {
// Unmask payload.
for ($i = 0; $i < $payload_length; $i++) {
$payload .= ($data[$i] ^ $masking_key[$i % 4]);
}
} else {
$payload = $data;
}
}
$this->logger->debug("Read '{opcode}' frame", [
'opcode' => $opcode,
'final' => $final,
'content-length' => strlen($payload),
]);
// if we received a ping, send a pong and wait for the next message
if ($opcode === 'ping') {
$this->logger->debug("Received 'ping', sending 'pong'.");
$this->send($payload, 'pong', true);
return [$payload, true, $opcode];
}
// if we received a pong, wait for the next message
if ($opcode === 'pong') {
$this->logger->debug("Received 'pong'.");
return [$payload, true, $opcode];
}
if ($opcode === 'close') {
$status_bin = '';
$status = '';
// Get the close status.
$status_bin = '';
$status = '';
if ($payload_length > 0) {
$status_bin = $payload[0] . $payload[1];
$status = current(unpack('n', $payload));
$this->close_status = $status;
}
// Get additional close message
if ($payload_length >= 2) {
$payload = substr($payload, 2);
}
$this->logger->debug("Received 'close', status: {$this->close_status}.");
if ($this->is_closing) {
$this->is_closing = false; // A close response, all done.
} else {
$this->send($status_bin . 'Close acknowledged: ' . $status, 'close', true); // Respond.
}
// Close the socket.
fclose($this->socket);
// Closing should not return message.
return [$payload, true, $opcode];
}
return [$payload, $final, $opcode];
}
/**
* Tell the socket to close.
*
* @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
* @param string $message A closing message, max 125 bytes.
*/
public function close(int $status = 1000, string $message = 'ttfn'): void
{
if (!$this->isConnected()) {
return;
}
$status_binstr = sprintf('%016b', $status);
$status_str = '';
foreach (str_split($status_binstr, 8) as $binstr) {
$status_str .= chr(bindec($binstr));
}
$this->send($status_str . $message, 'close', true);
$this->logger->debug("Closing with status: {$status_str}.");
$this->is_closing = true;
$this->receive(); // Receiving a close frame will close the socket now.
}
/**
* Disconnect from client/server.
*/
public function disconnect(): void
{
if ($this->isConnected()) {
fclose($this->socket);
$this->socket = null;
}
}
protected function write(string $data): void
{
$length = strlen($data);
$written = @fwrite($this->socket, $data);
if ($written === false) {
$this->throwException("Failed to write {$length} bytes.");
}
if ($written < strlen($data)) {
$this->throwException("Could only write {$written} out of {$length} bytes.");
}
$this->logger->debug("Wrote {$written} of {$length} bytes.");
}
protected function read(string $length): string
{
$data = '';
while (strlen($data) < $length) {
$buffer = @fread($this->socket, $length - strlen($data));
if (!$buffer) {
$meta = stream_get_meta_data($this->socket);
if (!empty($meta['timed_out'])) {
$message = 'Client read timeout';
$this->logger->error($message, $meta);
throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
}
}
if ($buffer === false) {
$read = strlen($data);
$this->throwException("Broken frame, read {$read} of stated {$length} bytes.");
}
if ($buffer === '') {
$this->throwException("Empty read; connection dead?");
}
$data .= $buffer;
$read = strlen($data);
$this->logger->debug("Read {$read} of {$length} bytes.");
}
return $data;
}
protected function throwException(string $message, int $code = 0): void
{
$meta = ['closed' => true];
if ($this->isConnected()) {
$meta = stream_get_meta_data($this->socket);
fclose($this->socket);
$this->socket = null;
}
if (!empty($meta['timed_out'])) {
$this->logger->error($message, $meta);
throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
}
if (!empty($meta['eof'])) {
$code = ConnectionException::EOF;
}
$this->logger->error($message, $meta);
throw new ConnectionException($message, $code, $meta);
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* Copyright (C) 2014-2020 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 Client extends Base
{
// 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,
];
protected $socket_uri;
/**
* @param string $uri A ws/wss-URI
* @param array $options
* Associative array containing:
* - context: Set the stream context. Default: empty context
* - timeout: Set the socket timeout in seconds. Default: 5
* - fragment_size: Set framgemnt size. Default: 4096
* - headers: Associative array of headers to set/override.
*/
public function __construct(string $uri, array $options = [])
{
$this->options = array_merge(self::$default_options, $options);
$this->socket_uri = $uri;
$this->setLogger($this->options['logger']);
}
public function __destruct()
{
if ($this->isConnected() && get_resource_type($this->socket) !== 'persistent stream') {
fclose($this->socket);
}
$this->socket = null;
}
/**
* Perform WebSocket handshake
*/
protected function connect(): void
{
$url_parts = parse_url($this->socket_uri);
if (empty($url_parts) || empty($url_parts['scheme']) || empty($url_parts['host'])) {
$error = "Invalid url '{$this->socket_uri}' provided.";
$this->logger->error($error);
throw new BadUriException($error);
}
$scheme = $url_parts['scheme'];
$host = $url_parts['host'];
$user = isset($url_parts['user']) ? $url_parts['user'] : '';
$pass = isset($url_parts['pass']) ? $url_parts['pass'] : '';
$port = isset($url_parts['port']) ? $url_parts['port'] : ($scheme === 'wss' ? 443 : 80);
$path = isset($url_parts['path']) ? $url_parts['path'] : '/';
$query = isset($url_parts['query']) ? $url_parts['query'] : '';
$fragment = isset($url_parts['fragment']) ? $url_parts['fragment'] : '';
$path_with_query = $path;
if (!empty($query)) {
$path_with_query .= '?' . $query;
}
if (!empty($fragment)) {
$path_with_query .= '#' . $fragment;
}
if (!in_array($scheme, ['ws', 'wss'])) {
$error = "Url should have scheme ws or wss, not '{$scheme}' from URI '{$this->socket_uri}'.";
$this->logger->error($error);
throw new BadUriException($error);
}
$host_uri = ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host;
// Set the stream context options if they're already set in the config
if (isset($this->options['context'])) {
// Suppress the error since we'll catch it below
if (@get_resource_type($this->options['context']) === 'stream-context') {
$context = $this->options['context'];
} else {
$error = "Stream context in \$options['context'] isn't a valid context.";
$this->logger->error($error);
throw new \InvalidArgumentException($error);
}
} else {
$context = stream_context_create();
}
$persistent = $this->options['persistent'] === true;
$flags = STREAM_CLIENT_CONNECT;
$flags = $persistent ? $flags | STREAM_CLIENT_PERSISTENT : $flags;
$error = $errno = $errstr = null;
set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
$this->logger->warning($message, ['severity' => $severity]);
$error = $message;
}, E_ALL);
// Open the socket.
$this->socket = stream_socket_client(
"{$host_uri}:{$port}",
$errno,
$errstr,
$this->options['timeout'],
$flags,
$context
);
restore_error_handler();
if (!$this->isConnected()) {
$error = "Could not open socket to \"{$host}:{$port}\": {$errstr} ({$errno}) {$error}.";
$this->logger->error($error);
throw new ConnectionException($error);
}
$address = "{$scheme}://{$host}{$path_with_query}";
if (!$persistent || ftell($this->socket) == 0) {
// Set timeout on the stream as well.
stream_set_timeout($this->socket, $this->options['timeout']);
// Generate the WebSocket key.
$key = self::generateKey();
// Default headers
$headers = [
'Host' => $host . ":" . $port,
'User-Agent' => 'websocket-client-php',
'Connection' => 'Upgrade',
'Upgrade' => 'websocket',
'Sec-WebSocket-Key' => $key,
'Sec-WebSocket-Version' => '13',
];
// Handle basic authentication.
if ($user || $pass) {
$headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass);
}
// Deprecated way of adding origin (use headers instead).
if (isset($this->options['origin'])) {
$headers['origin'] = $this->options['origin'];
}
// Add and override with headers from options.
if (isset($this->options['headers'])) {
$headers = array_merge($headers, $this->options['headers']);
}
$header = "GET " . $path_with_query . " HTTP/1.1\r\n" . implode(
"\r\n",
array_map(
function ($key, $value) {
return "$key: $value";
},
array_keys($headers),
$headers
)
) . "\r\n\r\n";
// Send headers.
$this->write($header);
// Get server response header (terminated with double CR+LF).
$response = '';
do {
$buffer = fgets($this->socket, 1024);
if ($buffer === false) {
$meta = stream_get_meta_data($this->socket);
$message = 'Client handshake error';
$this->logger->error($message, $meta);
throw new ConnectionException($message);
}
$response .= $buffer;
} while (substr_count($response, "\r\n\r\n") == 0);
// Validate response.
if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
$error = "Connection to '{$address}' failed: Server sent invalid upgrade response: {$response}";
$this->logger->error($error);
throw new ConnectionException($error);
}
$keyAccept = trim($matches[1]);
$expectedResonse
= base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
if ($keyAccept !== $expectedResonse) {
$error = 'Server sent bad upgrade response.';
$this->logger->error($error);
throw new ConnectionException($error);
}
}
$this->logger->info("Client connected to {$address}");
}
/**
* Generate a random string for WebSocket key.
*
* @return string Random string
*/
protected static function generateKey(): string
{
$key = '';
for ($i = 0; $i < 16; $i++) {
$key .= chr(rand(33, 126));
}
return base64_encode($key);
}
}

View File

@@ -0,0 +1,26 @@
<?php
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;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace WebSocket;
class Exception extends \Exception
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace WebSocket\Message;
class Binary extends Message
{
protected $opcode = 'binary';
}

View File

@@ -0,0 +1,8 @@
<?php
namespace WebSocket\Message;
class Close extends Message
{
protected $opcode = 'close';
}

View File

@@ -0,0 +1,25 @@
<?php
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");
}
}

View File

@@ -0,0 +1,53 @@
<?php
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);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace WebSocket\Message;
class Ping extends Message
{
protected $opcode = 'ping';
}

View File

@@ -0,0 +1,8 @@
<?php
namespace WebSocket\Message;
class Pong extends Message
{
protected $opcode = 'pong';
}

View File

@@ -0,0 +1,8 @@
<?php
namespace WebSocket\Message;
class Text extends Message
{
protected $opcode = 'text';
}

View File

@@ -0,0 +1,176 @@
<?php
/**
* Copyright (C) 2014-2020 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 Server extends Base
{
// Default options
protected static $default_options = [
'filter' => ['text', 'binary'],
'fragment_size' => 4096,
'logger' => null,
'port' => 8000,
'return_obj' => false,
'timeout' => null,
];
protected $addr;
protected $port;
protected $listening;
protected $request;
protected $request_path;
/**
* @param array $options
* Associative array containing:
* - timeout: Set the socket timeout in seconds.
* - fragment_size: Set framgemnt size. Default: 4096
* - port: Chose port for listening. Default 8000.
*/
public function __construct(array $options = [])
{
$this->options = array_merge(self::$default_options, $options);
$this->port = $this->options['port'];
$this->setLogger($this->options['logger']);
$error = $errno = $errstr = null;
set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
$this->logger->warning($message, ['severity' => $severity]);
$error = $message;
}, E_ALL);
do {
$this->listening = stream_socket_server("tcp://0.0.0.0:$this->port", $errno, $errstr);
} while ($this->listening === false && $this->port++ < 10000);
restore_error_handler();
if (!$this->listening) {
$error = "Could not open listening socket: {$errstr} ({$errno}) {$error}";
$this->logger->error($error);
throw new ConnectionException($error, (int)$errno);
}
$this->logger->info("Server listening to port {$this->port}");
}
public function __destruct()
{
if ($this->isConnected()) {
fclose($this->socket);
}
$this->socket = null;
}
public function getPort(): int
{
return $this->port;
}
public function getPath(): string
{
return $this->request_path;
}
public function getRequest(): array
{
return $this->request;
}
public function getHeader($header): ?string
{
foreach ($this->request as $row) {
if (stripos($row, $header) !== false) {
list($headername, $headervalue) = explode(":", $row);
return trim($headervalue);
}
}
return null;
}
public function accept(): bool
{
$this->socket = null;
return (bool)$this->listening;
}
protected function connect(): void
{
$error = null;
set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
$this->logger->warning($message, ['severity' => $severity]);
$error = $message;
}, E_ALL);
if (isset($this->options['timeout'])) {
$this->socket = stream_socket_accept($this->listening, $this->options['timeout']);
} else {
$this->socket = stream_socket_accept($this->listening);
}
restore_error_handler();
if (!$this->socket) {
$this->throwException("Server failed to connect. {$error}");
}
if (isset($this->options['timeout'])) {
stream_set_timeout($this->socket, $this->options['timeout']);
}
$this->logger->info("Client has connected to port {port}", [
'port' => $this->port,
'pier' => stream_socket_get_name($this->socket, true),
]);
$this->performHandshake();
}
protected function performHandshake(): void
{
$request = '';
do {
$buffer = stream_get_line($this->socket, 1024, "\r\n");
$request .= $buffer . "\n";
$metadata = stream_get_meta_data($this->socket);
} while (!feof($this->socket) && $metadata['unread_bytes'] > 0);
if (!preg_match('/GET (.*) HTTP\//mUi', $request, $matches)) {
$error = "No GET in request: {$request}";
$this->logger->error($error);
throw new ConnectionException($error);
}
$get_uri = trim($matches[1]);
$uri_parts = parse_url($get_uri);
$this->request = explode("\n", $request);
$this->request_path = $uri_parts['path'];
/// @todo Get query and fragment as well.
if (!preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $request, $matches)) {
$error = "Client had no Key in upgrade request: {$request}";
$this->logger->error($error);
throw new ConnectionException($error);
}
$key = trim($matches[1]);
/// @todo Validate key length and base 64...
$response_key = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$header = "HTTP/1.1 101 Switching Protocols\r\n"
. "Upgrade: websocket\r\n"
. "Connection: Upgrade\r\n"
. "Sec-WebSocket-Accept: $response_key\r\n"
. "\r\n";
$this->write($header);
$this->logger->debug("Handshake on {$get_uri}");
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace WebSocket;
class TimeoutException extends ConnectionException
{
}