Skip to content

Commit

Permalink
arthurkushman#30. Change method args semantics, refactor with trait, …
Browse files Browse the repository at this point in the history
…move vars to conf
  • Loading branch information
arthurkushman committed Nov 19, 2018
1 parent 1747a07 commit 0cf5760
Show file tree
Hide file tree
Showing 3 changed files with 349 additions and 242 deletions.
86 changes: 86 additions & 0 deletions src/Components/ClientConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
class ClientConfig
{

private $scheme;
private $host;
private $user;
private $password;
private $port;

private $timeout = WscCommonsContract::DEFAULT_TIMEOUT;
private $headers = [];
private $fragmentSize = WscCommonsContract::DEFAULT_FRAGMENT_SIZE;
Expand Down Expand Up @@ -76,4 +82,84 @@ public function setContext($context)
{
$this->context = $context;
}

/**
* @return mixed
*/
public function getScheme(): string
{
return $this->scheme;
}

/**
* @param void $scheme
*/
public function setScheme($scheme): void
{
$this->scheme = $scheme;
}

/**
* @return string
*/
public function getHost(): string
{
return $this->host;
}

/**
* @param void $host
*/
public function setHost($host): void
{
$this->host = $host;
}

/**
* @return string
*/
public function getUser(): string
{
return $this->user;
}

/**
* @param array $urlParts
*/
public function setUser(array $urlParts): void
{
$this->user = isset($urlParts['user']) ? $urlParts['user'] : '';
}

/**
* @return string
*/
public function getPassword(): string
{
return $this->password;
}

/**
* @param array $urlParts
*/
public function setPassword(array $urlParts): void
{
$this->password = isset($urlParts['pass']) ? $urlParts['pass'] : '';
}

/**
* @return string
*/
public function getPort(): string
{
return $this->port;
}

/**
* @param array $urlParts
*/
public function setPort(array $urlParts): void
{
$this->port = isset($urlParts['port']) ? $urlParts['port'] : ($this->scheme === 'wss' ? 443 : 80);
}
}
242 changes: 242 additions & 0 deletions src/Components/WSClientTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php

namespace WSSC\Components;

use WSSC\Contracts\CommonsContract;
use WSSC\Exceptions\BadOpcodeException;
use WSSC\Exceptions\BadUriException;
use WSSC\Exceptions\ConnectionException;

trait WSClientTrait
{

/**
* Validates whether server sent valid upgrade response
*
* @param ClientConfig $config
* @param string $pathWithQuery
* @param string $key
* @throws ConnectionException
*/
private function validateResponse(ClientConfig $config, string $pathWithQuery, string $key)
{
$response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n");
if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) {
$address = $config->getScheme() . '://' . $config->getHost() . $pathWithQuery;
throw new ConnectionException(
"Connection to '{$address}' failed: Server sent invalid upgrade response:\n"
. $response, CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
);
}

$keyAccept = trim($matches[1]);
$expectedResonse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT)));
if ($keyAccept !== $expectedResonse) {
throw new ConnectionException('Server sent bad upgrade response.',
CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE);
}
}

/**
* Gets host uri based on protocol
*
* @param ClientConfig $config
* @return string
* @throws BadUriException
*/
private function getHostUri(ClientConfig $config): string
{
if (in_array($config->getScheme(), ['ws', 'wss'], true) === false) {
throw new BadUriException(
"Url should have scheme ws or wss, not '{$config->getScheme()}' from URI '$this->socketUrl' .",
CommonsContract::CLIENT_INCORRECT_SCHEME
);
}

return ($config->getScheme() === 'wss' ? 'ssl' : 'tcp') . '://' . $config->getHost();
}

/**
* @param string $data
* @return float|int
* @throws ConnectionException
*/
private function getPayloadLength(string $data)
{
$payloadLength = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1
if ($payloadLength > self::MASK_125) {
if ($payloadLength === self::MASK_126) {
$data = $this->read(2); // 126: Payload is a 16-bit unsigned int
} else {
$data = $this->read(8); // 127: Payload is a 64-bit unsigned int
}
$payloadLength = bindec(self::sprintB($data));
}

return $payloadLength;
}

/**
* @param string $data
* @param int $payloadLength
* @return string
* @throws ConnectionException
*/
private function getPayloadData(string $data, int $payloadLength): string
{
// Masking?
$mask = (bool)(ord($data[1]) >> 7); // Bit 0 in byte 1
$payload = '';
$maskingKey = '';

// Get masking key.
if ($mask) {
$maskingKey = $this->read(4);
}

// Get the actual payload, if any (might not be for e.g. close frames.
if ($payloadLength > 0) {
$data = $this->read($payloadLength);

if ($mask) {
// Unmask payload.
for ($i = 0; $i < $payloadLength; $i++) {
$payload .= ($data[$i] ^ $maskingKey[$i % 4]);
}
} else {
$payload = $data;
}
}

return $payload;
}

/**
* @return null|string
* @throws \WSSC\Exceptions\BadOpcodeException
* @throws \InvalidArgumentException
* @throws BadOpcodeException
* @throws BadUriException
* @throws ConnectionException
* @throws \Exception
*/
protected function receiveFragment()
{
// Just read the main fragment information first.
$data = $this->read(2);

// Is this the final fragment? // Bit 0 in byte 0
/// @todo Handle huge payloads with multiple fragments.
$final = (bool)(ord($data[0]) & 1 << 7);

// Parse opcode
$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",
CommonsContract::CLIENT_BAD_OPCODE);
}

$opcode = $opcode_ints[$opcode_int];

// record the opcode if we are not receiving a continutation fragment
if ($opcode !== 'continuation') {
$this->lastOpcode = $opcode;
}

$payloadLength = $this->getPayloadLength($data);
$payload = $this->getPayloadData($data, $payloadLength);

if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) {
// Get the close status.
if ($payloadLength >= 2) {
$statusBin = $payload[0] . $payload[1];
$status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1])));
$this->closeStatus = $status;
$payload = substr($payload, 2);

if (!$this->isClosing) {
$this->send($statusBin . 'Close acknowledged: ' . $status,
CommonsContract::EVENT_TYPE_CLOSE); // Respond.
}
}

if ($this->isClosing) {
$this->isClosing = false; // A close response, all done.
}

fclose($this->socket);
$this->isConnected = false;
}

if (!$final) {
$this->hugePayload .= $payload;

return NULL;
} // this is the last fragment, and we are processing a huge_payload

if ($this->hugePayload) {
$payload = $this->hugePayload .= $payload;
$this->hugePayload = NULL;
}

return $payload;
}

/**
* @param $final
* @param $payload
* @param $opcode
* @param $masked
* @throws ConnectionException
* @throws \Exception
*/
protected function sendFragment($final, $payload, $opcode, $masked)
{
// Binary string for header.
$frameHeadBin = '';
// Write FIN, final fragment bit.
$frameHeadBin .= (bool)$final ? '1' : '0';
// RSV 1, 2, & 3 false and unused.
$frameHeadBin .= '000';
// Opcode rest of the byte.
$frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]);
// Use masking?
$frameHeadBin .= $masked ? '1' : '0';

// 7 bits of payload length...
$payloadLen = strlen($payload);
if ($payloadLen > self::MAX_BYTES_READ) {
$frameHeadBin .= decbin(self::MASK_127);
$frameHeadBin .= sprintf('%064b', $payloadLen);
} else if ($payloadLen > self::MASK_125) {
$frameHeadBin .= decbin(self::MASK_126);
$frameHeadBin .= sprintf('%016b', $payloadLen);
} else {
$frameHeadBin .= sprintf('%07b', $payloadLen);
}

$frame = '';

// Write frame head to frame.
foreach (str_split($frameHeadBin, 8) as $binstr) {
$frame .= chr(bindec($binstr));
}
// Handle masking
if ($masked) {
// generate a random mask:
$mask = '';
for ($i = 0; $i < 4; $i++) {
$mask .= chr(random_int(0, 255));
}
$frame .= $mask;
}

// Append payload to frame:
for ($i = 0; $i < $payloadLen; $i++) {
$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
}

$this->write($frame);
}
}
Loading

0 comments on commit 0cf5760

Please sign in to comment.