migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,621 @@
<?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/>
*
*/
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;
/**
* Class Client
*
* @package OC\Http
*/
class Client implements IClient {
/** @var GuzzleClient */
private $client;
/** @var IConfig */
private $config;
/** @var ICertificateManager */
private $certificateManager;
private IRemoteHostValidator $remoteHostValidator;
public function __construct(
IConfig $config,
ICertificateManager $certificateManager,
GuzzleClient $client,
IRemoteHostValidator $remoteHostValidator,
protected LoggerInterface $logger,
) {
$this->config = $config;
$this->client = $client;
$this->certificateManager = $certificateManager;
$this->remoteHostValidator = $remoteHostValidator;
}
private function buildRequestOptions(array $options): array {
$proxy = $this->getProxyUri();
$defaults = [
RequestOptions::VERIFY => $this->getCertBundle(),
RequestOptions::TIMEOUT => 30,
];
$options['nextcloud']['allow_local_address'] = $this->isLocalAddressAllowed($options);
if ($options['nextcloud']['allow_local_address'] === false) {
$onRedirectFunction = function (
\Psr\Http\Message\RequestInterface $request,
\Psr\Http\Message\ResponseInterface $response,
\Psr\Http\Message\UriInterface $uri
) use ($options) {
$this->preventLocalAddress($uri->__toString(), $options);
};
$defaults[RequestOptions::ALLOW_REDIRECTS] = [
'on_redirect' => $onRedirectFunction
];
}
// Only add RequestOptions::PROXY if Nextcloud is explicitly
// configured to use a proxy. This is needed in order not to override
// Guzzle default values.
if ($proxy !== null) {
$defaults[RequestOptions::PROXY] = $proxy;
}
$options = array_merge($defaults, $options);
if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
$options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
}
if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
$options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
}
// Fallback for save_to
if (isset($options['save_to'])) {
$options['sink'] = $options['save_to'];
unset($options['save_to']);
}
return $options;
}
private function getCertBundle(): string {
// 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->getSystemValueBool('installed', false)) {
return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
}
return $this->certificateManager->getAbsoluteBundlePath();
}
/**
* Returns a null or an associative array specifying the proxy URI for
* 'http' and 'https' schemes, in addition to a 'no' key value pair
* providing a list of host names that should not be proxied to.
*
* @return array|null
*
* The return array looks like:
* [
* 'http' => 'username:password@proxy.example.com',
* 'https' => 'username:password@proxy.example.com',
* 'no' => ['foo.com', 'bar.com']
* ]
*
*/
private function getProxyUri(): ?array {
$proxyHost = $this->config->getSystemValueString('proxy', '');
if ($proxyHost === '') {
return null;
}
$proxyUserPwd = $this->config->getSystemValueString('proxyuserpwd', '');
if ($proxyUserPwd !== '') {
$proxyHost = $proxyUserPwd . '@' . $proxyHost;
}
$proxy = [
'http' => $proxyHost,
'https' => $proxyHost,
];
$proxyExclude = $this->config->getSystemValue('proxyexclude', []);
if ($proxyExclude !== [] && $proxyExclude !== null) {
$proxy['no'] = $proxyExclude;
}
return $proxy;
}
private function isLocalAddressAllowed(array $options) : bool {
if (($options['nextcloud']['allow_local_address'] ?? false) ||
$this->config->getSystemValueBool('allow_local_remote_servers', false)) {
return true;
}
return false;
}
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->remoteHostValidator->isValid($host)) {
throw new LocalServerException('Host violates local access rules');
}
}
/**
* Sends a 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 IResponse
* @throws \Exception If the request could not get completed
*/
public function get(string $uri, array $options = []): IResponse {
$this->preventLocalAddress($uri, $options);
$response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
$isStream = isset($options['stream']) && $options['stream'];
return new Response($response, $isStream);
}
/**
* Sends a 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 IResponse
* @throws \Exception If the request could not get completed
*/
public function head(string $uri, array $options = []): IResponse {
$this->preventLocalAddress($uri, $options);
$response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
return new Response($response);
}
/**
* Sends a 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 IResponse
* @throws \Exception If the request could not get completed
*/
public function post(string $uri, array $options = []): IResponse {
$this->preventLocalAddress($uri, $options);
if (isset($options['body']) && is_array($options['body'])) {
$options['form_params'] = $options['body'];
unset($options['body']);
}
$response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
$isStream = isset($options['stream']) && $options['stream'];
return new Response($response, $isStream);
}
/**
* Sends a 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 IResponse
* @throws \Exception If the request could not get completed
*/
public function put(string $uri, array $options = []): IResponse {
$this->preventLocalAddress($uri, $options);
$response = $this->client->request('put', $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,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
public function delete(string $uri, array $options = []): IResponse {
$this->preventLocalAddress($uri, $options);
$response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
return new Response($response);
}
/**
* 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,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
public function options(string $uri, array $options = []): IResponse {
$this->preventLocalAddress($uri, $options);
$response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
return new Response($response);
}
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);
}
}
@@ -0,0 +1,97 @@
<?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/>
*
*/
namespace OC\Http\Client;
use GuzzleHttp\Client as GuzzleClient;
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
*
* @package OC\Http
*/
class ClientService implements IClientService {
/** @var IConfig */
private $config;
/** @var ICertificateManager */
private $certificateManager;
/** @var DnsPinMiddleware */
private $dnsPinMiddleware;
private IRemoteHostValidator $remoteHostValidator;
private IEventLogger $eventLogger;
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;
}
/**
* @return Client
*/
public function newClient(): IClient {
$handler = new CurlHandler();
$stack = HandlerStack::create($handler);
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]);
return new Client(
$this->config,
$this->certificateManager,
$client,
$this->remoteHostValidator,
$this->logger,
);
}
}
@@ -0,0 +1,165 @@
<?php
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/>.
*
*/
namespace OC\Http\Client;
use OC\Net\IpAddressClassifier;
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
) {
$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);
$top = count($labels) >= 2 ? array_pop($labels) : '';
$second = array_pop($labels);
$hostname = $second . '.' . $top;
$responses = $this->dnsGetRecord($hostname, DNS_SOA);
if ($responses === false || count($responses) === 0) {
return null;
}
return reset($responses);
}
private function dnsResolve(string $target, int $recursionCount) : array {
if ($recursionCount >= 10) {
return [];
}
$recursionCount++;
$targetIps = [];
$soaDnsEntry = $this->soaRecord($target);
$dnsNegativeTtl = $soaDnsEntry['minimum-ttl'] ?? null;
$dnsTypes = [DNS_A, DNS_AAAA, DNS_CNAME];
foreach ($dnsTypes as $dnsType) {
if ($this->negativeDnsCache->isNegativeCached($target, $dnsType)) {
continue;
}
$dnsResponses = $this->dnsGetRecord($target, $dnsType);
$canHaveCnameRecord = true;
if ($dnsResponses !== false && count($dnsResponses) > 0) {
foreach ($dnsResponses as $dnsResponse) {
if (isset($dnsResponse['ip'])) {
$targetIps[] = $dnsResponse['ip'];
$canHaveCnameRecord = false;
} elseif (isset($dnsResponse['ipv6'])) {
$targetIps[] = $dnsResponse['ipv6'];
$canHaveCnameRecord = false;
} elseif (isset($dnsResponse['target']) && $canHaveCnameRecord) {
$targetIps = array_merge($targetIps, $this->dnsResolve($dnsResponse['target'], $recursionCount));
$canHaveCnameRecord = true;
}
}
} elseif ($dnsNegativeTtl !== null) {
$this->negativeDnsCache->setNegativeCacheForDnsType($target, $dnsType, $dnsNegativeTtl);
}
}
return $targetIps;
}
/**
* Wrapper for dns_get_record
*/
protected function dnsGetRecord(string $hostname, int $type): array|false {
return \dns_get_record($hostname, $type);
}
public function addDnsPinning() {
return function (callable $handler) {
return function (
RequestInterface $request,
array $options
) use ($handler) {
if ($options['nextcloud']['allow_local_address'] === true) {
return $handler($request, $options);
}
$hostName = (string)$request->getUri()->getHost();
$port = $request->getUri()->getPort();
$ports = [
'80',
'443',
];
if ($port !== null) {
$ports[] = (string)$port;
}
$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) {
$curlResolves["$hostName:$port"] = [];
foreach ($targetIps as $ip) {
if ($this->ipAddressClassifier->isLocalAddress($ip)) {
// TODO: continue with all non-local IPs?
throw new LocalServerException('Host violates local access rules');
}
$curlResolves["$hostName:$port"][] = $ip;
}
}
// Coalesce the per-host:port ips back into a comma separated list
foreach ($curlResolves as $hostport => $ips) {
$options['curl'][CURLOPT_RESOLVE][] = "$hostport:" . implode(',', $ips);
}
return $handler($request, $options);
};
};
}
}
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
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);
}
}
@@ -0,0 +1,51 @@
<?php
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/>.
*
*/
namespace OC\Http\Client;
use OCP\ICache;
use OCP\ICacheFactory;
class NegativeDnsCache {
/** @var ICache */
private $cache;
public function __construct(ICacheFactory $memcache) {
$this->cache = $memcache->createLocal('NegativeDnsCache');
}
private function createCacheKey(string $domain, int $type) : string {
return $domain . "-" . (string)$type;
}
public function setNegativeCacheForDnsType(string $domain, int $type, int $ttl) : void {
$this->cache->set($this->createCacheKey($domain, $type), "true", $ttl);
}
public function isNegativeCached(string $domain, int $type) : bool {
return (bool)$this->cache->hasKey($this->createCacheKey($domain, $type));
}
}
@@ -0,0 +1,91 @@
<?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/>
*
*/
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;
/**
* @param ResponseInterface $response
* @param bool $stream
*/
public function __construct(ResponseInterface $response, $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 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);
if (count($headers) === 0) {
return '';
}
return $headers[0];
}
/**
* @return array
*/
public function getHeaders(): array {
return $this->response->getHeaders();
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Marco Ziech <marco@ziech.net>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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/>.
*
*/
namespace OC\Http;
class CookieHelper {
public const SAMESITE_NONE = 0;
public const SAMESITE_LAX = 1;
public const SAMESITE_STRICT = 2;
public static function setCookie(string $name,
string $value = '',
int $maxAge = 0,
string $path = '',
string $domain = '',
bool $secure = false,
bool $httponly = false,
int $samesite = self::SAMESITE_NONE) {
$header = sprintf(
'Set-Cookie: %s=%s',
$name,
rawurlencode($value)
);
if ($path !== '') {
$header .= sprintf('; Path=%s', $path);
}
if ($domain !== '') {
$header .= sprintf('; Domain=%s', $domain);
}
if ($maxAge > 0) {
$header .= sprintf('; Max-Age=%d', $maxAge);
}
if ($secure) {
$header .= '; Secure';
}
if ($httponly) {
$header .= '; HttpOnly';
}
if ($samesite === self::SAMESITE_LAX) {
$header .= '; SameSite=Lax';
} elseif ($samesite === self::SAMESITE_STRICT) {
$header .= '; SameSite=Strict';
}
header($header, false);
}
}
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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/>.
*
*/
namespace OC\Http\WellKnown;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Bootstrap\ServiceRegistration;
use OCP\AppFramework\QueryException;
use OCP\Http\WellKnown\IHandler;
use OCP\Http\WellKnown\IRequestContext;
use OCP\Http\WellKnown\IResponse;
use OCP\Http\WellKnown\JrdResponse;
use OCP\IRequest;
use OCP\IServerContainer;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function array_reduce;
class RequestManager {
/** @var Coordinator */
private $coordinator;
/** @var IServerContainer */
private $container;
/** @var LoggerInterface */
private $logger;
public function __construct(Coordinator $coordinator,
IServerContainer $container,
LoggerInterface $logger) {
$this->coordinator = $coordinator;
$this->container = $container;
$this->logger = $logger;
}
public function process(string $service, IRequest $request): ?IResponse {
$handlers = $this->loadHandlers();
$context = new class($request) implements IRequestContext {
/** @var IRequest */
private $request;
public function __construct(IRequest $request) {
$this->request = $request;
}
public function getHttpRequest(): IRequest {
return $this->request;
}
};
$subject = $request->getParam('resource');
$initialResponse = new JrdResponse($subject ?? '');
$finalResponse = array_reduce($handlers, function (?IResponse $previousResponse, IHandler $handler) use ($context, $service) {
return $handler->handle($service, $context, $previousResponse);
}, $initialResponse);
if ($finalResponse instanceof JrdResponse && $finalResponse->isEmpty()) {
return null;
}
return $finalResponse;
}
/**
* @return IHandler[]
*/
private function loadHandlers(): array {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
throw new RuntimeException("Well known handlers requested before the apps had been fully registered");
}
$registrations = $context->getWellKnownHandlers();
$this->logger->debug(count($registrations) . " well known handlers registered");
return array_filter(
array_map(function (ServiceRegistration $registration) {
/** @var ServiceRegistration<IHandler> $registration */
$class = $registration->getService();
try {
$handler = $this->container->get($class);
if (!($handler) instanceof IHandler) {
$this->logger->error("Well known handler $class is invalid");
return null;
}
return $handler;
} catch (QueryException $e) {
$this->logger->error("Could not load well known handler $class", [
'exception' => $e,
'app' => $registration->getAppId(),
]);
return null;
}
}, $registrations)
);
}
}