forked from grumpydevelop/federator
added proper application-signing
- we now check whether the request was sent from contentnation and approve the request with contentnations pub key - fix issue where inbox wasn't gotten as it also got a username - rough initial draft of parsing of connector-specific data (plugins/federator/contentnation::jsonToActivity) - added key paths and headername to config
This commit is contained in:
parent
0edc4c0db6
commit
352377887c
10 changed files with 147 additions and 29 deletions
|
@ -13,10 +13,15 @@ compiledir = '../cache'
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
rediscache = 'rediscache.php'
|
rediscache = 'rediscache.php'
|
||||||
dummy = 'dummyconnector.php'
|
# dummy = 'dummyconnector.php'
|
||||||
contentnation = 'contentnation.php'
|
contentnation = 'contentnation.php'
|
||||||
mastodon = 'mastodon.php'
|
|
||||||
|
|
||||||
[maintenance]
|
[maintenance]
|
||||||
username = 'federatoradmin'
|
username = 'federatoradmin'
|
||||||
password = '*change*me*as*well'
|
password = '*change*me*as*well'
|
||||||
|
|
||||||
|
[keys]
|
||||||
|
headerSenderName = 'contentnation'
|
||||||
|
contentnationPublicKeyPath = '../contentnation.pub'
|
||||||
|
federatorPrivateKeyPath = '../federator.key'
|
||||||
|
federatorPublicKeyPath = '../federator.pub'
|
14
contentnation.pub
Normal file
14
contentnation.pub
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArTNHQL76ZuM7meWvtfHC
|
||||||
|
DOAivi9D4m2u3JdgN2R/cMa4+U4jURVJ1BZBesVkW8bS7AhBpFjAOUSNDsvsB7Yf
|
||||||
|
mxUa8vKD7GgTLQPUhP10EeEZz+R/onDlTU7TCDVd1PDdQwlx/2aT+m7K2KwmnOC5
|
||||||
|
ZUO0jO7EtXn4qhA1qt8oRFRogQlzvMbLr6lYhkomBxn4XqezbtDw+HQjJ2Af5ECR
|
||||||
|
+tElbkEZWpEG/fwJvv1hhqqDToloOkK2YhMTiOZFesafH+AFQq1pYx6hFoa6TIkG
|
||||||
|
8aLoLuPY+IBZqXtPLyh1cIiBYqAiyo0lIHjtKjWnPbXhu83EZ3VOvpbopYonSCOX
|
||||||
|
0uHsbf1fn9NGhe5TSSxbz0SAGJgZTr2VvHinqZ0k3me4CS/HUzkvOtMdVtwJdqLp
|
||||||
|
N/pUfGRjeiDbO5JvOMrimUP3klVB54Nf0IIw7aMhD3yO7KGoxRIV89H6i5TJF3zY
|
||||||
|
WeirON6ejHapNw6WCWL7YY5WDsGuiMIuAcfwYAcsaqaKYktjqJZT1hejJNVqLhSU
|
||||||
|
ZaVBcl56/VO9lPoC8u7NXFfnT4h3bIfL8Ft3riabQzFSMXjLI2Q27BG7R5xNEo+u
|
||||||
|
aG2STkjKam/+q5VeUnpg1F0HBz/QuZ+GcsItD8uN+IjN9yZhJjryRwhn2KcaPIYz
|
||||||
|
upOsIXACYAm0kfqNRK5qdwECAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----
|
14
federator.pub
Normal file
14
federator.pub
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6kZOjPKQjQIo5dugu80e
|
||||||
|
gsQXPkWhGjSUgbJ5UNwcyFto4p2euhVqVnTVOCWeS9+dPQP14fuVowODceaZLmGg
|
||||||
|
sBqraBZ4FNb76ByBdfiqDmPvUP61hrdDCZ52IPMYq7e3knWVakGouSqyoa/TVf3l
|
||||||
|
5oa7qgYnRDvHQXkA51Dj/1BqW57WeBQzEd5nwFhhAKZuVLxC/+xEu6Ohf+6WC2qR
|
||||||
|
Dz/toI26A3QrMCgmt21ELxjTyNmUdTL6U8PjutiMZJ2sy5uhR7stRNzoWt0AnRJE
|
||||||
|
1NlwPU8tKpfXAv00zxTS4xuLt0zv2lNSSRfECeM2g86fXuhMB0NYd30Mgda+Svbu
|
||||||
|
MEFvOkB5xEAi1NRamETV9Ci/LBqShC1ZBcY5QdikH4S0awIsQA3YMsK0y4+gCY1S
|
||||||
|
oHwFjR+KhiGKBa4NaKsfFy3JL5OB6+8PF6z2ICbD26X1jJy9ScLHrljd/AKVNtXE
|
||||||
|
Jaz2NDrqmqdjCILxROTle5aNnOfpaAMmiszIWmZNuCWRBbrpVXPeOR3D+qLEld3u
|
||||||
|
z2l/i2ywfNtt0VrMhKMWjT99aPOHyMvInuZGYx2RVhzYyf5h3V6FCoD67ihInbCa
|
||||||
|
SfDGHKhEa6gQaIIZi2EfY2QbYbZG/4gX9BHfUlTYMoFgW5P2qS1c27tTi/1LkJKx
|
||||||
|
CZWiL/7VWZ/nx94SQPL76k0CAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -195,12 +195,11 @@ class Api extends Main
|
||||||
/**
|
/**
|
||||||
* check if the headers include a valid signature
|
* check if the headers include a valid signature
|
||||||
*
|
*
|
||||||
* @param string[] $headers
|
* @param string[] $headers the headers
|
||||||
* permission(s) to check for
|
|
||||||
* @throws Exceptions\PermissionDenied
|
* @throws Exceptions\PermissionDenied
|
||||||
* @return string|Exceptions\PermissionDenied
|
* @return string|Exceptions\PermissionDenied
|
||||||
*/
|
*/
|
||||||
public static function checkSignature($headers)
|
public function checkSignature($headers)
|
||||||
{
|
{
|
||||||
$signatureHeader = $headers['Signature'] ?? null;
|
$signatureHeader = $headers['Signature'] ?? null;
|
||||||
|
|
||||||
|
@ -209,13 +208,19 @@ class Api extends Main
|
||||||
throw new Exceptions\PermissionDenied("Missing Signature header");
|
throw new Exceptions\PermissionDenied("Missing Signature header");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$config = $this->getConfig();
|
||||||
|
|
||||||
// Parse Signature header
|
// Parse Signature header
|
||||||
preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
|
preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
|
||||||
$signatureParts = array_combine($matches[1], $matches[2]);
|
$signatureParts = array_combine($matches[1], $matches[2]);
|
||||||
|
|
||||||
$signature = base64_decode($signatureParts['signature']);
|
$signature = base64_decode($signatureParts['signature']);
|
||||||
$keyId = $signatureParts['keyId'];
|
|
||||||
$signedHeaders = explode(' ', $signatureParts['headers']);
|
$signedHeaders = explode(' ', $signatureParts['headers']);
|
||||||
|
if (isset($headers['X-Sender']) && $headers['X-Sender'] === $config['keys']['headerSenderName']) {
|
||||||
|
$pKeyPath = $_SERVER['DOCUMENT_ROOT'] . $config['keys']['contentnationPublicKeyPath'];
|
||||||
|
$publicKeyPem = file_get_contents($pKeyPath);
|
||||||
|
} else {
|
||||||
|
$keyId = $signatureParts['keyId'];
|
||||||
|
|
||||||
// Fetch public key from `keyId` (usually actor URL + #main-key)
|
// Fetch public key from `keyId` (usually actor URL + #main-key)
|
||||||
[$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
|
[$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
|
||||||
|
@ -232,10 +237,11 @@ class Api extends Main
|
||||||
}
|
}
|
||||||
|
|
||||||
$publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
|
$publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isset($publicKeyPem)) {
|
if (!isset($publicKeyPem) || $publicKeyPem === false) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
throw new Exceptions\PermissionDenied("Invalid public key format from actor with keyId: $keyId");
|
throw new Exceptions\PermissionDenied("Public key couldn't be determined");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconstruct the signed string
|
// Reconstruct the signed string
|
||||||
|
@ -251,6 +257,7 @@ class Api extends Main
|
||||||
|
|
||||||
$signedString .= strtolower($header) . ": " . $headerValue . "\n";
|
$signedString .= strtolower($header) . ": " . $headerValue . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
$signedString = rtrim($signedString);
|
$signedString = rtrim($signedString);
|
||||||
|
|
||||||
// Verify the signature
|
// Verify the signature
|
||||||
|
@ -261,12 +268,11 @@ class Api extends Main
|
||||||
}
|
}
|
||||||
if ($verified != 1) {
|
if ($verified != 1) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
throw new Exceptions\PermissionDenied("Signature verification failed for publicKey with keyId: $keyId");
|
throw new Exceptions\PermissionDenied("Signature verification failed for publicKey");
|
||||||
}
|
}
|
||||||
|
|
||||||
$actorId = $actor['id'];
|
|
||||||
// Signature is valid!
|
// Signature is valid!
|
||||||
return "Signature verified from actor: " . $actorId;
|
return "Signature verified.";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -69,6 +69,7 @@ class FedUsers implements APIInterface
|
||||||
} else {
|
} else {
|
||||||
switch ($paths[1]) {
|
switch ($paths[1]) {
|
||||||
case 'inbox':
|
case 'inbox':
|
||||||
|
$_username = NULL;
|
||||||
$handler = new FedUsers\Inbox($this->main);
|
$handler = new FedUsers\Inbox($this->main);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -106,14 +106,18 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
|
$input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
|
||||||
$host = $_SERVER['SERVER_NAME'];
|
$host = $_SERVER['SERVER_NAME'];
|
||||||
|
if (!is_array($input)) {
|
||||||
if (!is_array($activity)) {
|
|
||||||
error_log("Outbox::post Input wasn't of type array");
|
error_log("Outbox::post Input wasn't of type array");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$outboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
|
||||||
|
if (isset($allHeaders['X-Sender']) && $allHeaders['X-Sender'] === $this->main->getConfig()['keys']['headerSenderName']) {
|
||||||
|
$outboxActivity = $this->main->getConnector()->jsonToActivity($input);
|
||||||
|
} else {
|
||||||
|
$outboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($input);
|
||||||
|
}
|
||||||
|
|
||||||
if ($outboxActivity === false) {
|
if ($outboxActivity === false) {
|
||||||
error_log("Outbox::post couldn't create outboxActivity");
|
error_log("Outbox::post couldn't create outboxActivity");
|
||||||
|
@ -142,9 +146,20 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (empty($users)) { // todo remove, debugging for now
|
||||||
|
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
|
||||||
|
// Save the raw input and parsed JSON to a file for inspection
|
||||||
|
file_put_contents(
|
||||||
|
$rootDir . 'logs/outbox.log',
|
||||||
|
date('Y-m-d H:i:s') . ": ==== POST Outbox Activity ====\n" . json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
||||||
|
FILE_APPEND
|
||||||
|
);
|
||||||
|
}
|
||||||
if ($_user !== false && !in_array($_user, $users, true)) {
|
if ($_user !== false && !in_array($_user, $users, true)) {
|
||||||
$users[] = $_user;
|
$users[] = $_user;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
if (!isset($user)) {
|
if (!isset($user)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -152,6 +167,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
|
|
||||||
$this->postForUser($user, $outboxActivity);
|
$this->postForUser($user, $outboxActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
return json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,4 +56,12 @@ interface Connector
|
||||||
* @return \Federator\Data\Stats|false
|
* @return \Federator\Data\Stats|false
|
||||||
*/
|
*/
|
||||||
public function getRemoteStats();
|
public function getRemoteStats();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert jsonData to Activity format
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $jsonData the json data from our platfrom
|
||||||
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
|
*/
|
||||||
|
public function jsonToActivity(array $jsonData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -329,6 +329,39 @@ class ContentNation implements Connector
|
||||||
}
|
}
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert jsonData to Activity format
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $jsonData the json data from our platfrom
|
||||||
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
|
*/
|
||||||
|
public function jsonToActivity(array $jsonData)
|
||||||
|
{
|
||||||
|
$ap = [
|
||||||
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'type' => 'Create',
|
||||||
|
'id' => $jsonData['id'] ?? null,
|
||||||
|
'actor' => $jsonData['actor']['id'] ?? null,
|
||||||
|
'published' => $jsonData['object']['published'] ?? null,
|
||||||
|
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
'cc' => [$jsonData['related']['cc']['followers'] ?? null],
|
||||||
|
'object' => [
|
||||||
|
'type' => 'Note',
|
||||||
|
'id' => $jsonData['object']['id'] ?? null,
|
||||||
|
'summary' => $jsonData['object']['summary'] ?? '',
|
||||||
|
'content' => $jsonData['object']['content'] ?? '',
|
||||||
|
'published' => $jsonData['object']['published'] ?? null,
|
||||||
|
'attributedTo' => $jsonData['actor']['id'] ?? null,
|
||||||
|
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
'cc' => [$jsonData['related']['cc']['followers'] ?? null],
|
||||||
|
'url' => $jsonData['object']['url'] ?? null,
|
||||||
|
'inReplyTo' => $jsonData['related']['article']['id'] ?? null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Federator;
|
namespace Federator;
|
||||||
|
|
|
@ -57,6 +57,16 @@ class DummyConnector implements Connector
|
||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert jsonData to Activity format
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
||||||
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
|
*/
|
||||||
|
public function jsonToActivity(array $jsonData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get remote user by name
|
* get remote user by name
|
||||||
* @param string $_name user or profile name
|
* @param string $_name user or profile name
|
||||||
|
|
|
@ -90,6 +90,17 @@ class RedisCache implements Cache
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert jsonData to Activity format
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
||||||
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
|
*/
|
||||||
|
public function jsonToActivity(array $jsonData) {
|
||||||
|
error_log("rediscache::jsonToActivity not implemented");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
*
|
*
|
||||||
|
|
Loading…
Add table
Reference in a new issue