diff --git a/config.ini b/config.ini index 7411f48..5ade62e 100644 --- a/config.ini +++ b/config.ini @@ -13,10 +13,15 @@ compiledir = '../cache' [plugins] rediscache = 'rediscache.php' -dummy = 'dummyconnector.php' +# dummy = 'dummyconnector.php' contentnation = 'contentnation.php' -mastodon = 'mastodon.php' [maintenance] username = 'federatoradmin' -password = '*change*me*as*well' \ No newline at end of file +password = '*change*me*as*well' + +[keys] +headerSenderName = 'contentnation' +contentnationPublicKeyPath = '../contentnation.pub' +federatorPrivateKeyPath = '../federator.key' +federatorPublicKeyPath = '../federator.pub' \ No newline at end of file diff --git a/contentnation.pub b/contentnation.pub new file mode 100644 index 0000000..816fbc8 --- /dev/null +++ b/contentnation.pub @@ -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----- diff --git a/federator.pub b/federator.pub new file mode 100644 index 0000000..fa6d493 --- /dev/null +++ b/federator.pub @@ -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----- diff --git a/php/federator/api.php b/php/federator/api.php index d6c07b1..bb5679c 100644 --- a/php/federator/api.php +++ b/php/federator/api.php @@ -195,12 +195,11 @@ class Api extends Main /** * check if the headers include a valid signature * - * @param string[] $headers - * permission(s) to check for + * @param string[] $headers the headers * @throws Exceptions\PermissionDenied * @return string|Exceptions\PermissionDenied */ - public static function checkSignature($headers) + public function checkSignature($headers) { $signatureHeader = $headers['Signature'] ?? null; @@ -209,33 +208,40 @@ class Api extends Main throw new Exceptions\PermissionDenied("Missing Signature header"); } + $config = $this->getConfig(); + // Parse Signature header preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches); $signatureParts = array_combine($matches[1], $matches[2]); $signature = base64_decode($signatureParts['signature']); - $keyId = $signatureParts['keyId']; $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) - [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']); + // Fetch public key from `keyId` (usually actor URL + #main-key) + [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']); - if ($info['http_code'] != 200) { - http_response_code(500); - throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId"); + if ($info['http_code'] != 200) { + http_response_code(500); + throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId"); + } + + $actor = json_decode($publicKeyData, true); + + if (!is_array($actor) || !isset($actor['id'])) { + throw new Exceptions\PermissionDenied("Invalid actor data"); + } + + $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null; } - $actor = json_decode($publicKeyData, true); - - if (!is_array($actor) || !isset($actor['id'])) { - throw new Exceptions\PermissionDenied("Invalid actor data"); - } - - $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null; - - if (!isset($publicKeyPem)) { + if (!isset($publicKeyPem) || $publicKeyPem === false) { 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 @@ -251,6 +257,7 @@ class Api extends Main $signedString .= strtolower($header) . ": " . $headerValue . "\n"; } + $signedString = rtrim($signedString); // Verify the signature @@ -261,12 +268,11 @@ class Api extends Main } if ($verified != 1) { 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! - return "Signature verified from actor: " . $actorId; + return "Signature verified."; } /** diff --git a/php/federator/api/fedusers.php b/php/federator/api/fedusers.php index f3d1199..026164c 100644 --- a/php/federator/api/fedusers.php +++ b/php/federator/api/fedusers.php @@ -69,6 +69,7 @@ class FedUsers implements APIInterface } else { switch ($paths[1]) { case 'inbox': + $_username = NULL; $handler = new FedUsers\Inbox($this->main); break; default: diff --git a/php/federator/api/fedusers/outbox.php b/php/federator/api/fedusers/outbox.php index 76f296f..7bd7450 100644 --- a/php/federator/api/fedusers/outbox.php +++ b/php/federator/api/fedusers/outbox.php @@ -106,14 +106,18 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface return false; } - $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null; + $input = is_string($_rawInput) ? json_decode($_rawInput, true) : null; $host = $_SERVER['SERVER_NAME']; - - if (!is_array($activity)) { + if (!is_array($input)) { error_log("Outbox::post Input wasn't of type array"); 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) { 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)) { $users[] = $_user; } + foreach ($users as $user) { if (!isset($user)) { continue; @@ -152,6 +167,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface $this->postForUser($user, $outboxActivity); } + return json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } diff --git a/php/federator/connector/connector.php b/php/federator/connector/connector.php index 77ac69b..abee7eb 100644 --- a/php/federator/connector/connector.php +++ b/php/federator/connector/connector.php @@ -56,4 +56,12 @@ interface Connector * @return \Federator\Data\Stats|false */ public function getRemoteStats(); + + /** + * Convert jsonData to Activity format + * + * @param array $jsonData the json data from our platfrom + * @return \Federator\Data\ActivityPub\Common\Activity|false + */ + public function jsonToActivity(array $jsonData); } diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index 2705dfa..6afe12e 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -329,6 +329,39 @@ class ContentNation implements Connector } return $user; } + + /** + * Convert jsonData to Activity format + * + * @param array $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; diff --git a/plugins/federator/dummyconnector.php b/plugins/federator/dummyconnector.php index 822c27b..c621baf 100644 --- a/plugins/federator/dummyconnector.php +++ b/plugins/federator/dummyconnector.php @@ -57,6 +57,16 @@ class DummyConnector implements Connector return $stats; } + /** + * Convert jsonData to Activity format + * + * @param array $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 * @param string $_name user or profile name diff --git a/plugins/federator/rediscache.php b/plugins/federator/rediscache.php index 67b056c..0acccab 100644 --- a/plugins/federator/rediscache.php +++ b/plugins/federator/rediscache.php @@ -90,6 +90,17 @@ class RedisCache implements Cache return false; } + /** + * Convert jsonData to Activity format + * + * @param array $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 *