diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/ActivityPubPlugin.php b/ActivityPubPlugin.php old mode 100644 new mode 100755 index 58c11e5..a6660ff --- a/ActivityPubPlugin.php +++ b/ActivityPubPlugin.php @@ -232,6 +232,7 @@ class ActivityPubPlugin extends Plugin { $schema = Schema::get(); $schema->ensureTable('Activitypub_profile', Activitypub_profile::schemaDef()); + $schema->ensureTable('Activitypub_rsa', Activitypub_rsa::schemaDef()); $schema->ensureTable('Activitypub_pending_follow_requests', Activitypub_pending_follow_requests::schemaDef()); return true; } @@ -250,17 +251,18 @@ class ActivityPubPlugin extends Plugin */ public static function extractWebfingerIds($text, $preMention='@') { - $wmatches = array(); + $wmatches = []; $result = preg_match_all( '/(?isLocal() || $other->isLocal()) { + if (!$profile->isLocal() && $other->isLocal()) { return true; } @@ -524,7 +526,7 @@ class ActivityPubPlugin extends Plugin */ public function onStartUnsubscribe(Profile $profile, Profile $other) { - if (!$profile->isLocal() || $other->isLocal()) { + if (!$profile->isLocal() && $other->isLocal()) { return true; } @@ -724,6 +726,10 @@ class ActivityPubPlugin extends Plugin $profile = Profile::getKV($notice->profile_id); + if (!$profile->isLocal()) { + return true; + } + $other = array(); try { $other[] = Activitypub_profile::from_profile($notice->getProfile()); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/COPYING b/COPYING old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/actions/apactorfollowers.php b/actions/apactorfollowers.php old mode 100644 new mode 100755 diff --git a/actions/apactorfollowing.php b/actions/apactorfollowing.php old mode 100644 new mode 100755 diff --git a/actions/apactorinbox.php b/actions/apactorinbox.php old mode 100644 new mode 100755 diff --git a/actions/apactorliked.php b/actions/apactorliked.php old mode 100644 new mode 100755 diff --git a/actions/apactorprofile.php b/actions/apactorprofile.php old mode 100644 new mode 100755 diff --git a/actions/apsharedinbox.php b/actions/apsharedinbox.php old mode 100644 new mode 100755 diff --git a/actions/inbox/Accept.php b/actions/inbox/Accept.php old mode 100644 new mode 100755 diff --git a/actions/inbox/Announce.php b/actions/inbox/Announce.php old mode 100644 new mode 100755 diff --git a/actions/inbox/Create.php b/actions/inbox/Create.php old mode 100644 new mode 100755 index 66b23c6..f99ec90 --- a/actions/inbox/Create.php +++ b/actions/inbox/Create.php @@ -44,7 +44,7 @@ if (!isset($data->object->content)) { if (!isset($data->object->url)) { ActivityPubReturn::error("Object url was not specified."); } elseif (!filter_var($data->object->url, FILTER_VALIDATE_URL)) { - ActivityPubReturn::error("Invalid Object Url."); + ActivityPubReturn::error("Invalid Object URL."); } if (!isset($data->object->to)) { ActivityPubReturn::error("Object To was not specified."); diff --git a/actions/inbox/Delete.php b/actions/inbox/Delete.php old mode 100644 new mode 100755 diff --git a/actions/inbox/Follow.php b/actions/inbox/Follow.php old mode 100644 new mode 100755 diff --git a/actions/inbox/Like.php b/actions/inbox/Like.php old mode 100644 new mode 100755 diff --git a/actions/inbox/Reject.php b/actions/inbox/Reject.php old mode 100644 new mode 100755 diff --git a/actions/inbox/Undo.php b/actions/inbox/Undo.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_accept.php b/classes/Activitypub_accept.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_announce.php b/classes/Activitypub_announce.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_attachment.php b/classes/Activitypub_attachment.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_create.php b/classes/Activitypub_create.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_delete.php b/classes/Activitypub_delete.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_error.php b/classes/Activitypub_error.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_follow.php b/classes/Activitypub_follow.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_like.php b/classes/Activitypub_like.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_notice.php b/classes/Activitypub_notice.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_pending_follow_requests.php b/classes/Activitypub_pending_follow_requests.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_profile.php b/classes/Activitypub_profile.php old mode 100644 new mode 100755 index 2c3ce3f..2343c80 --- a/classes/Activitypub_profile.php +++ b/classes/Activitypub_profile.php @@ -42,8 +42,6 @@ class Activitypub_profile extends Profile { public $__table = 'Activitypub_profile'; - protected $_profile = null; - /** * Return table definition for Schema setup and DB_DataObject usage. * @@ -83,6 +81,9 @@ class Activitypub_profile extends Profile { $uri = ActivityPubPlugin::actor_uri($profile); $id = $profile->getID(); + $rsa = new Activitypub_rsa(); + $public_key = $rsa->ensure_public_key($profile); + unset($rsa); $res = [ '@context' => [ "https://www.w3.org/ns/activitystreams", @@ -112,6 +113,11 @@ class Activitypub_profile extends Profile 'summary' => ($desc = $profile->getDescription()) == null ? "" : $desc, 'url' => $profile->getUrl(), 'manuallyApprovesFollowers' => false, + 'publicKey' => [ + 'id' => $uri."#main-key", + 'owner' => $uri, + 'publicKeyPem' => $public_key + ], 'tag' => [], 'attachment' => [], 'icon' => [ @@ -122,7 +128,7 @@ class Activitypub_profile extends Profile ]; if ($profile->isLocal()) { - $res['endpoints']['sharedInbox'] = common_local_url("apSharedInbox", array("id" => $id)); + $res['endpoints']['sharedInbox'] = common_local_url('apSharedInbox'); } else { $aprofile = new Activitypub_profile(); $aprofile = $aprofile->from_profile($profile); @@ -133,7 +139,7 @@ class Activitypub_profile extends Profile } /** - * Insert the current objects variables into the database + * Insert the current object variables into the database * * @author Diogo Cordeiro * @access public diff --git a/classes/Activitypub_reject.php b/classes/Activitypub_reject.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_rsa.php b/classes/Activitypub_rsa.php new file mode 100755 index 0000000..21a62f7 --- /dev/null +++ b/classes/Activitypub_rsa.php @@ -0,0 +1,140 @@ +. + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @author Daniel Supernault + * @copyright 2018 Free Software Foundation http://fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link https://www.gnu.org/software/social/ + */ +if (!defined('GNUSOCIAL')) { + exit(1); +} + +/** + * ActivityPub Keys System + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ +class Activitypub_rsa extends Managed_DataObject +{ + public $__table = 'Activitypub_rsa'; + + /** + * Return table definition for Schema setup and DB_DataObject usage. + * + * @author Diogo Cordeiro + * @return array array of column definitions + */ + public static function schemaDef() + { + return [ + 'fields' => [ + 'profile_id' => ['type' => 'integer'], + 'private_key' => ['type' => 'varchar', 'length' => 191], + 'public_key' => ['type' => 'varchar', 'length' => 191], + 'created' => ['type' => 'datetime', 'not null' => true], + 'modified' => ['type' => 'datetime', 'not null' => true], + ], + 'primary key' => ['profile_id'], + 'unique keys' => [ + 'Activitypub_rsa_profile_id_key' => ['profile_id'], + 'Activitypub_rsa_private_key_key' => ['private_key'], + 'Activitypub_rsa_public_key_key' => ['public_key'], + ], + 'foreign keys' => [ + 'Activitypub_profile_profile_id_fkey' => ['profile', ['profile_id' => 'id']], + ], + ]; + } + + /** + * Guarantees a Public Key for a given profile. + * + * @author Diogo Cordeiro + * @param Profile $profile + * @return string The public key + * @throws Exception It should never occur + */ + public function ensure_public_key($profile) + { + $this->profile_id = $profile->getID(); + $apRSA = self::getKV('profile_id', $this->profile_id); + if (!$apRSA instanceof Activitypub_rsa) { + // No existing key pair for this profile + if ($profile->isLocal()) { + self::generate_keys($this->private_key, $this->public_key); + $this->store_keys(); + } else { + throw new Exception('No Keys for this Profile. That\'s odd.'); + } + } + return $apRSA->public_key; + } + + /** + * Insert the current object variables into the database. + * + * @author Diogo Cordeiro + * @access public + * @throws ServerException + */ + public function store_keys() + { + $this->created = $this->modified = common_sql_now(); + $ok = $this->insert(); + if ($ok === false) { + $profile->query('ROLLBACK'); + throw new ServerException('Cannot save ActivityPub RSA.'); + } + } + + /** + * Generates a pair of RSA keys. + * + * @author PHP Manual Contributed Notes + * @param string $private_key in/out + * @param string $public_key in/out + */ + public static function generate_keys(&$private_key, &$public_key) + { + $config = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]; + + // Create the private and public key + $res = openssl_pkey_new($config); + + // Extract the private key from $res to $private_key + openssl_pkey_export($res, $private_key); + + // Extract the public key from $res to $pubKey + $pubKey = openssl_pkey_get_details($res); + $public_key = $pubKey["key"]; + unset($pubKey); + } +} diff --git a/classes/Activitypub_tag.php b/classes/Activitypub_tag.php old mode 100644 new mode 100755 diff --git a/classes/Activitypub_undo.php b/classes/Activitypub_undo.php old mode 100644 new mode 100755 diff --git a/composer.json b/composer.json old mode 100644 new mode 100755 diff --git a/composer.lock b/composer.lock old mode 100644 new mode 100755 diff --git a/phpunit.xml b/phpunit.xml old mode 100644 new mode 100755 diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php old mode 100644 new mode 100755 diff --git a/tests/TestCase.php b/tests/TestCase.php old mode 100644 new mode 100755 diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php old mode 100644 new mode 100755 diff --git a/tests/Unit/ProfileObjectTest.php b/tests/Unit/ProfileObjectTest.php old mode 100644 new mode 100755 diff --git a/utils/discoveryhints.php b/utils/discoveryhints.php old mode 100644 new mode 100755 diff --git a/utils/explorer.php b/utils/explorer.php old mode 100644 new mode 100755 index 0ed9e8c..5e647b2 --- a/utils/explorer.php +++ b/utils/explorer.php @@ -91,7 +91,7 @@ class Activitypub_explorer private function ensure_proper_remote_uri($url) { $client = new HTTPClient(); - $headers = array(); + $headers = []; $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; $response = $client->get($url, $headers); @@ -118,35 +118,29 @@ class Activitypub_explorer private function grab_local_user($uri, $online = false) { // Ensure proper remote URI - // If an exceptiong ocurrs here it's better to just leave everything + // If an exception occurs here it's better to just leave everything // break than to continue processing if ($online && $this->ensure_proper_remote_uri($uri)) { $uri = $this->temp_res["id"]; } - try { - // Try standard ActivityPub route - // Is this a filthy little mudblood? - $aprofile = Activitypub_profile::getKV("uri", $uri); - if ($aprofile instanceof Activitypub_profile) { - $profile = $aprofile->local_profile(); - } else { - // Nope, this potential local user is not a remote user. - // Let's check for pure blood! - $profile = User::getByNickname($this->temp_res["preferredUsername"])->getProfile(); - } + + // Try standard ActivityPub route + // Is this a known filthy little mudblood? + $aprofile = Activitypub_profile::getKV("uri", $uri); + if ($aprofile instanceof Activitypub_profile) { + $profile = $aprofile->local_profile(); // We found something! $this->discovered_actor_profiles[]= $profile; unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system return true; - } catch (Exception $e) { - // We can safely ignore every exception here as we are returning false - // when it fails the lookup for existing local representation } + // If offline grabbing failed, attempt again with online resources if (!$online) { - $this->grab_local_user($uri, true); + return $this->grab_local_user($uri, true); } + return false; } @@ -202,6 +196,7 @@ class Activitypub_explorer */ private function store_profile($res) { + // ActivityPub Profile $aprofile = new Activitypub_profile; $aprofile->uri = $res['id']; $aprofile->nickname = $res['preferredUsername']; @@ -211,8 +206,15 @@ class Activitypub_explorer $aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']; $aprofile->do_insert(); + $profile = $aprofile->local_profile(); - return $aprofile->local_profile(); + // Public Key + $apRSA = new Activitypub_rsa(); + $apRSA->profile_id = $profile->getID(); + $apRSA->public_key = $res['publicKey']['publicKeyPem']; + $apRSA->store_keys(); + + return $profile; } /** @@ -225,7 +227,7 @@ class Activitypub_explorer */ private static function validate_remote_response($res) { - if (!isset($res['id'], $res['preferredUsername'], $res['name'], $res['summary'], $res['inbox'])) { + if (!isset($res['id'], $res['preferredUsername'], $res['name'], $res['summary'], $res['inbox'], $res['publicKey']['publicKeyPem'])) { return false; } diff --git a/utils/postman.php b/utils/postman.php old mode 100644 new mode 100755 index 5255f43..90e0682 --- a/utils/postman.php +++ b/utils/postman.php @@ -52,7 +52,7 @@ class Activitypub_postman * Create a postman to deliver something to someone * * @author Diogo Cordeiro - * @param Profile of sender + * @param Profile $from Profile of sender * @param Activitypub_profile $to array of destinataries */ public function __construct($from, $to = []) @@ -73,7 +73,7 @@ class Activitypub_postman */ public function follow() { - $data = Activitypub_follow::follow_to_array($this->actor->getUrl(), $this->to[0]->getUrl()); + $data = Activitypub_follow::follow_to_array(ActivityPubPlugin::actor_uri($this->actor), $this->to[0]->getUrl()); $this->client->setBody(json_encode($data)); $res = $this->client->post($this->to[0]->get_inbox(), $this->headers); $res_body = json_decode($res->getBody()); @@ -102,8 +102,8 @@ class Activitypub_postman { $data = Activitypub_undo::undo_to_array( Activitypub_follow::follow_to_array( - $this->actor->getUrl(), - $this->to[0]->getUrl() + ActivityPubPlugin::actor_uri($this->actor), + $this->to[0]->getUrl() ) ); $this->client->setBody(json_encode($data)); @@ -130,7 +130,7 @@ class Activitypub_postman public function like($notice) { $data = Activitypub_like::like_to_array( - $this->actor->getUrl(), + ActivityPubPlugin::actor_uri($this->actor), Activitypub_notice::notice_to_array($notice) ); $this->client->setBody(json_encode($data)); @@ -149,10 +149,10 @@ class Activitypub_postman { $data = Activitypub_undo::undo_to_array( Activitypub_like::like_to_array( - $this->actor->getUrl(), + ActivityPubPlugin::actor_uri($this->actor), Activitypub_notice::notice_to_array($notice) ) - ); + ); $this->client->setBody(json_encode($data)); foreach ($this->to_inbox() as $inbox) { $this->client->post($inbox, $this->headers); @@ -169,9 +169,9 @@ class Activitypub_postman { $data = Activitypub_create::create_to_array( $notice->getUri(), - $this->actor->getUrl(), - Activitypub_notice::notice_to_array($notice) - ); + ActivityPubPlugin::actor_uri($this->actor), + Activitypub_notice::notice_to_array($notice) + ); if (isset($notice->reply_to)) { $data["object"]["reply_to"] = $notice->getParent()->getUri(); } @@ -190,7 +190,7 @@ class Activitypub_postman public function announce($notice) { $data = Activitypub_announce::announce_to_array( - $this->actor->getUrl(), + ActivityPubPlugin::actor_uri($this->actor), Activitypub_notice::notice_to_array($notice) ); $this->client->setBody(json_encode($data)); @@ -236,7 +236,12 @@ class Activitypub_postman { $to_inboxes = array(); foreach ($this->to as $to_profile) { - $to_inboxes[] = $to_profile->get_inbox(); + $i = $to_profile->get_inbox(); + // Prevent delivering to self + if ($i == [common_local_url('apSharedInbox')]) { + continue; + } + $to_inboxes[] = $i; } return array_unique($to_inboxes);