Merge pull request #10633 from nupplaphil/task/httprequest_optimiziations
PSR-7 Part 2: Optimize HTTPRequest
This commit is contained in:
commit
cf0b7b709b
27 changed files with 876 additions and 594 deletions
|
@ -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
48
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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'])) {
|
||||
|
|
15
src/DI.php
15
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);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
110
src/Factory/HTTPClientFactory.php
Normal file
110
src/Factory/HTTPClientFactory.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
257
src/Network/HTTPClient.php
Normal 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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 [];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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],
|
||||
]
|
||||
]
|
||||
],
|
||||
];
|
||||
|
|
68
tests/DiceHttpMockHandlerTrait.php
Normal file
68
tests/DiceHttpMockHandlerTrait.php
Normal 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();
|
||||
}
|
||||
}
|
BIN
tests/datasets/curl/image.content
Normal file
BIN
tests/datasets/curl/image.content
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
71
tests/src/Util/ImagesTest.php
Normal file
71
tests/src/Util/ImagesTest.php
Normal 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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue