Update website

This commit is contained in:
Guilhem Lavaux 2025-02-11 21:30:02 +01:00
parent 0a686aeb9a
commit c4ffa0f6ee
4360 changed files with 1727 additions and 718385 deletions

View file

@ -118,6 +118,7 @@ final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface,
}
$request = new Request(implode('', $url), $method);
$request->setBodySizeLimit(0);
if ($options['http_version']) {
switch ((float) $options['http_version']) {

View file

@ -274,7 +274,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
if (file_exists($options['bindto'])) {
$curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
} elseif (!str_starts_with($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
$curlopts[\CURLOPT_INTERFACE] = $matches[1];
$curlopts[\CURLOPT_INTERFACE] = trim($matches[1], '[]');
$curlopts[\CURLOPT_LOCALPORT] = $matches[2];
} else {
$curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
@ -424,6 +424,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
try {
$location = self::parseUrl($location);
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
$url = self::resolveUrl($location, $url);
} catch (InvalidArgumentException $e) {
return null;
}
@ -436,16 +438,13 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
}
if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
if ($redirectHeaders && isset($location['authority'])) {
$requestHeaders = parse_url($location['authority'], \PHP_URL_HOST) === $redirectHeaders['host'] ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
} elseif ($noContent && $redirectHeaders) {
curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);
}
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
$url = self::resolveUrl($location, $url);
curl_setopt($ch, \CURLOPT_PROXY, self::getProxyUrl($options['proxy'], $url));
return implode('', $url);

View file

@ -197,7 +197,13 @@ trait HttpClientTrait
if ($resolve = $options['resolve'] ?? false) {
$options['resolve'] = [];
foreach ($resolve as $k => $v) {
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v;
if ('' === $v = (string) $v) {
$v = null;
} elseif ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
$v = substr($v, 1, -1);
}
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = $v;
}
}
@ -220,7 +226,13 @@ trait HttpClientTrait
if ($resolve = $defaultOptions['resolve'] ?? false) {
foreach ($resolve as $k => $v) {
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v];
if ('' === $v = (string) $v) {
$v = null;
} elseif ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
$v = substr($v, 1, -1);
}
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => $v];
}
}
@ -514,29 +526,37 @@ trait HttpClientTrait
*/
private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array
{
if (false === $parts = parse_url($url)) {
if ('/' !== ($url[0] ?? '') || false === $parts = parse_url($url.'#')) {
throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
}
unset($parts['fragment']);
$tail = '';
if (false === $parts = parse_url(\strlen($url) !== strcspn($url, '?#') ? $url : $url.$tail = '#')) {
throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
}
if ($query) {
$parts['query'] = self::mergeQueryString($parts['query'] ?? null, $query, true);
}
$scheme = $parts['scheme'] ?? null;
$host = $parts['host'] ?? null;
if (!$scheme && $host && !str_starts_with($url, '//')) {
$parts = parse_url(':/'.$url.$tail);
$parts['path'] = substr($parts['path'], 2);
$scheme = $host = null;
}
$port = $parts['port'] ?? 0;
if (null !== $scheme = $parts['scheme'] ?? null) {
if (null !== $scheme) {
if (!isset($allowedSchemes[$scheme = strtolower($scheme)])) {
throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s".', $url));
throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s": "%s" expected.', $url, implode('" or "', array_keys($allowedSchemes))));
}
$port = $allowedSchemes[$scheme] === $port ? 0 : $port;
$scheme .= ':';
}
if (null !== $host = $parts['host'] ?? null) {
if (null !== $host) {
if (!\defined('INTL_IDNA_VARIANT_UTS46') && preg_match('/[\x80-\xFF]/', $host)) {
throw new InvalidArgumentException(sprintf('Unsupported IDN "%s", try enabling the "intl" PHP extension or running "composer require symfony/polyfill-intl-idn".', $host));
}
@ -564,7 +584,7 @@ trait HttpClientTrait
'authority' => null !== $host ? '//'.(isset($parts['user']) ? $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '').'@' : '').$host : null,
'path' => isset($parts['path'][0]) ? $parts['path'] : null,
'query' => isset($parts['query']) ? '?'.$parts['query'] : null,
'fragment' => isset($parts['fragment']) ? '#'.$parts['fragment'] : null,
'fragment' => isset($parts['fragment']) && !$tail ? '#'.$parts['fragment'] : null,
];
}

View file

@ -80,12 +80,12 @@ class AmpListener implements EventListener
public function startSendingRequest(Request $request, Stream $stream): Promise
{
$host = $stream->getRemoteAddress()->getHost();
$this->info['primary_ip'] = $host;
if (false !== strpos($host, ':')) {
$host = '['.$host.']';
}
$this->info['primary_ip'] = $host;
$this->info['primary_port'] = $stream->getRemoteAddress()->getPort();
$this->info['pretransfer_time'] = microtime(true) - $this->info['start_time'];
$this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']);

View file

@ -34,19 +34,31 @@ class AmpResolver implements Dns\Resolver
public function resolve(string $name, ?int $typeRestriction = null): Promise
{
if (!isset($this->dnsMap[$name]) || !\in_array($typeRestriction, [Record::A, null], true)) {
$recordType = Record::A;
$ip = $this->dnsMap[$name] ?? null;
if (null !== $ip && str_contains($ip, ':')) {
$recordType = Record::AAAA;
}
if (null === $ip || $recordType !== ($typeRestriction ?? $recordType)) {
return Dns\resolver()->resolve($name, $typeRestriction);
}
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
return new Success([new Record($ip, $recordType, null)]);
}
public function query(string $name, int $type): Promise
{
if (!isset($this->dnsMap[$name]) || Record::A !== $type) {
$recordType = Record::A;
$ip = $this->dnsMap[$name] ?? null;
if (null !== $ip && str_contains($ip, ':')) {
$recordType = Record::AAAA;
}
if (null === $ip || $recordType !== $type) {
return Dns\resolver()->query($name, $type);
}
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
return new Success([new Record($ip, $recordType, null)]);
}
}

View file

@ -79,6 +79,9 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
if (str_starts_with($options['bindto'], 'host!')) {
$options['bindto'] = substr($options['bindto'], 5);
}
if ((\PHP_VERSION_ID < 80223 || 80300 <= \PHP_VERSION_ID && 80311 < \PHP_VERSION_ID) && '\\' === \DIRECTORY_SEPARATOR && '[' === $options['bindto'][0]) {
$options['bindto'] = preg_replace('{^\[[^\]]++\]}', '[$0]', $options['bindto']);
}
}
$hasContentLength = isset($options['normalized_headers']['content-length']);
@ -138,15 +141,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
$lastProgress = [0, 0];
$maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
$multi = $this->multi;
$resolve = static function (string $host, ?string $ip = null) use ($multi): ?string {
if (null !== $ip) {
$multi->dnsCache[$host] = $ip;
}
return $multi->dnsCache[$host] ?? null;
};
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration, $resolve) {
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}
@ -162,7 +157,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$lastProgress = $progress ?: $lastProgress;
}
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo, $resolve);
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
};
} elseif (0 < $options['max_duration']) {
$maxDuration = $options['max_duration'];
@ -330,7 +325,12 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
*/
private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
{
if (null === $ip = $multi->dnsCache[$host] ?? null) {
$flag = '' !== $host && '[' === $host[0] && ']' === $host[-1] && str_contains($host, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4;
$ip = \FILTER_FLAG_IPV6 === $flag ? substr($host, 1, -1) : $host;
if (filter_var($ip, \FILTER_VALIDATE_IP, $flag)) {
// The host is already an IP address
} elseif (null === $ip = $multi->dnsCache[$host] ?? null) {
$info['debug'] .= "* Hostname was NOT found in DNS cache\n";
$now = microtime(true);
@ -338,13 +338,15 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
throw new TransportException(sprintf('Could not resolve host "%s".', $host));
}
$info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
$multi->dnsCache[$host] = $ip = $ip[0];
$info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
$host = $ip;
} else {
$info['debug'] .= "* Hostname was found in DNS cache\n";
$host = str_contains($ip, ':') ? "[$ip]" : $ip;
}
$info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
$info['primary_ip'] = $ip;
if ($onProgress) {
@ -352,7 +354,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$onProgress();
}
return $ip;
return $host;
}
/**
@ -383,13 +385,14 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
try {
$url = self::parseUrl($location);
$locationHasHost = isset($url['authority']);
$url = self::resolveUrl($url, $info['url']);
} catch (InvalidArgumentException $e) {
$info['redirect_url'] = null;
return null;
}
$url = self::resolveUrl($url, $info['url']);
$info['redirect_url'] = implode('', $url);
if ($info['redirect_count'] >= $maxRedirects) {
@ -424,7 +427,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
[$host, $port] = self::parseHostPort($url, $info);
if (false !== (parse_url($location.'#', \PHP_URL_HOST) ?? false)) {
if ($locationHasHost) {
// Authorization and Cookie headers MUST NOT follow except for the initial host name
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
$requestHeaders[] = 'Host: '.$host.$port;

View file

@ -13,22 +13,25 @@ namespace Symfony\Component\HttpClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* Decorator that blocks requests to private networks by default.
*
* @author Hallison Boaventura <hallisonboaventura@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use AsyncDecoratorTrait;
private const PRIVATE_SUBNETS = [
'127.0.0.0/8',
@ -45,11 +48,14 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa
'::/128',
];
private $defaultOptions = self::OPTIONS_DEFAULTS;
private $client;
private $subnets;
private $ipFlags;
private $dnsCache;
/**
* @param string|array|null $subnets String or array of subnets using CIDR notation that will be used by IpUtils.
* @param string|array|null $subnets String or array of subnets using CIDR notation that should be considered private.
* If null is passed, the standard private subnets will be used.
*/
public function __construct(HttpClientInterface $client, $subnets = null)
@ -62,8 +68,23 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa
throw new \LogicException(sprintf('You cannot use "%s" if the HttpFoundation component is not installed. Try running "composer require symfony/http-foundation".', __CLASS__));
}
if (null === $subnets) {
$ipFlags = \FILTER_FLAG_IPV4 | \FILTER_FLAG_IPV6;
} else {
$ipFlags = 0;
foreach ((array) $subnets as $subnet) {
$ipFlags |= str_contains($subnet, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4;
}
}
if (!\defined('STREAM_PF_INET6')) {
$ipFlags &= ~\FILTER_FLAG_IPV6;
}
$this->client = $client;
$this->subnets = $subnets;
$this->subnets = null !== $subnets ? (array) $subnets : null;
$this->ipFlags = $ipFlags;
$this->dnsCache = new \ArrayObject();
}
/**
@ -71,59 +92,91 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$onProgress = $options['on_progress'] ?? null;
if (null !== $onProgress && !\is_callable($onProgress)) {
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
}
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions, true);
$redirectHeaders = parse_url($url['authority']);
$host = $redirectHeaders['host'];
$url = implode('', $url);
$dnsCache = $this->dnsCache;
$ip = self::dnsResolve($dnsCache, $host, $this->ipFlags, $options);
self::ipCheck($ip, $this->subnets, $this->ipFlags, $host, $url);
$onProgress = $options['on_progress'] ?? null;
$subnets = $this->subnets;
$lastUrl = '';
$ipFlags = $this->ipFlags;
$lastPrimaryIp = '';
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use ($onProgress, $subnets, &$lastUrl, &$lastPrimaryIp): void {
if ($info['url'] !== $lastUrl) {
$host = trim(parse_url($info['url'], PHP_URL_HOST) ?: '', '[]');
$resolve ??= static fn () => null;
if (($ip = $host)
&& !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)
&& !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)
&& !$ip = $resolve($host)
) {
if ($ip = @(dns_get_record($host, \DNS_A)[0]['ip'] ?? null)) {
$resolve($host, $ip);
} elseif ($ip = @(dns_get_record($host, \DNS_AAAA)[0]['ipv6'] ?? null)) {
$resolve($host, '['.$ip.']');
}
}
if ($ip && IpUtils::checkIp($ip, $subnets ?? self::PRIVATE_SUBNETS)) {
throw new TransportException(sprintf('Host "%s" is blocked for "%s".', $host, $info['url']));
}
$lastUrl = $info['url'];
}
if ($info['primary_ip'] !== $lastPrimaryIp) {
if ($info['primary_ip'] && IpUtils::checkIp($info['primary_ip'], $subnets ?? self::PRIVATE_SUBNETS)) {
throw new TransportException(sprintf('IP "%s" is blocked for "%s".', $info['primary_ip'], $info['url']));
}
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, $ipFlags, &$lastPrimaryIp): void {
if (!\in_array($info['primary_ip'] ?? '', ['', $lastPrimaryIp], true)) {
self::ipCheck($info['primary_ip'], $subnets, $ipFlags, null, $info['url']);
$lastPrimaryIp = $info['primary_ip'];
}
null !== $onProgress && $onProgress($dlNow, $dlSize, $info);
};
return $this->client->request($method, $url, $options);
}
if (0 >= $maxRedirects = $options['max_redirects']) {
return new AsyncResponse($this->client, $method, $url, $options);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
$options['max_redirects'] = 0;
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers'];
if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
return 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
});
}
return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use (&$method, &$options, $maxRedirects, &$redirectHeaders, $subnets, $ipFlags, $dnsCache): \Generator {
if (null !== $chunk->getError() || $chunk->isTimeout() || !$chunk->isFirst()) {
yield $chunk;
return;
}
$statusCode = $context->getStatusCode();
if ($statusCode < 300 || 400 <= $statusCode || null === $url = $context->getInfo('redirect_url')) {
$context->passthru();
yield $chunk;
return;
}
$host = parse_url($url, \PHP_URL_HOST);
$ip = self::dnsResolve($dnsCache, $host, $ipFlags, $options);
self::ipCheck($ip, $subnets, $ipFlags, $host, $url);
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
if (303 === $statusCode || 'POST' === $method && \in_array($statusCode, [301, 302], true)) {
$method = 'HEAD' === $method ? 'HEAD' : 'GET';
unset($options['body'], $options['json']);
if (isset($options['normalized_headers']['content-length']) || isset($options['normalized_headers']['content-type']) || isset($options['normalized_headers']['transfer-encoding'])) {
$filterContentHeaders = static function ($h) {
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
};
$options['header'] = array_filter($options['header'], $filterContentHeaders);
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
}
}
// Authorization and Cookie headers MUST NOT follow except for the initial host name
$options['headers'] = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
static $redirectCount = 0;
$context->setInfo('redirect_count', ++$redirectCount);
$context->replaceRequest($method, $url, $options);
if ($redirectCount >= $maxRedirects) {
$context->passthru();
}
});
}
/**
@ -143,14 +196,73 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);
return $clone;
}
public function reset()
{
$this->dnsCache->exchangeArray([]);
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
private static function dnsResolve(\ArrayObject $dnsCache, string $host, int $ipFlags, array &$options): string
{
if ($ip = filter_var(trim($host, '[]'), \FILTER_VALIDATE_IP) ?: $options['resolve'][$host] ?? false) {
return $ip;
}
if ($dnsCache->offsetExists($host)) {
return $dnsCache[$host];
}
if ((\FILTER_FLAG_IPV4 & $ipFlags) && $ip = gethostbynamel($host)) {
return $options['resolve'][$host] = $dnsCache[$host] = $ip[0];
}
if (!(\FILTER_FLAG_IPV6 & $ipFlags)) {
return $host;
}
if ($ip = dns_get_record($host, \DNS_AAAA)) {
$ip = $ip[0]['ipv6'];
} elseif (extension_loaded('sockets')) {
if (!$info = socket_addrinfo_lookup($host, 0, ['ai_socktype' => \SOCK_STREAM, 'ai_family' => \AF_INET6])) {
return $host;
}
$ip = socket_addrinfo_explain($info[0])['ai_addr']['sin6_addr'];
} elseif ('localhost' === $host || 'localhost.' === $host) {
$ip = '::1';
} else {
return $host;
}
return $options['resolve'][$host] = $dnsCache[$host] = $ip;
}
private static function ipCheck(string $ip, ?array $subnets, int $ipFlags, ?string $host, string $url): void
{
if (null === $subnets) {
// Quick check, but not reliable enough, see https://github.com/php/php-src/issues/16944
$ipFlags |= \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE;
}
if (false !== filter_var($ip, \FILTER_VALIDATE_IP, $ipFlags) && !IpUtils::checkIp($ip, $subnets ?? self::PRIVATE_SUBNETS)) {
return;
}
if (null !== $host) {
$type = 'Host';
} else {
$host = $ip;
$type = 'IP';
}
throw new TransportException($type.\sprintf(' "%s" is blocked for "%s".', $host, $url));
}
}

View file

@ -89,17 +89,10 @@ final class AmpResponse implements ResponseInterface, StreamableInterface
$info['max_duration'] = $options['max_duration'];
$info['debug'] = '';
$resolve = static function (string $host, ?string $ip = null) use ($multi): ?string {
if (null !== $ip) {
$multi->dnsCache[$host] = $ip;
}
return $multi->dnsCache[$host] ?? null;
};
$onProgress = $options['on_progress'] ?? static function () {};
$onProgress = $this->onProgress = static function () use (&$info, $onProgress, $resolve) {
$onProgress = $this->onProgress = static function () use (&$info, $onProgress) {
$info['total_time'] = microtime(true) - $info['start_time'];
$onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info, $resolve);
$onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info);
};
$pauseDeferred = new Deferred();

View file

@ -156,8 +156,8 @@ final class AsyncContext
$this->info['previous_info'][] = $info = $this->response->getInfo();
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve);
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
};
}
if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) {

View file

@ -51,8 +51,8 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve);
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
};
}
$this->response = $client->request($method, $url, ['buffer' => false] + $options);
@ -117,11 +117,20 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface
public function getInfo(?string $type = null)
{
if ('debug' === ($type ?? 'debug')) {
$debug = implode('', array_column($this->info['previous_info'] ?? [], 'debug'));
$debug .= $this->response->getInfo('debug');
if ('debug' === $type) {
return $debug;
}
}
if (null !== $type) {
return $this->info[$type] ?? $this->response->getInfo($type);
}
return $this->info + $this->response->getInfo();
return array_merge($this->info + $this->response->getInfo(), ['debug' => $debug]);
}
public function toStream(bool $throw = true)
@ -249,6 +258,7 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface
return;
}
$chunk = null;
foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) {
$r = $asyncMap[$response];
@ -291,6 +301,9 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface
}
}
if (null === $chunk) {
throw new \LogicException(\sprintf('"%s" is not compliant with HttpClientInterface: its "stream()" method didn\'t yield any chunks when it should have.', get_debug_type($client)));
}
if (null === $chunk->getError() && $chunk->isLast()) {
$r->yieldedState = self::LAST_CHUNK_YIELDED;
}

View file

@ -115,20 +115,13 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
curl_pause($ch, \CURLPAUSE_CONT);
if ($onProgress = $options['on_progress']) {
$resolve = static function (string $host, ?string $ip = null) use ($multi): ?string {
if (null !== $ip) {
$multi->dnsCache->hostnames[$host] = $ip;
}
return $multi->dnsCache->hostnames[$host] ?? null;
};
$url = isset($info['url']) ? ['url' => $info['url']] : [];
curl_setopt($ch, \CURLOPT_NOPROGRESS, false);
curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer, $resolve) {
curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) {
try {
rewind($debugBuffer);
$debug = ['debug' => stream_get_contents($debugBuffer)];
$onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug, $resolve);
$onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug);
} catch (\Throwable $e) {
$multi->handlesActivity[(int) $ch][] = null;
$multi->handlesActivity[(int) $ch][] = $e;
@ -327,7 +320,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) || (curl_error($ch) === 'OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 0' && -1.0 === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) && \in_array('close', array_map('strtolower', $responses[$id]->headers['connection']), true)) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
}
} finally {
$multi->performing = false;
@ -441,15 +434,6 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
$options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
} else {
$url = parse_url($location ?? ':');
if (isset($url['host']) && null !== $ip = $multi->dnsCache->hostnames[$url['host'] = strtolower($url['host'])] ?? null) {
// Populate DNS cache for redirects if needed
$port = $url['port'] ?? ('http' === ($url['scheme'] ?? parse_url(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL), \PHP_URL_SCHEME)) ? 80 : 443);
curl_setopt($ch, \CURLOPT_RESOLVE, ["{$url['host']}:$port:$ip"]);
$multi->dnsCache->removals["-{$url['host']}:$port"] = "-{$url['host']}:$port";
}
}
}

View file

@ -58,11 +58,11 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface,
$content = false;
}
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$traceInfo, $onProgress) {
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
$traceInfo = $info;
if (null !== $onProgress) {
$onProgress($dlNow, $dlSize, $info, $resolve);
$onProgress($dlNow, $dlSize, $info);
}
};

View file

@ -25,7 +25,7 @@
"php": ">=7.2.5",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/http-client-contracts": "^2.5.3",
"symfony/http-client-contracts": "^2.5.4",
"symfony/polyfill-php73": "^1.11",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.0|^2|^3"