w:@CzgM=j!(AQTJAx stD|q!As=o[l[d`hzgu=xa*:fNa93%<3<_/Jo}NpZd|K[,X/D/~˥Ky)\mD]+]Y_틗s%^HբmFp\FYLOcbKO'rď+[3;@?ʉbokg˜{jL P^оy.J,R3+|s*:a\cW=|̗J_ g)_2DJ5=׌z=xBZ@#.ۯK..@I=MȌ9%;1ߍid_W)KJDkIqۉ/&,v]h/AW u_23W{+\U[T6ìi_S0@z;PQģ2c[V@"atyސf@teƴݝp ET}9D1l-#rCK+ H>pk$ vp=W0"p,[α{66insa>z@OCS]}Kh5 J :ldJO"KJuFwl`$Gv/j p#Gs3r/'2 j1㕁)Fg+Rns8~Մf!7U}Z30i}AI"u^lEL2X`D6Fї,t80B]A<s1fqVcVj⍛$R~ȞGf>Q )b2ȅSA2xP\j L%)OU|RwgFN5K]O;t8 9 (f 9Z]#"i9+*Μ)ՎD3?0:Jk{+7w=/ jNJϒ(@chgs,QKE\A5[-&&{U!nbyPL6t[kV32,01o/!^r$l$CZfSt(cr~5擾 5?=hatƒ^+LO8z]S(0{QC֌AewyBBlDC4 |##o(~﮹/QM!aեoU9yiгsb >MXYE>TWIeV4䚟ٷ(ž'I#O&q =[Wzܾ. y(+ۄu8Cu3A;o2"lsJ AǾsS >)pvb$EAg6o>}if=nj}Ablorfd ssSu=v/0EdG x^<ţi懮uFZq:iSpr1saM&_7Raؙt\oN3'wq1s& kR6Df2 n$tL+07Lp~wwdQGbq1T t] bA> >W3loʇvjeƤ:*%ft .B C~+]Nߊ{+g~+4)gK \c`sN9ew+ w:<ZiU":w`(z8^3KvNxsE NsX+Y~$oÅ3U׀[Vs%qdrZ5P>q.(.-}B_ii=n {74-y]Ut!`/ST3Et @ɿ@ś}mo$wX˞ѐgv@$N0*ҽ\\]{{J`H4GN4Ypkۛ~H'u*S7۳Z_u3t4,Gߝ;xPqx`N<1FZ =~5`##l~:ߕ,e@qߙ<8F+ ^^4s&9^Ar v2Sx(uڈ SqYqjmF rc/vI3!gы]#<\SLfth!elO#E$m ~5VB+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); } }; } }