| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 | <?phpnamespace GuzzleHttp\Handler;use GuzzleHttp\Exception\ConnectException;use GuzzleHttp\Exception\RequestException;use GuzzleHttp\Promise as P;use GuzzleHttp\Promise\FulfilledPromise;use GuzzleHttp\Promise\PromiseInterface;use GuzzleHttp\Psr7;use GuzzleHttp\TransferStats;use GuzzleHttp\Utils;use Psr\Http\Message\RequestInterface;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\StreamInterface;use Psr\Http\Message\UriInterface;/** * HTTP handler that uses PHP's HTTP stream wrapper. * * @final */class StreamHandler{    /**     * @var array     */    private $lastHeaders = [];    /**     * Sends an HTTP request.     *     * @param RequestInterface $request Request to send.     * @param array            $options Request transfer options.     */    public function __invoke(RequestInterface $request, array $options): PromiseInterface    {        // Sleep if there is a delay specified.        if (isset($options['delay'])) {            \usleep($options['delay'] * 1000);        }        $startTime = isset($options['on_stats']) ? Utils::currentTime() : null;        try {            // Does not support the expect header.            $request = $request->withoutHeader('Expect');            // Append a content-length header if body size is zero to match            // cURL's behavior.            if (0 === $request->getBody()->getSize()) {                $request = $request->withHeader('Content-Length', '0');            }            return $this->createResponse(                $request,                $options,                $this->createStream($request, $options),                $startTime            );        } catch (\InvalidArgumentException $e) {            throw $e;        } catch (\Exception $e) {            // Determine if the error was a networking error.            $message = $e->getMessage();            // This list can probably get more comprehensive.            if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed                || false !== \strpos($message, 'Connection refused')                || false !== \strpos($message, "couldn't connect to host") // error on HHVM                || false !== \strpos($message, 'connection attempt failed')            ) {                $e = new ConnectException($e->getMessage(), $request, $e);            } else {                $e = RequestException::wrapException($request, $e);            }            $this->invokeStats($options, $request, $startTime, null, $e);            return P\Create::rejectionFor($e);        }    }    private function invokeStats(        array $options,        RequestInterface $request,        ?float $startTime,        ResponseInterface $response = null,        \Throwable $error = null    ): void {        if (isset($options['on_stats'])) {            $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []);            ($options['on_stats'])($stats);        }    }    /**     * @param resource $stream     */    private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime): PromiseInterface    {        $hdrs = $this->lastHeaders;        $this->lastHeaders = [];        try {            [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs);        } catch (\Exception $e) {            return P\Create::rejectionFor(                new RequestException('An error was encountered while creating the response', $request, null, $e)            );        }        [$stream, $headers] = $this->checkDecode($options, $headers, $stream);        $stream = Psr7\Utils::streamFor($stream);        $sink = $stream;        if (\strcasecmp('HEAD', $request->getMethod())) {            $sink = $this->createSink($stream, $options);        }        try {            $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);        } catch (\Exception $e) {            return P\Create::rejectionFor(                new RequestException('An error was encountered while creating the response', $request, null, $e)            );        }        if (isset($options['on_headers'])) {            try {                $options['on_headers']($response);            } catch (\Exception $e) {                return P\Create::rejectionFor(                    new RequestException('An error was encountered during the on_headers event', $request, $response, $e)                );            }        }        // Do not drain when the request is a HEAD request because they have        // no body.        if ($sink !== $stream) {            $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));        }        $this->invokeStats($options, $request, $startTime, $response, null);        return new FulfilledPromise($response);    }    private function createSink(StreamInterface $stream, array $options): StreamInterface    {        if (!empty($options['stream'])) {            return $stream;        }        $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+');        return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink);    }    /**     * @param resource $stream     */    private function checkDecode(array $options, array $headers, $stream): array    {        // Automatically decode responses when instructed.        if (!empty($options['decode_content'])) {            $normalizedKeys = Utils::normalizeHeaderKeys($headers);            if (isset($normalizedKeys['content-encoding'])) {                $encoding = $headers[$normalizedKeys['content-encoding']];                if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {                    $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream));                    $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];                    // Remove content-encoding header                    unset($headers[$normalizedKeys['content-encoding']]);                    // Fix content-length header                    if (isset($normalizedKeys['content-length'])) {                        $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];                        $length = (int) $stream->getSize();                        if ($length === 0) {                            unset($headers[$normalizedKeys['content-length']]);                        } else {                            $headers[$normalizedKeys['content-length']] = [$length];                        }                    }                }            }        }        return [$stream, $headers];    }    /**     * Drains the source stream into the "sink" client option.     *     * @param string $contentLength Header specifying the amount of     *                              data to read.     *     * @throws \RuntimeException when the sink option is invalid.     */    private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface    {        // If a content-length header is provided, then stop reading once        // that number of bytes has been read. This can prevent infinitely        // reading from a stream when dealing with servers that do not honor        // Connection: Close headers.        Psr7\Utils::copyToStream(            $source,            $sink,            (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1        );        $sink->seek(0);        $source->close();        return $sink;    }    /**     * Create a resource and check to ensure it was created successfully     *     * @param callable $callback Callable that returns stream resource     *     * @return resource     *     * @throws \RuntimeException on error     */    private function createResource(callable $callback)    {        $errors = [];        \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool {            $errors[] = [                'message' => $msg,                'file' => $file,                'line' => $line,            ];            return true;        });        try {            $resource = $callback();        } finally {            \restore_error_handler();        }        if (!$resource) {            $message = 'Error creating resource: ';            foreach ($errors as $err) {                foreach ($err as $key => $value) {                    $message .= "[$key] $value".\PHP_EOL;                }            }            throw new \RuntimeException(\trim($message));        }        return $resource;    }    /**     * @return resource     */    private function createStream(RequestInterface $request, array $options)    {        static $methods;        if (!$methods) {            $methods = \array_flip(\get_class_methods(__CLASS__));        }        if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {            throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);        }        // HTTP/1.1 streams using the PHP stream wrapper require a        // Connection: close header        if ($request->getProtocolVersion() == '1.1'            && !$request->hasHeader('Connection')        ) {            $request = $request->withHeader('Connection', 'close');        }        // Ensure SSL is verified by default        if (!isset($options['verify'])) {            $options['verify'] = true;        }        $params = [];        $context = $this->getDefaultContext($request);        if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {            throw new \InvalidArgumentException('on_headers must be callable');        }        if (!empty($options)) {            foreach ($options as $key => $value) {                $method = "add_{$key}";                if (isset($methods[$method])) {                    $this->{$method}($request, $context, $value, $params);                }            }        }        if (isset($options['stream_context'])) {            if (!\is_array($options['stream_context'])) {                throw new \InvalidArgumentException('stream_context must be an array');            }            $context = \array_replace_recursive($context, $options['stream_context']);        }        // Microsoft NTLM authentication only supported with curl handler        if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) {            throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');        }        $uri = $this->resolveHost($request, $options);        $contextResource = $this->createResource(            static function () use ($context, $params) {                return \stream_context_create($context, $params);            }        );        return $this->createResource(            function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) {                $resource = @\fopen((string) $uri, 'r', false, $contextResource);                $this->lastHeaders = $http_response_header ?? [];                if (false === $resource) {                    throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context);                }                if (isset($options['read_timeout'])) {                    $readTimeout = $options['read_timeout'];                    $sec = (int) $readTimeout;                    $usec = ($readTimeout - $sec) * 100000;                    \stream_set_timeout($resource, $sec, $usec);                }                return $resource;            }        );    }    private function resolveHost(RequestInterface $request, array $options): UriInterface    {        $uri = $request->getUri();        if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {            if ('v4' === $options['force_ip_resolve']) {                $records = \dns_get_record($uri->getHost(), \DNS_A);                if (false === $records || !isset($records[0]['ip'])) {                    throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);                }                return $uri->withHost($records[0]['ip']);            }            if ('v6' === $options['force_ip_resolve']) {                $records = \dns_get_record($uri->getHost(), \DNS_AAAA);                if (false === $records || !isset($records[0]['ipv6'])) {                    throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);                }                return $uri->withHost('['.$records[0]['ipv6'].']');            }        }        return $uri;    }    private function getDefaultContext(RequestInterface $request): array    {        $headers = '';        foreach ($request->getHeaders() as $name => $value) {            foreach ($value as $val) {                $headers .= "$name: $val\r\n";            }        }        $context = [            'http' => [                'method' => $request->getMethod(),                'header' => $headers,                'protocol_version' => $request->getProtocolVersion(),                'ignore_errors' => true,                'follow_location' => 0,            ],            'ssl' => [                'peer_name' => $request->getUri()->getHost(),            ],        ];        $body = (string) $request->getBody();        if ('' !== $body) {            $context['http']['content'] = $body;            // Prevent the HTTP handler from adding a Content-Type header.            if (!$request->hasHeader('Content-Type')) {                $context['http']['header'] .= "Content-Type:\r\n";            }        }        $context['http']['header'] = \rtrim($context['http']['header']);        return $context;    }    /**     * @param mixed $value as passed via Request transfer options.     */    private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void    {        $uri = null;        if (!\is_array($value)) {            $uri = $value;        } else {            $scheme = $request->getUri()->getScheme();            if (isset($value[$scheme])) {                if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {                    $uri = $value[$scheme];                }            }        }        if (!$uri) {            return;        }        $parsed = $this->parse_proxy($uri);        $options['http']['proxy'] = $parsed['proxy'];        if ($parsed['auth']) {            if (!isset($options['http']['header'])) {                $options['http']['header'] = [];            }            $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";        }    }    /**     * Parses the given proxy URL to make it compatible with the format PHP's stream context expects.     */    private function parse_proxy(string $url): array    {        $parsed = \parse_url($url);        if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {            if (isset($parsed['host']) && isset($parsed['port'])) {                $auth = null;                if (isset($parsed['user']) && isset($parsed['pass'])) {                    $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");                }                return [                    'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}",                    'auth' => $auth ? "Basic {$auth}" : null,                ];            }        }        // Return proxy as-is.        return [            'proxy' => $url,            'auth' => null,        ];    }    /**     * @param mixed $value as passed via Request transfer options.     */    private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void    {        if ($value > 0) {            $options['http']['timeout'] = $value;        }    }    /**     * @param mixed $value as passed via Request transfer options.     */    private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params): void    {        if (            $value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT            || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT            || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT            || (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT)        ) {            $options['http']['crypto_method'] = $value;            return;        }        throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');    }    /**     * @param mixed $value as passed via Request transfer options.     */    private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void    {        if ($value === false) {            $options['ssl']['verify_peer'] = false;            $options['ssl']['verify_peer_name'] = false;            return;        }        if (\is_string($value)) {            $options['ssl']['cafile'] = $value;            if (!\file_exists($value)) {                throw new \RuntimeException("SSL CA bundle not found: $value");            }        } elseif ($value !== true) {            throw new \InvalidArgumentException('Invalid verify request option');        }        $options['ssl']['verify_peer'] = true;        $options['ssl']['verify_peer_name'] = true;        $options['ssl']['allow_self_signed'] = false;    }    /**     * @param mixed $value as passed via Request transfer options.     */    private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void    {        if (\is_array($value)) {            $options['ssl']['passphrase'] = $value[1];            $value = $value[0];        }        if (!\file_exists($value)) {            throw new \RuntimeException("SSL certificate not found: {$value}");        }        $options['ssl']['local_cert'] = $value;    }    /**     * @param mixed $value as passed via Request transfer options.     */    private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void    {        self::addNotification(            $params,            static function ($code, $a, $b, $c, $transferred, $total) use ($value) {                if ($code == \STREAM_NOTIFY_PROGRESS) {                    // The upload progress cannot be determined. Use 0 for cURL compatibility:                    // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html                    $value($total, $transferred, 0, 0);                }            }        );    }    /**     * @param mixed $value as passed via Request transfer options.     */    private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void    {        if ($value === false) {            return;        }        static $map = [            \STREAM_NOTIFY_CONNECT => 'CONNECT',            \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',            \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',            \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',            \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',            \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',            \STREAM_NOTIFY_PROGRESS => 'PROGRESS',            \STREAM_NOTIFY_FAILURE => 'FAILURE',            \STREAM_NOTIFY_COMPLETED => 'COMPLETED',            \STREAM_NOTIFY_RESOLVE => 'RESOLVE',        ];        static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];        $value = Utils::debugResource($value);        $ident = $request->getMethod().' '.$request->getUri()->withFragment('');        self::addNotification(            $params,            static function (int $code, ...$passed) use ($ident, $value, $map, $args): void {                \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);                foreach (\array_filter($passed) as $i => $v) {                    \fwrite($value, $args[$i].': "'.$v.'" ');                }                \fwrite($value, "\n");            }        );    }    private static function addNotification(array &$params, callable $notify): void    {        // Wrap the existing function if needed.        if (!isset($params['notification'])) {            $params['notification'] = $notify;        } else {            $params['notification'] = self::callArray([                $params['notification'],                $notify,            ]);        }    }    private static function callArray(array $functions): callable    {        return static function (...$args) use ($functions) {            foreach ($functions as $fn) {                $fn(...$args);            }        };    }}
 |