From a876c208504af3ee801689886ec8cab8f3eeff00 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 18 Mar 2019 22:33:20 +0000 Subject: [PATCH] Use HTTP-Signature to authenticate when fetching photos. --- src/Module/Photo.php | 3 ++ src/Module/Proxy.php | 42 +++++++++----------- src/Util/HTTPSignature.php | 80 +++++++++++++++++++++++++++----------- 3 files changed, 80 insertions(+), 45 deletions(-) diff --git a/src/Module/Photo.php b/src/Module/Photo.php index 15ea261fb0..f5bbf4a774 100644 --- a/src/Module/Photo.php +++ b/src/Module/Photo.php @@ -45,6 +45,9 @@ class Photo extends BaseModule exit; } + /// @todo Add Authentication to enable fetching of non public content + // $requester = HTTPSignature::getSigner('', $_SERVER); + $customsize = 0; $photo = false; switch($a->argc) { diff --git a/src/Module/Proxy.php b/src/Module/Proxy.php index 1c980fe11e..54870abe05 100644 --- a/src/Module/Proxy.php +++ b/src/Module/Proxy.php @@ -10,8 +10,9 @@ use Friendica\Core\L10n; use Friendica\Core\System; use Friendica\Model\Photo; use Friendica\Object\Image; -use Friendica\Util\Network; +use Friendica\Util\HTTPSignature; use Friendica\Util\Proxy as ProxyUtils; +use Friendica\Core\Logger; /** * @brief Module Proxy @@ -81,38 +82,35 @@ class Proxy extends BaseModule // Try to use photo from db self::responseFromDB($request); - // // If script is here, the requested url has never cached before. // Let's fetch it, scale it if required, then save it in cache. // - // It shouldn't happen but it does - spaces in URL $request['url'] = str_replace(' ', '+', $request['url']); - $redirects = 0; - $fetchResult = Network::fetchUrlFull($request['url'], true, $redirects, 10); + $fetchResult = HTTPSignature::fetchRaw($request['url'], local_user(), true, ['timeout' => 10]); $img_str = $fetchResult->getBody(); - $tempfile = tempnam(get_temppath(), 'cache'); - file_put_contents($tempfile, $img_str); - $mime = mime_content_type($tempfile); - unlink($tempfile); - // If there is an error then return a blank image if ((substr($fetchResult->getReturnCode(), 0, 1) == '4') || (!$img_str)) { self::responseError(); // stop. } + $tempfile = tempnam(get_temppath(), 'cache'); + file_put_contents($tempfile, $img_str); + $mime = mime_content_type($tempfile); + unlink($tempfile); + $image = new Image($img_str, $mime); if (!$image->isValid()) { self::responseError(); // stop. } - + $basepath = $a->getBasePath(); - + // Store original image if ($direct_cache) { // direct cache , store under ./proxy/ @@ -159,8 +157,7 @@ class Proxy extends BaseModule $a = self::getApp(); $size = 1024; $sizetype = ''; - - + // Look for filename in the arguments if (($a->argc > 1) && !isset($_REQUEST['url'])) { if (isset($a->argv[3])) { @@ -211,7 +208,7 @@ class Proxy extends BaseModule } else { $url = defaults($_REQUEST, 'url', ''); } - + return [ 'url' => $url, 'urlhash' => 'pic:' . sha1($url), @@ -239,9 +236,9 @@ class Proxy extends BaseModule // Checking if caching into a folder in the webroot is activated and working $direct_cache = (is_dir($basepath . '/proxy') && is_writable($basepath . '/proxy')); - // we don't use direct cache if image url is passed in args and not in querystring + // we don't use direct cache if image url is passed in args and not in querystring $direct_cache = $direct_cache && ($a->argc > 1) && !isset($_REQUEST['url']); - + return $direct_cache; } @@ -277,8 +274,8 @@ class Proxy extends BaseModule * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function responseFromDB(&$request) { - + private static function responseFromDB(&$request) + { $photo = Photo::getPhoto($request['urlhash']); if ($photo !== false) { @@ -287,12 +284,13 @@ class Proxy extends BaseModule // stop. } } - + /** * @brief Output a blank image, without cache headers, in case of errors * */ - private static function responseError() { + private static function responseError() + { header('Content-type: image/png'); echo file_get_contents('images/blank.png'); exit(); @@ -319,5 +317,3 @@ class Proxy extends BaseModule exit(); } } - - diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index a3a73ce136..ba44bcc80b 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -328,43 +328,79 @@ class HTTPSignature */ public static function fetch($request, $uid) { - $owner = User::getOwnerDataById($uid); + $opts = ['accept_content' => 'application/activity+json, application/ld+json']; + $curlResult = self::fetchRaw($request, $uid, false, $opts); - if (!$owner) { - return; + if (empty($curlResult)) { + return false; } - // Header data that is about to be signed. - $host = parse_url($request, PHP_URL_HOST); - $path = parse_url($request, PHP_URL_PATH); - $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); + if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { + return false; + } - $headers = ['Date: ' . $date, 'Host: ' . $host]; + $content = json_decode($curlResult->getBody(), true); + if (empty($content) || !is_array($content)) { + return false; + } - $signed_data = "(request-target): get " . $path . "\ndate: ". $date . "\nhost: " . $host; + return $content; + } - $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); + /** + * @brief Fetches raw data for a user + * + * @param string $request request url + * @param integer $uid User id of the requester + * @param boolean $binary TRUE if asked to return binary results (file download) (default is "false") + * @param array $opts (optional parameters) assoziative array with: + * 'accept_content' => supply Accept: header with 'accept_content' as the value + * 'timeout' => int Timeout in seconds, default system config value or 60 seconds + * 'http_auth' => username:password + * 'novalidate' => do not validate SSL certs, default is to validate using our CA list + * 'nobody' => only return the header + * 'cookiejar' => path to cookie jar file + * + * @return object CurlResult + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function fetchRaw($request, $uid = 0, $binary = false, $opts = []) + { + if (!empty($uid)) { + $owner = User::getOwnerDataById($uid); + if (!$owner) { + return; + } - $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date host",signature="' . $signature . '"'; + // Header data that is about to be signed. + $host = parse_url($request, PHP_URL_HOST); + $path = parse_url($request, PHP_URL_PATH); + $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); - $headers[] = 'Accept: application/activity+json, application/ld+json'; + $headers = ['Date: ' . $date, 'Host: ' . $host]; - $curlResult = Network::curl($request, false, $redirects, ['header' => $headers]); - $return_code = $curlResult->getReturnCode(); + $signed_data = "(request-target): get " . $path . "\ndate: ". $date . "\nhost: " . $host; - Logger::log('Fetched for user ' . $uid . ' from ' . $request . ' returned ' . $return_code, Logger::DEBUG); + $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { - return false; + $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date host",signature="' . $signature . '"'; + } else { + $headers = []; } - $content = json_decode($curlResult->getBody(), true); - - if (empty($content) || !is_array($content)) { - return false; + if (!empty($opts['accept_content'])) { + $headers[] = 'Accept: ' . $opts['accept_content']; } - return $content; + $curl_opts = $opts; + $curl_opts['header'] = $headers; + + $curlResult = Network::curl($request, false, $redirects, $curl_opts); + $return_code = $curlResult->getReturnCode(); + + Logger::log('Fetched for user ' . $uid . ' from ' . $request . ' returned ' . $return_code, Logger::DEBUG); + + return $curlResult; } /** -- 2.39.5