]> git.mxchange.org Git - friendica.git/commitdiff
Add new OStatus\Salmon module class
authorHypolite Petovan <hypolite@mrpetovan.com>
Mon, 7 Nov 2022 01:39:52 +0000 (20:39 -0500)
committerHypolite Petovan <hypolite@mrpetovan.com>
Mon, 7 Nov 2022 01:42:05 +0000 (20:42 -0500)
- Add module instanciation in Module\DFRN\Notify

src/Module/DFRN/Notify.php
src/Module/OStatus/Salmon.php [new file with mode: 0644]
static/routes.config.php

index 32c11beb77189b65ec5f4221b657ada6b758a5d8..34cf21f11df66b8d03d9d0f832d350dd45360da5 100644 (file)
 
 namespace Friendica\Module\DFRN;
 
+use Friendica\App;
 use Friendica\BaseModule;
+use Friendica\Core\L10n;
 use Friendica\Core\Logger;
 use Friendica\Core\System;
+use Friendica\Database\Database;
 use Friendica\DI;
 use Friendica\Model\Contact;
 use Friendica\Model\Conversation;
 use Friendica\Model\User;
+use Friendica\Module\OStatus\Salmon;
+use Friendica\Module\Response;
 use Friendica\Protocol\DFRN;
 use Friendica\Protocol\Diaspora;
 use Friendica\Util\Network;
 use Friendica\Network\HTTPException;
+use Friendica\Util\Profiler;
+use Psr\Log\LoggerInterface;
 
 /**
  * DFRN Notify
  */
 class Notify extends BaseModule
 {
+       /** @var Database */
+       private $database;
+
+       public function __construct(Database $database, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
+       {
+               parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+               $this->database = $database;
+       }
+
        protected function post(array $request = [])
        {
                $postdata = Network::postdata();
@@ -54,8 +71,17 @@ class Notify extends BaseModule
                        }
                        $this->dispatchPrivate($user, $postdata);
                } elseif (!$this->dispatchPublic($postdata)) {
-                       require_once 'mod/salmon.php';
-                       salmon_post(DI::app(), $postdata);
+                       (new Salmon(
+                               $this->database,
+                               $this->l10n,
+                               $this->baseUrl,
+                               $this->args,
+                               $this->logger,
+                               $this->profiler,
+                               $this->response,
+                               $this->server,
+                               $this->parameters
+                       ))->rawContent($request);
                }
        }
 
diff --git a/src/Module/OStatus/Salmon.php b/src/Module/OStatus/Salmon.php
new file mode 100644 (file)
index 0000000..c530943
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2022, 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\Module\OStatus;
+
+use Friendica\App;
+use Friendica\Core\L10n;
+use Friendica\Core\Protocol;
+use Friendica\Database\Database;
+use Friendica\Model\GServer;
+use Friendica\Model\Post;
+use Friendica\Module\Response;
+use Friendica\Protocol\ActivityNamespace;
+use Friendica\Protocol\OStatus;
+use Friendica\Util\Crypto;
+use Friendica\Util\Network;
+use Friendica\Network\HTTPException;
+use Friendica\Protocol\Salmon as SalmonProtocol;
+use Friendica\Util\Profiler;
+use Friendica\Util\Strings;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Technical endpoint for the Salmon protocol
+ */
+class Salmon extends \Friendica\BaseModule
+{
+       /** @var Database */
+       private $database;
+
+       public function __construct(Database $database, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
+       {
+               parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+               $this->database = $database;
+       }
+
+       /**
+        * @param array $request
+        * @return void
+        * @throws HTTPException\AcceptedException
+        * @throws HTTPException\BadRequestException
+        * @throws HTTPException\InternalServerErrorException
+        * @throws HTTPException\OKException
+        * @throws \ImagickException
+        */
+       protected function rawContent(array $request = [])
+       {
+               $xml = Network::postdata();
+
+               $this->logger->debug('New Salmon', ['nickname' => $this->parameters['nickname'], 'xml' => $xml]);
+
+               // Despite having a route with a mandatory nickname parameter, this method can also be called from
+               // \Friendica\Module\DFRN\Notify->post where the same parameter is optional 🤷‍
+               $nickname = $this->parameters['nickname'] ?? '';
+
+               $importer = $this->database->selectFirst('user', [], ['nickname' => $nickname, 'account_expired' => false, 'account_removed' => false]);
+               if (!$this->database->isResult($importer)) {
+                       throw new HTTPException\InternalServerErrorException();
+               }
+
+               // parse the xml
+               $dom = simplexml_load_string($xml, 'SimpleXMLElement', 0, ActivityNamespace::SALMON_ME);
+
+               $base = null;
+
+               // figure out where in the DOM tree our data is hiding
+               if (!empty($dom->provenance->data)) {
+                       $base = $dom->provenance;
+               } elseif (!empty($dom->env->data)) {
+                       $base = $dom->env;
+               } elseif (!empty($dom->data)) {
+                       $base = $dom;
+               }
+
+               if (empty($base)) {
+                       $this->logger->notice('unable to locate salmon data in xml');
+                       throw new HTTPException\BadRequestException();
+               }
+
+               // Stash the signature away for now. We have to find their key or it won't be good for anything.
+               $signature = Strings::base64UrlDecode($base->sig);
+
+               // unpack the  data
+
+               // strip whitespace so our data element will return to one big base64 blob
+               $data = str_replace([" ", "\t", "\r", "\n"], ["", "", "", ""], $base->data);
+
+               // stash away some other stuff for later
+
+               $type     = $base->data[0]->attributes()->type[0];
+               $keyhash  = $base->sig[0]->attributes()->keyhash[0] ?? '';
+               $encoding = $base->encoding;
+               $alg      = $base->alg;
+
+               // Salmon magic signatures have evolved and there is no way of knowing ahead of time which
+               // flavour we have. We'll try and verify it regardless.
+
+               $stnet_signed_data = $data;
+
+               $signed_data = $data . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg);
+
+               $compliant_format = str_replace('=', '', $signed_data);
+
+
+               // decode the data
+               $data = Strings::base64UrlDecode($data);
+
+               $author      = OStatus::salmonAuthor($data, $importer);
+               $author_link = $author["author-link"];
+               if (!$author_link) {
+                       $this->logger->notice('Could not retrieve author URI.');
+                       throw new HTTPException\BadRequestException();
+               }
+
+               // Once we have the author URI, go to the web and try to find their public key
+
+               $this->logger->notice('Fetching key for ' . $author_link);
+
+               $key = SalmonProtocol::getKey($author_link, $keyhash);
+
+               if (!$key) {
+                       $this->logger->notice('Could not retrieve author key.');
+                       throw new HTTPException\BadRequestException();
+               }
+
+               $key_info = explode('.', $key);
+
+               $m = Strings::base64UrlDecode($key_info[1]);
+               $e = Strings::base64UrlDecode($key_info[2]);
+
+               $this->logger->info('Key details', ['info' => $key_info]);
+
+               $pubkey = Crypto::meToPem($m, $e);
+
+               // We should have everything we need now. Let's see if it verifies.
+
+               // Try GNU Social format
+               $verify = Crypto::rsaVerify($signed_data, $signature, $pubkey);
+               $mode   = 1;
+
+               if (!$verify) {
+                       $this->logger->notice('Message did not verify using protocol. Trying compliant format.');
+                       $verify = Crypto::rsaVerify($compliant_format, $signature, $pubkey);
+                       $mode   = 2;
+               }
+
+               if (!$verify) {
+                       $this->logger->notice('Message did not verify using padding. Trying old statusnet format.');
+                       $verify = Crypto::rsaVerify($stnet_signed_data, $signature, $pubkey);
+                       $mode   = 3;
+               }
+
+               if (!$verify) {
+                       $this->logger->notice('Message did not verify. Discarding.');
+                       throw new HTTPException\BadRequestException();
+               }
+
+               $this->logger->notice('Message verified with mode ' . $mode);
+
+
+               /*
+               *
+               * If we reached this point, the message is good. Now let's figure out if the author is allowed to send us stuff.
+               *
+               */
+
+               $contact = $this->database->selectFirst(
+                       'contact',
+                       [],
+                       [
+                               "`network` IN (?, ?)
+                       AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)
+                       AND `uid` = ?",
+                Protocol::OSTATUS, Protocol::DFRN,
+                               Strings::normaliseLink($author_link), $author_link, Strings::normaliseLink($author_link),
+                               $importer['uid']
+                       ]
+               );
+
+               if (!empty($contact['gsid'])) {
+                       GServer::setProtocol($contact['gsid'], Post\DeliveryData::OSTATUS);
+               }
+
+               // Have we ignored the person?
+               // If so we can not accept this post.
+
+               if (!empty($contact['blocked'])) {
+                       $this->logger->notice('Ignoring this author.');
+                       throw new HTTPException\AcceptedException();
+               }
+
+               // Placeholder for hub discovery.
+               $hub = '';
+
+               $contact = $contact ?: [];
+
+               OStatus::import($data, $importer, $contact, $hub);
+
+               throw new HTTPException\OKException();
+       }
+}
index 3cb1b47e226166e7d5149ee16f3e4d5b9a85ef1f..d72302da239fb0581d7287a4da5d76ae70ae3d59 100644 (file)
@@ -553,6 +553,8 @@ return [
                '/{sub1}/{sub2}/{url}' => [Module\Proxy::class, [R::GET]],
        ],
 
+       '/salmon/{nickname}'       => [Module\OStatus\Salmon::class, [        R::POST]],
+
        '/search' => [
                '[/]'                  => [Module\Search\Index::class, [R::GET]],
                '/acl'                 => [Module\Search\Acl::class,   [R::GET, R::POST]],