forked from grumpydevelop/federator
		
	major structure rework
- checkSignature for our fqdn now happens in the connector - cache public key for 1 hour - change author on some files - refactored outbox to api/v1/newcontent (CN->Federator communicates to here) - disabled post for outbox (as that should go to v1/newcontent) - initial support for receiving votes from CN, not fully working/clean yet
This commit is contained in:
		
							parent
							
								
									10a3b1e0f9
								
							
						
					
					
						commit
						8ea9bdcf9a
					
				
					 16 changed files with 500 additions and 205 deletions
				
			
		| 
						 | 
				
			
			@ -21,7 +21,5 @@ username = 'federatoradmin'
 | 
			
		|||
password = '*change*me*as*well'
 | 
			
		||||
 | 
			
		||||
[keys]
 | 
			
		||||
headerSenderName = 'contentnation'
 | 
			
		||||
contentnationPublicKeyPath = '../contentnation.pub'
 | 
			
		||||
federatorPrivateKeyPath = '../federator.key'
 | 
			
		||||
federatorPublicKeyPath = '../federator.pub'
 | 
			
		||||
| 
						 | 
				
			
			@ -3,4 +3,8 @@ service-uri = http://local.contentnation.net
 | 
			
		|||
 | 
			
		||||
[userdata]
 | 
			
		||||
path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here
 | 
			
		||||
url = 'https://userdata.contentnation.net'
 | 
			
		||||
url = 'https://userdata.contentnation.net'
 | 
			
		||||
 | 
			
		||||
[keys]
 | 
			
		||||
headerSenderName = 'contentnation'
 | 
			
		||||
publicKeyPath = '../contentnation.pub'
 | 
			
		||||
| 
						 | 
				
			
			@ -107,6 +107,9 @@ class Api extends Main
 | 
			
		|||
                    case 'dummy':
 | 
			
		||||
                        $handler = new Api\V1\Dummy($this);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 'newcontent':
 | 
			
		||||
                        $handler = new Api\V1\NewContent($this);
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +204,15 @@ class Api extends Main
 | 
			
		|||
     */
 | 
			
		||||
    public function checkSignature($headers)
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($headers['X-Sender'])) {
 | 
			
		||||
            try {
 | 
			
		||||
                return $this->connector->checkSignature($headers);
 | 
			
		||||
            } catch (Exceptions\PermissionDenied $e) {
 | 
			
		||||
                http_response_code(500);
 | 
			
		||||
                throw $e;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $signatureHeader = $headers['Signature'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!isset($signatureHeader)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -213,12 +225,11 @@ class Api extends Main
 | 
			
		|||
 | 
			
		||||
        $signature = base64_decode($signatureParts['signature']);
 | 
			
		||||
        $signedHeaders = explode(' ', $signatureParts['headers']);
 | 
			
		||||
        if (isset($headers['X-Sender']) && $headers['X-Sender'] === $this->config['keys']['headerSenderName']) {
 | 
			
		||||
            $pKeyPath = $_SERVER['DOCUMENT_ROOT'] . $this->config['keys']['contentnationPublicKeyPath'];
 | 
			
		||||
            $publicKeyPem = file_get_contents($pKeyPath);
 | 
			
		||||
        } else {
 | 
			
		||||
            $keyId = $signatureParts['keyId'];
 | 
			
		||||
        $keyId = $signatureParts['keyId'];
 | 
			
		||||
 | 
			
		||||
        $publicKeyPem = $this->cache->getPublicKey($keyId);
 | 
			
		||||
 | 
			
		||||
        if (!isset($publicKeyPem) || $publicKeyPem === false) {
 | 
			
		||||
            // Fetch public key from `keyId` (usually actor URL + #main-key)
 | 
			
		||||
            [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -233,11 +244,14 @@ class Api extends Main
 | 
			
		|||
            }
 | 
			
		||||
 | 
			
		||||
            $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($publicKeyPem) || $publicKeyPem === false) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Public key couldn't be determined");
 | 
			
		||||
            if (!isset($publicKeyPem) || $publicKeyPem === false) {
 | 
			
		||||
                http_response_code(500);
 | 
			
		||||
                throw new Exceptions\PermissionDenied("Public key couldn't be determined");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Cache the public key for 1 hour
 | 
			
		||||
            $this->cache->savePublicKey($keyId, $publicKeyPem);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reconstruct the signed string
 | 
			
		||||
| 
						 | 
				
			
			@ -264,7 +278,7 @@ class Api extends Main
 | 
			
		|||
        }
 | 
			
		||||
        if ($verified != 1) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Signature verification failed for publicKey");
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Signature verification failed");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Signature is valid!
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Api\FedUsers;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,182 +90,11 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
    /**
 | 
			
		||||
     * handle post call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user to add data to outbox
 | 
			
		||||
     * @param string|null $_user user to add data to outbox @unused-param
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
    {
 | 
			
		||||
        $_rawInput = file_get_contents('php://input');
 | 
			
		||||
 | 
			
		||||
        $allHeaders = getallheaders();
 | 
			
		||||
        try {
 | 
			
		||||
            $this->main->checkSignature($allHeaders);
 | 
			
		||||
        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
			
		||||
            error_log("Outbox::post Signature check failed: " . $e->getMessage());
 | 
			
		||||
            http_response_code(401);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
        if (!is_array($input)) {
 | 
			
		||||
            error_log("Outbox::post Input wasn't of type array");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sendTo = $outboxActivity->getCC();
 | 
			
		||||
        if ($outboxActivity->getType() === 'Undo') {
 | 
			
		||||
            $object = $outboxActivity->getObject();
 | 
			
		||||
            if ($object !== null) {
 | 
			
		||||
                $sendTo = $object->getCC();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($sendTo as $receiver) {
 | 
			
		||||
            if ($receiver === '' || !is_string($receiver)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (str_ends_with($receiver, '/followers')) {
 | 
			
		||||
                $followers = $this->fetchAllFollowers($receiver, $host);
 | 
			
		||||
                if (is_array($followers)) {
 | 
			
		||||
                    $users = array_merge($users, $followers);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->postForUser($user, $outboxActivity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle post call for specific user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_user user to add data to outbox
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $outboxActivity the activity that we received
 | 
			
		||||
     * @return boolean response
 | 
			
		||||
     */
 | 
			
		||||
    private function postForUser($_user, $outboxActivity)
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($_user)) {
 | 
			
		||||
            $dbh = $this->main->getDatabase();
 | 
			
		||||
            $cache = $this->main->getCache();
 | 
			
		||||
            $connector = $this->main->getConnector();
 | 
			
		||||
 | 
			
		||||
            // get user
 | 
			
		||||
            $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
                $_user,
 | 
			
		||||
                $connector,
 | 
			
		||||
                $cache
 | 
			
		||||
            );
 | 
			
		||||
            if ($user->id === null) {
 | 
			
		||||
                error_log("Outbox::postForUser couldn't find user: $_user");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
			
		||||
        // Save the raw input and parsed JSON to a file for inspection
 | 
			
		||||
        file_put_contents(
 | 
			
		||||
            $rootDir . 'logs/outbox_' . $_user . '.log',
 | 
			
		||||
            date('Y-m-d H:i:s') . ": ==== POST " . $_user . " Outbox Activity ====\n" . json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
			
		||||
            FILE_APPEND
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * fetch all followers from url and return the ones that belong to our server
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $collectionUrl The url of f.e. the posters followers
 | 
			
		||||
     * @param string $host our current host-url
 | 
			
		||||
     * @return string[] the names of the followers that are hosted on our server
 | 
			
		||||
     */
 | 
			
		||||
    private static function fetchAllFollowers(string $collectionUrl, string $host): array
 | 
			
		||||
    {
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        [$collectionResponse, $collectionInfo] = \Federator\Main::getFromRemote($collectionUrl, ['Accept: application/activity+json']);
 | 
			
		||||
        if ($collectionInfo['http_code'] != 200) {
 | 
			
		||||
            error_log("Outbox::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl");
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $collectionData = json_decode($collectionResponse, true);
 | 
			
		||||
        $nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!isset($nextPage)) {
 | 
			
		||||
            error_log("Outbox::fetchAllFollowers No 'first' or 'current' page in collection at $collectionUrl");
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Loop through all pages
 | 
			
		||||
        while ($nextPage) {
 | 
			
		||||
            [$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($nextPage, ['Accept: application/activity+json']);
 | 
			
		||||
            if ($pageInfo['http_code'] != 200) {
 | 
			
		||||
                error_log("Outbox::fetchAllFollowers Failed to fetch follower page at $nextPage");
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $pageData = json_decode($pageResponse, true);
 | 
			
		||||
            $items = $pageData['orderedItems'] ?? $pageData['items'] ?? [];
 | 
			
		||||
 | 
			
		||||
            foreach ($items as $followerUrl) {
 | 
			
		||||
                $parts = parse_url($followerUrl);
 | 
			
		||||
                if (!isset($parts['host']) || !str_ends_with($parts['host'], $host)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                [$actorResponse, $actorInfo] = \Federator\Main::getFromRemote($followerUrl, ['Accept: application/activity+json']);
 | 
			
		||||
                if ($actorInfo['http_code'] != 200) {
 | 
			
		||||
                    error_log("Outbox::fetchAllFollowers Failed to fetch actor data for follower: $followerUrl");
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $actorData = json_decode($actorResponse, true);
 | 
			
		||||
                if (isset($actorData['preferredUsername'])) {
 | 
			
		||||
                    $users[] = $actorData['preferredUsername'];
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $nextPage = $pageData['next'] ?? null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $users;
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										262
									
								
								php/federator/api/v1/newcontent.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								php/federator/api/v1/newcontent.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,262 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Api\V1;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Called from our application to inform us about new content (f.e. new posts on contentnation.net)
 | 
			
		||||
 */
 | 
			
		||||
class NewContent implements \Federator\Api\APIInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * main instance
 | 
			
		||||
     *
 | 
			
		||||
     * @var \Federator\Api $main
 | 
			
		||||
     */
 | 
			
		||||
    private $main;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * response from sub-calls
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $response
 | 
			
		||||
     */
 | 
			
		||||
    private $response;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * constructor
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Main $main main instance
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($main)
 | 
			
		||||
    {
 | 
			
		||||
        $this->main = $main;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * run given url path
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string> $paths path array split by /
 | 
			
		||||
     * @param \Federator\Data\User|false $user user who is calling us @unused-param
 | 
			
		||||
     * @return bool true on success
 | 
			
		||||
     */
 | 
			
		||||
    public function exec($paths, $user)
 | 
			
		||||
    {
 | 
			
		||||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
			
		||||
        $_username = $paths[2];
 | 
			
		||||
        if ($method === 'GET') { // unsupported
 | 
			
		||||
            throw new \Federator\Exceptions\InvalidArgument("GET not supported");
 | 
			
		||||
        }
 | 
			
		||||
        switch (sizeof($paths)) {
 | 
			
		||||
            case 3:
 | 
			
		||||
                $ret = $this->post($_username);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($ret) && $ret !== false) {
 | 
			
		||||
            $this->response = $ret;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->main->setResponseCode(404);
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle post call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user that triggered the post
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("NewContent::post called with user: $_user");
 | 
			
		||||
        $_rawInput = file_get_contents('php://input');
 | 
			
		||||
 | 
			
		||||
        $allHeaders = getallheaders();
 | 
			
		||||
        try {
 | 
			
		||||
            $this->main->checkSignature($allHeaders);
 | 
			
		||||
        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
			
		||||
            error_log("NewContent::post Signature check failed: " . $e->getMessage());
 | 
			
		||||
            http_response_code(401);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
        if (!is_array($input)) {
 | 
			
		||||
            error_log("NewContent::post Input wasn't of type array");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($allHeaders['X-Sender'])) {
 | 
			
		||||
            $newActivity = $this->main->getConnector()->jsonToActivity($input);
 | 
			
		||||
        } else {
 | 
			
		||||
            $newActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($input);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($newActivity === false) {
 | 
			
		||||
            error_log("NewContent::post couldn't create newActivity");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sendTo = $newActivity->getCC();
 | 
			
		||||
        if ($newActivity->getType() === 'Undo') {
 | 
			
		||||
            $object = $newActivity->getObject();
 | 
			
		||||
            if ($object !== null) {
 | 
			
		||||
                $sendTo = $object->getCC();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($sendTo as $receiver) {
 | 
			
		||||
            if ($receiver === '' || !is_string($receiver)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (str_ends_with($receiver, '/followers')) {
 | 
			
		||||
                $followers = $this->fetchAllFollowers($receiver, $host);
 | 
			
		||||
                if (is_array($followers)) {
 | 
			
		||||
                    $users = array_merge($users, $followers);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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/newContent.log',
 | 
			
		||||
                date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n" . json_encode($newActivity, 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;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->postForUser($user, $newActivity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle post call for specific user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_user user that triggered the post
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
 | 
			
		||||
     * @return boolean response
 | 
			
		||||
     */
 | 
			
		||||
    private function postForUser($_user, $newActivity)
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($_user)) {
 | 
			
		||||
            $dbh = $this->main->getDatabase();
 | 
			
		||||
            $cache = $this->main->getCache();
 | 
			
		||||
            $connector = $this->main->getConnector();
 | 
			
		||||
 | 
			
		||||
            // get user
 | 
			
		||||
            $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
                $_user,
 | 
			
		||||
                $connector,
 | 
			
		||||
                $cache
 | 
			
		||||
            );
 | 
			
		||||
            if ($user->id === null) {
 | 
			
		||||
                error_log("NewContent::postForUser couldn't find user: $_user");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
			
		||||
        // Save the raw input and parsed JSON to a file for inspection
 | 
			
		||||
        file_put_contents(
 | 
			
		||||
            $rootDir . 'logs/newcontent_' . $_user . '.log',
 | 
			
		||||
            date('Y-m-d H:i:s') . ": ==== POST " . $_user . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
			
		||||
            FILE_APPEND
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * fetch all followers from url and return the ones that belong to our server
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $collectionUrl The url of f.e. the posters followers
 | 
			
		||||
     * @param string $host our current host-url
 | 
			
		||||
     * @return string[] the names of the followers that are hosted on our server
 | 
			
		||||
     */
 | 
			
		||||
    private static function fetchAllFollowers(string $collectionUrl, string $host): array
 | 
			
		||||
    {
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        [$collectionResponse, $collectionInfo] = \Federator\Main::getFromRemote($collectionUrl, ['Accept: application/activity+json']);
 | 
			
		||||
        if ($collectionInfo['http_code'] != 200) {
 | 
			
		||||
            error_log("NewContent::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl");
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $collectionData = json_decode($collectionResponse, true);
 | 
			
		||||
        $nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!isset($nextPage)) {
 | 
			
		||||
            error_log("NewContent::fetchAllFollowers No 'first' or 'current' page in collection at $collectionUrl");
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Loop through all pages
 | 
			
		||||
        while ($nextPage) {
 | 
			
		||||
            [$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($nextPage, ['Accept: application/activity+json']);
 | 
			
		||||
            if ($pageInfo['http_code'] != 200) {
 | 
			
		||||
                error_log("NewContent::fetchAllFollowers Failed to fetch follower page at $nextPage");
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $pageData = json_decode($pageResponse, true);
 | 
			
		||||
            $items = $pageData['orderedItems'] ?? $pageData['items'] ?? [];
 | 
			
		||||
 | 
			
		||||
            foreach ($items as $followerUrl) {
 | 
			
		||||
                $parts = parse_url($followerUrl);
 | 
			
		||||
                if (!isset($parts['host']) || !str_ends_with($parts['host'], $host)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                [$actorResponse, $actorInfo] = \Federator\Main::getFromRemote($followerUrl, ['Accept: application/activity+json']);
 | 
			
		||||
                if ($actorInfo['http_code'] != 200) {
 | 
			
		||||
                    error_log("NewContent::fetchAllFollowers Failed to fetch actor data for follower: $followerUrl");
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $actorData = json_decode($actorResponse, true);
 | 
			
		||||
                if (isset($actorData['preferredUsername'])) {
 | 
			
		||||
                    $users[] = $actorData['preferredUsername'];
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $nextPage = $pageData['next'] ?? null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $users;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get internal represenation as json string
 | 
			
		||||
     * @return string json string or html
 | 
			
		||||
     */
 | 
			
		||||
    public function toJson()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->response;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -57,4 +57,21 @@ interface Cache extends \Federator\Connector\Connector
 | 
			
		|||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteUserBySession($_session, $_user, $user);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save the public key for a given keyId
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
			
		||||
     * @param string $publicKeyPem The public key PEM to cache
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function savePublicKey(string $keyId, string $publicKeyPem);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the public key for a given keyId
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
			
		||||
     * @return string|false The cached public key PEM or false if not found
 | 
			
		||||
     */
 | 
			
		||||
    public function getPublicKey(string $keyId);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,4 +64,13 @@ interface Connector
 | 
			
		|||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
     * @param string[] $headers the headers
 | 
			
		||||
     * @throws \Federator\Exceptions\PermissionDenied
 | 
			
		||||
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
			
		||||
     */
 | 
			
		||||
    public function checkSignature($headers);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data\ActivityPub\Common;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data\ActivityPub\Common;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Data\ActivityPub\Common;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\DIO;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -338,30 +338,128 @@ class ContentNation implements Connector
 | 
			
		|||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData)
 | 
			
		||||
    {
 | 
			
		||||
        // Common fields for all activity types
 | 
			
		||||
        $ap = [
 | 
			
		||||
            '@context' => 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
            'type' => 'Create',
 | 
			
		||||
            'type' => 'Create', // Default to 'Create'
 | 
			
		||||
            'id' => $jsonData['id'] ?? null,
 | 
			
		||||
            'actor' => $jsonData['actor']['id'] ?? null,
 | 
			
		||||
            'actor' => $jsonData['actor'] ?? 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,
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // Handle specific fields based on the type
 | 
			
		||||
        switch ($jsonData['type']) {
 | 
			
		||||
            case 'comment':
 | 
			
		||||
                $ap['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,
 | 
			
		||||
                ];
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            // todo fix this to properly handle votes, data is mocked for now
 | 
			
		||||
            case 'vote':
 | 
			
		||||
                // Handle voting on a comment or an article
 | 
			
		||||
                if ($jsonData['object']['type'] === 'Comment') {
 | 
			
		||||
                    $jsonData['object']['type'] = 'Note';
 | 
			
		||||
                }
 | 
			
		||||
                $ap['object'] = [
 | 
			
		||||
                    'id' => $jsonData['object']['id'] ?? null,
 | 
			
		||||
                    'type' => $jsonData['object']['type'] ?? 'Article',
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                // Include additional fields if voting on an article
 | 
			
		||||
                if ($ap['object']['type'] === 'Article') {
 | 
			
		||||
                    $ap['object']['name'] = $jsonData['object']['name'] ?? null;
 | 
			
		||||
                    $ap['object']['author'] = $jsonData['object']['author'] ?? null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Add vote-specific fields
 | 
			
		||||
                $ap['vote'] = [
 | 
			
		||||
                    'value' => $jsonData['vote']['value'] ?? null,
 | 
			
		||||
                    'type' => $jsonData['vote']['type'] ?? null,
 | 
			
		||||
                ];
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                // Handle unsupported types or fallback to default behavior
 | 
			
		||||
                throw new \InvalidArgumentException("Unsupported activity type: {$jsonData['type']}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
     * @param string[] $headers the headers
 | 
			
		||||
     * @throws \Federator\Exceptions\PermissionDenied
 | 
			
		||||
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
			
		||||
     */
 | 
			
		||||
    public function checkSignature($headers)
 | 
			
		||||
    {
 | 
			
		||||
        $signatureHeader = $headers['Signature'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!isset($signatureHeader)) {
 | 
			
		||||
            throw new \Federator\Exceptions\PermissionDenied("Missing Signature header");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName'])
 | 
			
		||||
        {
 | 
			
		||||
            throw new \Federator\Exceptions\PermissionDenied("Invalid sender name");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Parse Signature header
 | 
			
		||||
        preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
 | 
			
		||||
        $signatureParts = array_combine($matches[1], $matches[2]);
 | 
			
		||||
 | 
			
		||||
        $signature = base64_decode($signatureParts['signature']);
 | 
			
		||||
        $signedHeaders = explode(' ', $signatureParts['headers']);
 | 
			
		||||
 | 
			
		||||
        $pKeyPath = $_SERVER['DOCUMENT_ROOT'] . $this->config['keys']['publicKeyPath'];
 | 
			
		||||
        $publicKeyPem = file_get_contents($pKeyPath);
 | 
			
		||||
        if ($publicKeyPem === false) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new \Federator\Exceptions\PermissionDenied("Public key couldn't be determined");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reconstruct the signed string
 | 
			
		||||
        $signedString = '';
 | 
			
		||||
        foreach ($signedHeaders as $header) {
 | 
			
		||||
            if ($header === '(request-target)') {
 | 
			
		||||
                $method = strtolower($_SERVER['REQUEST_METHOD']);
 | 
			
		||||
                $path = $_SERVER['REQUEST_URI'];
 | 
			
		||||
                $headerValue = "$method $path";
 | 
			
		||||
            } else {
 | 
			
		||||
                $headerValue = $headers[ucwords($header, '-')] ?? '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $signedString .= strtolower($header) . ": " . $headerValue . "\n";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $signedString = rtrim($signedString);
 | 
			
		||||
 | 
			
		||||
        // Verify the signature
 | 
			
		||||
        $pubkeyRes = openssl_pkey_get_public($publicKeyPem);
 | 
			
		||||
        $verified = false;
 | 
			
		||||
        if ($pubkeyRes instanceof \OpenSSLAsymmetricKey && is_string($signature)) {
 | 
			
		||||
            $verified = openssl_verify($signedString, $signature, $pubkeyRes, OPENSSL_ALGO_SHA256);
 | 
			
		||||
        }
 | 
			
		||||
        if ($verified != 1) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new \Federator\Exceptions\PermissionDenied("Signature verification failed");
 | 
			
		||||
        }
 | 
			
		||||
        return "Signature verified.";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace Federator;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,18 @@ class DummyConnector implements Connector
 | 
			
		|||
        $user->session = $_session;
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
     * @param string[] $headers the headers @unused-param
 | 
			
		||||
     * @throws \Federator\Exceptions\PermissionDenied
 | 
			
		||||
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
			
		||||
     */
 | 
			
		||||
    public function checkSignature($headers)
 | 
			
		||||
    {
 | 
			
		||||
        return new \Federator\Exceptions\PermissionDenied("Dummy connector: no signature check");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace Federator;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,6 +41,13 @@ class RedisCache implements Cache
 | 
			
		|||
     */
 | 
			
		||||
    private $userTTL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * public key cache time to live in secods
 | 
			
		||||
     *
 | 
			
		||||
     * @var int $publicKeyPemTTL
 | 
			
		||||
     */
 | 
			
		||||
    private $publicKeyPemTTL;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * constructor
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +57,7 @@ class RedisCache implements Cache
 | 
			
		|||
        if ($config !== false) {
 | 
			
		||||
            $this->config = $config;
 | 
			
		||||
            $this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60;
 | 
			
		||||
            $this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config) ? intval($config['publickeypemttl'], 10) : 3600;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -176,6 +184,21 @@ class RedisCache implements Cache
 | 
			
		|||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the public key for a given keyId
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
			
		||||
     * @return string|false The cached public key PEM or false if not found
 | 
			
		||||
     */
 | 
			
		||||
    public function getPublicKey(string $keyId)
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->connected) {
 | 
			
		||||
            $this->connect();
 | 
			
		||||
        }
 | 
			
		||||
        $key = self::createKey('publickey', $keyId);
 | 
			
		||||
        return $this->redis->get($key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote followers by user
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -243,6 +266,34 @@ class RedisCache implements Cache
 | 
			
		|||
        $serialized = $user->toJson();
 | 
			
		||||
        $this->redis->setEx($key, $this->userTTL, $serialized,);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save the public key for a given keyId
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $keyId The keyId (e.g., actor URL + #main-key)
 | 
			
		||||
     * @param string $publicKeyPem The public key PEM to cache
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function savePublicKey(string $keyId, string $publicKeyPem)
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->connected) {
 | 
			
		||||
            $this->connect();
 | 
			
		||||
        }
 | 
			
		||||
        $key = self::createKey('publickey', $keyId);
 | 
			
		||||
        $this->redis->setEx($key, $this->publicKeyPemTTL, $publicKeyPem); // TTL = 1 hour
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
     * @param string[] $headers the headers @unused-param
 | 
			
		||||
     * @throws \Federator\Exceptions\PermissionDenied
 | 
			
		||||
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
			
		||||
     */
 | 
			
		||||
    public function checkSignature($headers)
 | 
			
		||||
    {
 | 
			
		||||
        return new \Federator\Exceptions\PermissionDenied("RedisCache: no signature check");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace Federator;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,6 @@
 | 
			
		|||
host = localhost
 | 
			
		||||
port = 6379
 | 
			
		||||
username = federator
 | 
			
		||||
password = redis*change*password
 | 
			
		||||
userttl = 10
 | 
			
		||||
publickeypemttl = 3600
 | 
			
		||||
statsttl = 60
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue