/** * 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; }
public function releaseConnection(Uri $uri, ConnectorContext $context, int $ttl = null, int $remaining = null) { $key = \sprintf('%s://%s', $uri->getScheme(), $uri->getHostWithPort(true)); if ($ttl === null) { $ttl = $this->maxLifetime; } else { $ttl = \min($ttl, $this->maxLifetime); } if ($remaining === null) { $remaining = $this->max; } else { $remaining = \min($remaining, $this->max); } if ($remaining > 0 && $ttl > 0 && $context->socket->isAlive()) { $context->connected = true; if (empty($this->conns[$key])) { $this->conns[$key] = new \SplQueue(); } if (!empty($this->connecting[$key])) { $defer = \array_shift($this->connecting[$key]); return $defer->resolve($context); } $context->expires = \time() + $ttl; $context->remaining = $remaining; $this->conns[$key]->enqueue($context); return; } $context->socket->close(); $this->connectionDisposed($key); }
protected function connectSocket(Uri $uri) : Awaitable { $host = $uri->getHost(); if (Address::isResolved($host)) { $factory = new SocketFactory(new Address($host) . ':' . $uri->getPort(), 'tcp'); } else { $factory = new SocketFactory($uri->getHostWithPort(true), 'tcp'); } $factory->setTcpNoDelay(true); if ($this->protocols && Socket::isAlpnSupported()) { $factory->setOption('ssl', 'alpn_protocols', \implode(',', $this->protocols)); } return $factory->createSocketStream(5, $uri->getScheme() === 'https'); }
/** * {@inheritdoc} */ public function getConnectorContext(Uri $uri) : Awaitable { $key = \sprintf('%s://%s', $uri->getScheme(), $uri->getHostWithPort(true)); if (isset($this->connections[$key])) { if ($this->connections[$key]->isAlive()) { $context = new ConnectorContext(); $context->connected = true; $context->conn = $this->connections[$key]; return new Success($context); } unset($this->connections[$key]); } if (isset($this->connecting[$key])) { $defer = new Deferred(); $this->connecting[$key]->enqueue($defer); return $defer; } $this->connecting[$key] = new \SplQueue(); $context = new ConnectorContext(function ($context) use($key) { if ($this->connecting[$key]->isEmpty()) { unset($this->connecting[$key]); } else { $this->connecting[$key]->dequeue()->resolve($context); } }); return new Success($context); }