/**
  * Coroutine that connects to a WebSocket server.
  * 
  * @param Uri $uri HTTP URI (use http / https as scheme instead of ws / wss).
  * @param array $options Stream context options (e.g. SSL settings) to be passed to SocketStream::connect().
  * @param LoggerInterface $logger
  * @return WebSocketConnector
  */
 public static function connect(Uri $uri, array $options = [], LoggerInterface $logger = NULL) : \Generator
 {
     $secure = $uri->getScheme() === 'https';
     $host = $uri->getHost();
     $port = $uri->getPort() ?? ($secure ? 443 : 80);
     $socket = (yield from SocketStream::connect($host, $port, 'tcp', 0, array_replace_recursive($options, ['socket' => ['tcp_nodelay' => true]])));
     try {
         if ($secure) {
             yield from $socket->encryptClient();
         }
         $nonce = base64_encode(random_bytes(16));
         $message = sprintf("GET /%s HTTP/1.1\r\n", $uri->getPath());
         $message .= "Upgrade: websocket\r\n";
         $message .= "Connection: Upgrade\r\n";
         $message .= "Content-Length: 0\r\n";
         $message .= sprintf("Host: %s\r\n", $host);
         $message .= sprintf("Sec-WebSocket-Key: %s\r\n", $nonce);
         $message .= "Sec-WebSocket-Version: 13\r\n";
         $message .= "\r\n";
         yield from $socket->write($message);
         $m = NULL;
         $line = (yield from $socket->readLine());
         if (!preg_match("'^HTTP/(1\\.[01])\\s+([1-5][0-9]{2})\\s*(.*)\$'i", $line, $m)) {
             throw new WebSocketException('Server did not respond with a valid HTTP/1 status line');
         }
         $response = new HttpResponse();
         $response = $response->withStatus((int) $m[2], trim($m[3]));
         $response = $response->withProtocolVersion($m[1]);
         while (!$socket->eof()) {
             $line = (yield from $socket->readLine());
             if ($line === '') {
                 break;
             }
             $header = array_map('trim', explode(':', $line, 2));
             $response = $response->withAddedHeader(...$header);
         }
         if ($response->getProtocolVersion() !== '1.1') {
             throw new WebSocketException('WebSocket handshake requires HTTP/1.1 response');
         }
         if ($response->getStatusCode() !== 101) {
             throw new WebSocketException('WebSocket protocol switching failed');
         }
         $accept = base64_encode(sha1($nonce . self::GUID, true));
         if ($accept !== $response->getHeaderLine('Sec-WebSocket-Accept')) {
             throw new WebSocketException('Invalid WebSocket Accept header received');
         }
     } catch (\Throwable $e) {
         $socket->close();
         throw $e;
     }
     $conn = new static($socket, (yield eventEmitter()), $logger);
     $conn->watcher = (yield runTask($conn->handleFrames(), 'WebSocket Message Handler'));
     $conn->watcher->setAutoShutdown(true);
     return $conn;
 }
Exemplo n.º 2
0
 /**
  * Reads the next WebSocket frame from the socket.
  * 
  * This method will unmask frames as needed and asserts frame size constraints.
  * 
  * @return Frame
  */
 protected function readNextFrame() : \Generator
 {
     list($byte1, $byte2) = \array_map('ord', \str_split((yield $this->socket->readBuffer(2, true)), 1));
     $masked = $byte2 & Frame::MASKED ? true : false;
     if ($this->client && $masked) {
         throw new ConnectionException('Received masked frame from server', Frame::PROTOCOL_ERROR);
     }
     if (!$this->client && !$masked) {
         throw new ConnectionException('Received unmasked frame from client', Frame::PROTOCOL_ERROR);
     }
     // Parse extended length fields:
     $len = $byte2 & Frame::LENGTH;
     if ($len === 0x7e) {
         $len = \unpack('n', (yield $this->socket->readBuffer(2, true)))[1];
     } elseif ($len === 0x7f) {
         $lp = \unpack('N2', (yield $this->socket->readBuffer(8, true)));
         // 32 bit int check:
         if (\PHP_INT_MAX === 0x7fffffff) {
             if ($lp[1] !== 0 || $lp[2] < 0) {
                 throw new ConnectionException('Max payload size exceeded', Frame::MESSAGE_TOO_BIG);
             }
             $len = $lp[2];
         } else {
             $len = $lp[1] << 32 | $lp[2];
             if ($len < 0) {
                 throw new ConnectionException('Cannot use most significant bit in 64 bit length field', Frame::MESSAGE_TOO_BIG);
             }
         }
     }
     if ($len < 0) {
         throw new ConnectionException('Payload length must not be negative', Frame::MESSAGE_TOO_BIG);
     }
     if ($len > $this->maxFrameSize) {
         throw new ConnectionException(\sprintf('Maximum frame size of %u bytes exceeded', $this->maxFrameSize), Frame::MESSAGE_TOO_BIG);
     }
     // Read and unmask frame data.
     if ($this->client) {
         $data = (yield $this->socket->readBuffer($len, true));
     } else {
         $key = (yield $this->socket->readBuffer(4, true));
         $data = (yield $this->socket->readBuffer($len, true)) ^ \str_pad($key, $len, $key, \STR_PAD_RIGHT);
     }
     return new Frame($byte1 & Frame::OPCODE, $data, $byte1 & Frame::FINISHED ? true : false, $byte1 & Frame::RESERVED);
 }
Exemplo n.º 3
0
 public static function connect(string $dsn, string $username, string $password, LoggerInterface $logger = NULL) : \Generator
 {
     if ('mysql:' !== substr($dsn, 0, 6)) {
         throw new \InvalidArgumentException(sprintf('Invalid MySQL DSN: "%s"', $dsn));
     }
     $settings = [];
     foreach (explode(';', substr($dsn, 6)) as $part) {
         list($k, $v) = array_map('trim', explode('=', $part));
         switch ($k) {
             case 'host':
             case 'dbname':
             case 'unix_socket':
                 $settings[$k] = $v;
                 break;
             case 'port':
                 $settings[$k] = (int) $v;
                 break;
             default:
                 throw new \InvalidArgumentException(sprintf('Unknown MySQL DSN param: "%s"', $k));
         }
     }
     if (empty($settings['host']) && empty($settings['unix_socket'])) {
         throw new \InvalidArgumentException('Neighter MySQL host nor Unix domain socket specified in MySQL DSN');
     }
     if (!empty($settings['unix_socket'])) {
         if (!Socket::isUnixSocketSupported()) {
             throw new \RuntimeException(sprintf('Cannot connect to MySQL socket "%s", PHP was not compiled with support for Unix domain sockets', $settings['unix_socket']));
         }
         $client = new Client(yield from SocketStream::fromUrl('unix://' . $settings['unix_socket']), $logger);
     } else {
         $client = new Client(yield from SocketStream::connect($settings['host'], $settings['port'] ?? 3306), $logger);
     }
     yield from $client->handleHandshake($username, $password);
     $conn = new static($client, (yield currentExecutor()), $logger);
     if (!empty($settings['dbname'])) {
         yield from $conn->changeDefaultSchema($settings['dbname']);
     }
     return $conn;
 }
Exemplo n.º 4
0
 /**
  * {@inheritdoc}
  */
 public function upgradeConnection(SocketStream $socket, HttpRequest $request, HttpResponse $response) : \Generator
 {
     $endpoint = $response->getAttribute(Endpoint::class);
     if (!$endpoint instanceof Endpoint) {
         throw new \InvalidArgumentException('No endpoint object passed to WebSocket handler');
     }
     if ($this->logger) {
         $this->logger->debug('HTTP/{protocol} connection from {peer} upgraded to WebSocket', ['protocol' => $request->getProtocolVersion(), 'peer' => $socket->getRemoteAddress()]);
     }
     $conn = new Connection($socket, false, $response->getHeaderLine('Sec-WebSocket-Protocol'));
     if ($this->logger) {
         $conn->setLogger($this->logger);
     }
     if ($deflate = $response->getAttribute(PerMessageDeflate::class)) {
         $conn->enablePerMessageDeflate($deflate);
     }
     yield from $this->delegateToEndpoint($conn, $endpoint);
 }
Exemplo n.º 5
0
 private static function handshake(SocketStream $socket) : \Generator
 {
     $transmitter = new SocketTransmitter($socket);
     list($type, $data) = (yield from $transmitter->receive());
     if ($type !== SocketTransmitter::TYPE_HANDSHAKE || !isset($data['id'])) {
         return $socket->close();
     }
     $id = $data['id'];
     if (empty(self::$handshakes[$id])) {
         return $socket->close();
     }
     list($worker, $defer) = self::$handshakes[$id];
     unset(self::$handshakes[$id]);
     if (empty(self::$handshakes) && self::$serverAwait !== null) {
         self::$serverAwait->cancel(new PoolShutdownException('Handshakes done'));
     }
     $worker->connect($socket, $transmitter);
     self::$sharedWorkers[$id] = $worker;
     $defer->resolve($worker);
 }
Exemplo n.º 6
0
 /**
  * Write the given frame to the socket (does not honor concurrency control via writer).
  *
  * @param Frame $frame
  * @return int Number of transmitted bytes.
  */
 public function writeFrame(Frame $frame) : Awaitable
 {
     return $this->socket->write($frame->encode($this->client ? \random_bytes(4) : null));
 }
Exemplo n.º 7
0
 /**
  * Perform a direct upgrade of the connection to HTTP/2.
  * 
  * @param HttpDriverContext $context HTTP context related to the HTTP endpoint.
  * @param SocketStream $socket The underlying socket transport.
  * @param HttpRequest $request The HTTP request that caused the connection upgrade.
  * @param callable $action Server action to be performed for each incoming HTTP request.
  */
 protected function upgradeConnectionDirect(HttpDriverContext $context, SocketStream $socket, HttpRequest $request, callable $action) : \Generator
 {
     $preface = (yield $socket->readBuffer(\strlen(Connection::PREFACE_BODY), true));
     if ($preface !== Connection::PREFACE_BODY) {
         throw new StatusException(Http::BAD_REQUEST, 'Invalid HTTP/2 connection preface body');
     }
     if ($this->logger) {
         $this->logger->info('{ip} "{method} {target} HTTP/{protocol}" {status} {size}', ['ip' => $request->getClientAddress(), 'method' => $request->getMethod(), 'target' => $request->getRequestTarget(), 'protocol' => $request->getProtocolVersion(), 'status' => Http::SWITCHING_PROTOCOLS, 'size' => '-']);
     }
     $conn = new Connection($socket, new HPack($this->hpackContext), $this->logger);
     (yield $conn->performServerHandshake(null, true));
     if ($this->logger) {
         $this->logger->info('HTTP/{protocol} connection from {peer} upgraded to HTTP/2', ['protocol' => $request->getProtocolVersion(), 'peer' => $socket->getRemoteAddress()]);
     }
     $remotePeer = $socket->getRemoteAddress();
     try {
         while (null !== ($received = (yield $conn->nextRequest($context)))) {
             new Coroutine($this->processRequest($conn, $action, ...$received), true);
         }
     } finally {
         try {
             $conn->shutdown();
         } finally {
             if ($this->logger) {
                 $this->logger->debug('Closed HTTP/2 connection to {peer}', ['peer' => $remotePeer]);
             }
         }
     }
 }
Exemplo n.º 8
0
 protected function sendRequest(SocketStream $socket, HttpRequest $request, int &$sent) : \Generator
 {
     $request = $this->normalizeRequest($request);
     $body = $request->getBody();
     $size = (yield $body->getSize());
     $sendfile = false;
     if ($body instanceof FileBody && $socket->isSendfileSupported()) {
         $sendfile = true;
         $chunk = $size ? '' : null;
     } else {
         if ($request->getProtocolVersion() == '1.0' && $size === null) {
             if (!$body->isCached()) {
                 $body = new BufferedBody((yield $body->getReadableStream()));
             }
             (yield $body->discard());
             $size = (yield $body->getSize());
         }
         $bodyStream = (yield $body->getReadableStream());
         $clen = $size === null ? 4089 : 4096;
         $chunk = (yield $bodyStream->readBuffer($clen));
         $len = \strlen($chunk ?? '');
         if ($chunk === null) {
             $size = 0;
         } elseif ($len < $clen) {
             $size = $len;
         }
     }
     $buffer = $this->serializeHeaders($request, $size);
     $expect = false;
     if ($this->expectContinue && $chunk !== null && $request->getProtocolVersion() == '1.1') {
         $expect = true;
         $buffer .= "Expect: 100-continue\r\n";
     }
     (yield $socket->write($buffer . "\r\n"));
     (yield $socket->flush());
     if ($expect) {
         if (!\preg_match("'^HTTP/1\\.1\\s+100(?:\$|\\s)'i", $line = (yield $socket->readLine()))) {
             try {
                 return $line;
             } finally {
                 if (isset($bodyStream)) {
                     $bodyStream->close();
                 }
             }
         }
     }
     if ($sendfile) {
         if ($size) {
             $sent += (yield LoopConfig::currentFilesystem()->sendfile($body->getFile(), $socket->getSocket(), $size));
         }
     } elseif ($size === null) {
         $sent += (yield $socket->write(\dechex($len) . "\r\n" . $chunk . "\r\n"));
         if ($len === $clen) {
             // Align each chunk with length and line breaks to fit into 4 KB payload.
             $sent += (yield new CopyBytes($bodyStream, $socket, true, null, 4089, function (string $chunk) {
                 return \dechex(\strlen($chunk)) . "\r\n" . $chunk . "\r\n";
             }));
         }
         (yield $socket->write("0\r\n\r\n"));
     } elseif ($size > 0) {
         $sent += (yield $socket->write($chunk));
         if ($len === $clen) {
             $sent += (yield new CopyBytes($bodyStream, $socket, true, $size - $len));
         }
     }
     (yield $socket->flush());
 }
Exemplo n.º 9
0
 protected function sendDeferredResponse(SocketStream $socket, HttpRequest $request, HttpResponse $response, bool $nobody, bool $close) : \Generator
 {
     if ($this->logger) {
         $this->logger->info('{ip} "{method} {target} HTTP/{protocol}" {status} {size}', ['ip' => $request->getClientAddress(), 'method' => $request->getMethod(), 'target' => $request->getRequestTarget(), 'protocol' => $request->getProtocolVersion(), 'status' => $response->getStatusCode(), 'size' => '-']);
     }
     if (!$nobody) {
         $close = true;
     }
     (yield $socket->write($this->serializeHeaders($response, $close, null, $nobody, true) . "\r\n"));
     (yield $socket->flush());
     if ($nobody) {
         $response->getBody()->close(false);
         return !$close;
     }
     $watcher = null;
     $task = new Coroutine(function () use(&$watcher, $socket, $request, $response) {
         $body = $response->getBody();
         $body->start($request, $this->logger);
         $bodyStream = (yield $body->getReadableStream());
         $e = null;
         try {
             while (null !== ($chunk = (yield $bodyStream->read()))) {
                 (yield $socket->write($chunk));
             }
         } catch (StreamClosedException $e) {
             // Client disconnected from server.
         } finally {
             try {
                 $bodyStream->close();
             } finally {
                 $body->close($e ? true : false);
             }
             $watcher->cancel(new \RuntimeException());
         }
         return false;
     }, true);
     $watcher = new Coroutine(function () use($socket, $task) {
         $socket = $socket->getSocket();
         while (\is_resource($socket) && !\feof($socket)) {
             (yield new AwaitRead($socket));
         }
         $task->cancel(new StreamClosedException('Client disconnected'));
     });
     return (yield $task);
 }
Exemplo n.º 10
0
 /**
  * Coroutine that processes inbound FCGI records.
  */
 protected function handleIncomingRecords() : \Generator
 {
     static $header = 'Cversion/Ctype/nid/nlen/Cpad/x';
     try {
         $peer = $this->socket->getRemoteAddress();
         if ($this->logger) {
             $this->logger->debug('Accepted new FCGI connection from {peer}', ['peer' => $peer]);
         }
         while (true) {
             list($version, $type, $id, $len, $pad) = \array_values(\unpack($header, (yield $this->socket->readBuffer(8, true))));
             $payload = $len > 0 ? (yield $this->socket->readBuffer($len, true)) : '';
             if ($pad > 0) {
                 (yield $this->socket->readBuffer($pad, true));
             }
             $record = new Record($version, $type, $id, $payload);
             switch ($record->type) {
                 case Record::FCGI_BEGIN_REQUEST:
                     list($role, $flags) = \array_values(\unpack('nrole/Cflags/x5', $record->data));
                     if ($role != self::FCGI_RESPONDER) {
                         throw new \RuntimeException('Unsupported FGCI role');
                     }
                     $this->handlers[$id] = new Handler($id, $this, $this->context, $flags & self::FCGI_KEEP_CONNECTION ? true : false);
                     if ($this->logger) {
                         $this->handlers[$id]->setLogger($this->logger);
                     }
                     break;
                 case Record::FCGI_ABORT_REQUEST:
                     if (!(yield $this->closeHandler($id))) {
                         return;
                     }
                     break;
                 case Record::FCGI_PARAMS:
                     $this->handlers[$id]->handleParams($record);
                     break;
                 case Record::FCGI_STDIN:
                     yield from $this->handlers[$id]->handleStdin($record, $this->incoming);
                     break;
             }
         }
     } finally {
         $this->socket->close();
         $this->processor = null;
         if ($this->logger) {
             $this->logger->debug('Closed FCGi connection to {peer}', ['peer' => $peer]);
         }
     }
 }