OStatus support removed

This commit is contained in:
Michael 2024-08-24 04:27:00 +00:00
commit e8a3be6820
87 changed files with 773 additions and 4383 deletions

View file

@ -1,148 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\OStatus;
use Friendica\App;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Module\Response;
use Friendica\Network\HTTPException;
use Friendica\Protocol\OStatus;
use Friendica\Util\Network;
use Friendica\Util\Profiler;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
class PubSub extends \Friendica\BaseModule
{
/** @var Database */
private $database;
/** @var App\Request */
private $request;
public function __construct(App\Request $request, 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;
$this->request = $request;
}
protected function post(array $request = [])
{
$xml = Network::postdata();
$this->logger->info('Feed arrived.', ['from' => $this->request->getRemoteAddress(), 'for' => $this->args->getCommand(), 'user-agent' => $this->server['HTTP_USER_AGENT']]);
$this->logger->debug('Data stream.', ['xml' => $xml]);
$this->logger->debug('Got request data.', ['request' => $request]);
$nickname = $this->parameters['nickname'] ?? '';
$contact_id = $this->parameters['cid'] ?? 0;
$importer = $this->database->selectFirst('user', [], ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!$importer) {
throw new HTTPException\OKException();
}
$condition = ['id' => $contact_id, 'uid' => $importer['uid'], 'subhub' => true, 'blocked' => false];
$contact = $this->database->selectFirst('contact', [], $condition);
if (!$contact) {
$author = OStatus::salmonAuthor($xml, $importer);
if (!empty($author['contact-id'])) {
$condition = ['id' => $author['contact-id'], 'uid' => $importer['uid'], 'subhub' => true, 'blocked' => false];
$contact = $this->database->selectFirst('contact', [], $condition);
$this->logger->notice('No record found for nickname, using author entry instead.', ['nickname' => $nickname, 'contact-id' => $contact_id, 'author-contact-id' => $author['contact-id']]);
}
if (!$contact) {
$this->logger->notice("Contact wasn't found - ignored.", ['author-link' => $author['author-link'], 'contact-id' => $contact_id, 'nickname' => $nickname, 'xml' => $xml]);
throw new HTTPException\OKException();
}
}
if (!empty($contact['gsid'])) {
GServer::setProtocol($contact['gsid'], Post\DeliveryData::OSTATUS);
}
if (!in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND]) && ($contact['network'] != Protocol::FEED)) {
$this->logger->notice('Contact is not expected to share with us - ignored.', ['contact-id' => $contact['id']]);
throw new HTTPException\OKException();
}
// We only import feeds from OStatus here
if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS])) {
$this->logger->warning('Unexpected network', ['contact' => $contact, 'network' => $contact['network']]);
throw new HTTPException\OKException();
}
$this->logger->info('Import item from Contact.', ['nickname' => $nickname, 'contact-nickname' => $contact['nick'], 'contact-id' => $contact['id']]);
$feedhub = '';
Item::incrementOutbound(Protocol::OSTATUS);
OStatus::import($xml, $importer, $contact, $feedhub);
throw new HTTPException\OKException();
}
protected function rawContent(array $request = [])
{
$nickname = $this->parameters['nickname'] ?? '';
$contact_id = $this->parameters['cid'] ?? 0;
$hub_mode = trim($request['hub_mode'] ?? '');
$hub_topic = trim($request['hub_topic'] ?? '');
$hub_challenge = trim($request['hub_challenge'] ?? '');
$hub_verify = trim($request['hub_verify_token'] ?? '');
$this->logger->notice('Subscription start.', ['from' => $this->request->getRemoteAddress(), 'mode' => $hub_mode, 'nickname' => $nickname]);
$this->logger->debug('Data: ', ['get' => $request]);
$owner = $this->database->selectFirst('user', ['uid'], ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!$owner) {
$this->logger->notice('Local account not found.', ['nickname' => $nickname]);
throw new HTTPException\NotFoundException();
}
$condition = ['uid' => $owner['uid'], 'id' => $contact_id, 'blocked' => false, 'pending' => false];
if (!empty($hub_verify)) {
$condition['hub-verify'] = $hub_verify;
}
$contact = $this->database->selectFirst('contact', ['id', 'poll'], $condition);
if (!$contact) {
$this->logger->notice('Contact not found.', ['contact' => $contact_id]);
throw new HTTPException\NotFoundException();
}
if (!empty($hub_topic) && !Strings::compareLink($hub_topic, $contact['poll'])) {
$this->logger->notice("Hub topic isn't valid for Contact.", ['hub_topic' => $hub_topic, 'contact_poll' => $contact['poll']]);
throw new HTTPException\NotFoundException();
}
// We must initiate an unsubscribe request with a verify_token.
// Don't allow outsiders to unsubscribe us.
if (($hub_mode === 'unsubscribe') && empty($hub_verify)) {
$this->logger->notice('Bogus unsubscribe');
throw new HTTPException\NotFoundException();
}
if (!empty($hub_mode)) {
Contact::update(['subhub' => $hub_mode === 'subscribe'], ['id' => $contact['id']]);
$this->logger->notice('Success for contact.', ['mode' => $hub_mode, 'contact' => $contact_id]);
}
$this->httpExit($hub_challenge);
}
}

View file

@ -1,165 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\OStatus;
use Friendica\App;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Database\Database;
use Friendica\Model\PushSubscriber;
use Friendica\Module\Response;
use Friendica\Network\HTTPClient\Capability\ICanSendHttpRequests;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\HTTPException;
use Friendica\Util\Profiler;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/**
* An open, simple, web-scale and decentralized pubsub protocol.
*
* Part of the OStatus stack.
*
* See https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html
*
* @version 0.4
*/
class PubSubHubBub extends \Friendica\BaseModule
{
/** @var IManageConfigValues */
private $config;
/** @var Database */
private $database;
/** @var ICanSendHttpRequests */
private $httpClient;
/** @var App\Request */
private $request;
public function __construct(App\Request $request, ICanSendHttpRequests $httpClient, Database $database, IManageConfigValues $config, 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->config = $config;
$this->database = $database;
$this->httpClient = $httpClient;
$this->request = $request;
}
protected function post(array $request = [])
{
// PuSH subscription must be considered "public" so just block it
// if public access isn't enabled.
if ($this->config->get('system', 'block_public')) {
throw new HTTPException\ForbiddenException();
}
$this->logger->debug('Got request data.', ['request' => $request]);
// Subscription request from subscriber
// https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.5.1
// Example from GNU Social:
// [hub_mode] => subscribe
// [hub_callback] => http://status.local/main/push/callback/1
// [hub_verify] => sync
// [hub_verify_token] => af11...
// [hub_secret] => af11...
// [hub_topic] => http://friendica.local/dfrn_poll/sazius
$hub_mode = $request['hub_mode'] ?? '';
$hub_callback = $request['hub_callback'] ?? '';
$hub_verify_token = $request['hub_verify_token'] ?? '';
$hub_secret = $request['hub_secret'] ?? '';
$hub_topic = $request['hub_topic'] ?? '';
// check for valid hub_mode
if ($hub_mode === 'subscribe') {
$subscribe = 1;
} elseif ($hub_mode === 'unsubscribe') {
$subscribe = 0;
} else {
$this->logger->notice('Invalid hub_mod - ignored.', ['mode' => $hub_mode]);
throw new HTTPException\NotFoundException();
}
$this->logger->info('hub_mode request details.', ['from' => $this->request->getRemoteAddress(), 'mode' => $hub_mode]);
$nickname = $this->parameters['nickname'] ?? $hub_topic;
// Extract nickname and strip any .atom extension
$nickname = basename($nickname, '.atom');
if (!$nickname) {
$this->logger->notice('Empty nick, ignoring.');
throw new HTTPException\NotFoundException();
}
// fetch user from database given the nickname
$condition = ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false];
$owner = $this->database->selectFirst('user', ['uid', 'nickname'], $condition);
if (!$owner) {
$this->logger->notice('Local account not found', ['nickname' => $nickname, 'topic' => $hub_topic, 'callback' => $hub_callback]);
throw new HTTPException\NotFoundException();
}
// get corresponding row from contact table
$condition = ['uid' => $owner['uid'], 'blocked' => false, 'pending' => false, 'self' => true];
$contact = $this->database->selectFirst('contact', ['poll'], $condition);
if (!$contact) {
$this->logger->notice('Self contact for user not found.', ['uid' => $owner['uid']]);
throw new HTTPException\NotFoundException();
}
// sanity check that topic URLs are the same
$hub_topic2 = str_replace('/feed/', '/dfrn_poll/', $hub_topic);
$self = $this->baseUrl . '/api/statuses/user_timeline/' . $owner['nickname'] . '.atom';
if (!Strings::compareLink($hub_topic, $contact['poll']) && !Strings::compareLink($hub_topic2, $contact['poll']) && !Strings::compareLink($hub_topic, $self)) {
$this->logger->notice('Hub topic invalid', ['hub_topic' => $hub_topic, 'poll' => $contact['poll']]);
throw new HTTPException\NotFoundException();
}
// do subscriber verification according to the PuSH protocol
$hub_challenge = Strings::getRandomHex(40);
$params = http_build_query([
'hub.mode' => $subscribe == 1 ? 'subscribe' : 'unsubscribe',
'hub.topic' => $hub_topic,
'hub.challenge' => $hub_challenge,
'hub.verify_token' => $hub_verify_token,
// lease time is hard coded to one week (in seconds)
// we don't actually enforce the lease time because GNU
// Social/StatusNet doesn't honour it (yet)
'hub.lease_seconds' => 604800,
]);
$hub_callback = rtrim($hub_callback, ' ?&#');
$separator = parse_url($hub_callback, PHP_URL_QUERY) === null ? '?' : '&';
$fetchResult = $this->httpClient->get($hub_callback . $separator . $params, HttpClientAccept::DEFAULT, [HttpClientOptions::REQUEST => HttpClientRequest::PUBSUB]);
$body = $fetchResult->getBodyString();
$returnCode = $fetchResult->getReturnCode();
// give up if the HTTP return code wasn't a success (2xx)
if ($returnCode < 200 || $returnCode > 299) {
$this->logger->notice('Subscriber verification ignored', ['hub_topic' => $hub_topic, 'callback' => $hub_callback, 'returnCode' => $returnCode]);
throw new HTTPException\NotFoundException();
}
// check that the correct hub_challenge code was echoed back
if (trim($body) !== $hub_challenge) {
$this->logger->notice('Subscriber did not echo back hub.challenge, ignoring.', ['hub_challenge' => $hub_challenge, 'body' => trim($body)]);
throw new HTTPException\NotFoundException();
}
PushSubscriber::renew($owner['uid'], $nickname, $subscribe, $hub_callback, $hub_topic, $hub_secret);
throw new HTTPException\AcceptedException();
}
}

View file

@ -1,79 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\OStatus;
use Friendica\App;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Module\Response;
use Friendica\Navigation\SystemMessages;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
class Repair extends \Friendica\BaseModule
{
/** @var IHandleUserSessions */
private $session;
/** @var SystemMessages */
private $systemMessages;
/** @var Database */
private $database;
/** @var App\Page */
private $page;
public function __construct(App\Page $page, Database $database, SystemMessages $systemMessages, IHandleUserSessions $session, 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->session = $session;
$this->systemMessages = $systemMessages;
$this->database = $database;
$this->page = $page;
}
protected function content(array $request = []): string
{
if (!$this->session->getLocalUserId()) {
$this->systemMessages->addNotice($this->t('Permission denied.'));
$this->baseUrl->redirect('login');
}
$uid = $this->session->getLocalUserId();
$counter = intval($request['counter'] ?? 0);
$condition = ['uid' => $uid, 'network' => Protocol::OSTATUS, 'rel' => [Contact::FRIEND, Contact::SHARING]];
$total = $this->database->count('contact', $condition);
if ($total) {
$contacts = Contact::selectToArray(['url'], $condition, ['order' => ['url'], 'limit' => [$counter++, 1]]);
if ($contacts) {
Contact::createFromProbeForUser($this->session->getLocalUserId(), $contacts[0]['url']);
$this->page['htmlhead'] .= '<meta http-equiv="refresh" content="5; url=ostatus/repair?counter=' . $counter . '">';
}
}
$tpl = Renderer::getMarkupTemplate('ostatus/repair.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'title' => $this->t('Resubscribing to OStatus contacts'),
'keep' => $this->t('Keep this window open until done.'),
'done' => $this->t('✔ Done'),
'nocontacts' => $this->t('No OStatus contacts to resubscribe to.'),
],
'$total' => $total,
'$counter' => $counter,
'$contact' => $contacts[0] ?? null,
]);
}
}

View file

@ -1,205 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
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\Item;
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 post(array $request = [])
{
$xml = Network::postdata();
$this->logger->debug('Got request data.', ['request' => $request]);
$nickname = $this->parameters['nickname'] ?? '';
if (empty($nickname)) {
throw new HTTPException\BadRequestException('nickname parameter is mandatory');
}
$this->logger->debug('New Salmon', ['nickname' => $nickname, 'xml' => $xml]);
$importer = $this->database->selectFirst('user', [], ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => 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();
}
$this->logger->info('Key details', ['info' => $key]);
$pubkey = SalmonProtocol::magicKeyToPem($key);
// 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 ?: [];
Item::incrementOutbound(Protocol::OSTATUS);
OStatus::import($data, $importer, $contact, $hub);
throw new HTTPException\OKException();
}
}