From 2f2f4c135f182cc70b3810275537e3f160423614 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sun, 10 Mar 2024 14:38:48 -0700 Subject: [PATCH] use signed gets for all parts of activitypub fetching including author profile info and likes/reposts --- lib/XRay.php | 7 +++++-- lib/XRay/Fetcher.php | 18 +++++++++++++++--- lib/XRay/Formats/ActivityStreams.php | 24 +++++++++++++++++++++--- lib/XRay/Parser.php | 10 +++++++++- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/lib/XRay.php b/lib/XRay.php index da1c687..0d7928f 100644 --- a/lib/XRay.php +++ b/lib/XRay.php @@ -30,11 +30,14 @@ class XRay { public function parse($url, $opts_or_body=false, $opts_for_body=[]) { if(!$opts_or_body || is_array($opts_or_body)) { $fetch = new XRay\Fetcher($this->http); - if (is_array($opts_or_body)) { + if(is_array($opts_or_body)) { $fetch_opts = array_merge($this->defaultOptions, $opts_or_body); } else { $fetch_opts = $this->defaultOptions; } + if(is_array($fetch_opts) && isset($fetch_opts['httpsig'])) { + $fetch->httpsig($fetch_opts['httpsig']); + } $response = $fetch->fetch($url, $fetch_opts); if(!empty($response['error'])) return $response; @@ -47,7 +50,7 @@ class XRay { $opts = $opts_for_body; $code = null; } - $parser = new XRay\Parser($this->http); + $parser = new XRay\Parser($this->http, $fetch); // Merge provided options with default options, allowing provided options to override defaults. $opts = array_merge($this->defaultOptions, $opts); diff --git a/lib/XRay/Fetcher.php b/lib/XRay/Fetcher.php index 55a6de0..1bb8d9e 100644 --- a/lib/XRay/Fetcher.php +++ b/lib/XRay/Fetcher.php @@ -6,12 +6,17 @@ use DateTime; class Fetcher { private $http; + private $httpsig; use HTTPSig; public function __construct($http) { $this->http = $http; } + + public function httpsig($key) { + $this->httpsig = $key; + } public function fetch($url, $opts=[]) { if($opts == false) $opts = []; @@ -66,7 +71,7 @@ class Fetcher { $headers = []; - if(isset($opts['httpsig'])) { + if($this->httpsig) { // If we're making a signed GET, include the default headers that mastodon requires as part of the signature $date = new DateTime('UTC'); $date = $date->format('D, d M Y H:i:s \G\M\T'); @@ -92,8 +97,7 @@ class Fetcher { $headers['Authorization'] = 'Bearer ' . $opts['token']; if(isset($opts['httpsig'])) { - echo "Signing HTTP Request\n"; - $this->_httpSign($headers, $opts['httpsig']); + $this->_httpSign($headers, $this->httpsig); } $result = $this->http->get($url, $this->_headersToCurlArray($headers)); @@ -176,6 +180,14 @@ class Fetcher { 'code' => $result['code'], ]; } + + public function signed_get($url, $headers) { + $date = new DateTime('UTC'); + $date = $date->format('D, d M Y H:i:s \G\M\T'); + $headers = array_merge($headers, $this->_headersToSign($url, $date)); + $this->_httpSign($headers, $this->httpsig); + return $this->http->get($url, $this->_headersToCurlArray($headers)); + } private function _fetch_github($url, $opts) { $fields = ['github_access_token']; diff --git a/lib/XRay/Formats/ActivityStreams.php b/lib/XRay/Formats/ActivityStreams.php index 06bcf4e..a636683 100644 --- a/lib/XRay/Formats/ActivityStreams.php +++ b/lib/XRay/Formats/ActivityStreams.php @@ -5,6 +5,8 @@ use DateTime; use \p3k\XRay\PostType; class ActivityStreams extends Format { + + private static $fetcher; public static function is_as2_json($document) { if(is_array($document) && isset($document['@context'])) { @@ -28,6 +30,8 @@ class ActivityStreams extends Format { public static function parse($http_response, $http, $opts=[]) { $as2 = $http_response['body']; $url = $http_response['url']; + + self::$fetcher = $opts['fetcher'] ?? null; if(!isset($as2['type'])) return false; @@ -156,7 +160,11 @@ class ActivityStreams extends Format { $authorURL = $as2['actor']; } if($authorURL) { - $authorResponse = $http->get($authorURL, ['Accept: application/activity+json,application/json']); + if(self::$fetcher) + $authorResponse = self::$fetcher->signed_get($authorURL, ['Accept' => 'application/activity+json,application/json']); + else + $authorResponse = $http->get($authorURL, ['Accept: application/activity+json,application/json']); + if($authorResponse && !empty($authorResponse['body'])) { $authorProfile = json_decode($authorResponse['body'], true); $author = self::parseAsHCard($authorProfile, $authorURL, $http, $opts); @@ -168,7 +176,12 @@ class ActivityStreams extends Format { // If this is a repost, fetch the reposted content if($as2['type'] == 'Announce' && isset($as2['object']) && is_string($as2['object'])) { $data['repost-of'] = [$as2['object']]; - $reposted = $http->get($as2['object'], ['Accept: application/activity+json,application/json']); + + if(self::$fetcher) + $reposted = self::$fetcher->signed_get($as2['object'], ['Accept: application/activity+json,application/json']); + else + $reposted = $http->get($as2['object'], ['Accept: application/activity+json,application/json']); + if($reposted && !empty($reposted['body'])) { $repostedData = json_decode($reposted['body'], true); if($repostedData) { @@ -184,7 +197,12 @@ class ActivityStreams extends Format { // If this is a like, fetch the liked post if($as2['type'] == 'Like' && isset($as2['object']) && is_string($as2['object'])) { $data['like-of'] = [$as2['object']]; - $liked = $http->get($as2['object'], ['Accept: application/activity+json,application/json']); + + if(self::$fetcher) + $liked = self::$fetcher->signed_get($as2['object'], ['Accept: application/activity+json,application/json']); + else + $liked = $http->get($as2['object'], ['Accept: application/activity+json,application/json']); + if($liked && !empty($liked['body'])) { $likedData = json_decode($liked['body'], true); if($likedData) { diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php index 952c6ff..aeecbd8 100644 --- a/lib/XRay/Parser.php +++ b/lib/XRay/Parser.php @@ -6,9 +6,11 @@ use DOMDocument, DOMXPath; class Parser { private $http; + private $fetcher; - public function __construct($http) { + public function __construct($http, $fetcher=false) { $this->http = $http; + $this->fetcher = $fetcher; } public function parse($http_response, $opts=[]) { @@ -109,6 +111,9 @@ class Parser { // Check if an ActivityStreams JSON object was passed in if(Formats\ActivityStreams::is_as2_json($body)) { + if($this->fetcher) { + $opts['fetcher'] = $this->fetcher; + } $data = Formats\ActivityStreams::parse($http_response, $this->http, $opts); $data['source-format'] = 'activity+json'; return $data; @@ -138,6 +143,9 @@ class Parser { $data['source-format'] = 'mf2+json'; return $data; } elseif($parsed && Formats\ActivityStreams::is_as2_json($parsed)) { + if($this->fetcher) { + $opts['fetcher'] = $this->fetcher; + } // Check if an ActivityStreams JSON string was passed in $http_response['body'] = $parsed; $data = Formats\ActivityStreams::parse($http_response, $this->http, $opts);