No salmon anymore

This commit is contained in:
Michael 2026-02-25 10:53:23 +00:00
commit 1bab29c9cc
10 changed files with 29 additions and 811 deletions

View file

@ -18,7 +18,6 @@ use Friendica\DI;
use Friendica\Core\Config\Factory\Config;
use Friendica\Module\BaseAdmin;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\Probe;
use Friendica\Util\DateTimeFormat;
class Summary extends BaseAdmin
@ -100,13 +99,13 @@ class Summary extends BaseAdmin
}
// Check server vitality
if (!self::checkSelfHostMeta()) {
$well_known = DI::baseUrl() . Probe::HOST_META;
if (!self::checkSelfNodeinfo()) {
$well_known = DI::baseUrl() . '/.well-known/nodeinfo';
$warningtext[] = DI::l10n()->t(
'<a href="%s">%s</a> is not reachable on your system. This is a severe configuration issue that prevents server to server communication. See <a href="%s">the installation page</a> for help.',
$well_known,
$well_known,
DI::baseUrl() . '/help/admin/install'
DI::baseUrl() . '/help/admin/install',
);
}
@ -133,7 +132,7 @@ class Summary extends BaseAdmin
$warningtext[] = DI::l10n()->t(
'Friendica\'s system.basepath was updated from \'%s\' to \'%s\'. Please remove the system.basepath from your db to avoid differences.',
$currBasepath,
$confBasepath
$confBasepath,
);
} elseif (!is_dir($currBasepath)) {
DI::logger()->alert('Friendica\'s system.basepath is wrong.', [
@ -143,7 +142,7 @@ class Summary extends BaseAdmin
$warningtext[] = DI::l10n()->t(
'Friendica\'s current system.basepath \'%s\' is wrong and the config file \'%s\' isn\'t used.',
$currBasepath,
$confBasepath
$confBasepath,
);
} else {
DI::logger()->alert('Friendica\'s system.basepath is wrong.', [
@ -153,7 +152,7 @@ class Summary extends BaseAdmin
$warningtext[] = DI::l10n()->t(
'Friendica\'s current system.basepath \'%s\' is not equal to the config file \'%s\'. Please fix your configuration.',
$currBasepath,
$confBasepath
$confBasepath,
);
}
}
@ -172,11 +171,11 @@ class Summary extends BaseAdmin
'php.ini' => php_ini_loaded_file(),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'memory_limit' => ini_get('memory_limit')
'memory_limit' => ini_get('memory_limit'),
],
'mysql' => [
'max_allowed_packet' => DBA::getVariable('max_allowed_packet'),
]
],
];
$addons = [];
@ -218,10 +217,10 @@ class Summary extends BaseAdmin
]);
}
private static function checkSelfHostMeta()
private static function checkSelfNodeinfo()
{
// Fetch the host-meta to check if this really is a vital server
return DI::httpClient()->get(DI::baseUrl() . Probe::HOST_META, HttpClientAccept::XRD_XML)->isSuccess();
// Fetch the webfinger to check if this really is a vital server
return DI::httpClient()->get(DI::baseUrl() . '/.well-known/nodeinfo', HttpClientAccept::JSON)->isSuccess();
}
}

View file

@ -27,7 +27,7 @@ class WebFinger extends BaseModule
$res = '';
if (!empty($addr)) {
$res = Probe::lrdd($addr);
$res = Probe::getWebfingerArray($addr);
$res = print_r($res, true);
}

View file

@ -1,72 +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\WellKnown;
use Friendica\BaseModule;
use Friendica\DI;
use Friendica\Module\Response;
use Friendica\Util\Crypto;
use Friendica\Util\XML;
/**
* Prints the metadata for describing this host
* @see https://tools.ietf.org/html/rfc6415
*/
class HostMeta extends BaseModule
{
protected function rawContent(array $request = [])
{
$config = DI::config();
if (!$config->get('system', 'site_pubkey', false)) {
$res = Crypto::newKeypair(1024);
$config->set('system', 'site_prvkey', $res['prvkey']);
$config->set('system', 'site_pubkey', $res['pubkey']);
}
$domain = (string)DI::baseUrl();
XML::fromArray([
'XRD' => [
'@attributes' => [
'xmlns' => 'http://docs.oasis-open.org/ns/xri/xrd-1.0',
],
'hm:Host' => DI::baseUrl()->getHost(),
'1:link' => [
'@attributes' => [
'rel' => 'lrdd',
'type' => 'application/xrd+xml',
'template' => $domain . '/xrd?uri={uri}'
]
],
'2:link' => [
'@attributes' => [
'rel' => 'lrdd',
'type' => 'application/json',
'template' => $domain . '/.well-known/webfinger?resource={uri}'
]
],
'3:link' => [
'@attributes' => [
'rel' => 'acct-mgmt',
'href' => $domain . '/amcd'
]
],
'4:link' => [
'@attributes' => [
'rel' => 'http://services.mozilla.com/amcd/0.1',
'href' => $domain . '/amcd'
]
],
],
], $xml, false, ['hm' => 'http://host-meta.net/xrd/1.0']);
$this->httpExit($xml->saveXML(), Response::TYPE_XML, 'application/xrd+xml');
}
}

View file

@ -28,7 +28,6 @@ use Friendica\Protocol\ATProtocol;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\Email;
use Friendica\Protocol\Feed;
use Friendica\Protocol\Salmon;
use Friendica\Util\Crypto;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\HTTPSignature;
@ -42,8 +41,8 @@ use GuzzleHttp\Psr7\Uri;
*/
class Probe
{
const HOST_META = '/.well-known/host-meta';
const WEBFINGER = '/.well-known/webfinger?resource={uri}';
public const HOST_META = '/.well-known/host-meta';
public const WEBFINGER = '/.well-known/webfinger?resource={uri}';
/**
* @var string Base URL
@ -105,7 +104,7 @@ class Probe
'account-type', 'community', 'keywords', 'location', 'about', 'xmpp', 'matrix',
'hide', 'batch', 'notify', 'poll', 'request', 'confirm', 'subscribe', 'poco',
'openwebauth', 'following', 'followers', 'inbox', 'outbox', 'sharedinbox',
'priority', 'network', 'pubkey', 'manually-approve', 'baseurl', 'gsid'
'priority', 'network', 'pubkey', 'manually-approve', 'baseurl', 'gsid',
];
$numeric_fields = ['gsid', 'account-type'];
@ -124,9 +123,9 @@ class Probe
foreach ($fields as $field) {
if (isset($data[$field])) {
if (in_array($field, $numeric_fields)) {
$newdata[$field] = (int)$data[$field];
$newdata[$field] = (int) $data[$field];
} elseif (in_array($field, $boolean_fields)) {
$newdata[$field] = (bool)$data[$field];
$newdata[$field] = (bool) $data[$field];
} else {
$newdata[$field] = trim($data[$field]);
}
@ -185,161 +184,6 @@ class Probe
return $parts['host'] == $own_host;
}
/**
* Probes for webfinger path via "host-meta"
*
* We have to check if the servers in the future still will offer this.
* It seems as if it was dropped from the standard.
*
* @param string $host The host part of an url
*
* @return array with template and type of the webfinger template for JSON or XML
* @throws HTTPException\InternalServerErrorException
*/
private static function hostMeta(string $host): array
{
// Reset the static variable
self::$baseurl = '';
// Handles the case when the hostname contains the scheme
if (!parse_url($host, PHP_URL_SCHEME)) {
$ssl_url = 'https://' . $host . self::HOST_META;
$url = 'http://' . $host . self::HOST_META;
} else {
$ssl_url = $host . self::HOST_META;
$url = '';
}
$xrd_timeout = DI::config()->get('system', 'xrd_timeout', 20);
DI::logger()->info('Probing', ['host' => $host, 'ssl_url' => $ssl_url, 'url' => $url]);
$xrd = null;
try {
$curlResult = DI::httpClient()->get($ssl_url, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]);
} catch (\Throwable $th) {
DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]);
return [];
}
$ssl_connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0);
$host_url = $host;
if ($curlResult->isSuccess()) {
$xml = $curlResult->getBodyString();
$xrd = XML::parseString($xml, true);
if (!empty($url)) {
$host_url = 'https://' . $host;
}
} elseif ($curlResult->isTimeout()) {
DI::logger()->info('Probing timeout', ['url' => $ssl_url]);
self::$isTimeout = true;
return [];
}
if ($ssl_connection_error && !is_object($xrd) && !empty($url)) {
try {
$curlResult = DI::httpClient()->get($url, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]);
} catch (\Throwable $th) {
DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]);
return [];
}
$connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0);
if ($curlResult->isTimeout()) {
DI::logger()->info('Probing timeout', ['url' => $url]);
self::$isTimeout = true;
return [];
} elseif ($connection_error && $ssl_connection_error) {
self::$isTimeout = true;
return [];
}
$xml = $curlResult->getBodyString();
$xrd = XML::parseString($xml, true);
$host_url = 'http://' . $host;
}
if (!is_object($xrd)) {
DI::logger()->info('No xrd object found', ['host' => $host]);
return [];
}
$links = XML::elementToArray($xrd);
if (!isset($links['xrd']['link'])) {
DI::logger()->info('No xrd data found', ['host' => $host]);
return [];
}
$lrdd = [];
foreach ($links['xrd']['link'] as $value => $link) {
if (!empty($link['@attributes'])) {
$attributes = $link['@attributes'];
} elseif ($value == '@attributes') {
$attributes = $link;
} else {
continue;
}
if (!empty($attributes['rel']) && $attributes['rel'] == 'lrdd' && !empty($attributes['template'])) {
$type = (empty($attributes['type']) ? '' : $attributes['type']);
$lrdd[$type] = $attributes['template'];
}
}
if (Network::isUrlBlocked($host_url)) {
DI::logger()->info('Domain is blocked', ['url' => $host]);
return [];
}
self::$baseurl = $host_url;
DI::logger()->info('Probing successful', ['host' => $host]);
return $lrdd;
}
/**
* Check an URI for LRDD data
*
* @param string $uri Address that should be probed
* @return array uri data
* @throws HTTPException\InternalServerErrorException
*/
public static function lrdd(string $uri): array
{
$data = self::getWebfingerArray($uri);
if (empty($data)) {
return [];
}
$webfinger = $data['webfinger'];
if (empty($webfinger['links'])) {
DI::logger()->info('No webfinger links found', ['uri' => $uri]);
return [];
}
$data = [];
foreach ($webfinger['links'] as $link) {
$data[] = ['@attributes' => $link];
}
if (!empty($webfinger['aliases']) && is_array($webfinger['aliases'])) {
foreach ($webfinger['aliases'] as $alias) {
$data[] = [
'@attributes' => [
'rel' => 'alias',
'href' => $alias,
]
];
}
}
return $data;
}
/**
* Fetch information (protocol endpoints and user information) about a given uri
*
@ -553,7 +397,6 @@ class Probe
public static function getWebfingerArray(string $uri): array
{
$parts = parse_url($uri);
$lrdd = [];
if (!empty($parts['scheme']) && !empty($parts['host'])) {
$host = $parts['host'];
@ -576,12 +419,9 @@ class Probe
}
$webfinger = self::getWebfinger($parts['scheme'] . '://' . $host . self::WEBFINGER, HttpClientAccept::JRD_JSON, $uri, $addr);
if (empty($webfinger) && !is_null($webfinger)) {
$lrdd = self::hostMeta($host);
}
if (empty($webfinger) && empty($lrdd)) {
while (empty($lrdd) && empty($webfinger) && (count($path_parts) > 1)) {
if (empty($webfinger)) {
while (empty($webfinger) && (count($path_parts) > 1)) {
$host .= '/' . array_shift($path_parts);
$baseurl = $parts['scheme'] . '://' . $host;
@ -590,12 +430,9 @@ class Probe
}
$webfinger = self::getWebfinger($parts['scheme'] . '://' . $host . self::WEBFINGER, HttpClientAccept::JRD_JSON, $uri, $addr);
if (empty($webfinger) && !is_null($webfinger)) {
$lrdd = self::hostMeta($host);
}
}
if (empty($lrdd) && empty($webfinger)) {
if (empty($webfinger)) {
return [];
}
}
@ -623,10 +460,6 @@ class Probe
}
if (empty($webfinger)) {
$lrdd = self::hostMeta($host);
if (self::$isTimeout) {
return [];
}
$baseurl = self::$baseurl;
}
} else {
@ -634,16 +467,6 @@ class Probe
return [];
}
if (empty($webfinger)) {
foreach ($lrdd as $type => $template) {
if ($webfinger) {
continue;
}
$webfinger = self::getWebfinger($template, $type, $uri, $addr);
}
}
if (empty($webfinger)) {
return [];
}
@ -786,12 +609,6 @@ class Probe
} else {
$result['networks'][Protocol::DIASPORA] = self::diaspora($webfinger);
}
if ((!$result && ($network == '')) || ($network == Protocol::OSTATUS)) {
$result = self::ostatus($webfinger);
}
if (in_array($network, ['', Protocol::ZOT])) {
$result = self::zot($webfinger, $result);
}
if (empty($result['network']) && empty($ap_profile['network']) || ($network == Protocol::FEED)) {
$result = self::feed($uri);
} else {
@ -825,177 +642,6 @@ class Probe
return $result;
}
/**
* Check for Zot contact
*
* @param array $webfinger Webfinger data
* @param array $data previously probed data
*
* @return array Zot data
* @throws HTTPException\InternalServerErrorException
*/
private static function zot(array $webfinger, array $data): array
{
$zot_url = '';
foreach ($webfinger['links'] as $link) {
if (($link['rel'] == 'http://purl.org/zot/protocol/6.0') && !empty($link['href'])) {
$zot_url = $link['href'];
}
}
if ($zot_url === '') {
return $data;
}
foreach ($webfinger['aliases'] as $alias) {
if (substr($alias, 0, 5) == 'acct:') {
$data['addr'] = substr($alias, 5);
}
}
if (!empty($webfinger['subject']) && (substr($webfinger['subject'], 0, 5) == 'acct:')) {
$data['addr'] = substr($webfinger['subject'], 5);
}
if (!empty($webfinger['properties'])) {
if (!empty($webfinger['properties']['http://webfinger.net/ns/name'])) {
$data['name'] = $webfinger['properties']['http://webfinger.net/ns/name'];
}
if (!empty($webfinger['properties']['http://xmlns.com/foaf/0.1/name'])) {
$data['name'] = $webfinger['properties']['http://xmlns.com/foaf/0.1/name'];
}
if (!empty($webfinger['properties']['https://w3id.org/security/v1#publicKeyPem'])) {
$data['pubkey'] = $webfinger['properties']['https://w3id.org/security/v1#publicKeyPem'];
}
if (empty($data['network']) && !empty($webfinger['properties']['http://purl.org/zot/federation'])) {
$networks = explode(',', $webfinger['properties']['http://purl.org/zot/federation']);
if (in_array('zot6', $networks)) {
$data['network'] = Protocol::ZOT;
}
}
}
foreach ($webfinger['links'] as $link) {
if (($link['rel'] == ActivityNamespace::WEBFINGERAVATAR) && !empty($link['href'])) {
$data['photo'] = $link['href'];
} elseif (($link['rel'] == 'http://openid.net/specs/connect/1.0/issuer') && !empty($link['href'])) {
$data['baseurl'] = trim($link['href'], '/');
} elseif (($link['rel'] == 'http://webfinger.net/rel/blog') && !empty($link['href'])) {
$data['url'] = $link['href'];
}
}
$data = self::pollZot($zot_url, $data);
if (!empty($data['url']) && !empty($webfinger['aliases']) && is_array($webfinger['aliases'])) {
foreach ($webfinger['aliases'] as $alias) {
if (Network::isValidHttpUrl($alias) && !Strings::compareLink($alias, $data['url'])) {
$data['alias'] = $alias;
}
}
}
return $data;
}
private static function pollZot(string $url, array $data): array
{
try {
$curlResult = DI::httpClient()->get($url, 'application/x-zot+json', [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]);
} catch (\Throwable $th) {
DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]);
return $data;
}
if ($curlResult->isTimeout()) {
return $data;
}
$content = $curlResult->getBodyString();
if (!$content) {
return $data;
}
$json = json_decode($content, true);
if (!is_array($json)) {
return $data;
}
if (empty($data['network'])) {
if (!empty($json['protocols']) && in_array('zot6', $json['protocols'])) {
$data['network'] = Protocol::ZOT;
}
}
if (!empty($json['public_key'])) {
$data['pubkey'] = $json['public_key'];
}
if (!empty($json['name'])) {
$data['name'] = $json['name'];
}
if (!empty($json['username'])) {
$data['nick'] = $json['username'];
}
if (!empty($json['photo']) && !empty($json['photo']['url'])) {
$data['photo'] = $json['photo']['url'];
}
if (!empty($json['locations'])) {
foreach ($json['locations'] as $location) {
if ($location['deleted'] || (parse_url($url, PHP_URL_HOST) != $location['host'])) {
continue;
}
if (!empty($location['address'])) {
$data['addr'] = $location['address'];
}
if (!empty($location['id_url'])) {
$data['url'] = $location['id_url'];
}
if (!empty($location['callback'])) {
$data['confirm'] = $location['callback'];
}
}
}
if (!empty($json['primary_location']) && !empty($json['primary_location']['connections_url'])) {
$data['poco'] = $json['primary_location']['connections_url'];
}
if (isset($json['searchable'])) {
$data['hide'] = !$json['searchable'];
}
if (!empty($json['public_forum'])) {
$data['community'] = $json['public_forum'];
$data['account-type'] = User::ACCOUNT_TYPE_COMMUNITY;
} elseif (($json['channel_type'] ?? '') == 'normal') {
$data['account-type'] = User::ACCOUNT_TYPE_PERSON;
}
if (!empty($json['profile'])) {
$profile = $json['profile'];
if (!empty($profile['description'])) {
$data['about'] = $profile['description'];
}
if (!empty($profile['keywords'])) {
$keywords = implode(', ', $profile['keywords']);
if (!empty($keywords)) {
$data['keywords'] = $keywords;
}
}
$loc = [];
if (!empty($profile['region'])) {
$loc['region'] = $profile['region'];
}
if (!empty($profile['country'])) {
$loc['country-name'] = $profile['country'];
}
$location = Profile::formatLocation($loc);
if (!empty($location)) {
$data['location'] = $location;
}
}
return $data;
}
/**
* Perform a webfinger request.
*
@ -1013,7 +659,7 @@ class Probe
$curlResult = DI::httpClient()->get(
$url,
$type,
[HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout', 20), HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]
[HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout', 20), HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO],
);
} catch (\Throwable $e) {
DI::logger()->notice($e->getMessage(), ['url' => $url, 'type' => $type, 'class' => get_class($e)]);
@ -1171,7 +817,7 @@ class Probe
}
if (isset($json['hide'])) {
$data['hide'] = (bool)$json['hide'];
$data['hide'] = (bool) $json['hide'];
} else {
$data['hide'] = false;
}
@ -1209,12 +855,6 @@ class Probe
$data['baseurl'] = trim($link['href'], '/');
} elseif (($link['rel'] == ActivityNamespace::DIASPORA_GUID) && !empty($link['href'])) {
$data['guid'] = $link['href'];
} elseif (($link['rel'] == 'diaspora-public-key') && !empty($link['href'])) {
$data['pubkey'] = base64_decode($link['href']);
if (strstr($data['pubkey'], 'RSA ')) {
$data['pubkey'] = Crypto::rsaToPem($data['pubkey']);
}
}
}
@ -1389,31 +1029,17 @@ class Probe
$hcard_url = $link['href'];
} elseif (($link['rel'] == ActivityNamespace::DIASPORA_SEED) && !empty($link['href'])) {
$data['baseurl'] = trim($link['href'], '/');
} elseif (($link['rel'] == ActivityNamespace::DIASPORA_GUID) && !empty($link['href'])) {
$data['guid'] = $link['href'];
} elseif (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && (($link['type'] ?? '') == 'text/html') && !empty($link['href'])) {
} elseif (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && !empty($link['href'])) {
$data['url'] = $link['href'];
} elseif (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && empty($link['type']) && !empty($link['href'])) {
$profile_url = $link['href'];
} elseif (($link['rel'] == ActivityNamespace::FEED) && !empty($link['href'])) {
$data['poll'] = $link['href'];
} elseif (($link['rel'] == ActivityNamespace::POCO) && !empty($link['href'])) {
$data['poco'] = $link['href'];
} elseif (($link['rel'] == 'salmon') && !empty($link['href'])) {
$data['notify'] = $link['href'];
} elseif (($link['rel'] == 'diaspora-public-key') && !empty($link['href'])) {
$data['pubkey'] = base64_decode($link['href']);
if (strstr($data['pubkey'], 'RSA ')) {
$data['pubkey'] = Crypto::rsaToPem($data['pubkey']);
}
}
}
if (empty($data['url']) && !empty($profile_url)) {
$data['url'] = $profile_url;
}
if (empty($data['url']) || empty($hcard_url)) {
return [];
}
@ -1464,138 +1090,6 @@ class Probe
return $data;
}
/**
* Check for OStatus contact
*
* @param array $webfinger Webfinger data
* @param bool $short Short detection mode
*
* @return array|bool OStatus data or "false" on error or "true" on short mode
* @throws HTTPException\InternalServerErrorException
*/
private static function ostatus(array $webfinger, bool $short = false)
{
$data = [];
if (!empty($webfinger['aliases']) && is_array($webfinger['aliases'])) {
foreach ($webfinger['aliases'] as $alias) {
if (strstr($alias, '@') && !Network::isValidHttpUrl($alias)) {
$data['addr'] = str_replace('acct:', '', $alias);
}
}
}
if (
!empty($webfinger['subject']) && strstr($webfinger['subject'], '@')
&& !Network::isValidHttpUrl($webfinger['subject'])
) {
$data['addr'] = str_replace('acct:', '', $webfinger['subject']);
}
if (!empty($webfinger['links'])) {
// The array is reversed to take into account the order of preference for same-rel links
// See: https://tools.ietf.org/html/rfc7033#section-4.4.4
foreach (array_reverse($webfinger['links']) as $link) {
if (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE)
&& (($link['type'] ?? '') == 'text/html')
&& ($link['href'] != '')
) {
$data['url'] = $data['alias'] = $link['href'];
} elseif (($link['rel'] == 'salmon') && !empty($link['href'])) {
$data['notify'] = $link['href'];
} elseif (($link['rel'] == ActivityNamespace::FEED) && !empty($link['href'])) {
$data['poll'] = $link['href'];
} elseif (($link['rel'] == 'magic-public-key') && !empty($link['href'])) {
$pubkey = $link['href'];
if (substr($pubkey, 0, 5) === 'data:') {
if (strstr($pubkey, ',')) {
$pubkey = substr($pubkey, strpos($pubkey, ',') + 1);
} else {
$pubkey = substr($pubkey, 5);
}
} elseif (Strings::normaliseLink($pubkey) == 'http://') {
$curlResult = DI::httpClient()->get($pubkey, HttpClientAccept::MAGIC_KEY, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]);
if ($curlResult->isTimeout()) {
self::$isTimeout = true;
return $short ? false : [];
}
DI::logger()->debug('Fetched public key', ['Content-Type' => $curlResult->getHeader('Content-Type'), 'url' => $pubkey]);
$pubkey = $curlResult->getBodyString();
}
try {
$data['pubkey'] = Salmon::magicKeyToPem($pubkey);
} catch (\Throwable $e) {
}
}
}
}
if (
isset($data['notify']) && isset($data['pubkey'])
&& isset($data['poll'])
&& isset($data['url'])
) {
$data['network'] = Protocol::OSTATUS;
$data['manually-approve'] = false;
} else {
return $short ? false : [];
}
if ($short) {
return true;
}
// Fetch all additional data from the feed
try {
$curlResult = DI::httpClient()->get($data['poll'], HttpClientAccept::FEED_XML, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]);
} catch (\Throwable $th) {
DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]);
return [];
}
if ($curlResult->isTimeout()) {
self::$isTimeout = true;
return [];
}
$feed = $curlResult->getBodyString();
$feed_data = Feed::import($feed);
if (!$feed_data) {
return [];
}
if (!empty($feed_data['header']['author-name'])) {
$data['name'] = $feed_data['header']['author-name'];
}
if (!empty($feed_data['header']['author-nick'])) {
$data['nick'] = $feed_data['header']['author-nick'];
}
if (!empty($feed_data['header']['author-avatar'])) {
$data['photo'] = self::fixAvatar($feed_data['header']['author-avatar'], $data['url']);
}
if (!empty($feed_data['header']['author-id'])) {
$data['alias'] = $feed_data['header']['author-id'];
}
if (!empty($feed_data['header']['author-location'])) {
$data['location'] = $feed_data['header']['author-location'];
}
if (!empty($feed_data['header']['author-about'])) {
$data['about'] = $feed_data['header']['author-about'];
}
// OStatus has serious issues when the url doesn't fit (ssl vs. non ssl)
// So we take the value that we just fetched, although the other one worked as well
if (!empty($feed_data['header']['author-link'])) {
$data['url'] = $feed_data['header']['author-link'];
}
if ($data['url'] == $data['alias']) {
$data['alias'] = '';
}
/// @todo Fetch location and "about" from the feed as well
return $data;
}
/**
* Checks HTML page for RSS feed link
*
@ -1691,7 +1185,7 @@ class Probe
unset($baseParts['query']);
unset($baseParts['fragment']);
return (string)Uri::fromParts((array)(array)$baseParts);
return (string) Uri::fromParts((array) (array) $baseParts);
}
/**
@ -1930,7 +1424,7 @@ class Probe
* @return string fixed avatar path
* @throws \Exception
*/
public static function fixAvatar(string $avatar, string $base): string
private static function fixAvatar(string $avatar, string $base): string
{
$base_parts = parse_url($base);
@ -2007,7 +1501,7 @@ class Probe
// Check the 'noscrape' endpoint when it is a Friendica server
$gserver = DBA::selectFirst('gserver', ['noscrape'], [
"`nurl` = ? AND `noscrape` != ''",
Strings::normaliseLink($data['baseurl'])
Strings::normaliseLink($data['baseurl']),
]);
if (!DBA::isResult($gserver)) {
return '';
@ -2203,14 +1697,14 @@ class Probe
'poco' => $owner['poco'],
'network' => Protocol::DIASPORA,
'pubkey' => $owner['upubkey'],
]
]
],
],
];
} catch (Exception $e) {
// Default values for nonexistent targets
$data = [
'name' => $url, 'nick' => $url, 'url' => $url, 'network' => Protocol::PHANTOM,
'photo' => DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO
'photo' => DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO,
];
}

View file

@ -1,43 +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\Protocol;
use Friendica\Protocol\Salmon\Format\Magic;
use phpseclib3\Crypt\PublicKeyLoader;
/**
* Salmon Protocol class
*
* The Salmon Protocol is a message exchange protocol running over HTTP designed to decentralize commentary
* and annotations made against newsfeed articles such as blog posts.
*/
class Salmon
{
/**
* @param string $pubkey public key
* @return string
* @throws \Exception
*/
public static function salmonKey(string $pubkey): string
{
\phpseclib3\Crypt\RSA::addFileFormat(Magic::class);
return PublicKeyLoader::load($pubkey)->toString('Magic');
}
/**
* @param string $magic Magic key format starting with "RSA."
* @return string
*/
public static function magicKeyToPem(string $magic): string
{
\phpseclib3\Crypt\RSA::addFileFormat(Magic::class);
return (string) PublicKeyLoader::load($magic);
}
}

View file

@ -1,63 +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\Protocol\Salmon\Format;
use Friendica\Util\Strings;
use phpseclib3\Math\BigInteger;
/**
* This custom public RSA key format class is meant to be used with the \phpseclib3\Crypto\RSA::addFileFormat method.
*
* It handles Salmon's specific magic key string starting with "RSA." and which MIME type is application/magic-key or
* application/magic-public-key
*
* @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#anchor13
*/
class Magic
{
public static function load($key, $password = ''): array
{
if (!is_string($key)) {
throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
}
$key_info = explode('.', $key);
if (count($key_info) !== 3) {
throw new \UnexpectedValueException('Key should have three components separated by periods');
}
if ($key_info[0] !== 'RSA') {
throw new \UnexpectedValueException('Key first component should be "RSA"');
}
if (preg_match('#[+/]#', $key_info[1])
|| preg_match('#[+/]#', $key_info[1])
) {
throw new \UnexpectedValueException('Wrong encoding, expecting Base64URLencoding');
}
$m = Strings::base64UrlDecode($key_info[1]);
$e = Strings::base64UrlDecode($key_info[2]);
if (!$m || !$e) {
throw new \UnexpectedValueException('Base64 decoding produced an error');
}
return [
'modulus' => new BigInteger($m, 256),
'publicExponent' => new BigInteger($e, 256),
'isPublicKey' => true,
];
}
public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = []): string
{
return 'RSA.' . Strings::base64UrlEncode($n->toBytes(), true) . '.' . Strings::base64UrlEncode($e->toBytes(), true);
}
}

View file

@ -161,7 +161,6 @@ return [
'/' => [Module\Home::class, [R::GET]],
'/.well-known' => [
'/host-meta' => [Module\WellKnown\HostMeta::class, [R::GET]],
'/nodeinfo' => [Module\WellKnown\NodeInfo::class, [R::GET]],
'/security.txt' => [Module\WellKnown\SecurityTxt::class, [R::GET]],
'/webfinger' => [Module\Xrd::class, [R::GET]],

View file

@ -1 +0,0 @@
RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB

View file

@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb7KAWWy1L6lrPtHfAuYVUC4ywo48cm
W9e0ZvP/RQ6gWFIoAUhQ3CQsxtuxTPs7nXcKYCQdLw7jykJ7efZCkbMCAwEAAQ==
-----END PUBLIC KEY-----

View file

@ -1,91 +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\Test\src\Protocol;
use Friendica\Protocol\Salmon;
class SalmonTest extends \PHPUnit\Framework\TestCase
{
public function dataMagic(): array
{
return [
'salmon' => [
'magic' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/salmon-public-magic'),
'pem' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/salmon-public-pem'),
],
];
}
/**
* @dataProvider dataMagic
*
* @param $magic
* @param $pem
* @return void
* @throws \Exception
*/
public function testSalmonKey($magic, $pem)
{
$this->assertEquals($magic, Salmon::salmonKey($pem));
}
/**
* @dataProvider dataMagic
*
* @param $magic
* @param $pem
* @return void
*/
public function testMagicKeyToPem($magic, $pem)
{
$this->assertEquals($pem, Salmon::magicKeyToPem($magic));
}
public function dataMagicFailure(): array
{
return [
'empty string' => [
'magic' => '',
],
'Missing algo' => [
'magic' => 'tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB',
],
'Missing modulus' => [
'magic' => 'RSA.AQAB',
],
'Missing exponent' => [
'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw',
],
'Missing key parts' => [
'magic' => 'RSA.',
],
'Too many parts' => [
'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB.AQAB',
],
'Wrong encoding' => [
'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8/9FDqBYUigBSFDcJCzG27FM+zuddwpgJB0vDuPKQnt59kKRsw.AQAB',
],
'Wrong algo' => [
'magic' => 'ECDSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB',
],
];
}
/**
* @dataProvider dataMagicFailure
*
* @param $magic
* @return void
*/
public function testMagicKeyToPemFailure($magic)
{
$this->expectException(\Throwable::class);
Salmon::magicKeyToPem($magic);
}
}