/** * 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; }
/** * Generate a public resource URI. * * The generated URI will be in origin-form, no scheme and authority). * * @param string $path K1 resource path (k1:// URL scheme is optional). * @return string URI in origin-form (no scheme and authority). * * @throws \RuntimeException When the resource exists but is not a public resource. */ public function resourceUri(string $path) : string { if (!$this->locator->isPublicFile($path)) { throw new \RuntimeException(\sprintf('Path "%s" does not refer to a public resource', $path)); } if ('k1://' === \substr($path, 0, 5)) { $path = \substr($path, 5); } return $this->resourcePath . '/' . Uri::encode($path, false); }
public function withUri($uri) : HttpRequest { $uri = Uri::parse($uri); $request = clone $this; $request->uri = $uri; $host = $uri->getHost(); if ($host != '') { $host .= $uri->getPort() ? ':' . $uri->getPort() : ''; $request = $request->withHeader('Host', $host); } return $request; }
/** * Create an HTTP redirect response. * * @param string $uri The target URI to redirect the recipient to. * @param int $status HTTP status code to be used for the redirect. * * @throws \InvalidArgumentException When the given HTTP status code is not usable as a redirect code. */ public function __construct($uri, int $status = Http::SEE_OTHER) { switch ($status) { case Http::MOVED_PERMANENTLY: case Http::FOUND: case Http::SEE_OTHER: case Http::TEMPORARY_REDIRECT: case Http::PERMANENT_REDIRECT: // These are valid redirect codes ;) break; default: throw new \InvalidArgumentException(\sprintf('Invalid HTTP redirect status code: "%s"', $status)); } parent::__construct($status, ['Location' => (string) Uri::parse($uri)]); }
/** * {@inheritdoc} */ public function __invoke(HttpRequest $request, NextMiddleware $next) : \Generator { static $methods = [Http::HEAD => true, Http::GET => true]; if (empty($methods[$request->getMethod()])) { return yield from $next($request); } $path = '/' . \trim(Uri::decode($request->getRequestTarget()), '/'); if (0 !== \strpos($path, $this->base)) { return yield from $next($request); } $path = \substr($path, \strlen($this->base)); try { $file = $this->locator->locatePublicFile($path); } catch (ResourceNotFoundException $e) { return yield from $next($request); } $ext = false === ($pos = \strpos($file, '.')) ? '' : \strtolower(\substr($file, $pos + 1)); foreach ($this->processors as $processor) { $result = $processor($request, $file, $ext); if ($result instanceof \Generator) { $result = (yield from $result); } if ($result instanceof HttpResponse) { $response = $result; break; } } if (!isset($response)) { $response = new FileResponse($file); } if (!$response->hasHeader('Cache-Control')) { $response = $response->withHeader('Cache-Control', \sprintf('public, max-age=%u', $this->ttl)); if ($request->getProtocolVersion() === '1.0') { $response = $response->withHeader('Expires', \gmdate(Http::DATE_RFC1123, \time() + $this->ttl)); } } return $response; }
/** * Automatically follow HTTP redirects according to HTTP status code and location header. * * Uncached HTTP request bodies will be cached prior to being sent to the remote endpoint. * * @param HttpRequest $request * @param NextMiddleware $next * @return HttpResponse * * @throws TooManyRedirectsException When the maximum number of redirects for a single HTTP request has been exceeded. */ public function __invoke(HttpRequest $request, NextMiddleware $next) : \Generator { $body = $request->getBody(); if (!$body->isCached()) { $request = $request->withBody(new BufferedBody((yield $body->getReadableStream()))); } for ($i = -1; $i < $this->maxRedirects; $i++) { $response = (yield from $next($request)); switch ($response->getStatusCode()) { case Http::MOVED_PERMANENTLY: case Http::FOUND: case Http::SEE_OTHER: $request = $request->withMethod(Http::GET); $request = $request->withoutHeader('Content-Type'); $request = $request->withBody(new StringBody()); break; case Http::TEMPORARY_REDIRECT: case Http::PERMANENT_REDIRECT: // Replay request to a different URL. break; default: return $response; } try { $uri = Uri::parse($response->getHeaderLine('Location')); $target = $uri->getPath(); if ('' !== ($query = $uri->getQuery())) { $target .= '?' . $query; } $request = $request->withUri($uri); $request = $request->withRequestTarget($target); } finally { (yield $response->getBody()->discard()); } } throw new TooManyRedirectsException(\sprintf('Limit of %s HTTP redirects exceeded', $this->maxRedirects)); }
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); }
/** * Assemble an HTTP request from received FCGI params. */ protected function buildRequest() : HttpRequest { static $extra = ['CONTENT_TYPE' => 'Content-Type', 'CONTENT_LENGTH' => 'Content-Length', 'CONTENT_MD5' => 'Content-MD5']; $uri = \strtolower($this->params['REQUEST_SCHEME'] ?? 'http') . '://'; $uri .= $this->params['HTTP_HOST'] ?? $this->context->getPeerName(); if (!empty($this->params['SERVER_PORT'])) { $uri .= ':' . (int) $this->params['SERVER_PORT']; } $uri = Uri::parse($uri . '/' . \ltrim($this->params['REQUEST_URI'] ?? '', '/')); $request = new HttpRequest($uri, $this->params['REQUEST_METHOD'] ?? Http::GET, [], '1.1'); foreach ($this->params as $k => $v) { if ('HTTP_' === \substr($k, 0, 5)) { switch ($k) { case 'HTTP_TRANSFER_ENCODING': case 'HTTP_CONTENT_ENCODING': case 'HTTP_KEEP_ALIVE': // Skip these headers... break; default: $request = $request->withAddedHeader(\str_replace('_', '-', \substr($k, 5)), (string) $v); } } } foreach ($extra as $k => $v) { if (isset($this->params[$k])) { $request = $request->withHeader($v, $this->params[$k]); } } $addresses = [$this->conn->getRemoteAddress()]; if (isset($this->params['REMOTE_ADDR'])) { $addresses = \array_merge([$this->params['REMOTE_ADDR']], $addresses); } $request = $request->withAddress(...$addresses); $request = $request->withAttribute(HttpDriverContext::class, $this->context); return $request->withBody(new StreamBody(new ReadableChannelStream($this->body))); }
public function getBaseUri() : Uri { return Uri::parse(\sprintf('%s://%s/', $this->server->isEncrypted() ? 'https' : 'http', $this->socketFactory->getPeer())); }
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); }
/** * Parse the next HTTP request that arrives via the given stream. * * @param SocketStream $socket * @return HttpRequest */ protected function parseNextRequest(HttpDriverContext $context, SocketStream $socket) : \Generator { $request = (yield new Timeout(30, new Coroutine($this->parser->parseRequest($socket)))); $request->getBody()->setCascadeClose(false); if ($request->getProtocolVersion() == '1.1') { if (\in_array('100-continue', $request->getHeaderTokenValues('Expect'), true)) { $request->getBody()->setExpectContinue($socket); } } $peerName = $context->getPeerName(); $protocol = $context->isEncrypted() ? 'https' : 'http'; $proxy = $context->getProxySettings(); $parts = \explode(':', $socket->getRemoteAddress()); \array_pop($parts); $ip = \implode(':', $parts); $addresses = [$ip === '' ? '127.0.0.1' : $ip]; if ($proxy->isTrustedProxy($ip)) { $host = $proxy->getHost($request); if ($host === null) { if (!$request->hasHeader('Host')) { return $request; } $peerName = $request->getHeaderLine('Host'); } else { $peerName = $host; } $protocol = $proxy->getScheme($request) ?? $protocol; $addresses = \array_merge($proxy->getAddresses($request), $addresses); } elseif ($request->hasHeader('Host')) { $peerName = $request->getHeaderLine('Host'); } elseif ($request->getProtocolVersion() === '1.1') { return $request; } $request = $request->withAddress(...$addresses); $request = $request->withAttribute(HttpDriverContext::class, $context); $target = $request->getRequestTarget(); if (\substr($target, 0, 1) === '/') { $request = $request->withUri(Uri::parse(\sprintf('%s://%s/%s', $protocol, $peerName, \ltrim($target, '/')))); } else { $request = $request->withUri(Uri::parse(\sprintf('%s://%s/', $protocol, $peerName))); } return $request; }