File manager - Edit - /usr/local/cpanel/base/3rdparty/roundcube/vendor/mstilkerich/carddavclient/src/HttpClientAdapterGuzzle.php
Back
<?php /* * CardDAV client library for PHP ("PHP-CardDavClient"). * * Copyright (c) 2020-2021 Michael Stilkerich <ms@mike2k.de> * Licensed under the MIT license. See COPYING file in the project root for details. */ declare(strict_types=1); namespace MStilkerich\CardDavClient; use GuzzleHttp\{Client, HandlerStack, Middleware, MessageFormatter}; use Psr\Http\Message\RequestInterface as Psr7Request; use Psr\Http\Message\ResponseInterface as Psr7Response; use Psr\Http\Message\UriInterface as Psr7Uri; use Psr\Http\Client\ClientInterface as Psr18ClientInterface; use Psr\Log\NullLogger; use MStilkerich\CardDavClient\Exception\{ClientException, NetworkException}; /** * Adapter for the Guzzle HTTP client library. * * @psalm-import-type HttpOptions from Account * @psalm-import-type RequestOptions from HttpClientAdapter * * @psalm-type GuzzleAllowRedirectCfg = array{ * max?: int, * strict?: bool, * referer?: bool, * protocols?: list<string>, * on_redirect?: callable(Psr7Request, Psr7Response, Psr7Uri): void, * track_redirects?: bool * } * * @psalm-type GuzzleRequestOptions = array{ * headers?: array<string, string | list<string>>, * query?: array<string, string>, * body?: string | resource | \Psr\Http\Message\StreamInterface, * allow_redirects?: bool | GuzzleAllowRedirectCfg, * auth?: null | array{string, string} | array{string, string, string}, * curl?: array, * base_uri?: string, * http_errors?: bool, * handler?: HandlerStack * } * * @package Internal\Communication */ class HttpClientAdapterGuzzle extends HttpClientAdapter { /** * A list of authentication schemes that can be handled by Guzzle itself, independent on whether it works only with * the Guzzle Curl HTTP handler or not. Strings must be lowercase! * * @psalm-var list<lowercase-string> * @var array<int, string> */ private const GUZZLE_KNOWN_AUTHSCHEMES = [ 'basic', 'digest', 'ntlm' ]; /** * A list of authentication schemes that can be handled by this HttpClientAdapter. * * @psalm-var list<lowercase-string> * @var array<int, string> */ private $known_authschemes; /** * The Client object of the Guzzle HTTP library. * @var Client */ private $client; /** * The HTTP authentication scheme to use. Null if not determined yet. * @var ?string * @psalm-var ?lowercase-string */ private $authScheme; /** * HTTP authentication schemes tried without success, to avoid trying again. * @psalm-var list<lowercase-string> * @var array<int, string> */ private $failedAuthSchemes = []; /** * Maps lowercase auth-schemes to their CURLAUTH_XXX constant. Only values not part of GUZZLE_KNOWN_AUTHSCHEMES are * relevant here. * @var null|array<lowercase-string, int> */ private static $schemeToCurlOpt; /** Constructs a HttpClientAdapterGuzzle object. * * @param string $base_uri Base URI to be used when relative URIs are given to requests. * @param HttpOptions $httpOptions Options for HTTP communication, including authentication credentials. */ public function __construct(string $base_uri, array $httpOptions) { parent::__construct($base_uri, $httpOptions); if (!isset(self::$schemeToCurlOpt)) { self::$schemeToCurlOpt = []; if (extension_loaded("curl")) { self::$schemeToCurlOpt['curlany'] = CURLAUTH_ANY; // if CURL is compiled without support for SPNEGO, CURLAUTH_NEGOTIATE is not defined if (defined('CURLAUTH_NEGOTIATE')) { self::$schemeToCurlOpt['negotiate'] = CURLAUTH_NEGOTIATE; } } } if ($httpOptions['preemptive_basic_auth'] ?? false) { if ($this->checkCredentialsAvailable('basic')) { $this->authScheme = 'basic'; } else { Config::$logger->warning("Ignoring option preemptive_basic_auth as username/password are not set"); } } $this->known_authschemes = array_merge( [ 'bearer' ], self::GUZZLE_KNOWN_AUTHSCHEMES, array_keys(self::$schemeToCurlOpt) ); $stack = HandlerStack::create(); if (!(Config::$httplogger instanceof NullLogger)) { $stack->push(Middleware::log( Config::$httplogger, new MessageFormatter(Config::$options['guzzle_logformat']) )); } $guzzleOptions = $this->prepareGuzzleOptions(); $guzzleOptions['handler'] = $stack; $guzzleOptions['http_errors'] = false; // no exceptions on 4xx/5xx status, also required by PSR-18 $guzzleOptions['base_uri'] = $base_uri; $guzzleOptions['verify'] = false; $this->client = new Client($guzzleOptions); } /** * Sends a PSR-7 request and returns a PSR-7 response. * * The given URI may be relative to the base URI given on construction of this object or a full URL. * Authentication is only attempted in case the domain name of the request URI matches that of the base URI * (subdomains may differ). * * @psalm-param RequestOptions $options * @param array<string,mixed> $options * Options for the HTTP client, and default request options. May include any of the options accepted by * {@see HttpClientAdapter::sendRequest()}. */ public function sendRequest(string $method, string $uri, array $options = []): Psr7Response { $doAuth = $this->checkSameDomainAsBase($uri); $triedSabreWorkaround = false; try { /* * Try the request (until first success, or error other than 401 encountered, or all auth schemes tried) * - Only once in case no authentication shall be attempted * - Retry with CURLANY in case we encounter an error that looks like Sabre/Curl incompatibility * - If we get 401 reply, try all auth schemes the server offers and we can support, until success or all * failed */ do { $guzzleOptions = $this->prepareGuzzleOptions($options, $doAuth); // Check if the URI looks local, turn off SSL verification. if ($this->isLocalhost($uri) || $this->isLocalhost($this->baseUri)) { $guzzleOptions['verify'] = false; } $response = $this->client->request($method, $uri, $guzzleOptions); if ($doAuth) { if ((!$triedSabreWorkaround) && $this->checkSabreCurlIncompatibility($method, $response)) { // try workaround $triedSabreWorkaround = true; continue; } elseif ($response->getStatusCode() == 401) { if ($this->authScheme != null) { $this->failedAuthSchemes[] = $this->authScheme; } // try next auth scheme $authSchemes = $this->getSupportedAuthSchemes($response); if (empty($authSchemes)) { Config::$logger->debug("None of the available auth schemes worked"); $this->authScheme = null; break; } else { $this->authScheme = $authSchemes[0]; Config::$logger->debug("Trying auth scheme " . $this->authScheme); } } else { break; } } } while ($doAuth); return $response; } catch (\GuzzleHttp\Exception\RequestException $e) { // thrown in the event of a networking error or too many redirects throw new NetworkException($e->getMessage(), intval($e->getCode()), $e->getRequest(), $e); } catch (\InvalidArgumentException | \GuzzleHttp\Exception\GuzzleException $e) { // Anything else throw new ClientException($e->getMessage(), intval($e->getCode()), $e); } } /** * Checks if a request was rejected because of an incompatibility between curl and sabre/dav. * * Background: When using DIGEST authentication, it is required to first send a request to the server to determine * the parameters for the DIGEST authentication. This request is supposed to fail with 401 and the client can * determine the parameters from the WWW-Authenticate header and try again with the proper Authentication header. * Curl optimizes the first request by omitting the request body as it expects the request to fail anyway. * * Now sabre/dav has a feature that allows to reply to certain REPORT requests without the need for authentication. * This is specifically useful for Caldav, which may want to make available certain information from a calendar to * anonymous users (e.g. free/busy time). Therefore, the authentication is done at a later time than the first * attempt to evaluate the REPORT. A REPORT request requires a body, and thus sabre/dav will bail out with an * internal server error instead of a 401, normally causing the client library to fail. The problem specifically * only occurs for REPORT requests, for other requests such as PROPFIND the problem is not triggered in sabre and an * expected 401 response is returned. * * Read all about it {@link https://github.com/sabre-io/dav/issues/932 here}. * * As a sidenote, nextcloud is not affected even though it uses sabre/dav, because the feature causing the server * errors can be disabled and is in nextcloud. But there are other servers (Baïkal) using sabre/dav that are * affected. * * As a workaround, it is possible to ask curl to do negotiation of the authentication scheme to use, but providing * the authentication scheme CURLAUTH_ANY. With this, curl will not assume that the initial request might fail (as * not authentication may be needed), and thus the initial request will include the request body. The downside of * this is that even when we know the authentication scheme supported by a server (e.g. basic), this setting will * cause twice the number of requests being sent to the server. * * Because it doesn't seem that this issue will get fixed, and the widespread usage of sabre/dav, I decided to * include this workaround in the carddavclient library that specifically detects the situation and applies the * above workaround without affecting the efficiency of communication when talking to other servers. * * We detect the situation by the following indicators: * - We have the curl extension loaded * - REPORT request was sent * - DIGEST was used as auth scheme * - Result status code is 500 * - The response includes the known error message: * ```xml * <?xml version="1.0" encoding="utf-8"?> * <d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> * <s:sabredav-version>4.1.2</s:sabredav-version> * <s:exception>Sabre\Xml\ParseException</s:exception> * <s:message>The input element to parse is empty. Do not attempt to parse</s:message> * </d:error> * ``` */ private function checkSabreCurlIncompatibility(string $method, Psr7Response $response): bool { if ( extension_loaded("curl") && $response->getStatusCode() == 500 && strcasecmp($method, "REPORT") == 0 && $this->authScheme == "digest" ) { $body = (string) $response->getBody(); if (strpos($body, "The input element to parse is empty. Do not attempt to parse") !== false) { Config::$logger->debug("Attempting workaround for Sabre/Dav / curl incompatibility"); $this->authScheme = "curlany"; return true; } } return false; } /** * Prepares options for the Guzzle request. * * @psalm-param RequestOptions $options * @param array<string,mixed> $options * @psalm-return GuzzleRequestOptions * @param bool $doAuth True to attempt authentication. False will only try unauthenticated access. */ private function prepareGuzzleOptions(array $options = [], bool $doAuth = false): array { $guzzleOptions = []; // First we need to merge multi-value options, e.g. HTTP headers. We do not attempt to merge individual // values though, the values in options take precedence. if (isset($options['headers']) && isset($this->httpOptions['headers'])) { $options['headers'] = $options['headers'] + $this->httpOptions['headers']; } // Now merge the request options with the default options. In case of merged options above, options will already // contain the result of the merge in case both arrays have an entry for it. $options = $options + $this->httpOptions; // These options are also known to Guzzle and can directly be passed along foreach ([ "headers", "body", "verify", "query" ] as $copyopt) { if (isset($options[$copyopt])) { $guzzleOptions[$copyopt] = $options[$copyopt]; } } if (($options["allow_redirects"] ?? true) === false) { $guzzleOptions["allow_redirects"] = false; } else { $guzzleOptions["allow_redirects"] = [ 'max' => 5, 'strict' => true, // keep original method, i.e. do not perform GET on redirection target 'referer' => false, 'protocols' => ['http', 'https'], 'track_redirects' => false ]; } $authScheme = $this->authScheme; if ($doAuth && isset($authScheme)) { Config::$logger->debug("Using auth scheme $authScheme"); if (in_array($authScheme, self::GUZZLE_KNOWN_AUTHSCHEMES)) { $guzzleOptions['auth'] = [ $this->httpOptions['username'] ?? "", $this->httpOptions['password'] ?? "", $authScheme ]; } elseif (isset(self::$schemeToCurlOpt[$authScheme])) { if (isset($_SERVER['KRB5CCNAME'])) { putenv("KRB5CCNAME=" . $_SERVER['KRB5CCNAME']); } $guzzleOptions["curl"] = [ CURLOPT_HTTPAUTH => self::$schemeToCurlOpt[$authScheme], CURLOPT_USERNAME => $this->httpOptions['username'] ?? "", CURLOPT_PASSWORD => $this->httpOptions['password'] ?? "" ]; } else { // handled by HttpClientAdapterGuzzle directly if ($authScheme == "bearer" && isset($this->httpOptions['bearertoken'])) { $authHeader = sprintf("%s %s", 'Bearer', $this->httpOptions['bearertoken']); /** @psalm-var GuzzleRequestOptions $guzzleOptions */ $guzzleOptions["headers"]["Authorization"] = $authHeader; } } } // On occasion, we get CURL error 16 Error in the HTTP2 framing layer // Until the source of this is clear, disable HTTP2 for now /* $curlLoaded = extension_loaded("curl"); if ($curlLoaded && (curl_version()["features"] & CURL_VERSION_HTTP2 !== 0)) { $guzzleOptions["version"] = 2.0; // HTTP2 } */ /** @psalm-var GuzzleRequestOptions $guzzleOptions */ return $guzzleOptions; } /** * Extracts HTTP authentication schemes from a WWW-Authenticate header. * * The schemes offered by the server in the WWW-Authenticate header are intersected with those supported by Guzzle / * curl. Schemes that have been tried with this object without success are filtered. * * @param Psr7Response $response A status 401 response returned by the server. * * @psalm-return list<lowercase-string> * @return array<int,string> An array of authentication schemes that can be tried. */ private function getSupportedAuthSchemes(Psr7Response $response): array { $authHeaders = $response->getHeader("WWW-Authenticate"); $schemes = []; $availableSchemes = array_diff($this->known_authschemes, $this->failedAuthSchemes); foreach ($authHeaders as $authHeader) { $authHeader = trim($authHeader); $srvSchemes = []; foreach (preg_split("/\s*,\s*/", $authHeader) as $challenge) { if (preg_match("/^([^ =]+)(\s+[^=].*)?$/", $challenge, $matches)) { // filter auth-params $srvSchemes[] = strtolower($matches[1]); } } // some providers do not advertise Bearer auth in the WWW-Authenticate header, this is a hack to work around if ( // Google: https://issuetracker.google.com/issues/189153568 strstr($authHeader, 'realm="Google APIs"') !== false // Yahoo || strstr($authHeader, 'realm="progrss"') !== false // Nextcloud, user_oidc: https://github.com/nextcloud/user_oidc/issues/603#issuecomment-2575914376 || strstr($authHeader, 'realm="Nextcloud"') !== false ) { $srvSchemes[] = 'bearer'; } foreach ($srvSchemes as $scheme) { if (in_array($scheme, $availableSchemes) && $this->checkCredentialsAvailable($scheme)) { $schemes[] = $scheme; } } } return $schemes; } /** * Checks if a URI points to localhost. * * @param string $uri The URI to check. * @return bool True if the URI is localhost, false otherwise. */ private function isLocalhost(string $uri): bool { return strpos($uri, 'https://127.0.0.1') === 0 || strpos($uri, 'https://localhost') === 0; } } // vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120
| ver. 1.4 |
Github
|
.
| PHP 8.1.34 | Generation time: 0.04 |
proxy
|
phpinfo
|
Settings