Merge pull request #10633 from nupplaphil/task/httprequest_optimiziations

PSR-7 Part 2: Optimize HTTPRequest
This commit is contained in:
Hypolite Petovan 2021-08-25 12:54:59 -04:00 committed by GitHub
commit cf0b7b709b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 876 additions and 594 deletions

View file

@ -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": [
{

48
composer.lock generated
View file

@ -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",

View file

@ -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'])) {

View file

@ -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);
}
//

View file

@ -0,0 +1,110 @@
<?php
namespace Friendica\Factory;
use Friendica\App;
use Friendica\BaseFactory;
use Friendica\Core\Config\IConfig;
use Friendica\Network\HTTPClient;
use Friendica\Network\IHTTPClient;
use Friendica\Util\Profiler;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\RequestOptions;
use mattwright\URLResolver;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;
class HTTPClientFactory extends BaseFactory
{
/** @var IConfig */
private $config;
/** @var Profiler */
private $profiler;
/** @var App\BaseURL */
private $baseUrl;
public function __construct(LoggerInterface $logger, IConfig $config, Profiler $profiler, App\BaseURL $baseUrl)
{
parent::__construct($logger);
$this->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);
}
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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(

View file

@ -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)) {

257
src/Network/HTTPClient.php Normal file
View file

@ -0,0 +1,257 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU APGL 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 <https://www.gnu.org/licenses/>.
*
*/
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
]
);
}
}

View file

@ -1,501 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU APGL 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 <https://www.gnu.org/licenses/>.
*
*/
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;
}
}

View file

@ -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);
}

View file

@ -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 [];

View file

@ -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();

View file

@ -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");

View file

@ -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;

View file

@ -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();
}

View file

@ -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;

View file

@ -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);

View file

@ -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],
]
]
],
];

View file

@ -0,0 +1,68 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -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);

View file

@ -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);
}
/**

View file

@ -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 = '
<!DOCTYPE html>
<html lang="en-us">
@ -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
}
}
}
}

View file

@ -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]);
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Friendica\Test\src\Util;
use Friendica\Test\DiceHttpMockHandlerTrait;
use Friendica\Test\MockedTest;
use Friendica\Util\Images;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Response;
class ImagesTest extends MockedTest
{
use DiceHttpMockHandlerTrait;
protected function setUp(): void
{
parent::setUp();
$this->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));
}
}