mirror of
https://github.com/friendica/friendica
synced 2026-03-17 03:29:03 +01:00
No salmon anymore
This commit is contained in:
parent
574a8eeb36
commit
1bab29c9cc
10 changed files with 29 additions and 811 deletions
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]],
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb7KAWWy1L6lrPtHfAuYVUC4ywo48cm
|
||||
W9e0ZvP/RQ6gWFIoAUhQ3CQsxtuxTPs7nXcKYCQdLw7jykJ7efZCkbMCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue