/**
  * 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;
 }
Beispiel #2
0
 /**
  * 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);
 }
Beispiel #3
0
 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)]);
 }
Beispiel #5
0
 /**
  * {@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);
 }
Beispiel #8
0
 /**
  * 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)));
 }
Beispiel #9
0
 public function getBaseUri() : Uri
 {
     return Uri::parse(\sprintf('%s://%s/', $this->server->isEncrypted() ? 'https' : 'http', $this->socketFactory->getPeer()));
 }
Beispiel #10
0
 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');
 }
Beispiel #11
0
 /**
  * {@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);
 }
Beispiel #12
0
 /**
  * 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;
 }