]> git.mxchange.org Git - friendica.git/commitdiff
Merge pull request #6071 from annando/ap-fetch-signed
authorHypolite Petovan <hypolite@mrpetovan.com>
Sun, 4 Nov 2018 06:56:09 +0000 (01:56 -0500)
committerGitHub <noreply@github.com>
Sun, 4 Nov 2018 06:56:09 +0000 (01:56 -0500)
Support for fetching non-public content / preparations for forum posts

src/Protocol/ActivityPub.php
src/Protocol/ActivityPub/Processor.php
src/Protocol/ActivityPub/Receiver.php
src/Util/HTTPSignature.php
src/Util/Network.php

index 107cc423a6b88f9344576f0dcb5e1f09fbe35248..beb5be125d3df9bf98e332e19216b8f4fb2fae1b 100644 (file)
@@ -7,6 +7,7 @@ namespace Friendica\Protocol;
 use Friendica\Util\Network;
 use Friendica\Core\Protocol;
 use Friendica\Model\APContact;
+use Friendica\Util\HTTPSignature;
 
 /**
  * @brief ActivityPub Protocol class
@@ -59,11 +60,16 @@ class ActivityPub
        /**
         * Fetches ActivityPub content from the given url
         *
-        * @param string $url content url
+        * @param string  $url content url
+        * @param integer $uid User ID for the signature
         * @return array
         */
-       public static function fetchContent($url)
+       public static function fetchContent($url, $uid = 0)
        {
+               if (!empty($uid)) {
+                       return HTTPSignature::fetch($url, 1);
+               }
+
                $curlResult = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json, application/ld+json']);
                if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
                        return false;
index 7d970c00390b2c87ee6b5bd3e23b1a0745ac7643..1e5001010855ae68b8b8beaf25b75b6ed4903398 100644 (file)
@@ -301,7 +301,9 @@ class Processor
                        return;
                }
 
-               $object = ActivityPub::fetchContent($url);
+               $uid = ActivityPub\Receiver::getFirstUserFromReceivers($child['receiver']);
+
+               $object = ActivityPub::fetchContent($url, $uid);
                if (empty($object)) {
                        Logger::log('Activity ' . $url . ' was not fetchable, aborting.');
                        return;
index 80c2a5f08492659f674f4ddef48b844fae444c5a..379fb7a7fcf3a529ddc41b73bff5189acd4a5e58 100644 (file)
@@ -111,12 +111,13 @@ class Receiver
        /**
         * Fetches the object type for a given object id
         *
-        * @param array  $activity
-        * @param string $object_id Object ID of the the provided object
+        * @param array   $activity
+        * @param string  $object_id Object ID of the the provided object
+        * @param integer $uid User ID
         *
         * @return string with object type
         */
-       private static function fetchObjectType($activity, $object_id)
+       private static function fetchObjectType($activity, $object_id, $uid = 0)
        {
                if (!empty($activity['as:object'])) {
                        $object_type = JsonLD::fetchElement($activity['as:object'], '@type');
@@ -135,7 +136,7 @@ class Receiver
                        return 'as:' . $profile['type'];
                }
 
-               $data = ActivityPub::fetchContent($object_id);
+               $data = ActivityPub::fetchContent($object_id, $uid);
                if (!empty($data)) {
                        $object = JsonLD::compact($data);
                        $type = JsonLD::fetchElement($object, '@type');
@@ -171,12 +172,15 @@ class Receiver
 
                // When it is a delivery to a personal inbox we add that user to the receivers
                if (!empty($uid)) {
-                       $owner = User::getOwnerDataById($uid);
                        $additional = ['uid:' . $uid => $uid];
                        $receivers = array_merge($receivers, $additional);
+               } else {
+                       // We possibly need some user to fetch private content,
+                       // so we fetch the first out ot the list.
+                       $uid = self::getFirstUserFromReceivers($receivers);
                }
 
-               Logger::log('Receivers: ' . json_encode($receivers), Logger::DEBUG);
+               Logger::log('Receivers: ' . $uid . ' - ' . json_encode($receivers), Logger::DEBUG);
 
                $object_id = JsonLD::fetchElement($activity, 'as:object');
                if (empty($object_id)) {
@@ -184,14 +188,14 @@ class Receiver
                        return [];
                }
 
-               $object_type = self::fetchObjectType($activity, $object_id);
+               $object_type = self::fetchObjectType($activity, $object_id, $uid);
 
                // Fetch the content only on activities where this matters
                if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) {
                        if ($type == 'as:Announce') {
                                $trust_source = false;
                        }
-                       $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source);
+                       $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source, $uid);
                        if (empty($object_data)) {
                                Logger::log("Object data couldn't be processed", Logger::DEBUG);
                                return [];
@@ -216,7 +220,7 @@ class Receiver
 
                        // An Undo is done on the object of an object, so we need that type as well
                        if ($type == 'as:Undo') {
-                               $object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object']);
+                               $object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $uid);
                        }
                }
 
@@ -235,6 +239,22 @@ class Receiver
                return $object_data;
        }
 
+       /**
+        * Fetches the first uider id from the receiver array
+        *
+        * @param array $receivers Array with receivers
+        * @return integer user id;
+        */
+       public static function getFirstUserFromReceivers($receivers)
+       {
+               foreach ($receivers as $receiver) {
+                       if (!empty($receiver)) {
+                               return $receiver;
+                       }
+               }
+               return 0;
+       }
+
        /**
         * Store the unprocessed data into the conversation table
         * This has to be done outside the regular function,
@@ -395,10 +415,11 @@ class Receiver
         *
         * @param array $activity
         * @param string $actor
+        * @param array $tags
         *
         * @return array with receivers (user id)
         */
-       private static function getReceivers($activity, $actor)
+       private static function getReceivers($activity, $actor, $tags = [])
        {
                $receivers = [];
 
@@ -446,24 +467,34 @@ class Receiver
                                }
 
                                if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
-                                       $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
-                                       $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
-                                               'network' => $networks, 'archive' => false, 'pending' => false];
-                                       $contacts = DBA::select('contact', ['uid'], $condition);
-                                       while ($contact = DBA::fetch($contacts)) {
-                                               if ($contact['uid'] != 0) {
-                                                       $receivers['uid:' . $contact['uid']] = $contact['uid'];
-                                               }
-                                       }
-                                       DBA::close($contacts);
+                                       $receivers = array_merge($receivers, self::getReceiverForActor($actor, $tags));
                                        continue;
                                }
 
+                               // Fetching all directly addressed receivers
                                $condition = ['self' => true, 'nurl' => normalise_link($receiver)];
-                               $contact = DBA::selectFirst('contact', ['uid'], $condition);
+                               $contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition);
                                if (!DBA::isResult($contact)) {
                                        continue;
                                }
+
+                               // Check if the potential receiver is following the actor
+                               // Exception: The receiver is targetted via "to" or this is a comment
+                               if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::ACCOUNT_TYPE_COMMUNITY)) {
+                                       $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
+                                       $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
+                                               'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']];
+
+                                       // Forum posts are only accepted from forum contacts
+                                       if ($contact['contact-type'] == Contact::ACCOUNT_TYPE_COMMUNITY) {
+                                               $condition['rel'] = [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER];
+                                       }
+
+                                       if (!DBA::exists('contact', $condition)) {
+                                               continue;
+                                       }
+                               }
+
                                $receivers['uid:' . $contact['uid']] = $contact['uid'];
                        }
                }
@@ -473,6 +504,71 @@ class Receiver
                return $receivers;
        }
 
+       /**
+        * Fetch the receiver list of a given actor
+        *
+        * @param string $actor
+        * @param array $tags
+        *
+        * @return array with receivers (user id)
+        */
+       public static function getReceiverForActor($actor, $tags)
+       {
+               $receivers = [];
+               $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
+               $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
+                       'network' => $networks, 'archive' => false, 'pending' => false];
+               $contacts = DBA::select('contact', ['uid', 'rel'], $condition);
+               while ($contact = DBA::fetch($contacts)) {
+                       if (self::isValidReceiverForActor($contact, $actor, $tags)) {
+                               $receivers['uid:' . $contact['uid']] = $contact['uid'];
+                       }
+               }
+               DBA::close($contacts);
+               return $receivers;
+       }
+
+       /**
+        * Tests if the contact is a valid receiver for this actor
+        *
+        * @param array $contact
+        * @param string $actor
+        * @param array $tags
+        *
+        * @return array with receivers (user id)
+        */
+       private static function isValidReceiverForActor($contact, $actor, $tags)
+       {
+               // Public contacts are no valid receiver
+               if ($contact['uid'] == 0) {
+                       return false;
+               }
+
+               // Are we following the contact? Then this is a valid receiver
+               if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) {
+                       return true;
+               }
+
+               // When the possible receiver isn't a community, then it is no valid receiver
+               $owner = User::getOwnerDataById($contact['uid']);
+               if (empty($owner) || ($owner['contact-type'] != Contact::ACCOUNT_TYPE_COMMUNITY)) {
+                       return false;
+               }
+
+               // Is the community account tagged?
+               foreach ($tags as $tag) {
+                       if ($tag['type'] != 'Mention') {
+                               continue;
+                       }
+
+                       if ($tag['href'] == $owner['url']) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
        /**
         * Switches existing contacts to ActivityPub
         *
@@ -559,16 +655,17 @@ class Receiver
         * @param string  $object_id    Object ID of the the provided object
         * @param array   $object       The provided object array
         * @param boolean $trust_source Do we trust the provided object?
+        * @param integer $uid          User ID for the signature that we use to fetch data
         *
         * @return array with trusted and valid object data
         */
-       private static function fetchObject($object_id, $object = [], $trust_source = false)
+       private static function fetchObject($object_id, $object = [], $trust_source = false, $uid = 0)
        {
                // By fetching the type we check if the object is complete.
                $type = JsonLD::fetchElement($object, '@type');
 
                if (!$trust_source || empty($type)) {
-                       $data = ActivityPub::fetchContent($object_id);
+                       $data = ActivityPub::fetchContent($object_id, $uid);
                        if (!empty($data)) {
                                $object = JsonLD::compact($data);
                                Logger::log('Fetched content for ' . $object_id, Logger::DEBUG);
@@ -604,7 +701,7 @@ class Receiver
                        if (empty($object_id)) {
                                return false;
                        }
-                       return self::fetchObject($object_id);
+                       return self::fetchObject($object_id, [], false, $uid);
                }
 
                Logger::log('Unhandled object type: ' . $type, Logger::DEBUG);
@@ -736,7 +833,7 @@ class Receiver
                        }
                }
 
-               $object_data['receiver'] = self::getReceivers($object, $object_data['actor']);
+               $object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags']);
 
                // Common object data:
 
index 408f4d978941e21bf42d023bef0b5a569e266cf3..956d6ff3728d36b4d55a8b8abe4f81c406f636ce 100644 (file)
@@ -90,7 +90,7 @@ class HTTPSignature
                        $key = $key($sig_block['keyId']);
                }
 
-               Logger::log('Got keyID ' . $sig_block['keyId']);
+               Logger::log('Got keyID ' . $sig_block['keyId'], Logger::DEBUG);
 
                if (!$key) {
                        return $result;
@@ -308,11 +308,59 @@ class HTTPSignature
                $postResult = Network::post($target, $content, $headers);
                $return_code = $postResult->getReturnCode();
 
-               Logger::log('Transmit to ' . $target . ' returned ' . $return_code);
+               Logger::log('Transmit to ' . $target . ' returned ' . $return_code, Logger::DEBUG);
 
                return ($return_code >= 200) && ($return_code <= 299);
        }
 
+       /**
+        * @brief Fetches JSON data for a user
+        *
+        * @param string $request request url
+        * @param integer $uid User id of the requester
+        *
+        * @return array JSON array
+        */
+       public static function fetch($request, $uid)
+       {
+               $owner = User::getOwnerDataById($uid);
+
+               if (!$owner) {
+                       return;
+               }
+
+               // Header data that is about to be signed.
+               $host = parse_url($request, PHP_URL_HOST);
+               $path = parse_url($request, PHP_URL_PATH);
+
+               $headers = ['Host: ' . $host];
+
+               $signed_data = "(request-target): get " . $path . "\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) host",signature="' . $signature . '"';
+
+               $headers[] = 'Accept: application/activity+json, application/ld+json';
+
+               $curlResult = Network::curl($request, false, $redirects, ['header' => $headers]);
+               $return_code = $curlResult->getReturnCode();
+
+               Logger::log('Fetched for user ' . $uid . ' from ' . $request . ' returned ' . $return_code, Logger::DEBUG);
+
+               if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
+                       return false;
+               }
+
+               $content = json_decode($curlResult->getBody(), true);
+
+               if (empty($content) || !is_array($content)) {
+                       return false;
+               }
+
+               return $content;
+       }
+
        /**
         * @brief Gets a signer from a given HTTP request
         *
index 255b3c8c4d3e1b8d1f41d1b8358b27251102a5d2..0ff34f120ad9669f19015984c40f81c1bbd4d9d3 100644 (file)
@@ -83,6 +83,7 @@ class Network
         *                           '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
+        *                           'header' => header array
         *
         * @return CurlResult
         */
@@ -136,6 +137,10 @@ class Network
                        );
                }
 
+               if (!empty($opts['header'])) {
+                       curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['header']);
+               }
+
                @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                @curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());