aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Http/Client
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Http/Client')
-rw-r--r--lib/private/Http/Client/Client.php609
-rw-r--r--lib/private/Http/Client/ClientService.php54
-rw-r--r--lib/private/Http/Client/DnsPinMiddleware.php67
-rw-r--r--lib/private/Http/Client/GuzzlePromiseAdapter.php124
-rw-r--r--lib/private/Http/Client/NegativeDnsCache.php26
-rw-r--r--lib/private/Http/Client/Response.php63
6 files changed, 637 insertions, 306 deletions
diff --git a/lib/private/Http/Client/Client.php b/lib/private/Http/Client/Client.php
index 2e370395132..553a8921a80 100644
--- a/lib/private/Http/Client/Client.php
+++ b/lib/private/Http/Client/Client.php
@@ -1,46 +1,24 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Carlos Ferreira <carlos@reendex.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Mohammed Abdellatif <m.latief@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Scott Shambarger <devel@shambarger.net>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Http\Client;
use GuzzleHttp\Client as GuzzleClient;
+use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\RequestOptions;
use OCP\Http\Client\IClient;
+use OCP\Http\Client\IPromise;
use OCP\Http\Client\IResponse;
use OCP\Http\Client\LocalServerException;
use OCP\ICertificateManager;
use OCP\IConfig;
use OCP\Security\IRemoteHostValidator;
+use Psr\Log\LoggerInterface;
use function parse_url;
/**
@@ -61,7 +39,8 @@ class Client implements IClient {
IConfig $config,
ICertificateManager $certificateManager,
GuzzleClient $client,
- IRemoteHostValidator $remoteHostValidator
+ IRemoteHostValidator $remoteHostValidator,
+ protected LoggerInterface $logger,
) {
$this->config = $config;
$this->client = $client;
@@ -74,7 +53,7 @@ class Client implements IClient {
$defaults = [
RequestOptions::VERIFY => $this->getCertBundle(),
- RequestOptions::TIMEOUT => 30,
+ RequestOptions::TIMEOUT => IClient::DEFAULT_REQUEST_TIMEOUT,
];
$options['nextcloud']['allow_local_address'] = $this->isLocalAddressAllowed($options);
@@ -82,7 +61,7 @@ class Client implements IClient {
$onRedirectFunction = function (
\Psr\Http\Message\RequestInterface $request,
\Psr\Http\Message\ResponseInterface $response,
- \Psr\Http\Message\UriInterface $uri
+ \Psr\Http\Message\UriInterface $uri,
) use ($options) {
$this->preventLocalAddress($uri->__toString(), $options);
};
@@ -122,7 +101,7 @@ class Client implements IClient {
// If the instance is not yet setup we need to use the static path as
// $this->certificateManager->getAbsoluteBundlePath() tries to instantiate
// a view
- if ($this->config->getSystemValue('installed', false) === false) {
+ if (!$this->config->getSystemValueBool('installed', false)) {
return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
}
@@ -145,14 +124,14 @@ class Client implements IClient {
*
*/
private function getProxyUri(): ?array {
- $proxyHost = $this->config->getSystemValue('proxy', '');
+ $proxyHost = $this->config->getSystemValueString('proxy', '');
- if ($proxyHost === '' || $proxyHost === null) {
+ if ($proxyHost === '') {
return null;
}
- $proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
- if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
+ $proxyUserPwd = $this->config->getSystemValueString('proxyuserpwd', '');
+ if ($proxyUserPwd !== '') {
$proxyHost = $proxyUserPwd . '@' . $proxyHost;
}
@@ -170,8 +149,8 @@ class Client implements IClient {
}
private function isLocalAddressAllowed(array $options) : bool {
- if (($options['nextcloud']['allow_local_address'] ?? false) ||
- $this->config->getSystemValueBool('allow_local_remote_servers', false)) {
+ if (($options['nextcloud']['allow_local_address'] ?? false)
+ || $this->config->getSystemValueBool('allow_local_remote_servers', false)) {
return true;
}
@@ -179,16 +158,17 @@ class Client implements IClient {
}
protected function preventLocalAddress(string $uri, array $options): void {
- if ($this->isLocalAddressAllowed($options)) {
- return;
- }
-
$host = parse_url($uri, PHP_URL_HOST);
if ($host === false || $host === null) {
throw new LocalServerException('Could not detect any host');
}
+
+ if ($this->isLocalAddressAllowed($options)) {
+ return;
+ }
+
if (!$this->remoteHostValidator->isValid($host)) {
- throw new LocalServerException('Host violates local access rules');
+ throw new LocalServerException('Host "' . $host . '" violates local access rules');
}
}
@@ -197,27 +177,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'query' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => ['
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -233,22 +213,22 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => ['
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -263,27 +243,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => ['
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -304,27 +284,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => ['
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -335,31 +315,66 @@ class Client implements IClient {
}
/**
+ * Sends a PATCH request
+ *
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ */
+ public function patch(string $uri, array $options = []): IResponse {
+ $this->preventLocalAddress($uri, $options);
+ $response = $this->client->request('patch', $uri, $this->buildRequestOptions($options));
+ return new Response($response);
+ }
+
+ /**
* Sends a DELETE request
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => ['
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -370,31 +385,31 @@ class Client implements IClient {
}
/**
- * Sends a options request
+ * Sends an OPTIONS request
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => ['
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -403,4 +418,268 @@ class Client implements IClient {
$response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
return new Response($response);
}
+
+ /**
+ * Get the response of a Throwable thrown by the request methods when possible
+ *
+ * @param \Throwable $e
+ * @return IResponse
+ * @throws \Throwable When $e did not have a response
+ * @since 29.0.0
+ */
+ public function getResponseFromThrowable(\Throwable $e): IResponse {
+ if (method_exists($e, 'hasResponse') && method_exists($e, 'getResponse') && $e->hasResponse()) {
+ return new Response($e->getResponse());
+ }
+
+ throw $e;
+ }
+
+ /**
+ * Sends a HTTP request
+ *
+ * @param string $method The HTTP method to use
+ * @param string $uri
+ * @param array $options Array such as
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ */
+ public function request(string $method, string $uri, array $options = []): IResponse {
+ $this->preventLocalAddress($uri, $options);
+ $response = $this->client->request($method, $uri, $this->buildRequestOptions($options));
+ $isStream = isset($options['stream']) && $options['stream'];
+ return new Response($response, $isStream);
+ }
+
+ protected function wrapGuzzlePromise(PromiseInterface $promise): IPromise {
+ return new GuzzlePromiseAdapter(
+ $promise,
+ $this->logger
+ );
+ }
+
+ /**
+ * Sends an asynchronous GET request
+ *
+ * @param string $uri
+ * @param array $options Array such as
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IPromise
+ */
+ public function getAsync(string $uri, array $options = []): IPromise {
+ $this->preventLocalAddress($uri, $options);
+ $response = $this->client->requestAsync('get', $uri, $this->buildRequestOptions($options));
+ return $this->wrapGuzzlePromise($response);
+ }
+
+ /**
+ * Sends an asynchronous HEAD request
+ *
+ * @param string $uri
+ * @param array $options Array such as
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IPromise
+ */
+ public function headAsync(string $uri, array $options = []): IPromise {
+ $this->preventLocalAddress($uri, $options);
+ $response = $this->client->requestAsync('head', $uri, $this->buildRequestOptions($options));
+ return $this->wrapGuzzlePromise($response);
+ }
+
+ /**
+ * Sends an asynchronous POST request
+ *
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IPromise
+ */
+ public function postAsync(string $uri, array $options = []): IPromise {
+ $this->preventLocalAddress($uri, $options);
+
+ if (isset($options['body']) && is_array($options['body'])) {
+ $options['form_params'] = $options['body'];
+ unset($options['body']);
+ }
+
+ return $this->wrapGuzzlePromise($this->client->requestAsync('post', $uri, $this->buildRequestOptions($options)));
+ }
+
+ /**
+ * Sends an asynchronous PUT request
+ *
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IPromise
+ */
+ public function putAsync(string $uri, array $options = []): IPromise {
+ $this->preventLocalAddress($uri, $options);
+ $response = $this->client->requestAsync('put', $uri, $this->buildRequestOptions($options));
+ return $this->wrapGuzzlePromise($response);
+ }
+
+ /**
+ * Sends an asynchronous DELETE request
+ *
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IPromise
+ */
+ public function deleteAsync(string $uri, array $options = []): IPromise {
+ $this->preventLocalAddress($uri, $options);
+ $response = $this->client->requestAsync('delete', $uri, $this->buildRequestOptions($options));
+ return $this->wrapGuzzlePromise($response);
+ }
+
+ /**
+ * Sends an asynchronous OPTIONS request
+ *
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
+ * @return IPromise
+ */
+ public function optionsAsync(string $uri, array $options = []): IPromise {
+ $this->preventLocalAddress($uri, $options);
+ $response = $this->client->requestAsync('options', $uri, $this->buildRequestOptions($options));
+ return $this->wrapGuzzlePromise($response);
+ }
}
diff --git a/lib/private/Http/Client/ClientService.php b/lib/private/Http/Client/ClientService.php
index bbc2330176f..b719f3d369d 100644
--- a/lib/private/Http/Client/ClientService.php
+++ b/lib/private/Http/Client/ClientService.php
@@ -1,39 +1,25 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Http\Client;
use GuzzleHttp\Client as GuzzleClient;
-use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Middleware;
+use OCP\Diagnostics\IEventLogger;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\ICertificateManager;
use OCP\IConfig;
use OCP\Security\IRemoteHostValidator;
+use Psr\Http\Message\RequestInterface;
+use Psr\Log\LoggerInterface;
/**
* Class ClientService
@@ -48,15 +34,21 @@ class ClientService implements IClientService {
/** @var DnsPinMiddleware */
private $dnsPinMiddleware;
private IRemoteHostValidator $remoteHostValidator;
+ private IEventLogger $eventLogger;
- public function __construct(IConfig $config,
- ICertificateManager $certificateManager,
- DnsPinMiddleware $dnsPinMiddleware,
- IRemoteHostValidator $remoteHostValidator) {
+ public function __construct(
+ IConfig $config,
+ ICertificateManager $certificateManager,
+ DnsPinMiddleware $dnsPinMiddleware,
+ IRemoteHostValidator $remoteHostValidator,
+ IEventLogger $eventLogger,
+ protected LoggerInterface $logger,
+ ) {
$this->config = $config;
$this->certificateManager = $certificateManager;
$this->dnsPinMiddleware = $dnsPinMiddleware;
$this->remoteHostValidator = $remoteHostValidator;
+ $this->eventLogger = $eventLogger;
}
/**
@@ -65,7 +57,14 @@ class ClientService implements IClientService {
public function newClient(): IClient {
$handler = new CurlHandler();
$stack = HandlerStack::create($handler);
- $stack->push($this->dnsPinMiddleware->addDnsPinning());
+ if ($this->config->getSystemValueBool('dns_pinning', true)) {
+ $stack->push($this->dnsPinMiddleware->addDnsPinning());
+ }
+ $stack->push(Middleware::tap(function (RequestInterface $request) {
+ $this->eventLogger->start('http:request', $request->getMethod() . ' request to ' . $request->getRequestTarget());
+ }, function () {
+ $this->eventLogger->end('http:request');
+ }), 'event logger');
$client = new GuzzleClient(['handler' => $stack]);
@@ -74,6 +73,7 @@ class ClientService implements IClientService {
$this->certificateManager,
$client,
$this->remoteHostValidator,
+ $this->logger,
);
}
}
diff --git a/lib/private/Http/Client/DnsPinMiddleware.php b/lib/private/Http/Client/DnsPinMiddleware.php
index c6a58972fdd..96e0f71adbe 100644
--- a/lib/private/Http/Client/DnsPinMiddleware.php
+++ b/lib/private/Http/Client/DnsPinMiddleware.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021, Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Http\Client;
@@ -30,23 +13,15 @@ use OCP\Http\Client\LocalServerException;
use Psr\Http\Message\RequestInterface;
class DnsPinMiddleware {
- /** @var NegativeDnsCache */
- private $negativeDnsCache;
- private IpAddressClassifier $ipAddressClassifier;
public function __construct(
- NegativeDnsCache $negativeDnsCache,
- IpAddressClassifier $ipAddressClassifier
+ private NegativeDnsCache $negativeDnsCache,
+ private IpAddressClassifier $ipAddressClassifier,
) {
- $this->negativeDnsCache = $negativeDnsCache;
- $this->ipAddressClassifier = $ipAddressClassifier;
}
/**
* Fetch soa record for a target
- *
- * @param string $target
- * @return array|null
*/
private function soaRecord(string $target): ?array {
$labels = explode('.', $target);
@@ -55,7 +30,7 @@ class DnsPinMiddleware {
$second = array_pop($labels);
$hostname = $second . '.' . $top;
- $responses = dns_get_record($hostname, DNS_SOA);
+ $responses = $this->dnsGetRecord($hostname, DNS_SOA);
if ($responses === false || count($responses) === 0) {
return null;
@@ -74,15 +49,21 @@ class DnsPinMiddleware {
$soaDnsEntry = $this->soaRecord($target);
$dnsNegativeTtl = $soaDnsEntry['minimum-ttl'] ?? null;
+ $canHaveCnameRecord = true;
- $dnsTypes = [DNS_A, DNS_AAAA, DNS_CNAME];
+ $dnsTypes = \defined('AF_INET6') || @inet_pton('::1')
+ ? [DNS_A, DNS_AAAA, DNS_CNAME]
+ : [DNS_A, DNS_CNAME];
foreach ($dnsTypes as $dnsType) {
+ if ($canHaveCnameRecord === false && $dnsType === DNS_CNAME) {
+ continue;
+ }
+
if ($this->negativeDnsCache->isNegativeCached($target, $dnsType)) {
continue;
}
- $dnsResponses = dns_get_record($target, $dnsType);
- $canHaveCnameRecord = true;
+ $dnsResponses = $this->dnsGetRecord($target, $dnsType);
if ($dnsResponses !== false && count($dnsResponses) > 0) {
foreach ($dnsResponses as $dnsResponse) {
if (isset($dnsResponse['ip'])) {
@@ -93,7 +74,6 @@ class DnsPinMiddleware {
$canHaveCnameRecord = false;
} elseif (isset($dnsResponse['target']) && $canHaveCnameRecord) {
$targetIps = array_merge($targetIps, $this->dnsResolve($dnsResponse['target'], $recursionCount));
- $canHaveCnameRecord = true;
}
}
} elseif ($dnsNegativeTtl !== null) {
@@ -104,17 +84,24 @@ class DnsPinMiddleware {
return $targetIps;
}
- public function addDnsPinning() {
+ /**
+ * Wrapper for dns_get_record
+ */
+ protected function dnsGetRecord(string $hostname, int $type): array|false {
+ return \dns_get_record($hostname, $type);
+ }
+
+ public function addDnsPinning(): callable {
return function (callable $handler) {
return function (
RequestInterface $request,
- array $options
+ array $options,
) use ($handler) {
if ($options['nextcloud']['allow_local_address'] === true) {
return $handler($request, $options);
}
- $hostName = (string)$request->getUri()->getHost();
+ $hostName = $request->getUri()->getHost();
$port = $request->getUri()->getPort();
$ports = [
@@ -128,6 +115,10 @@ class DnsPinMiddleware {
$targetIps = $this->dnsResolve(idn_to_utf8($hostName), 0);
+ if (empty($targetIps)) {
+ throw new LocalServerException('No DNS record found for ' . $hostName);
+ }
+
$curlResolves = [];
foreach ($ports as $port) {
@@ -136,7 +127,7 @@ class DnsPinMiddleware {
foreach ($targetIps as $ip) {
if ($this->ipAddressClassifier->isLocalAddress($ip)) {
// TODO: continue with all non-local IPs?
- throw new LocalServerException('Host violates local access rules');
+ throw new LocalServerException('Host "' . $ip . '" (' . $hostName . ':' . $port . ') violates local access rules');
}
$curlResolves["$hostName:$port"][] = $ip;
}
diff --git a/lib/private/Http/Client/GuzzlePromiseAdapter.php b/lib/private/Http/Client/GuzzlePromiseAdapter.php
new file mode 100644
index 00000000000..03a9ed9a599
--- /dev/null
+++ b/lib/private/Http/Client/GuzzlePromiseAdapter.php
@@ -0,0 +1,124 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Http\Client;
+
+use Exception;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Promise\PromiseInterface;
+use LogicException;
+use OCP\Http\Client\IPromise;
+use OCP\Http\Client\IResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * A wrapper around Guzzle's PromiseInterface
+ *
+ * @see \GuzzleHttp\Promise\PromiseInterface
+ * @since 28.0.0
+ */
+class GuzzlePromiseAdapter implements IPromise {
+ public function __construct(
+ protected PromiseInterface $promise,
+ protected LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * Appends fulfillment and rejection handlers to the promise, and returns
+ * a new promise resolving to the return value of the called handler.
+ *
+ * @param ?callable(IResponse): void $onFulfilled Invoked when the promise fulfills. Gets an \OCP\Http\Client\IResponse passed in as argument
+ * @param ?callable(Exception): void $onRejected Invoked when the promise is rejected. Gets an \Exception passed in as argument
+ *
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function then(
+ ?callable $onFulfilled = null,
+ ?callable $onRejected = null,
+ ): IPromise {
+ if ($onFulfilled !== null) {
+ $wrappedOnFulfilled = static function (ResponseInterface $response) use ($onFulfilled) {
+ $onFulfilled(new Response($response));
+ };
+ } else {
+ $wrappedOnFulfilled = null;
+ }
+
+ if ($onRejected !== null) {
+ $wrappedOnRejected = static function (RequestException $e) use ($onRejected) {
+ $onRejected($e);
+ };
+ } else {
+ $wrappedOnRejected = null;
+ }
+
+ $this->promise->then($wrappedOnFulfilled, $wrappedOnRejected);
+ return $this;
+ }
+
+ /**
+ * Get the state of the promise ("pending", "rejected", or "fulfilled").
+ *
+ * The three states can be checked against the constants defined:
+ * STATE_PENDING, STATE_FULFILLED, and STATE_REJECTED.
+ *
+ * @return IPromise::STATE_*
+ * @since 28.0.0
+ */
+ public function getState(): string {
+ $state = $this->promise->getState();
+ if ($state === PromiseInterface::FULFILLED) {
+ return self::STATE_FULFILLED;
+ }
+ if ($state === PromiseInterface::REJECTED) {
+ return self::STATE_REJECTED;
+ }
+ if ($state === PromiseInterface::PENDING) {
+ return self::STATE_PENDING;
+ }
+
+ $this->logger->error('Unexpected promise state "{state}" returned by Guzzle', [
+ 'state' => $state,
+ ]);
+ return self::STATE_PENDING;
+ }
+
+ /**
+ * Cancels the promise if possible.
+ *
+ * @link https://github.com/promises-aplus/cancellation-spec/issues/7
+ * @since 28.0.0
+ */
+ public function cancel(): void {
+ $this->promise->cancel();
+ }
+
+ /**
+ * Waits until the promise completes if possible.
+ *
+ * Pass $unwrap as true to unwrap the result of the promise, either
+ * returning the resolved value or throwing the rejected exception.
+ *
+ * If the promise cannot be waited on, then the promise will be rejected.
+ *
+ * @param bool $unwrap
+ *
+ * @return mixed
+ *
+ * @throws LogicException if the promise has no wait function or if the
+ * promise does not settle after waiting.
+ * @since 28.0.0
+ */
+ public function wait(bool $unwrap = true): mixed {
+ return $this->promise->wait($unwrap);
+ }
+}
diff --git a/lib/private/Http/Client/NegativeDnsCache.php b/lib/private/Http/Client/NegativeDnsCache.php
index 6c7585b11e3..ca8a477d6be 100644
--- a/lib/private/Http/Client/NegativeDnsCache.php
+++ b/lib/private/Http/Client/NegativeDnsCache.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021, Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Lukas Reschke <lukas@statuscode.ch>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Http\Client;
@@ -38,11 +20,11 @@ class NegativeDnsCache {
}
private function createCacheKey(string $domain, int $type) : string {
- return $domain . "-" . (string)$type;
+ return $domain . '-' . (string)$type;
}
public function setNegativeCacheForDnsType(string $domain, int $type, int $ttl) : void {
- $this->cache->set($this->createCacheKey($domain, $type), "true", $ttl);
+ $this->cache->set($this->createCacheKey($domain, $type), 'true', $ttl);
}
public function isNegativeCached(string $domain, int $type) : bool {
diff --git a/lib/private/Http/Client/Response.php b/lib/private/Http/Client/Response.php
index 054c902fcc5..1e4cb3b8fa2 100644
--- a/lib/private/Http/Client/Response.php
+++ b/lib/private/Http/Client/Response.php
@@ -1,77 +1,35 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Http\Client;
use OCP\Http\Client\IResponse;
use Psr\Http\Message\ResponseInterface;
-/**
- * Class Response
- *
- * @package OC\Http
- */
class Response implements IResponse {
- /** @var ResponseInterface */
- private $response;
-
- /**
- * @var bool
- */
- private $stream;
+ private ResponseInterface $response;
+ private bool $stream;
- /**
- * @param ResponseInterface $response
- * @param bool $stream
- */
- public function __construct(ResponseInterface $response, $stream = false) {
+ public function __construct(ResponseInterface $response, bool $stream = false) {
$this->response = $response;
$this->stream = $stream;
}
- /**
- * @return string|resource
- */
public function getBody() {
- return $this->stream ?
- $this->response->getBody()->detach():
- $this->response->getBody()->getContents();
+ return $this->stream
+ ? $this->response->getBody()->detach()
+ :$this->response->getBody()->getContents();
}
- /**
- * @return int
- */
public function getStatusCode(): int {
return $this->response->getStatusCode();
}
- /**
- * @param string $key
- * @return string
- */
public function getHeader(string $key): string {
$headers = $this->response->getHeader($key);
@@ -82,9 +40,6 @@ class Response implements IResponse {
return $headers[0];
}
- /**
- * @return array
- */
public function getHeaders(): array {
return $this->response->getHeaders();
}