diff --git a/composer.json b/composer.json index 2dd5dec7b9..bf0559254e 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,8 @@ "npm-asset/perfect-scrollbar": "0.6.16", "npm-asset/textcomplete": "^0.18.2", "npm-asset/typeahead.js": "^0.11.1", - "minishlink/web-push": "^6.0" + "minishlink/web-push": "^6.0", + "mattwright/urlresolver": "^2.0" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index 5e8f1a20aa..906a681e45 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7d6dee6e449da931e8fe209e61b2e78e", + "content-hash": "c9e0a9eacc23d884012042eeab01cc8b", "packages": [ { "name": "asika/simple-console", @@ -1133,6 +1133,52 @@ ], "time": "2017-07-19T15:11:19+00:00" }, + { + "name": "mattwright/urlresolver", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/mattwright/URLResolver.php.git", + "reference": "416039192cb6d9158bdacd68349bceff8739b857" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mattwright/URLResolver.php/zipball/416039192cb6d9158bdacd68349bceff8739b857", + "reference": "416039192cb6d9158bdacd68349bceff8739b857", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-mbstring": "*", + "php": ">=5.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "mattwright\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Wright", + "email": "mw@mattwright.com" + } + ], + "description": "PHP class that attempts to resolve URLs to a final, canonical link.", + "homepage": "https://github.com/mattwright/URLResolver.php", + "keywords": [ + "canonical", + "link", + "redirect", + "resolve", + "url" + ], + "time": "2019-01-18T00:59:34+00:00" + }, { "name": "michelf/php-markdown", "version": "1.9.0", diff --git a/src/Core/Search.php b/src/Core/Search.php index a3588b3bd3..efe8331b85 100644 --- a/src/Core/Search.php +++ b/src/Core/Search.php @@ -227,7 +227,7 @@ class Search $return = Contact::searchByName($search, $mode); } else { $p = $page > 1 ? 'p=' . $page : ''; - $curlResult = DI::httpRequest()->get(self::getGlobalDirectory() . '/search/people?' . $p . '&q=' . urlencode($search), ['accept_content' => 'application/json']); + $curlResult = DI::httpRequest()->get(self::getGlobalDirectory() . '/search/people?' . $p . '&q=' . urlencode($search), ['accept_content' => ['application/json']]); if ($curlResult->isSuccess()) { $searchResult = json_decode($curlResult->getBody(), true); if (!empty($searchResult['profiles'])) { diff --git a/src/DI.php b/src/DI.php index cbc353161c..7f6f28f33c 100644 --- a/src/DI.php +++ b/src/DI.php @@ -39,6 +39,17 @@ abstract class DI self::$dice = $dice; } + /** + * Returns a clone of the current dice instance + * This usefull for overloading the current instance with mocked methods during tests + * + * @return Dice + */ + public static function getDice() + { + return clone self::$dice; + } + // // common instances // @@ -407,11 +418,11 @@ abstract class DI // /** - * @return Network\IHTTPRequest + * @return Network\IHTTPClient */ public static function httpRequest() { - return self::$dice->create(Network\IHTTPRequest::class); + return self::$dice->create(Network\IHTTPClient::class); } // diff --git a/src/Factory/HTTPClientFactory.php b/src/Factory/HTTPClientFactory.php new file mode 100644 index 0000000000..7f7d5e5d73 --- /dev/null +++ b/src/Factory/HTTPClientFactory.php @@ -0,0 +1,110 @@ +config = $config; + $this->profiler = $profiler; + $this->baseUrl = $baseUrl; + } + + /** + * Creates a IHTTPClient for communications with HTTP endpoints + * + * @param HandlerStack|null $handlerStack (optional) A handler replacement (just usefull at test environments) + * + * @return IHTTPClient + */ + public function createClient(HandlerStack $handlerStack = null): IHTTPClient + { + $proxy = $this->config->get('system', 'proxy'); + + if (!empty($proxy)) { + $proxyuser = $this->config->get('system', 'proxyuser'); + + if (!empty($proxyuser)) { + $proxy = $proxyuser . '@' . $proxy; + } + } + + $logger = $this->logger; + + $onRedirect = function ( + RequestInterface $request, + ResponseInterface $response, + UriInterface $uri + ) use ($logger) { + $logger->notice('Curl redirect.', ['url' => $request->getUri(), 'to' => $uri, 'method' => $request->getMethod()]); + }; + + $userAgent = FRIENDICA_PLATFORM . " '" . + FRIENDICA_CODENAME . "' " . + FRIENDICA_VERSION . '-' . + DB_UPDATE_VERSION . '; ' . + $this->baseUrl->get(); + + $guzzle = new Client([ + RequestOptions::ALLOW_REDIRECTS => [ + 'max' => 8, + 'on_redirect' => $onRedirect, + 'track_redirect' => true, + 'strict' => true, + 'referer' => true, + ], + RequestOptions::HTTP_ERRORS => false, + // Without this setting it seems as if some webservers send compressed content + // This seems to confuse curl so that it shows this uncompressed. + /// @todo We could possibly set this value to "gzip" or something similar + RequestOptions::DECODE_CONTENT => '', + RequestOptions::FORCE_IP_RESOLVE => ($this->config->get('system', 'ipv4_resolve') ? 'v4' : null), + RequestOptions::CONNECT_TIMEOUT => 10, + RequestOptions::TIMEOUT => $this->config->get('system', 'curl_timeout', 60), + // by default we will allow self-signed certs + // but it can be overridden + RequestOptions::VERIFY => (bool)$this->config->get('system', 'verifyssl'), + RequestOptions::PROXY => $proxy, + RequestOptions::HEADERS => [ + 'User-Agent' => $userAgent, + ], + 'handler' => $handlerStack ?? HandlerStack::create(), + ]); + + $resolver = new URLResolver(); + $resolver->setUserAgent($userAgent); + $resolver->setMaxRedirects(10); + $resolver->setRequestTimeout(10); + // if the file is too large then exit + $resolver->setMaxResponseDataSize(1000000); + // Designate a temporary file that will store cookies during the session. + // Some websites test the browser for cookie support, so this enhances results. + $resolver->setCookieJar(get_temppath() . '/url_resolver.cookie', true); + + return new HTTPClient($logger, $this->profiler, $guzzle, $resolver); + } +} diff --git a/src/Model/GServer.php b/src/Model/GServer.php index eb99b1bbc8..f81b2acc14 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -1619,11 +1619,11 @@ class GServer } elseif ($curlResult->inHeader('x-diaspora-version')) { $serverdata['platform'] = 'diaspora'; $serverdata['network'] = Protocol::DIASPORA; - $serverdata['version'] = $curlResult->getHeader('x-diaspora-version'); + $serverdata['version'] = $curlResult->getHeader('x-diaspora-version')[0] ?? ''; } elseif ($curlResult->inHeader('x-friendica-version')) { $serverdata['platform'] = 'friendica'; $serverdata['network'] = Protocol::DFRN; - $serverdata['version'] = $curlResult->getHeader('x-friendica-version'); + $serverdata['version'] = $curlResult->getHeader('x-friendica-version')[0] ?? ''; } else { return $serverdata; } @@ -1728,8 +1728,7 @@ class GServer if (!empty($accesstoken)) { $api = 'https://instances.social/api/1.0/instances/list?count=0'; - $header = ['Authorization: Bearer '.$accesstoken]; - $curlResult = DI::httpRequest()->get($api, ['header' => $header]); + $curlResult = DI::httpRequest()->get($api, ['header' => ['Authorization' => ['Bearer ' . $accesstoken]]]); if ($curlResult->isSuccess()) { $servers = json_decode($curlResult->getBody(), true); diff --git a/src/Model/Storage/ExternalResource.php b/src/Model/Storage/ExternalResource.php index 918bcf8ac3..413050c306 100644 --- a/src/Model/Storage/ExternalResource.php +++ b/src/Model/Storage/ExternalResource.php @@ -50,7 +50,7 @@ class ExternalResource implements IStorage } try { - $fetchResult = HTTPSignature::fetchRaw($data->url, $data->uid, ['accept_content' => '']); + $fetchResult = HTTPSignature::fetchRaw($data->url, $data->uid, ['accept_content' => []]); } catch (Exception $exception) { throw new ReferenceStorageException(sprintf('External resource failed to get %s', $reference), $exception->getCode(), $exception); } diff --git a/src/Module/Magic.php b/src/Module/Magic.php index 45fde43f64..12747dca7c 100644 --- a/src/Module/Magic.php +++ b/src/Module/Magic.php @@ -88,9 +88,10 @@ class Magic extends BaseModule $exp = explode('/profile/', $contact['url']); $basepath = $exp[0]; - $header = []; - $header['Accept'] = 'application/x-dfrn+json, application/x-zot+json'; - $header['X-Open-Web-Auth'] = Strings::getRandomHex(); + $header = [ + 'Accept' => ['application/x-dfrn+json', 'application/x-zot+json'], + 'X-Open-Web-Auth' => [Strings::getRandomHex()], + ]; // Create a header that is signed with the local users private key. $header = HTTPSignature::createSig( diff --git a/src/Module/Proxy.php b/src/Module/Proxy.php index 04fe00db12..cd11d9e3ab 100644 --- a/src/Module/Proxy.php +++ b/src/Module/Proxy.php @@ -75,7 +75,7 @@ class Proxy extends BaseModule $request['url'] = str_replace(' ', '+', $request['url']); // Fetch the content with the local user - $fetchResult = HTTPSignature::fetchRaw($request['url'], local_user(), ['accept_content' => '', 'timeout' => 10]); + $fetchResult = HTTPSignature::fetchRaw($request['url'], local_user(), ['accept_content' => [], 'timeout' => 10]); $img_str = $fetchResult->getBody(); if (!$fetchResult->isSuccess() || empty($img_str)) { diff --git a/src/Network/HTTPClient.php b/src/Network/HTTPClient.php new file mode 100644 index 0000000000..f4293cd086 --- /dev/null +++ b/src/Network/HTTPClient.php @@ -0,0 +1,257 @@ +. + * + */ + +namespace Friendica\Network; + +use Friendica\Core\System; +use Friendica\Util\Network; +use Friendica\Util\Profiler; +use GuzzleHttp\Client; +use GuzzleHttp\Cookie\FileCookieJar; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\TransferException; +use GuzzleHttp\RequestOptions; +use mattwright\URLResolver; +use Psr\Http\Message\ResponseInterface; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LoggerInterface; + +/** + * Performs HTTP requests to a given URL + */ +class HTTPClient implements IHTTPClient +{ + /** @var LoggerInterface */ + private $logger; + /** @var Profiler */ + private $profiler; + /** @var Client */ + private $client; + /** @var URLResolver */ + private $resolver; + + public function __construct(LoggerInterface $logger, Profiler $profiler, Client $client, URLResolver $resolver) + { + $this->logger = $logger; + $this->profiler = $profiler; + $this->client = $client; + $this->resolver = $resolver; + } + + /** + * @throws HTTPException\InternalServerErrorException + */ + protected function request(string $method, string $url, array $opts = []): IHTTPResult + { + $this->profiler->startRecording('network'); + $this->logger->debug('Request start.', ['url' => $url, 'method' => $method]); + + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + + if (strlen($url) > 1000) { + $this->logger->debug('URL is longer than 1000 characters.', ['url' => $url, 'callstack' => System::callstack(20)]); + $this->profiler->stopRecording(); + return CurlResult::createErrorCurl(substr($url, 0, 200)); + } + + $parts2 = []; + $parts = parse_url($url); + $path_parts = explode('/', $parts['path'] ?? ''); + foreach ($path_parts as $part) { + if (strlen($part) <> mb_strlen($part)) { + $parts2[] = rawurlencode($part); + } else { + $parts2[] = $part; + } + } + $parts['path'] = implode('/', $parts2); + $url = Network::unparseURL($parts); + + if (Network::isUrlBlocked($url)) { + $this->logger->info('Domain is blocked.', ['url' => $url]); + $this->profiler->stopRecording(); + return CurlResult::createErrorCurl($url); + } + + $conf = []; + + if (!empty($opts['cookiejar'])) { + $jar = new FileCookieJar($opts['cookiejar']); + $conf[RequestOptions::COOKIES] = $jar; + } + + $header = []; + + if (!empty($opts['accept_content'])) { + $header['Accept'] = $opts['accept_content']; + } + + if (!empty($opts['header'])) { + $header = array_merge($opts['header'], $header); + } + + if (!empty($opts['headers'])) { + $this->logger->notice('Wrong option \'headers\' used.'); + $header = array_merge($opts['headers'], $header); + } + + $conf[RequestOptions::HEADERS] = array_merge($this->client->getConfig(RequestOptions::HEADERS), $header); + + if (!empty($opts['timeout'])) { + $conf[RequestOptions::TIMEOUT] = $opts['timeout']; + } + + $conf[RequestOptions::ON_HEADERS] = function (ResponseInterface $response) use ($opts) { + if (!empty($opts['content_length']) && + $response->getHeaderLine('Content-Length') > $opts['content_length']) { + throw new TransferException('The file is too big!'); + } + }; + + try { + $this->logger->debug('http request config.', ['url' => $url, 'method' => $method, 'options' => $conf]); + + switch ($method) { + case 'get': + case 'head': + case 'post': + case 'put': + case 'delete': + $response = $this->client->$method($url, $conf); + break; + default: + throw new TransferException('Invalid method'); + } + return new GuzzleResponse($response, $url); + } catch (TransferException $exception) { + if ($exception instanceof RequestException && + $exception->hasResponse()) { + return new GuzzleResponse($exception->getResponse(), $url, $exception->getCode(), ''); + } else { + return new CurlResult($url, '', ['http_code' => $exception->getCode()], $exception->getCode(), ''); + } + } catch (InvalidArgumentException $argumentException) { + $this->logger->info('Invalid Argument for HTTP call.', ['url' => $url, 'method' => $method, 'exception' => $argumentException]); + return new CurlResult($url, '', ['http_code' => $argumentException->getCode()], $argumentException->getCode(), $argumentException->getMessage()); + } finally { + $this->logger->debug('Request stop.', ['url' => $url, 'method' => $method]); + $this->profiler->stopRecording(); + } + } + + /** {@inheritDoc} + * + * @throws HTTPException\InternalServerErrorException + */ + public function head(string $url, array $opts = []): IHTTPResult + { + return $this->request('head', $url, $opts); + } + + /** + * {@inheritDoc} + */ + public function get(string $url, array $opts = []): IHTTPResult + { + return $this->request('get', $url, $opts); + } + + /** + * {@inheritDoc} + */ + public function post(string $url, $params, array $headers = [], int $timeout = 0): IHTTPResult + { + $opts = []; + + $opts[RequestOptions::JSON] = $params; + + if (!empty($headers)) { + $opts['headers'] = $headers; + } + + if (!empty($timeout)) { + $opts[RequestOptions::TIMEOUT] = $timeout; + } + + return $this->request('post', $url, $opts); + } + + /** + * {@inheritDoc} + */ + public function finalUrl(string $url) + { + $this->profiler->startRecording('network'); + + if (Network::isLocalLink($url)) { + $this->logger->debug('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + + if (Network::isUrlBlocked($url)) { + $this->logger->info('Domain is blocked.', ['url' => $url]); + return $url; + } + + if (Network::isRedirectBlocked($url)) { + $this->logger->info('Domain should not be redirected.', ['url' => $url]); + return $url; + } + + $url = Network::stripTrackingQueryParams($url); + + $url = trim($url, "'"); + + $urlResult = $this->resolver->resolveURL($url); + + if ($urlResult->didErrorOccur()) { + throw new TransferException($urlResult->getErrorMessageString()); + } + + return $urlResult->getURL(); + } + + /** + * {@inheritDoc} + */ + public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '') + { + $ret = $this->fetchFull($url, $timeout, $accept_content, $cookiejar); + + return $ret->getBody(); + } + + /** + * {@inheritDoc} + */ + public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '') + { + return $this->get( + $url, + [ + 'timeout' => $timeout, + 'accept_content' => $accept_content, + 'cookiejar' => $cookiejar + ] + ); + } +} diff --git a/src/Network/HTTPRequest.php b/src/Network/HTTPRequest.php deleted file mode 100644 index b08e918326..0000000000 --- a/src/Network/HTTPRequest.php +++ /dev/null @@ -1,501 +0,0 @@ -. - * - */ - -namespace Friendica\Network; - -use DOMDocument; -use DomXPath; -use Friendica\App; -use Friendica\Core\Config\IConfig; -use Friendica\Core\System; -use Friendica\Util\Network; -use Friendica\Util\Profiler; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Exception\TransferException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\UriInterface; -use Psr\Log\LoggerInterface; - -/** - * Performs HTTP requests to a given URL - */ -class HTTPRequest implements IHTTPRequest -{ - /** @var LoggerInterface */ - private $logger; - /** @var Profiler */ - private $profiler; - /** @var IConfig */ - private $config; - /** @var string */ - private $baseUrl; - - public function __construct(LoggerInterface $logger, Profiler $profiler, IConfig $config, App\BaseURL $baseUrl) - { - $this->logger = $logger; - $this->profiler = $profiler; - $this->config = $config; - $this->baseUrl = $baseUrl->get(); - } - - /** {@inheritDoc} - * - * @throws HTTPException\InternalServerErrorException - */ - public function head(string $url, array $opts = []) - { - $opts['nobody'] = true; - - return $this->get($url, $opts); - } - - /** - * {@inheritDoc} - */ - public function get(string $url, array $opts = []) - { - $this->profiler->startRecording('network'); - - if (Network::isLocalLink($url)) { - $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); - } - - if (strlen($url) > 1000) { - $this->logger->debug('URL is longer than 1000 characters.', ['url' => $url, 'callstack' => System::callstack(20)]); - $this->profiler->stopRecording(); - return CurlResult::createErrorCurl(substr($url, 0, 200)); - } - - $parts2 = []; - $parts = parse_url($url); - $path_parts = explode('/', $parts['path'] ?? ''); - foreach ($path_parts as $part) { - if (strlen($part) <> mb_strlen($part)) { - $parts2[] = rawurlencode($part); - } else { - $parts2[] = $part; - } - } - $parts['path'] = implode('/', $parts2); - $url = Network::unparseURL($parts); - - if (Network::isUrlBlocked($url)) { - $this->logger->info('Domain is blocked.', ['url' => $url]); - $this->profiler->stopRecording(); - return CurlResult::createErrorCurl($url); - } - - $curlOptions = []; - - if (!empty($opts['cookiejar'])) { - $curlOptions[CURLOPT_COOKIEJAR] = $opts["cookiejar"]; - $curlOptions[CURLOPT_COOKIEFILE] = $opts["cookiejar"]; - } - - // These settings aren't needed. We're following the location already. - // $curlOptions[CURLOPT_FOLLOWLOCATION] =true; - // $curlOptions[CURLOPT_MAXREDIRS] = 5; - - $curlOptions[CURLOPT_HTTPHEADER] = []; - - if (!empty($opts['accept_content'])) { - array_push($curlOptions[CURLOPT_HTTPHEADER], 'Accept: ' . $opts['accept_content']); - } - - if (!empty($opts['header'])) { - $curlOptions[CURLOPT_HTTPHEADER] = array_merge($opts['header'], $curlOptions[CURLOPT_HTTPHEADER]); - } - - $curlOptions[CURLOPT_RETURNTRANSFER] = true; - $curlOptions[CURLOPT_USERAGENT] = $this->getUserAgent(); - - $range = intval($this->config->get('system', 'curl_range_bytes', 0)); - - if ($range > 0) { - $curlOptions[CURLOPT_RANGE] = '0-' . $range; - } - - // Without this setting it seems as if some webservers send compressed content - // This seems to confuse curl so that it shows this uncompressed. - /// @todo We could possibly set this value to "gzip" or something similar - $curlOptions[CURLOPT_ENCODING] = ''; - - if (!empty($opts['headers'])) { - $this->logger->notice('Wrong option \'headers\' used.'); - $curlOptions[CURLOPT_HTTPHEADER] = array_merge($opts['headers'], $curlOptions[CURLOPT_HTTPHEADER]); - } - - if (!empty($opts['nobody'])) { - $curlOptions[CURLOPT_NOBODY] = $opts['nobody']; - } - - $curlOptions[CURLOPT_CONNECTTIMEOUT] = 10; - - if (!empty($opts['timeout'])) { - $curlOptions[CURLOPT_TIMEOUT] = $opts['timeout']; - } else { - $curl_time = $this->config->get('system', 'curl_timeout', 60); - $curlOptions[CURLOPT_TIMEOUT] = intval($curl_time); - } - - // by default we will allow self-signed certs - // but you can override this - - $check_cert = $this->config->get('system', 'verifyssl'); - $curlOptions[CURLOPT_SSL_VERIFYPEER] = ($check_cert) ? true : false; - - if ($check_cert) { - $curlOptions[CURLOPT_SSL_VERIFYHOST] = 2; - } - - $proxy = $this->config->get('system', 'proxy'); - - if (!empty($proxy)) { - $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = 1; - $curlOptions[CURLOPT_PROXY] = $proxy; - $proxyuser = $this->config->get('system', 'proxyuser'); - - if (!empty($proxyuser)) { - $curlOptions[CURLOPT_PROXYUSERPWD] = $proxyuser; - } - } - - if ($this->config->get('system', 'ipv4_resolve', false)) { - $curlOptions[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; - } - - $logger = $this->logger; - - $onRedirect = function( - RequestInterface $request, - ResponseInterface $response, - UriInterface $uri - ) use ($logger) { - $logger->notice('Curl redirect.', ['url' => $request->getUri(), 'to' => $uri]); - }; - - $onHeaders = function (ResponseInterface $response) use ($opts) { - if (!empty($opts['content_length']) && - $response->getHeaderLine('Content-Length') > $opts['content_length']) { - throw new TransferException('The file is too big!'); - } - }; - - $client = new Client([ - 'allow_redirect' => [ - 'max' => 8, - 'on_redirect' => $onRedirect, - 'track_redirect' => true, - 'strict' => true, - 'referer' => true, - ], - 'on_headers' => $onHeaders, - 'curl' => $curlOptions - ]); - - try { - $response = $client->get($url); - return new GuzzleResponse($response, $url); - } catch (TransferException $exception) { - if ($exception instanceof RequestException && - $exception->hasResponse()) { - return new GuzzleResponse($exception->getResponse(), $url, $exception->getCode(), ''); - } else { - return new CurlResult($url, '', ['http_code' => $exception->getCode()], $exception->getCode(), ''); - } - } finally { - $this->profiler->stopRecording(); - } - } - - /** - * {@inheritDoc} - * - * @param int $redirects The recursion counter for internal use - default 0 - * - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public function post(string $url, $params, array $headers = [], int $timeout = 0, &$redirects = 0) - { - $this->profiler->startRecording('network'); - - if (Network::isLocalLink($url)) { - $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); - } - - if (Network::isUrlBlocked($url)) { - $this->logger->info('Domain is blocked.' . ['url' => $url]); - $this->profiler->stopRecording(); - return CurlResult::createErrorCurl($url); - } - - $ch = curl_init($url); - - if (($redirects > 8) || (!$ch)) { - $this->profiler->stopRecording(); - return CurlResult::createErrorCurl($url); - } - - $this->logger->debug('Post_url: start.', ['url' => $url]); - - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $params); - curl_setopt($ch, CURLOPT_USERAGENT, $this->getUserAgent()); - - if ($this->config->get('system', 'ipv4_resolve', false)) { - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - } - - @curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - - if (intval($timeout)) { - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - } else { - $curl_time = $this->config->get('system', 'curl_timeout', 60); - curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time)); - } - - if (!empty($headers)) { - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } - - $check_cert = $this->config->get('system', 'verifyssl'); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false)); - - if ($check_cert) { - @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - } - - $proxy = $this->config->get('system', 'proxy'); - - if (!empty($proxy)) { - curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1); - curl_setopt($ch, CURLOPT_PROXY, $proxy); - $proxyuser = $this->config->get('system', 'proxyuser'); - if (!empty($proxyuser)) { - curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser); - } - } - - // don't let curl abort the entire application - // if it throws any errors. - - $s = @curl_exec($ch); - - $curl_info = curl_getinfo($ch); - - $curlResponse = new CurlResult($url, $s, $curl_info, curl_errno($ch), curl_error($ch)); - - if (!Network::isRedirectBlocked($url) && $curlResponse->isRedirectUrl()) { - $redirects++; - $this->logger->info('Post redirect.', ['url' => $url, 'to' => $curlResponse->getRedirectUrl()]); - curl_close($ch); - $this->profiler->stopRecording(); - return $this->post($curlResponse->getRedirectUrl(), $params, $headers, $redirects, $timeout); - } - - curl_close($ch); - - $this->profiler->stopRecording(); - - // Very old versions of Lighttpd don't like the "Expect" header, so we remove it when needed - if ($curlResponse->getReturnCode() == 417) { - $redirects++; - - if (empty($headers)) { - $headers = ['Expect:']; - } else { - if (!in_array('Expect:', $headers)) { - array_push($headers, 'Expect:'); - } - } - $this->logger->info('Server responds with 417, applying workaround', ['url' => $url]); - return $this->post($url, $params, $headers, $redirects, $timeout); - } - - $this->logger->debug('Post_url: End.', ['url' => $url]); - - return $curlResponse; - } - - /** - * {@inheritDoc} - */ - public function finalUrl(string $url, int $depth = 1, bool $fetchbody = false) - { - if (Network::isLocalLink($url)) { - $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); - } - - if (Network::isUrlBlocked($url)) { - $this->logger->info('Domain is blocked.', ['url' => $url]); - return $url; - } - - if (Network::isRedirectBlocked($url)) { - $this->logger->info('Domain should not be redirected.', ['url' => $url]); - return $url; - } - - $url = Network::stripTrackingQueryParams($url); - - if ($depth > 10) { - return $url; - } - - $url = trim($url, "'"); - - $this->profiler->startRecording('network'); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_NOBODY, 1); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_USERAGENT, $this->getUserAgent()); - - curl_exec($ch); - $curl_info = @curl_getinfo($ch); - $http_code = $curl_info['http_code']; - curl_close($ch); - - $this->profiler->stopRecording(); - - if ($http_code == 0) { - return $url; - } - - if (in_array($http_code, ['301', '302'])) { - if (!empty($curl_info['redirect_url'])) { - return $this->finalUrl($curl_info['redirect_url'], ++$depth, $fetchbody); - } elseif (!empty($curl_info['location'])) { - return $this->finalUrl($curl_info['location'], ++$depth, $fetchbody); - } - } - - // Check for redirects in the meta elements of the body if there are no redirects in the header. - if (!$fetchbody) { - return $this->finalUrl($url, ++$depth, true); - } - - // if the file is too large then exit - if ($curl_info["download_content_length"] > 1000000) { - return $url; - } - - // if it isn't a HTML file then exit - if (!empty($curl_info["content_type"]) && !strstr(strtolower($curl_info["content_type"]), "html")) { - return $url; - } - - $this->profiler->startRecording('network'); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_NOBODY, 0); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_USERAGENT, $this->getUserAgent()); - - $body = curl_exec($ch); - curl_close($ch); - - $this->profiler->stopRecording(); - - if (trim($body) == "") { - return $url; - } - - // Check for redirect in meta elements - $doc = new DOMDocument(); - @$doc->loadHTML($body); - - $xpath = new DomXPath($doc); - - $list = $xpath->query("//meta[@content]"); - foreach ($list as $node) { - $attr = []; - if ($node->attributes->length) { - foreach ($node->attributes as $attribute) { - $attr[$attribute->name] = $attribute->value; - } - } - - if (@$attr["http-equiv"] == 'refresh') { - $path = $attr["content"]; - $pathinfo = explode(";", $path); - foreach ($pathinfo as $value) { - if (substr(strtolower($value), 0, 4) == "url=") { - return $this->finalUrl(substr($value, 4), ++$depth); - } - } - } - } - - return $url; - } - - /** - * {@inheritDoc} - */ - public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '') - { - $ret = $this->fetchFull($url, $timeout, $accept_content, $cookiejar); - - return $ret->getBody(); - } - - /** - * {@inheritDoc} - */ - public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '') - { - return $this->get( - $url, - [ - 'timeout' => $timeout, - 'accept_content' => $accept_content, - 'cookiejar' => $cookiejar - ] - ); - } - - /** - * {@inheritDoc} - */ - public function getUserAgent() - { - return - FRIENDICA_PLATFORM . " '" . - FRIENDICA_CODENAME . "' " . - FRIENDICA_VERSION . '-' . - DB_UPDATE_VERSION . '; ' . - $this->baseUrl; - } -} diff --git a/src/Network/IHTTPRequest.php b/src/Network/IHTTPClient.php similarity index 81% rename from src/Network/IHTTPRequest.php rename to src/Network/IHTTPClient.php index b496c7feba..f7064e1c0f 100644 --- a/src/Network/IHTTPRequest.php +++ b/src/Network/IHTTPClient.php @@ -24,7 +24,7 @@ namespace Friendica\Network; /** * Interface for calling HTTP requests and returning their responses */ -interface IHTTPRequest +interface IHTTPClient { /** * Fetches the content of an URL @@ -60,8 +60,8 @@ interface IHTTPRequest * Send a HEAD to an URL. * * @param string $url URL to fetch - * @param array $opts (optional parameters) assoziative array with: - * 'accept_content' => supply Accept: header with 'accept_content' as the value + * @param array $opts (optional parameters) associative array with: + * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value * 'timeout' => int Timeout in seconds, default system config value or 60 seconds * 'cookiejar' => path to cookie jar file * 'header' => header array @@ -74,8 +74,8 @@ interface IHTTPRequest * Send a GET to an URL. * * @param string $url URL to fetch - * @param array $opts (optional parameters) assoziative array with: - * 'accept_content' => supply Accept: header with 'accept_content' as the value + * @param array $opts (optional parameters) associative array with: + * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value * 'timeout' => int Timeout in seconds, default system config value or 60 seconds * 'cookiejar' => path to cookie jar file * 'header' => header array @@ -104,21 +104,9 @@ interface IHTTPRequest * through HTTP code or meta refresh tags. Stops after 10 redirections. * * @param string $url A user-submitted URL - * @param int $depth The current redirection recursion level (internal) - * @param bool $fetchbody Wether to fetch the body or not after the HEAD requests * * @return string A canonical URL * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @see ParseUrl::getSiteinfo - * - * @todo Remove the $fetchbody parameter that generates an extraneous HEAD request */ - public function finalUrl(string $url, int $depth = 1, bool $fetchbody = false); - - /** - * Returns the current UserAgent as a String - * - * @return string the UserAgent as a String - */ - public function getUserAgent(); + public function finalUrl(string $url); } diff --git a/src/Network/Probe.php b/src/Network/Probe.php index 4cacfedf40..bdc12aadc1 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -170,7 +170,7 @@ class Probe Logger::info('Probing', ['host' => $host, 'ssl_url' => $ssl_url, 'url' => $url, 'callstack' => System::callstack(20)]); $xrd = null; - $curlResult = DI::httpRequest()->get($ssl_url, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']); + $curlResult = DI::httpRequest()->get($ssl_url, ['timeout' => $xrd_timeout, 'accept_content' => ['application/xrd+xml']]); $ssl_connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); if ($curlResult->isSuccess()) { $xml = $curlResult->getBody(); @@ -187,7 +187,7 @@ class Probe } if (!is_object($xrd) && !empty($url)) { - $curlResult = DI::httpRequest()->get($url, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']); + $curlResult = DI::httpRequest()->get($url, ['timeout' => $xrd_timeout, 'accept_content' => ['application/xrd+xml']]); $connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); if ($curlResult->isTimeout()) { Logger::info('Probing timeout', ['url' => $url]); @@ -940,7 +940,7 @@ class Probe { $xrd_timeout = DI::config()->get('system', 'xrd_timeout', 20); - $curlResult = DI::httpRequest()->get($url, ['timeout' => $xrd_timeout, 'accept_content' => $type]); + $curlResult = DI::httpRequest()->get($url, ['timeout' => $xrd_timeout, 'accept_content' => [$type]]); if ($curlResult->isTimeout()) { self::$istimeout = true; return []; diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index cd63291eed..141bce5427 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -976,7 +976,7 @@ class DFRN $content_type = ($public_batch ? "application/magic-envelope+xml" : "application/json"); - $postResult = DI::httpRequest()->post($dest_url, $envelope, ["Content-Type: " . $content_type]); + $postResult = DI::httpRequest()->post($dest_url, $envelope, ['Content-Type' => $content_type]); $xml = $postResult->getBody(); $curl_stat = $postResult->getReturnCode(); diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index b38e0506b9..32c0ce0428 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -3022,7 +3022,7 @@ class Diaspora if (!intval(DI::config()->get("system", "diaspora_test"))) { $content_type = (($public_batch) ? "application/magic-envelope+xml" : "application/json"); - $postResult = DI::httpRequest()->post($dest_url . "/", $envelope, ["Content-Type: " . $content_type]); + $postResult = DI::httpRequest()->post($dest_url . "/", $envelope, ['Content-Type' => $content_type]); $return_code = $postResult->getReturnCode(); } else { Logger::log("test_mode"); diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index 215c08fa08..5a0135f9cb 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -727,7 +727,7 @@ class OStatus self::$conv_list[$conversation] = true; - $curlResult = DI::httpRequest()->get($conversation, ['accept_content' => 'application/atom+xml, text/html']); + $curlResult = DI::httpRequest()->get($conversation, ['accept_content' => ['application/atom+xml', 'text/html']]); if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { return; @@ -921,7 +921,7 @@ class OStatus } $stored = false; - $curlResult = DI::httpRequest()->get($related, ['accept_content' => 'application/atom+xml, text/html']); + $curlResult = DI::httpRequest()->get($related, ['accept_content' => ['application/atom+xml', 'text/html']]); if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { return; diff --git a/src/Protocol/Salmon.php b/src/Protocol/Salmon.php index 53367f6d09..8d17f96783 100644 --- a/src/Protocol/Salmon.php +++ b/src/Protocol/Salmon.php @@ -156,8 +156,8 @@ class Salmon // slap them $postResult = DI::httpRequest()->post($url, $salmon, [ - 'Content-type: application/magic-envelope+xml', - 'Content-length: ' . strlen($salmon) + 'Content-type' => 'application/magic-envelope+xml', + 'Content-length' => strlen($salmon), ]); $return_code = $postResult->getReturnCode(); @@ -181,8 +181,8 @@ class Salmon // slap them $postResult = DI::httpRequest()->post($url, $salmon, [ - 'Content-type: application/magic-envelope+xml', - 'Content-length: ' . strlen($salmon) + 'Content-type' => 'application/magic-envelope+xml', + 'Content-length' => strlen($salmon), ]); $return_code = $postResult->getReturnCode(); } @@ -204,8 +204,8 @@ class Salmon // slap them $postResult = DI::httpRequest()->post($url, $salmon, [ - 'Content-type: application/magic-envelope+xml', - 'Content-length: ' . strlen($salmon)]); + 'Content-type' => 'application/magic-envelope+xml', + 'Content-length' => strlen($salmon)]); $return_code = $postResult->getReturnCode(); } diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index e2de810a60..eab778b820 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -29,6 +29,7 @@ use Friendica\Model\APContact; use Friendica\Model\Contact; use Friendica\Model\User; use Friendica\Network\CurlResult; +use Friendica\Network\IHTTPResult; /** * Implements HTTP Signatures per draft-cavage-http-signatures-07. @@ -139,6 +140,9 @@ class HTTPSignature public static function createSig($head, $prvkey, $keyid = 'Key') { $return_headers = []; + if (!empty($head)) { + $return_headers = $head; + } $alg = 'sha512'; $algorithm = 'rsa-sha512'; @@ -148,15 +152,7 @@ class HTTPSignature $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"'; - $sighead = 'Authorization: Signature ' . $headerval; - - if ($head) { - foreach ($head as $k => $v) { - $return_headers[] = $k . ': ' . $v; - } - } - - $return_headers[] = $sighead; + $return_headers['Authorization'] = ['Signature ' . $headerval]; return $return_headers; } @@ -175,6 +171,9 @@ class HTTPSignature $fields = ''; foreach ($head as $k => $v) { + if (is_array($v)) { + $v = implode(', ', $v); + } $headers .= strtolower($k) . ': ' . trim($v) . "\n"; if ($fields) { $fields .= ' '; @@ -290,15 +289,20 @@ class HTTPSignature $content_length = strlen($content); $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); - $headers = ['Date: ' . $date, 'Content-Length: ' . $content_length, 'Digest: ' . $digest, 'Host: ' . $host]; + $headers = [ + 'Date' => $date, + 'Content-Length' => $content_length, + 'Digest' => $digest, + 'Host' => $host + ]; $signed_data = "(request-target): post " . $path . "\ndate: ". $date . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date content-length digest host",signature="' . $signature . '"'; + $headers['Signature'] = 'keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date content-length digest host",signature="' . $signature . '"'; - $headers[] = 'Content-Type: application/activity+json'; + $headers['Content-Type'] = 'application/activity+json'; $postResult = DI::httpRequest()->post($target, $content, $headers); $return_code = $postResult->getReturnCode(); @@ -409,10 +413,10 @@ class HTTPSignature * 'nobody' => only return the header * 'cookiejar' => path to cookie jar file * - * @return CurlResult CurlResult + * @return IHTTPResult CurlResult * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function fetchRaw($request, $uid = 0, $opts = ['accept_content' => 'application/activity+json, application/ld+json']) + public static function fetchRaw($request, $uid = 0, $opts = ['accept_content' => ['application/activity+json', 'application/ld+json']]) { $header = []; @@ -434,17 +438,14 @@ class HTTPSignature $path = parse_url($request, PHP_URL_PATH); $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); - $header = ['Date: ' . $date, 'Host: ' . $host]; + $header['Date'] = $date; + $header['Host'] = $host; $signed_data = "(request-target): get " . $path . "\ndate: ". $date . "\nhost: " . $host; $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - $header[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date host",signature="' . $signature . '"'; - } - - if (!empty($opts['accept_content'])) { - $header[] = 'Accept: ' . $opts['accept_content']; + $header['Signature'] = 'keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date host",signature="' . $signature . '"'; } $curl_opts = $opts; diff --git a/src/Worker/PubSubPublish.php b/src/Worker/PubSubPublish.php index a5381c18d1..a3e2ee4ad0 100644 --- a/src/Worker/PubSubPublish.php +++ b/src/Worker/PubSubPublish.php @@ -59,11 +59,12 @@ class PubSubPublish $hmac_sig = hash_hmac("sha1", $params, $subscriber['secret']); - $headers = ["Content-type: application/atom+xml", - sprintf("Link: <%s>;rel=hub,<%s>;rel=self", + $headers = [ + 'Content-type' => 'application/atom+xml', + 'Link' => sprintf("<%s>;rel=hub,<%s>;rel=self", DI::baseUrl() . '/pubsubhubbub/' . $subscriber['nickname'], $subscriber['topic']), - "X-Hub-Signature: sha1=" . $hmac_sig]; + 'X-Hub-Signature' => 'sha1=' . $hmac_sig]; Logger::log('POST ' . print_r($headers, true) . "\n" . $params, Logger::DATA); diff --git a/static/dependencies.config.php b/static/dependencies.config.php index f07a61807e..2068b6b1ad 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -220,8 +220,11 @@ return [ ['getBackend', [], Dice::CHAIN_CALL], ], ], - Network\IHTTPRequest::class => [ - 'instanceOf' => Network\HTTPRequest::class, + Network\IHTTPClient::class => [ + 'instanceOf' => Factory\HTTPClientFactory::class, + 'call' => [ + ['createClient', [], Dice::CHAIN_CALL], + ], ], Factory\Api\Mastodon\Error::class => [ 'constructParams' => [ @@ -232,5 +235,5 @@ return [ 'constructParams' => [ [Dice::INSTANCE => Util\ReversedFileReader::class], ] - ] + ], ]; diff --git a/tests/DiceHttpMockHandlerTrait.php b/tests/DiceHttpMockHandlerTrait.php new file mode 100644 index 0000000000..969b76b5b0 --- /dev/null +++ b/tests/DiceHttpMockHandlerTrait.php @@ -0,0 +1,68 @@ +. + * + */ + +namespace Friendica\Test; + +use Dice\Dice; +use Friendica\DI; +use Friendica\Factory\HTTPClientFactory; +use Friendica\Network\IHTTPClient; +use GuzzleHttp\HandlerStack; + +/** + * This class injects a mockable handler into the IHTTPClient dependency per Dice + */ +trait DiceHttpMockHandlerTrait +{ + /** + * Handler for mocking requests anywhere for testing purpose + * + * @var HandlerStack + */ + protected $httpRequestHandler; + + protected function setupHttpMockHandler(): void + { + if (!empty($this->httpRequestHandler) && $this->httpRequestHandler instanceof HandlerStack) { + return; + } + + $this->httpRequestHandler = HandlerStack::create(); + + $dice = DI::getDice(); + // addRule() clones the current instance and returns a new one, so no concurrency problems :-) + $newDice = $dice->addRule(IHTTPClient::class, [ + 'instanceOf' => HTTPClientFactory::class, + 'call' => [ + ['createClient', [$this->httpRequestHandler], Dice::CHAIN_CALL], + ], + ]); + + DI::init($newDice); + } + + protected function tearDown(): void + { + \Mockery::close(); + + parent::tearDown(); + } +} diff --git a/tests/datasets/curl/image.content b/tests/datasets/curl/image.content new file mode 100644 index 0000000000..98285c3e11 Binary files /dev/null and b/tests/datasets/curl/image.content differ diff --git a/tests/src/Core/InstallerTest.php b/tests/src/Core/InstallerTest.php index 8c72b7b2b9..1263fe7e67 100644 --- a/tests/src/Core/InstallerTest.php +++ b/tests/src/Core/InstallerTest.php @@ -26,7 +26,7 @@ use Dice\Dice; use Friendica\Core\Config\Cache; use Friendica\DI; use Friendica\Network\IHTTPResult; -use Friendica\Network\IHTTPRequest; +use Friendica\Network\IHTTPClient; use Friendica\Test\MockedTest; use Friendica\Test\Util\VFSTrait; use Mockery; @@ -331,7 +331,7 @@ class InstallerTest extends MockedTest ->andReturn('test Error'); // Mocking the CURL Request - $networkMock = Mockery::mock(IHTTPRequest::class); + $networkMock = Mockery::mock(IHTTPClient::class); $networkMock ->shouldReceive('fetchFull') ->with('https://test/install/testrewrite') @@ -342,7 +342,7 @@ class InstallerTest extends MockedTest ->andReturn($IHTTPResult); $this->dice->shouldReceive('create') - ->with(IHTTPRequest::class) + ->with(IHTTPClient::class) ->andReturn($networkMock); DI::init($this->dice); @@ -378,7 +378,7 @@ class InstallerTest extends MockedTest ->andReturn('204'); // Mocking the CURL Request - $networkMock = Mockery::mock(IHTTPRequest::class); + $networkMock = Mockery::mock(IHTTPClient::class); $networkMock ->shouldReceive('fetchFull') ->with('https://test/install/testrewrite') @@ -389,7 +389,7 @@ class InstallerTest extends MockedTest ->andReturn($IHTTPResultW); $this->dice->shouldReceive('create') - ->with(IHTTPRequest::class) + ->with(IHTTPClient::class) ->andReturn($networkMock); DI::init($this->dice); diff --git a/tests/src/Core/StorageManagerTest.php b/tests/src/Core/StorageManagerTest.php index 9e8e3aa2c2..93fc0b6648 100644 --- a/tests/src/Core/StorageManagerTest.php +++ b/tests/src/Core/StorageManagerTest.php @@ -34,7 +34,7 @@ use Friendica\Factory\ConfigFactory; use Friendica\Model\Config\Config; use Friendica\Model\Storage; use Friendica\Core\Session; -use Friendica\Network\HTTPRequest; +use Friendica\Network\HTTPClient; use Friendica\Test\DatabaseTest; use Friendica\Test\Util\Database\StaticDatabase; use Friendica\Test\Util\VFSTrait; @@ -55,7 +55,7 @@ class StorageManagerTest extends DatabaseTest private $logger; /** @var L10n */ private $l10n; - /** @var HTTPRequest */ + /** @var HTTPClient */ private $httpRequest; protected function setUp(): void @@ -84,7 +84,7 @@ class StorageManagerTest extends DatabaseTest $this->l10n = \Mockery::mock(L10n::class); - $this->httpRequest = \Mockery::mock(HTTPRequest::class); + $this->httpRequest = \Mockery::mock(HTTPClient::class); } /** diff --git a/tests/src/Network/ProbeTest.php b/tests/src/Network/ProbeTest.php index 58711cb916..79c323adc2 100644 --- a/tests/src/Network/ProbeTest.php +++ b/tests/src/Network/ProbeTest.php @@ -3,10 +3,21 @@ namespace Friendica\Test\src\Network; use Friendica\Network\Probe; -use PHPUnit\Framework\TestCase; +use Friendica\Test\DiceHttpMockHandlerTrait; +use Friendica\Test\FixtureTest; +use GuzzleHttp\Middleware; -class ProbeTest extends TestCase +class ProbeTest extends FixtureTest { + use DiceHttpMockHandlerTrait; + + protected function setUp(): void + { + parent::setUp(); + + $this->setupHttpMockHandler(); + } + const TEMPLATENOBASE = ' @@ -105,4 +116,122 @@ class ProbeTest extends TestCase } } } + + public function dataUri() + { + return [ + '@-first' => [ + 'uri' => '@Artists4Future_Muenchen@climatejustice.global', + 'assertUri' => 'Artists4Future_Muenchen@climatejustice.global', + 'assertInfos' => [ + 'name' => 'Artists4Future München', + 'nick' => 'Artists4Future_Muenchen', + 'url' => 'https://climatejustice.global/users/Artists4Future_Muenchen', + 'alias' => 'https://climatejustice.global/@Artists4Future_Muenchen', + 'photo' => 'https://cdn.masto.host/climatejusticeglobal/accounts/avatars/000/021/220/original/05ee9e827a5b47fc.jpg', + 'header' => 'https://cdn.masto.host/climatejusticeglobal/accounts/headers/000/021/220/original/9b98b75cf696cd11.jpg', + 'account-type' => 0, + 'about' => 'Wir sind Künstler oder einfach gerne kreativ tätig und setzen uns unabhängig von politischen Parteien für den Klimaschutz ein. Die Bedingungen zu schaffen, die die [url=https://climatejustice.global/tags/Klimakrise]#Klimakrise[/url] verhindern/eindämmen (gemäß den Forderungen der [url=https://climatejustice.global/tags/Fridays4Future]#Fridays4Future[/url]) ist Aufgabe der Politik, muss aber gesamtgesellschaftlich getragen werden. Mit unseren künstlerischen Aktionen wollen wir einen anderen Zugang anbieten für wissenschaftlich rationale Argumente, speziell zur Erderwärmung und ihre Konsequenzen.', + 'hide' => 0, + 'batch' => 'https://climatejustice.global/inbox', + 'notify' => 'https://climatejustice.global/users/Artists4Future_Muenchen/inbox', + 'poll' => 'https://climatejustice.global/users/Artists4Future_Muenchen/outbox', + 'subscribe' => 'https://climatejustice.global/authorize_interaction?uri={uri}', + 'following' => 'https://climatejustice.global/users/Artists4Future_Muenchen/following', + 'followers' => 'https://climatejustice.global/users/Artists4Future_Muenchen/followers', + 'inbox' => 'https://climatejustice.global/users/Artists4Future_Muenchen/inbox', + 'outbox' => 'https://climatejustice.global/users/Artists4Future_Muenchen/outbox', + 'sharedinbox' => 'https://climatejustice.global/inbox', + 'priority' => 0, + 'network' => 'apub', + 'pubkey' => '-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6pYKPuDKb+rmBB869uPV +uLYFPosGxMUfenWqfWmFKzEqJ87rAft0IQDAL6dCoYE55ov/lEDNROhasTZLirZf +M5b7/1JmwMrAfEiaciuYqDWT3/yDpnekOIdzP5iSClg4zt7e6HRFuClqo4+b6hIE +DTMV4ksItvq/92MIu62pZ2SZr5ADPPZ/914lJ86hIH5BanbE8ZFzDS9vJA7V74rt +Vvkr5c/OiUyuODNYApSl87Ez8cuj8Edt89YWkDCajQn3EkmXGeJY/VRjEDfcyk6r +AvdUa0ArjXud3y3NkakVFZ0d7tmB20Vn9s/CfYHU8FXzbI1kFkov2BX899VVP5Ay +xQIDAQAB +-----END PUBLIC KEY-----', + 'manually-approve' => 0, + 'baseurl' => 'https://climatejustice.global', + ] + ] + ]; + } + + /** + * @dataProvider dataUri + */ + public function testCleanUri(string $uri, string $assertUri, array $assertInfos) + { + self::markTestIncomplete('hard work due mocking 19 different http-requests'); + + /** + * Requests: + * + * GET : https://climatejustice.global/.well-known/webfinger?resource=acct:Artists4Future_Muenchen%40climatejustice.global + * 200 + * GET : http://localhost/.well-known/nodeinfo + * 200 + * GET : http://localhost/statistics.json + * 404 + * GET : http://localhost + * 200 + * GET : http://localhost/friendica/json + * 404 + * GET : http://localhost/friendika/json + * 404 + * GET : http://localhost/poco + * 403 + * GET : http://localhost/api/v1/directory?limit=1 + * 200 + * GET : http://localhost/.well-known/x-social-relay + * 200 + * GET : http://localhost/friendica + * 404 + * GET : https://climatejustice.global/users/Artists4Future_Muenchen + * 200 + * GET : https://climatejustice.global/users/Artists4Future_Muenchen/following + * 200 + * GET : https://climatejustice.global/users/Artists4Future_Muenchen/followers + * 200 + * GET : https://climatejustice.global/users/Artists4Future_Muenchen/outbox + * 200 + * GET : https://climatejustice.global/.well-known/nodeinfo + * 200 + * GET : https://climatejustice.global/nodeinfo/2.0 + * 200 + * GET : https://climatejustice.global/poco + * 404 + * GET : https://climatejustice.global/api/v1/directory?limit=1 + * 200 + * GET : https://climatejustice.global/.well-known/webfinger?resource=acct%3AArtists4Future_Muenchen%40climatejustice.global + * 200 + * + */ + + $container = []; + $history = Middleware::history($container); + + $this->httpRequestHandler->push($history); + + $cleaned = Probe::cleanURI($uri); + self::assertEquals($assertUri, $cleaned); + self::assertArraySubset($assertInfos, Probe::uri($cleaned, '', 0)); + + + // Iterate over the requests and responses + foreach ($container as $transaction) { + echo $transaction['request']->getMethod() . " : " . $transaction['request']->getUri() . PHP_EOL; + //> GET, HEAD + if ($transaction['response']) { + echo $transaction['response']->getStatusCode() . PHP_EOL; + //> 200, 200 + } elseif ($transaction['error']) { + echo $transaction['error']; + //> exception + } + } + } } diff --git a/tests/src/Util/HTTPSignatureTest.php b/tests/src/Util/HTTPSignatureTest.php index 440e1e8db9..a2d1389759 100644 --- a/tests/src/Util/HTTPSignatureTest.php +++ b/tests/src/Util/HTTPSignatureTest.php @@ -29,9 +29,11 @@ use PHPUnit\Framework\TestCase; */ class HTTPSignatureTest extends TestCase { - public function testParseSigheader() + public function dataParseSigned() { - $header = 'keyId="test-key-a", algorithm="hs2019", + return [ + 'signed1' => [ + 'header' => 'keyId="test-key-a", algorithm="hs2019", created=1402170695, headers="(request-target) (created) host date content-type digest content-length", @@ -40,16 +42,111 @@ class HTTPSignatureTest extends TestCase XVsM9jy+Q1+RIlD9wfWoPHhqhoXt35ZkasuIDPF/AETuObs9QydlsqONwbK+T dQguDK/8Va1Pocl6wK1uLwqcXlxhPEb55EmdYB9pddDyHTADING7K4qMwof2m C3t8Pb0yoLZoZX5a4Or4FrCCKK/9BHAhq/RsVk0dTENMbTB4i7cHvKQu+o9xu - YWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg=="'; + YWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg=="', + 'assertion' => [ + 'keyId' => 'test-key-a', + 'algorithm' => 'hs2019', + 'created' => '1402170695', + 'expires' => null, + 'headers' => ['(request-target)', '(created)', 'host', 'date', 'content-type', 'digest', 'content-length'], + 'signature' => base64_decode('KXUj1H3ZOhv3Nk4xlRLTn4bOMlMOmFiud3VXrMa9MaLCxnVmrqOX5BulRvB65YW/wQp0oT/nNQpXgOYeY8ovmHlpkRyz5buNDqoOpRsCpLGxsIJ9cX8XVsM9jy+Q1+RIlD9wfWoPHhqhoXt35ZkasuIDPF/AETuObs9QydlsqONwbK+TdQguDK/8Va1Pocl6wK1uLwqcXlxhPEb55EmdYB9pddDyHTADING7K4qMwof2mC3t8Pb0yoLZoZX5a4Or4FrCCKK/9BHAhq/RsVk0dTENMbTB4i7cHvKQu+o9xuYWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg=='), + ], + ], + 'signed2' => [ + 'signature' => 'Signature keyId="acct:admin@friendica.local",algorithm="rsa-sha512",created=1402170695,headers="accept x-open-web-auth",signature="lZzgtUOtyko/ZUjTRq6kUye6VmLbF2Zrku9TMl+9LEEdt7rxXOd+jpRr8L0s0G0oYSrbMzArgnk5S2XAz1XLi6GhoEgpyKJNmpnYUc/GvD86k0Cmb12TQF7B4Zv8k6h5LLCHppMi5myNIqQ95mqzVeWewCTgUupZVaqJxUAve1Uyx12jlzfYo6HAprXHhZKbLSAVfg+qiS8ufVp4coCYguioSMtMd4QvCFtAO3rFGwZ5yizXmPiKjhLQqxoy15+1VifTeAy8KJ+nPy+XeakpiYTfbPuCvaDqAMqboFx+J5epKS7M2T581oIgpSmQpHpkfCU8AT/JJ99Kktyzfn+ctK3Q8bKSjZQOT9S66S1b04jvFMggDM7p8Zu+Nr/SaS7ro5Yr7Fc200CM7yoRvef7MKQ7o0HASP8sMQwtSB4k+NlNBLUu9Eo/6P16bzx27cg3yGyuh15qmU8dL9heHqBL+E/m96BTZKIB5D81TkO/m0MpvJIxR0NfS9qKFcCAoev8kfcGcnVxtcxuCys7M/iW4ykJoMhFJDnsfN7mygOledeTmug8ARm0WpxUhtoNqhQrDXjAGbuPnQ1B/fPNwKVrHdFa2i/va0kgwa5+yh2ACqyMfrKCSkv2Prni3iKTKs8W0o/bF6FFeSqPsCGxneSMoYx3FOaSy6ts2gyuAwEozSg="', + 'assertion' => [ + 'keyId' => 'acct:admin@friendica.local', + 'algorithm' => 'rsa-sha512', + 'created' => '1402170695', + 'expires' => null, + 'headers' => ['accept', 'x-open-web-auth'], + 'signature' => base64_decode('lZzgtUOtyko/ZUjTRq6kUye6VmLbF2Zrku9TMl+9LEEdt7rxXOd+jpRr8L0s0G0oYSrbMzArgnk5S2XAz1XLi6GhoEgpyKJNmpnYUc/GvD86k0Cmb12TQF7B4Zv8k6h5LLCHppMi5myNIqQ95mqzVeWewCTgUupZVaqJxUAve1Uyx12jlzfYo6HAprXHhZKbLSAVfg+qiS8ufVp4coCYguioSMtMd4QvCFtAO3rFGwZ5yizXmPiKjhLQqxoy15+1VifTeAy8KJ+nPy+XeakpiYTfbPuCvaDqAMqboFx+J5epKS7M2T581oIgpSmQpHpkfCU8AT/JJ99Kktyzfn+ctK3Q8bKSjZQOT9S66S1b04jvFMggDM7p8Zu+Nr/SaS7ro5Yr7Fc200CM7yoRvef7MKQ7o0HASP8sMQwtSB4k+NlNBLUu9Eo/6P16bzx27cg3yGyuh15qmU8dL9heHqBL+E/m96BTZKIB5D81TkO/m0MpvJIxR0NfS9qKFcCAoev8kfcGcnVxtcxuCys7M/iW4ykJoMhFJDnsfN7mygOledeTmug8ARm0WpxUhtoNqhQrDXjAGbuPnQ1B/fPNwKVrHdFa2i/va0kgwa5+yh2ACqyMfrKCSkv2Prni3iKTKs8W0o/bF6FFeSqPsCGxneSMoYx3FOaSy6ts2gyuAwEozSg='), + ] + ] + ]; + } - $headers = HTTPSignature::parseSigheader($header); - self::assertSame([ - 'keyId' => 'test-key-a', - 'algorithm' => 'hs2019', - 'created' => '1402170695', - 'expires' => null, - 'headers' => ['(request-target)', '(created)', 'host', 'date', 'content-type', 'digest', 'content-length'], - 'signature' => base64_decode('KXUj1H3ZOhv3Nk4xlRLTn4bOMlMOmFiud3VXrMa9MaLCxnVmrqOX5BulRvB65YW/wQp0oT/nNQpXgOYeY8ovmHlpkRyz5buNDqoOpRsCpLGxsIJ9cX8XVsM9jy+Q1+RIlD9wfWoPHhqhoXt35ZkasuIDPF/AETuObs9QydlsqONwbK+TdQguDK/8Va1Pocl6wK1uLwqcXlxhPEb55EmdYB9pddDyHTADING7K4qMwof2mC3t8Pb0yoLZoZX5a4Or4FrCCKK/9BHAhq/RsVk0dTENMbTB4i7cHvKQu+o9xuYWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg=='), - ], $headers); + public function dataHeader() + { + return [ + 'signed' => [ + 'privKey' => '-----BEGIN PRIVATE KEY----- +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 +G1vVmRgkLDqhc4+r3wDz3qy6JpV7tg== +-----END PRIVATE KEY-----', + 'keyId' => 'acct:admin@friendica.local', + 'header' => [ + 'Accept' => ['application/x-dfrn+json', 'application/x-zot+json'], + 'X-Open-Web-Auth' => ['1dde649b855fd1aae542a91c4edd8c3a7a4c59d8eaf3136cdee05dfc16a30bac'], + ], + 'signature' => 'Signature keyId="acct:admin@friendica.local",algorithm="rsa-sha512",headers="accept x-open-web-auth",signature="cb09/wdmRdFhrQUczL0lR6LTkVh8qb/vYh70DFCW40QrzvuUYHzJ+GqqJW6tcCb2rXP4t+aobWKfZ4wFMBtVbejAiCgF01pzEBJfDevdyu7khlfKo+Gtw1CGk4/0so1QmqASeHdlG3ID3GnmuovqZn2v5f5D+1UAJ6Pu6mtAXrGRntoEM/oqYBAsRrMtDSDAE4tnOSDu2YfVJJLfyUX1ZWjUK0ejZXZ0YTDJwU8PqRcRX17NhBaDq82dEHlfhD7I/aOclwVbfKIi5Ud619XCxPq0sAAYso17MhUm40oBCJVze2x4pswJhv9IFYohtR5G/fKkz2eosu3xeRflvGicS4JdclhTRgCGWDy10DV76FiXiS5N2clLXreHItWEXlZKZx6m9zAZoEX92VRnc4BY4jDsRR89Pl88hELaxiNipviyHS7XYZTRnkLM+nvtOcxkHSCbEs7oGw+AX+pLHoU5otNOqy+ZbXQ1cUvFOBYZmYdX3DiWaLfBKraakPkslJuU3yJu95X1qYmQTpOZDR4Ma/yf5fmWJh5D9ywnXxxd6RaupoO6HTtIl6gmsfcsyZNi5hRbbgPI3BiQwGYVGF6qzJpEOMzEyHyAuFeanhicc8b+P+DCwXni5sjM7ntKwShbCBP80KHSdoumORin3/PYgHCmHZVv71N0HNlPZnyVzZw="', + ] + ]; + } + + /** + * @dataProvider dataParseSigned + */ + public function testParseSigheader(string $signature, array $assertion) + { + $headers = HTTPSignature::parseSigheader($signature); + self::assertEquals($assertion, $headers); + } + + /** + * @dataProvider dataHeader + */ + public function testSignHeader(string $privKey, string $keyId, array $header, string $signature) + { + $signed = HTTPSignature::createSig($header, $privKey, $keyId); + self::assertEquals($signature, $signed['Authorization'][0]); } } diff --git a/tests/src/Util/ImagesTest.php b/tests/src/Util/ImagesTest.php new file mode 100644 index 0000000000..ddadf9547f --- /dev/null +++ b/tests/src/Util/ImagesTest.php @@ -0,0 +1,71 @@ +setupHttpMockHandler(); + } + + public function dataImages() + { + return [ + 'image' => [ + 'url' => 'https://pbs.twimg.com/profile_images/2365515285/9re7kx4xmc0eu9ppmado.png', + 'headers' => [ + 'Server' => 'tsa_b', + 'Content-Type' => 'image/png', + 'Cache-Control' => 'max-age=604800,must-revalidate', + 'Last-Modified' => 'Thu,04Nov201001:42:54GMT', + 'Content-Length' => '24875', + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Expose-Headers' => 'Content-Length', + 'Date' => 'Mon,23Aug202112:39:00GMT', + 'Connection' => 'keep-alive', + ], + 'data' => file_get_contents(__DIR__ . '/../../datasets/curl/image.content'), + 'assertion' => [ + '0' => '400', + '1' => '400', + '2' => '3', + '3' => 'width="400" height="400"', + 'bits' => '8', + 'mime' => 'image/png', + 'size' => '24875', + ] + ], + 'emptyUrl' => [ + 'url' => '', + 'headers' => [], + 'data' => '', + 'assertion' => [], + ], + ]; + } + + /** + * Test the Images::getInfoFromURL() method (only remote images, not local/relative!) + * + * @dataProvider dataImages + */ + public function testGetInfoFromRemotURL(string $url, array $headers, string $data, array $assertion) + { + $this->httpRequestHandler->setHandler(new MockHandler([ + new Response(200, $headers, $data), + ])); + + self::assertArraySubset($assertion, Images::getInfoFromURL($url)); + } +}