diff --git a/autoload-phpunit.php b/autoload-phpunit.php index f91f973..9ffd259 100644 --- a/autoload-phpunit.php +++ b/autoload-phpunit.php @@ -59,11 +59,17 @@ function tableExists(EasyDB $db, string $tableName): bool mkdir(__DIR__ . '/tmp/db/', 0777, true); } $temp = __DIR__ . '/tmp/db/' . sodium_bin2hex(random_bytes(16)) . '-test.db'; - $pkdConfig->withDatabase(new EasyDBCache(new PDO('sqlite:' . $temp))); + $mainPDO = new PDO('sqlite:' . $temp); + $mainPDO->exec('PRAGMA jounral_mode=WAL'); + $mainPDO->exec('PRAGMA busy_timeout=5000'); + $pkdConfig->withDatabase(new EasyDBCache($mainPDO)); chmod($temp, 0777); // Create second DB connection for testing concurrency - $GLOBALS['PKD_PHPUNIT_DB'] = new EasyDBCache(new PDO('sqlite:' . $temp)); + $secondPDO = new PDO('sqlite:' . $temp); + $secondPDO->exec('PRAGMA jounral_mode=WAL'); + $secondPDO->exec('PRAGMA busy_timeout=5000'); + $GLOBALS['PKD_PHPUNIT_DB'] = new EasyDBCache($secondPDO); // Call cleanup-test-db.php to cleanup test file after phpunit is finished. if (getenv('AUTO_CLEANUP_TEST_DB')) { diff --git a/config/hpke.php b/config/hpke.php index e673160..2a946f3 100644 --- a/config/hpke.php +++ b/config/hpke.php @@ -49,5 +49,6 @@ 'encaps-key' => Base64UrlSafe::encodeUnpadded($encapsKey->bytes), ], JSON_PRETTY_PRINT)); + chmod(__DIR__ . '/hpke.json', 0600); } return new HPKE($hpke, $decapsKey, $encapsKey); diff --git a/docs/reference/classes/meta.md b/docs/reference/classes/meta.md index a873fff..947973b 100644 --- a/docs/reference/classes/meta.md +++ b/docs/reference/classes/meta.md @@ -28,10 +28,11 @@ Server configuration parameters | `$hostname` | `string` | (readonly) | | `$cacheKey` | `string` | (readonly) | | `$httpCacheTtl` | `int` | (readonly) | +| `$serverAllowsBurnDown` | `bool` | (readonly) | ### Methods -#### [`__construct`](../../../src/Meta/Params.php#L19-L45) +#### [`__construct`](../../../src/Meta/Params.php#L19-L46) Returns `void` @@ -45,34 +46,39 @@ These parameters MUST be public and MUST have a default value - `$hostname`: `string` = 'localhost' - `$cacheKey`: `string` = '' - `$httpCacheTtl`: `int` = 60 +- `$serverAllowsBurnDown`: `bool` = true **Throws:** `DependencyException` -#### [`getActorUsername`](../../../src/Meta/Params.php#L47-L50) +#### [`getActorUsername`](../../../src/Meta/Params.php#L48-L51) Returns `string` -#### [`getCacheKey`](../../../src/Meta/Params.php#L52-L55) +#### [`getBurnDownEnabled`](../../../src/Meta/Params.php#L53-L56) + +Returns `bool` + +#### [`getCacheKey`](../../../src/Meta/Params.php#L58-L61) Returns `string` -#### [`getHashFunction`](../../../src/Meta/Params.php#L57-L60) +#### [`getHashFunction`](../../../src/Meta/Params.php#L63-L66) Returns `string` -#### [`getHostname`](../../../src/Meta/Params.php#L62-L65) +#### [`getHostname`](../../../src/Meta/Params.php#L68-L71) Returns `string` -#### [`getHttpCacheTtl`](../../../src/Meta/Params.php#L67-L70) +#### [`getHttpCacheTtl`](../../../src/Meta/Params.php#L73-L76) Returns `int` -#### [`getOtpMaxLife`](../../../src/Meta/Params.php#L72-L75) +#### [`getOtpMaxLife`](../../../src/Meta/Params.php#L78-L81) Returns `int` -#### [`getEmptyTreeRoot`](../../../src/Meta/Params.php#L77-L80) +#### [`getEmptyTreeRoot`](../../../src/Meta/Params.php#L83-L86) Returns `string` diff --git a/docs/reference/classes/ratelimit.md b/docs/reference/classes/ratelimit.md index 1b2d738..61615ae 100644 --- a/docs/reference/classes/ratelimit.md +++ b/docs/reference/classes/ratelimit.md @@ -142,7 +142,7 @@ Returns `?DateTimeImmutable` **Throws:** `DateMalformedIntervalStringException` -#### [`getIntervalFromFailureCount`](../../../src/RateLimit/DefaultRateLimiting.php#L249-L262) +#### [`getIntervalFromFailureCount`](../../../src/RateLimit/DefaultRateLimiting.php#L249-L263) Returns `DateInterval` @@ -152,7 +152,7 @@ Returns `DateInterval` **Throws:** `DateMalformedIntervalStringException` -#### [`recordPenalty`](../../../src/RateLimit/DefaultRateLimiting.php#L268-L277) +#### [`recordPenalty`](../../../src/RateLimit/DefaultRateLimiting.php#L269-L278) Returns `void` @@ -165,7 +165,7 @@ Returns `void` **Throws:** `DateMalformedIntervalStringException` -#### [`increaseFailures`](../../../src/RateLimit/DefaultRateLimiting.php#L282-L296) +#### [`increaseFailures`](../../../src/RateLimit/DefaultRateLimiting.php#L283-L297) Returns `FediE2EE\PKDServer\RateLimit\RateLimitData` @@ -235,7 +235,7 @@ Returns `string` - `$array`: `array` -#### [`getRequestActor`](../../../src/RateLimit/DefaultRateLimiting.php#L155-L175) +#### [`getRequestActor`](../../../src/RateLimit/DefaultRateLimiting.php#L155-L173) Returns `?string` @@ -243,7 +243,7 @@ Returns `?string` - `$request`: `Psr\Http\Message\ServerRequestInterface` -#### [`getRequestDomain`](../../../src/RateLimit/DefaultRateLimiting.php#L177-L185) +#### [`getRequestDomain`](../../../src/RateLimit/DefaultRateLimiting.php#L175-L183) Returns `?string` diff --git a/docs/reference/classes/requesthandlers-api.md b/docs/reference/classes/requesthandlers-api.md index 6fbfbaa..6e4efa3 100644 --- a/docs/reference/classes/requesthandlers-api.md +++ b/docs/reference/classes/requesthandlers-api.md @@ -320,13 +320,13 @@ static · Returns `string` ### Methods -#### [`__construct`](../../../src/RequestHandlers/Api/BurnDown.php#L49-L52) +#### [`__construct`](../../../src/RequestHandlers/Api/BurnDown.php#L54-L57) Returns `void` **Throws:** `DependencyException` -#### [`handle`](../../../src/RequestHandlers/Api/BurnDown.php#L69-L91) +#### [`handle`](../../../src/RequestHandlers/Api/BurnDown.php#L79-L106) Returns `Psr\Http\Message\ResponseInterface` @@ -336,7 +336,7 @@ Returns `Psr\Http\Message\ResponseInterface` - `$request`: `Psr\Http\Message\ServerRequestInterface` -**Throws:** `CacheException`, `CertaintyException`, `CryptoException`, `DependencyException`, `HPKEException`, `JsonException`, `NotImplementedException`, `ParserException`, `SodiumException`, `TableException`, `InvalidArgumentException` +**Throws:** `BaseJsonException`, `BundleException`, `CacheException`, `CertaintyException`, `ConcurrentException`, `CryptoException`, `DateMalformedStringException`, `DependencyException`, `HPKEException`, `InvalidArgumentException`, `JsonException`, `NotImplementedException`, `ParserException`, `RandomException`, `SodiumException`, `TableException` #### [`getVerifiedStream`](../../../src/RequestHandlers/Api/BurnDown.php#L39-L62) @@ -2707,7 +2707,7 @@ static · Returns `string` ### Methods -#### [`handle`](../../../src/RequestHandlers/Api/Info.php#L34-L45) +#### [`handle`](../../../src/RequestHandlers/Api/Info.php#L34-L46) Returns `Psr\Http\Message\ResponseInterface` diff --git a/docs/reference/classes/tables.md b/docs/reference/classes/tables.md index d5180c2..f850ee1 100644 --- a/docs/reference/classes/tables.md +++ b/docs/reference/classes/tables.md @@ -342,7 +342,7 @@ Return the witness data (including public key) for a given origin **Throws:** `TableException` -#### [`addWitnessCosignature`](../../../src/Tables/MerkleState.php#L112-L157) +#### [`addWitnessCosignature`](../../../src/Tables/MerkleState.php#L112-L160) **API** · Returns `bool` @@ -354,7 +354,7 @@ Return the witness data (including public key) for a given origin **Throws:** `CryptoException`, `DependencyException`, `JsonException`, `NotImplementedException`, `ProtocolException`, `SodiumException`, `TableException` -#### [`getCosignatures`](../../../src/Tables/MerkleState.php#L162-L180) +#### [`getCosignatures`](../../../src/Tables/MerkleState.php#L165-L183) Returns `array` @@ -362,7 +362,7 @@ Returns `array` - `$leafId`: `int` -#### [`countCosignatures`](../../../src/Tables/MerkleState.php#L182-L192) +#### [`countCosignatures`](../../../src/Tables/MerkleState.php#L185-L195) Returns `int` @@ -370,13 +370,13 @@ Returns `int` - `$leafId`: `int` -#### [`getLatestRoot`](../../../src/Tables/MerkleState.php#L200-L209) +#### [`getLatestRoot`](../../../src/Tables/MerkleState.php#L203-L212) **API** · Returns `string` **Throws:** `DependencyException`, `SodiumException` -#### [`insertLeaf`](../../../src/Tables/MerkleState.php#L228-L292) +#### [`insertLeaf`](../../../src/Tables/MerkleState.php#L231-L295) **API** · Returns `bool` @@ -386,11 +386,11 @@ Insert leaf with retry logic for deadlocks - `$leaf`: `FediE2EE\PKDServer\Tables\Records\MerkleLeaf` - `$inTransaction`: `callable` -- `$maxRetries`: `int` = 5 +- `$maxRetries`: `int` = 20 **Throws:** `ConcurrentException`, `CryptoException`, `DependencyException`, `NotImplementedException`, `RandomException`, `SodiumException` -#### [`getLeafByRoot`](../../../src/Tables/MerkleState.php#L311-L327) +#### [`getLeafByRoot`](../../../src/Tables/MerkleState.php#L314-L330) **API** · Returns `?FediE2EE\PKDServer\Tables\Records\MerkleLeaf` @@ -398,7 +398,7 @@ Insert leaf with retry logic for deadlocks - `$root`: `string` -#### [`getLeafByID`](../../../src/Tables/MerkleState.php#L332-L348) +#### [`getLeafByID`](../../../src/Tables/MerkleState.php#L335-L351) **API** · Returns `?FediE2EE\PKDServer\Tables\Records\MerkleLeaf` @@ -406,7 +406,7 @@ Insert leaf with retry logic for deadlocks - `$primaryKey`: `int` -#### [`getHashesSince`](../../../src/Tables/MerkleState.php#L388-L430) +#### [`getHashesSince`](../../../src/Tables/MerkleState.php#L391-L435) **API** · Returns `array` @@ -994,7 +994,7 @@ Returns `void` **Throws:** `JsonException`, `TableException` -#### [`getHistory`](../../../src/Tables/ReplicaHistory.php#L71-L82) +#### [`getHistory`](../../../src/Tables/ReplicaHistory.php#L71-L84) Returns `array` @@ -1006,7 +1006,7 @@ Returns `array` **Throws:** `JsonException` -#### [`getHistorySince`](../../../src/Tables/ReplicaHistory.php#L88-L108) +#### [`getHistorySince`](../../../src/Tables/ReplicaHistory.php#L90-L110) Returns `array` diff --git a/docs/reference/classes/traits.md b/docs/reference/classes/traits.md index 0efe5b6..e536cbd 100644 --- a/docs/reference/classes/traits.md +++ b/docs/reference/classes/traits.md @@ -873,7 +873,7 @@ Returns `string` - `$array`: `array` -#### [`getRequestActor`](../../../src/Traits/NetworkTrait.php#L155-L175) +#### [`getRequestActor`](../../../src/Traits/NetworkTrait.php#L155-L173) Returns `?string` @@ -881,7 +881,7 @@ Returns `?string` - `$request`: `Psr\Http\Message\ServerRequestInterface` -#### [`getRequestDomain`](../../../src/Traits/NetworkTrait.php#L177-L185) +#### [`getRequestDomain`](../../../src/Traits/NetworkTrait.php#L175-L183) Returns `?string` diff --git a/docs/reference/routes.md b/docs/reference/routes.md index a63a45a..42e632d 100644 --- a/docs/reference/routes.md +++ b/docs/reference/routes.md @@ -8,7 +8,7 @@ This document lists all API routes defined via `#[Route]` attributes. |---------------|---------------|--------| | `/` | [`RequestHandlers\IndexPage`](../../src/RequestHandlers/IndexPage.php) | `handle` | | `/.well-known/webfinger` | [`RequestHandlers\ActivityPub\Finger`](../../src/RequestHandlers/ActivityPub/Finger.php) | `handle` | -| `/api/burndown` | [`RequestHandlers\Api\Revoke`](../../src/RequestHandlers/Api/Revoke.php) | `handle` | +| `/api/burndown` | [`RequestHandlers\Api\BurnDown`](../../src/RequestHandlers/Api/BurnDown.php) | `handle` | | `/api/checkpoint` | [`RequestHandlers\Api\Checkpoint`](../../src/RequestHandlers/Api/Checkpoint.php) | `handle` | | `/api/extensions` | [`RequestHandlers\Api\Extensions`](../../src/RequestHandlers/Api/Extensions.php) | `handle` | | `/api/history` | [`RequestHandlers\Api\History`](../../src/RequestHandlers/Api/History.php) | `handle` | @@ -24,7 +24,7 @@ This document lists all API routes defined via `#[Route]` attributes. | `/api/replicas/{replica_id}/actor/{actor_id}/keys/key/{key_id}` | [`RequestHandlers\Api\ReplicaInfo`](../../src/RequestHandlers/Api/ReplicaInfo.php) | `actorKey` | | `/api/replicas/{replica_id}/history` | [`RequestHandlers\Api\ReplicaInfo`](../../src/RequestHandlers/Api/ReplicaInfo.php) | `history` | | `/api/replicas/{replica_id}/history/since/{hash}` | [`RequestHandlers\Api\ReplicaInfo`](../../src/RequestHandlers/Api/ReplicaInfo.php) | `historySince` | -| `/api/revoke` | [`RequestHandlers\Api\BurnDown`](../../src/RequestHandlers/Api/BurnDown.php) | `handle` | +| `/api/revoke` | [`RequestHandlers\Api\Revoke`](../../src/RequestHandlers/Api/Revoke.php) | `handle` | | `/api/server-public-key` | [`RequestHandlers\Api\ServerPublicKey`](../../src/RequestHandlers/Api/ServerPublicKey.php) | `handle` | | `/api/totp/disenroll` | [`RequestHandlers\Api\TotpDisenroll`](../../src/RequestHandlers/Api/TotpDisenroll.php) | `handle` | | `/api/totp/enroll` | [`RequestHandlers\Api\TotpEnroll`](../../src/RequestHandlers/Api/TotpEnroll.php) | `handle` | diff --git a/public/index.php b/public/index.php index 63e76d9..3dcdd10 100644 --- a/public/index.php +++ b/public/index.php @@ -29,6 +29,9 @@ if ($ex instanceof RateLimitException) { // Rate-limited by the Middleware http_response_code(420); + if (!is_null($ex->rateLimitedUntil)) { + header('Retry-After: ' . $ex->rateLimitedUntil->format(DateTimeInterface::ATOM)); + } echo $ex->getMessage(), PHP_EOL; if (!is_null($ex->rateLimitedUntil)) { echo 'Try again after: ', diff --git a/src/Meta/Params.php b/src/Meta/Params.php index 16b5009..25b6d24 100644 --- a/src/Meta/Params.php +++ b/src/Meta/Params.php @@ -23,6 +23,7 @@ public function __construct( public string $hostname = 'localhost', public string $cacheKey = '', public int $httpCacheTtl = 60, + public bool $serverAllowsBurnDown = true, ) { if (!Tree::isHashFunctionAllowed($this->hashAlgo)) { throw new DependencyException('Disallowed hash algorithm'); @@ -49,6 +50,11 @@ public function getActorUsername(): string return $this->actorUsername; } + public function getBurnDownEnabled(): bool + { + return $this->serverAllowsBurnDown; + } + public function getCacheKey(): string { return $this->cacheKey; diff --git a/src/Protocol/Payload.php b/src/Protocol/Payload.php index 9a8c0d7..f3a71f5 100644 --- a/src/Protocol/Payload.php +++ b/src/Protocol/Payload.php @@ -9,7 +9,7 @@ ProtocolMessageInterface }; use FediE2EE\PKDServer\Traits\JsonTrait; -use function array_key_exists, json_decode; +use function array_key_exists; readonly class Payload { @@ -42,7 +42,7 @@ public function decode(): array */ public function getMerkleTreePayload(): string { - $decoded = json_decode($this->rawJson, true); + $decoded = self::jsonDecode($this->rawJson); if (array_key_exists('key-id', $decoded)) { unset($decoded['key-id']); } diff --git a/src/RateLimit/DefaultRateLimiting.php b/src/RateLimit/DefaultRateLimiting.php index 3aa23a6..f01ce67 100644 --- a/src/RateLimit/DefaultRateLimiting.php +++ b/src/RateLimit/DefaultRateLimiting.php @@ -187,7 +187,7 @@ public function getCooledDown(RateLimitData $data): RateLimitData break; } - } while ($data->failures > 0); + } while ($failures > 0); // Either way, return the updated rate-limit info: return $data->withFailures($failures)->withCooldownStart($start); } @@ -251,7 +251,8 @@ public function getIntervalFromFailureCount(int $failures): DateInterval if ($failures < 1) { return new DateInterval('PT0S'); } - $milliseconds = $this->baseDelay << ($failures - 1); + $shift = min($failures - 1, 60); + $milliseconds = $this->baseDelay << $shift; $seconds = (int) floor($milliseconds / 1000); $us = ($milliseconds % 1000) * 1000; $interval = DateInterval::createFromDateString($seconds . ' seconds + ' . $us . ' microseconds'); diff --git a/src/RateLimit/Storage/Filesystem.php b/src/RateLimit/Storage/Filesystem.php index 9f36e28..7f423fa 100644 --- a/src/RateLimit/Storage/Filesystem.php +++ b/src/RateLimit/Storage/Filesystem.php @@ -93,7 +93,7 @@ public function set(string $type, string $identifier, RateLimitData $data): bool 'expires' => time() + $this->ttl, 'data' => self::jsonEncode($data), ]); - return file_put_contents($file, $bundled) !== false; + return file_put_contents($file, $bundled, LOCK_EX) !== false; } /** diff --git a/src/RequestHandlers/Api/BurnDown.php b/src/RequestHandlers/Api/BurnDown.php index ca086b0..f6cbbb5 100644 --- a/src/RequestHandlers/Api/BurnDown.php +++ b/src/RequestHandlers/Api/BurnDown.php @@ -3,6 +3,7 @@ namespace FediE2EE\PKDServer\RequestHandlers\Api; use FediE2EE\PKD\Crypto\Exceptions\{ + BundleException, CryptoException, HttpSignatureException, JsonException, @@ -12,11 +13,13 @@ use FediE2EE\PKDServer\Exceptions\{ ActivityPubException, CacheException, + ConcurrentException, DependencyException, FetchException, ProtocolException, TableException }; +use DateMalformedStringException; use FediE2EE\PKDServer\{ Meta\Route, Protocol @@ -25,10 +28,12 @@ ActivityStreamsTrait, ReqTrait }; +use JsonException as BaseJsonException; use Override; use ParagonIE\Certainty\Exception\CertaintyException; use ParagonIE\HPKE\HPKEException; use Psr\SimpleCache\InvalidArgumentException; +use Random\RandomException; use Psr\Http\Message\{ ResponseInterface, ServerRequestInterface @@ -52,24 +57,34 @@ public function __construct() } /** + * @throws BaseJsonException + * @throws BundleException * @throws CacheException * @throws CertaintyException + * @throws ConcurrentException * @throws CryptoException + * @throws DateMalformedStringException * @throws DependencyException * @throws HPKEException + * @throws InvalidArgumentException * @throws JsonException * @throws NotImplementedException * @throws ParserException + * @throws RandomException * @throws SodiumException * @throws TableException - * @throws InvalidArgumentException */ - #[Route("/api/revoke")] + #[Route("/api/burndown")] #[Override] public function handle(ServerRequestInterface $request): ResponseInterface { + if (!$this->config()->getParams()->getBurnDownEnabled()) { + return $this->error('BurnDown is not enabled'); + } try { $as = $this->getVerifiedStream($request); + // We set $isActivityPub to false here because this payload is sent over HTTP. + // This is important because BurnDown MUST NOT be sent over ActivityPub. /** @var array{action: string, result: bool, latest-root: string} $result */ $result = $this->protocol->process($as, false); return $this->json([ @@ -86,7 +101,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface '!pkd-context' => 'fedi-e2ee:v1/api/burndown', 'time' => $this->time(), 'status' => false, - ]); + ], 400); } } } diff --git a/src/RequestHandlers/Api/HistoryCosign.php b/src/RequestHandlers/Api/HistoryCosign.php index 6bbfba0..dabf087 100644 --- a/src/RequestHandlers/Api/HistoryCosign.php +++ b/src/RequestHandlers/Api/HistoryCosign.php @@ -88,8 +88,8 @@ public function handle(ServerRequestInterface $request): ResponseInterface ); return $this->json([ '!pkd-context' => 'fedi-e2ee:v1/api/history/cosign', - 'status' => $status, - 'current-time' => $this->time(), + 'success' => $status, + 'time' => $this->time(), ]); } catch (Throwable $ex) { $this->config()->getLogger()->error( diff --git a/src/RequestHandlers/Api/Info.php b/src/RequestHandlers/Api/Info.php index bb00f68..e40eb2f 100644 --- a/src/RequestHandlers/Api/Info.php +++ b/src/RequestHandlers/Api/Info.php @@ -40,6 +40,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface '!pkd-context' => 'fedi-e2ee:v1/api/info', 'current-time' => $this->time(), 'actor' => $actor, + 'burndown-enabled' => $params->getBurnDownEnabled(), 'public-key' => $keys->publicKey->toString(), ]); } diff --git a/src/RequestHandlers/Api/Revoke.php b/src/RequestHandlers/Api/Revoke.php index a0b7d29..fdc4cba 100644 --- a/src/RequestHandlers/Api/Revoke.php +++ b/src/RequestHandlers/Api/Revoke.php @@ -64,7 +64,7 @@ public function __construct() * @throws SodiumException * @throws TableException */ - #[Route("/api/burndown")] + #[Route("/api/revoke")] #[Override] public function handle(ServerRequestInterface $request): ResponseInterface { diff --git a/src/Tables/MerkleState.php b/src/Tables/MerkleState.php index 8efcdc7..1a3b3f0 100644 --- a/src/Tables/MerkleState.php +++ b/src/Tables/MerkleState.php @@ -125,10 +125,13 @@ public function addWitnessCosignature(string $origin, string $merkleRoot, string // Validate hostname: $hostname = $this->config()->getParams()->hostname; - if ($tmp['hostname'] !== $hostname) { + if (!hash_equals($tmp['hostname'], $hostname)) { // If hostname is formatted as a URL, just grab the hostname: $parsedHost = self::parseUrlHost($tmp['hostname']); - if ($parsedHost !== $hostname) { + if (!is_string($parsedHost)) { + throw new ProtocolException('Invalid hostname'); + } + if (!hash_equals($parsedHost, $hostname)) { // Both mismatched? Bail out. throw new ProtocolException('Hostname mismatch'); } @@ -225,7 +228,7 @@ public function getLatestRoot(): string * * @api */ - public function insertLeaf(MerkleLeaf $leaf, callable $inTransaction, int $maxRetries = 5): bool + public function insertLeaf(MerkleLeaf $leaf, callable $inTransaction, int $maxRetries = 20): bool { $attempt = 0; $lockChallenge = sodium_bin2hex(random_bytes(32)); @@ -240,7 +243,7 @@ public function insertLeaf(MerkleLeaf $leaf, callable $inTransaction, int $maxRe } catch (ConcurrentException $e) { if ($attempt < $maxRetries - 1) { $attempt++; - usleep(random_int(10000, 100000)); // Random backoff 10-100ms + usleep(random_int(10000, 1000000)); // Random backoff 10-1000ms continue; } throw $e; @@ -405,8 +408,10 @@ public function getHashesSince(string $oldRoot, int $limit, int $offset = 0): ar "SELECT publickeyhash, contents, contenthash, wrappedkeys, root, created, signature FROM pkd_merkle_leaves WHERE merkleleafid > ? - LIMIT {$limit} OFFSET {$offset}", - $oldRootID + LIMIT ? OFFSET ?", + $oldRootID, + $limit, + $offset ); $return = []; $keyWrapping = new KeyWrapping($this->config()); @@ -459,8 +464,9 @@ protected function insertLeafInternal( $storedChallenge = $row['lock_challenge'] ?? null; break; case "sqlite": - $this->db->beginTransaction(); - $this->db->exec("PRAGMA busy_timeout=5000"); + $this->db->exec("PRAGMA busy_timeout=1000"); + $this->db->getPdo()->setAttribute(PDO::ATTR_TIMEOUT, 1); + $this->db->exec("BEGIN EXCLUSIVE TRANSACTION"); $row = self::assertArray($this->db->row( "SELECT merkle_state, lock_challenge FROM pkd_merkle_state WHERE 1" @@ -562,8 +568,18 @@ protected function insertLeafInternal( $inTransaction(); // We only commit this transaction if all was successful: + if ($this->db->getDriver() === 'sqlite') { + $this->db->exec("END TRANSACTION"); + return true; + } return $this->db->commit(); } finally { + if ($this->db->getDriver() === 'sqlite') { + try { + $this->db->exec("ROLLBACK"); + } catch (PDOException) { + } + } // @phpstan-ignore-next-line $wrap = !$this->db->inTransaction(); // @phpstan-ignore-next-line diff --git a/src/Tables/PublicKeys.php b/src/Tables/PublicKeys.php index db1dbc4..a043204 100644 --- a/src/Tables/PublicKeys.php +++ b/src/Tables/PublicKeys.php @@ -386,7 +386,7 @@ protected function verifyBurnDownTotp( } $totp = $totpTable->getTotpByDomain($domain); if (!$totp) { - return; + throw new ProtocolException('No TOTP secret enrolled for this domain'); } $ts = $this->verifyTOTP($totp['secret'], $otp); if (is_null($ts)) { diff --git a/src/Tables/ReplicaHistory.php b/src/Tables/ReplicaHistory.php index 7a408fe..98b391d 100644 --- a/src/Tables/ReplicaHistory.php +++ b/src/Tables/ReplicaHistory.php @@ -75,8 +75,10 @@ public function getHistory(int $peerID, int $limit = 100, int $offset = 0): arra FROM pkd_replica_history WHERE peer = ? ORDER BY replicahistoryid DESC - LIMIT {$limit} OFFSET {$offset}", - $peerID + LIMIT ? OFFSET ?", + $peerID, + $limit, + $offset ); return $this->formatHistory($results); } diff --git a/src/Traits/NetworkTrait.php b/src/Traits/NetworkTrait.php index f8e8810..c6c10b2 100644 --- a/src/Traits/NetworkTrait.php +++ b/src/Traits/NetworkTrait.php @@ -169,9 +169,7 @@ public function getRequestActor(ServerRequestInterface $request): ?string if (!is_string($actorID)) { return null; } - - $parsed = parse_url($actorID); - return $parsed['host'] ?? null; + return $actorID; } public function getRequestDomain(ServerRequestInterface $request): ?string diff --git a/src/Traits/ProtocolMethodTrait.php b/src/Traits/ProtocolMethodTrait.php index 9f9d643..6d4621e 100644 --- a/src/Traits/ProtocolMethodTrait.php +++ b/src/Traits/ProtocolMethodTrait.php @@ -39,7 +39,6 @@ trait ProtocolMethodTrait { protected const int ENCRYPTION_REQUIRED = 1; protected const int ENCRYPTION_DISALLOWED = 2; - protected const int ENCRYPTION_OPTIONAL = 3; /** * @throws ConcurrentException @@ -95,6 +94,9 @@ protected function protocolMethod( } // Before we do any insets, we should make sure we're not in a dangling transaction: if ($this->config()->getDb()->inTransaction()) { + if ($this->config()->getDb()->getDriver() === 'sqlite') { + $this->config()->getDb()->exec('END TRANSACTION'); + } $this->config()->getDb()->rollBack(); } throw new TableException('Could not insert new leaf'); diff --git a/tests/ActivityPub/WebFingerTest.php b/tests/ActivityPub/WebFingerTest.php index c6fe79a..8d86946 100644 --- a/tests/ActivityPub/WebFingerTest.php +++ b/tests/ActivityPub/WebFingerTest.php @@ -170,7 +170,6 @@ public function testExceptionCodes(): void try { $method = new ReflectionMethod(WebFinger::class, 'getPublicKeyFromActivityPub'); - $method->setAccessible(true); $method->invoke($webFinger, 'https://example.com/users/alice'); $this->fail('Expected FetchException was not thrown'); } catch (FetchException $e) { diff --git a/tests/HttpTestTrait.php b/tests/HttpTestTrait.php index 5c1be66..22d056e 100644 --- a/tests/HttpTestTrait.php +++ b/tests/HttpTestTrait.php @@ -196,6 +196,9 @@ public function truncateTables(): void } else { $db = $GLOBALS['pkdConfig']->getDb(); } + if ($db->inTransaction()) { + $db->rollback(); + } $tables = [ 'pkd_merkle_witness_cosignatures', 'pkd_merkle_witnesses', diff --git a/tests/Integration/ApiTest.php b/tests/Integration/ApiTest.php index 47c6160..9862122 100644 --- a/tests/Integration/ApiTest.php +++ b/tests/Integration/ApiTest.php @@ -125,6 +125,7 @@ class ApiTest extends TestCase public function setUp(): void { $this->config = $this->getConfig(); + $this->assertNotInTransaction(); $this->truncateTables(); } diff --git a/tests/Integration/CosignLifecycleTest.php b/tests/Integration/CosignLifecycleTest.php index 72e3771..5300081 100644 --- a/tests/Integration/CosignLifecycleTest.php +++ b/tests/Integration/CosignLifecycleTest.php @@ -238,7 +238,7 @@ public function testCosignLifecycle(): void $response = $cosignHandler->handle($request); $body = json_decode($response->getBody()->getContents(), true); $this->assertSame(200, $response->getStatusCode()); - $this->assertTrue($body['status']); + $this->assertTrue($body['success']); $countAgain = $merkleState->countCosignatures($leaf->primaryKey); $this->assertNotSame($numCosigs, $countAgain, 'Number of cosignatures did not increase'); $tree = $cosign->getTree(); diff --git a/tests/Integration/RewrapLifecycleTest.php b/tests/Integration/RewrapLifecycleTest.php index 60c0243..0cc3b9e 100644 --- a/tests/Integration/RewrapLifecycleTest.php +++ b/tests/Integration/RewrapLifecycleTest.php @@ -113,6 +113,7 @@ class RewrapLifecycleTest extends TestCase public function setUp(): void { $this->config = $this->getConfig(); + $this->assertNotInTransaction(); $this->truncateTables(); } diff --git a/tests/Integration/VectorsTest.php b/tests/Integration/VectorsTest.php index 68279f6..85cd4f5 100644 --- a/tests/Integration/VectorsTest.php +++ b/tests/Integration/VectorsTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace FediE2EE\PKDServer\Tests\Integration; +use DateMalformedStringException; use FediE2EE\PKD\Crypto\ActivityPub\WebFinger as PKDCryptoWebFinger; use FediE2EE\PKD\Crypto\AttributeEncryption\AttributeKeyMap; use FediE2EE\PKD\Crypto\Exceptions\{ @@ -22,6 +23,8 @@ }; use FediE2EE\PKD\Crypto\Protocol\Handler; use GuzzleHttp\Exception\GuzzleException; +use ParagonIE\CipherSweet\Exception\CipherSweetException; +use Random\RandomException; use FediE2EE\PKD\Crypto\{ SecretKey, SymmetricKey @@ -42,10 +45,10 @@ }; use FediE2EE\PKDServer\Exceptions\{ CacheException, + ConcurrentException, DependencyException, ProtocolException, - TableException -}; + TableException}; use FediE2EE\PKDServer\Tables\{ Actors, AuxData, @@ -61,7 +64,10 @@ Peer }; use FediE2EE\PKDServer\Tests\HttpTestTrait; -use FediE2EE\PKDServer\Traits\ConfigTrait; +use FediE2EE\PKDServer\Traits\{ + ConfigTrait, + TOTPTrait +}; use GuzzleHttp\Psr7\Response; use JsonException; use ParagonIE\Certainty\Exception\CertaintyException; @@ -106,6 +112,7 @@ class VectorsTest extends TestCase { use ConfigTrait; use HttpTestTrait; + use TOTPTrait; private const string TEST_VECTORS_PATH = PKD_SERVER_ROOT . '/tests/TestVectors/test-vectors.json'; @@ -543,8 +550,11 @@ private function executeUndoFireproof( /** * @throws BundleException * @throws CacheException + * @throws CipherSweetException + * @throws ConcurrentException * @throws CryptoException * @throws CryptoJsonException + * @throws DateMalformedStringException * @throws DependencyException * @throws GuzzleException * @throws HPKEException @@ -552,6 +562,7 @@ private function executeUndoFireproof( * @throws NetworkException * @throws NotImplementedException * @throws ProtocolException + * @throws RandomException * @throws SodiumException * @throws TableException */ @@ -565,7 +576,13 @@ private function executeBurnDown( $operator = $this->extractOperatorFromDescription($step); $operatorKey = $this->identityKeys[$operator]; - $otp = '00000000'; + $actorDomain = parse_url($targetActor, PHP_URL_HOST); + $totpSecret = random_bytes(20); + /** @var TOTP $totpTable */ + $totpTable = $this->table('TOTP'); + $totpTable->saveSecret($actorDomain, $totpSecret); + $otp = self::generateTOTP($totpSecret); + $burnDown = new BurnDownAction($targetActor, $operator, null, $otp); $akm = (new AttributeKeyMap()) ->addKey('actor', SymmetricKey::generate()) diff --git a/tests/Meta/ParamsTest.php b/tests/Meta/ParamsTest.php index adcfcd3..e7e9b4a 100644 --- a/tests/Meta/ParamsTest.php +++ b/tests/Meta/ParamsTest.php @@ -25,6 +25,7 @@ public function testDefaults(): void $this->assertSame('', $params->getCacheKey()); $this->assertSame(60, $params->httpCacheTtl); $this->assertSame(60, $params->getHttpCacheTtl()); + $this->assertSame(true, $params->getBurnDownEnabled()); } public function testInvalidHashAlgo(): void @@ -121,7 +122,8 @@ public function testConstructorExplicit(): void actorUsername: 'alice', hostname: 'example.com', cacheKey: 'test-key', - httpCacheTtl: 100 + httpCacheTtl: 100, + serverAllowsBurnDown: false, ); $this->assertSame('sha512', $params->hashAlgo); $this->assertSame(30, $params->otpMaxLife); @@ -129,5 +131,6 @@ public function testConstructorExplicit(): void $this->assertSame('example.com', $params->hostname); $this->assertSame('test-key', $params->cacheKey); $this->assertSame(100, $params->httpCacheTtl); + $this->assertSame(false, $params->serverAllowsBurnDown); } } diff --git a/tests/ProtocolTest.php b/tests/ProtocolTest.php index 1fdb6e1..20ad864 100644 --- a/tests/ProtocolTest.php +++ b/tests/ProtocolTest.php @@ -23,7 +23,6 @@ Actions\MoveIdentity, Actions\RevokeAuxData, Actions\RevokeKey, - Actions\RevokeKeyThirdParty, Actions\UndoFireproof, Handler }; @@ -37,7 +36,10 @@ SecretKey, SymmetricKey }; -use FediE2EE\PKDServer\Traits\ConfigTrait; +use FediE2EE\PKDServer\Traits\{ + ConfigTrait, + TOTPTrait +}; use FediE2EE\PKDServer\Exceptions\{ CacheException, ConcurrentException, @@ -117,6 +119,7 @@ class ProtocolTest extends TestCase { use ConfigTrait; use HttpTestTrait; + use TOTPTrait; protected Protocol $protocol; @@ -479,14 +482,19 @@ public function testBurnDown(): void $pkTable = $this->table('PublicKeys'); $this->assertCount(1, $pkTable->getPublicKeysFor($canonActor)); + $totpSecret = random_bytes(20); + /** @var TOTP $totpTable */ + $totpTable = $this->table('TOTP'); + $totpTable->saveSecret('example.com', $totpSecret); + $otp = self::generateTOTP($totpSecret); + // 3. BurnDown (plaintext - not HPKE encrypted, but with attribute encryption) $latestRoot3 = $merkleState->getLatestRoot(); - $burnDown = new BurnDown($canonActor, $canonOperator); + $burnDown = new BurnDown($canonActor, $canonOperator, null, $otp); $akm3 = new AttributeKeyMap() ->addKey('actor', SymmetricKey::generate()) ->addKey('operator', SymmetricKey::generate()); - $encryptedMsg3 = $burnDown->encrypt($akm3); - $bundle3 = $handler->handle($encryptedMsg3, $operatorKey, $akm3, $latestRoot3, $operatorKeyId); + $bundle3 = $handler->handle($burnDown, $operatorKey, $akm3, $latestRoot3); $this->assertNotInTransaction(); $this->assertTrue($this->protocol->burnDown($bundle3->toString(), $canonOperator)); $this->ensureMerkleStateUnlocked(); @@ -717,14 +725,19 @@ public function testUndoFireproof(): void $this->assertKeyRewrapped($latestRoot6, 'Key should be rewrapped after undoFireproof'); $this->ensureMerkleStateUnlocked(); + $totpSecret = random_bytes(20); + /** @var TOTP $totpTable */ + $totpTable = $this->table('TOTP'); + $totpTable->saveSecret('example.com', $totpSecret); + $otp = self::generateTOTP($totpSecret); + // 5. BurnDown (should succeed - plaintext with attribute encryption) $latestRoot7 = $merkleState->getLatestRoot(); - $burnDown = new BurnDown($canonicalActor, $canonicalOperator); + $burnDown = new BurnDown($canonicalActor, $canonicalOperator, null, $otp); $akm5 = new AttributeKeyMap() ->addKey('actor', SymmetricKey::generate()) ->addKey('operator', SymmetricKey::generate()); - $encryptedMsg5 = $burnDown->encrypt($akm5); - $bundle5 = $handler->handle($encryptedMsg5, $operatorKey, $akm5, $latestRoot7, $operatorKeyId); + $bundle5 = $handler->handle($burnDown, $operatorKey, $akm5, $latestRoot7); $this->assertNotInTransaction(); $this->assertTrue( $this->protocol->burnDown($bundle5->toString(), $canonicalOperator) diff --git a/tests/RequestHandlers/Api/BurnDownTest.php b/tests/RequestHandlers/Api/BurnDownTest.php index ca75e5a..bae84a0 100644 --- a/tests/RequestHandlers/Api/BurnDownTest.php +++ b/tests/RequestHandlers/Api/BurnDownTest.php @@ -41,6 +41,7 @@ }; use FediE2EE\PKDServer\{ AppCache, + Meta\Params, Traits\ConfigTrait, Math, Protocol, @@ -66,6 +67,7 @@ Peer }; use FediE2EE\PKDServer\Tests\HttpTestTrait; +use FediE2EE\PKDServer\Traits\TOTPTrait; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Psr7\{ Response, @@ -116,10 +118,12 @@ #[UsesClass(Math::class)] #[UsesClass(RewrapConfig::class)] #[UsesClass(Peer::class)] +#[UsesClass(Params::class)] class BurnDownTest extends TestCase { use ConfigTrait; use HttpTestTrait; + use TOTPTrait; /** * @throws ArrayKeyException @@ -152,8 +156,8 @@ public function testHandle(): void { // Create two actors on the SAME domain: one to burn down, one as operator // Per spec, BurnDown must be from an operator on the same instance as the target - [$actorHandle, $canonActor] = $this->makeDummyActor(); - [$operatorHandle, $canonOperator] = $this->makeDummyActor(); + [$actorHandle, $canonActor] = $this->makeDummyActor('text-burndown.example.com'); + [$operatorHandle, $canonOperator] = $this->makeDummyActor('text-burndown.example.com'); $actorKey = SecretKey::generate(); $operatorKey = SecretKey::generate(); @@ -168,6 +172,7 @@ public function testHandle(): void $serverHpke = $config->getHPKE(); $handler = new Handler(); + $this->assertNotInTransaction(); // 1. Add key for the actor (victim) $latestRoot1 = $merkleState->getLatestRoot(); $addKey1 = new AddKey($canonActor, $actorKey->getPublicKey()); @@ -219,9 +224,14 @@ public function testHandle(): void ])) ); + $totpSecret = random_bytes(20); + /** @var TOTP $totpTable */ + $totpTable = $this->table('TOTP'); + $totpTable->saveSecret('text-burndown.example.com', $totpSecret); + $latestRoot3 = $merkleState->getLatestRoot(); // Note: BurnDownAction canonicalizes actor but NOT operator, so operator must be canonical URL - $otp = '12345678'; + $otp = self::generateTOTP($totpSecret); $now = (new DateTimeImmutable('NOW')); $burnDown = new BurnDownAction($actorHandle, $canonOperator, $now, $otp); $akm3 = (new AttributeKeyMap()) @@ -231,6 +241,7 @@ public function testHandle(): void // OTP is a top-level Bundle field (not part of the signed/encrypted message) $bundleData = json_decode($bundle3->toJson(), true); + $otp = self::generateTOTP($totpSecret); $bundleData['otp'] = $otp; $bundleJson = json_encode($bundleData, JSON_UNESCAPED_SLASHES); @@ -366,7 +377,7 @@ public function testHandleInvalidSignature(): void // Handle request - should fail signature verification $this->clearOldTransaction($config); $response = $burnDownHandler->handle($request); - $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(400, $response->getStatusCode()); $body = json_decode($response->getBody()->getContents(), true); $this->assertSame('fedi-e2ee:v1/api/burndown', $body['!pkd-context']); diff --git a/tests/RequestHandlers/Api/HistoryCosignTest.php b/tests/RequestHandlers/Api/HistoryCosignTest.php index 847e9e7..cb13b77 100644 --- a/tests/RequestHandlers/Api/HistoryCosignTest.php +++ b/tests/RequestHandlers/Api/HistoryCosignTest.php @@ -313,10 +313,10 @@ public function testSuccessfulCosign(): void $this->assertSame(200, $response->getStatusCode()); $body = json_decode($response->getBody()->getContents(), true); $this->assertSame('fedi-e2ee:v1/api/history/cosign', $body['!pkd-context']); - $this->assertArrayHasKey('status', $body); - $this->assertTrue($body['status']); - $this->assertArrayHasKey('current-time', $body); - $this->assertIsString($body['current-time']); + $this->assertArrayHasKey('success', $body); + $this->assertTrue($body['success']); + $this->assertArrayHasKey('time', $body); + $this->assertIsString($body['time']); $this->assertNotInTransaction(); } } diff --git a/tests/RequestHandlers/Api/HistoryTest.php b/tests/RequestHandlers/Api/HistoryTest.php index e173429..50fc6f2 100644 --- a/tests/RequestHandlers/Api/HistoryTest.php +++ b/tests/RequestHandlers/Api/HistoryTest.php @@ -106,6 +106,7 @@ public function testHandle(): void $serverHpke->encapsKey, $serverHpke->cs ); + $this->assertNotInTransaction(); $protocol->addKey($encryptedForServer, $canonical); $newRoot = $merkleState->getLatestRoot(); diff --git a/tests/RequestHandlers/Api/InfoTest.php b/tests/RequestHandlers/Api/InfoTest.php index 4b255be..b1d7b0f 100644 --- a/tests/RequestHandlers/Api/InfoTest.php +++ b/tests/RequestHandlers/Api/InfoTest.php @@ -5,6 +5,7 @@ use FediE2EE\PKD\Crypto\Exceptions\NotImplementedException; use FediE2EE\PKDServer\AppCache; use FediE2EE\PKDServer\Exceptions\DependencyException; +use FediE2EE\PKDServer\Meta\Params; use FediE2EE\PKDServer\RequestHandlers\Api\Info; use FediE2EE\PKDServer\ServerConfig; use FediE2EE\PKDServer\Tests\HttpTestTrait; @@ -19,6 +20,7 @@ #[CoversClass(Info::class)] #[UsesClass(AppCache::class)] +#[UsesClass(Params::class)] #[UsesClass(ServerConfig::class)] class InfoTest extends TestCase { @@ -47,8 +49,9 @@ public function testHandle(): void $this->assertArrayHasKey('!pkd-context', $decoded); $this->assertArrayHasKey('current-time', $decoded); $this->assertArrayHasKey('actor', $decoded); + $this->assertArrayHasKey('burndown-enabled', $decoded); $this->assertArrayHasKey('public-key', $decoded); - $this->assertCount(4, $decoded, 'Response should have exactly 4 keys'); + $this->assertCount(5, $decoded, 'Response should have exactly 5 keys'); // Verify values $this->assertLessThanOrEqual(time(), (int) $decoded['current-time']); @@ -58,6 +61,7 @@ public function testHandle(): void $params = $config->getParams(); $expectedActor = $params->actorUsername . '@' . $params->hostname; $this->assertSame($expectedActor, $decoded['actor']); + $this->assertSame($params->serverAllowsBurnDown, $decoded['burndown-enabled']); $this->assertNotInTransaction(); } } diff --git a/tests/RequestHandlers/Api/ReplicasTest.php b/tests/RequestHandlers/Api/ReplicasTest.php index ee245f2..9cc464d 100644 --- a/tests/RequestHandlers/Api/ReplicasTest.php +++ b/tests/RequestHandlers/Api/ReplicasTest.php @@ -107,6 +107,7 @@ public function testHandle(): void if (!($peersTable instanceof Peers)) { $this->fail('peers table is not the right type'); } + $this->assertNotInTransaction(); /** @var Peer $newPeer */ $newPeer = $peersTable->create( $this->config->getSigningKeys()->publicKey, diff --git a/tests/RequestHandlers/Api/RevokeTest.php b/tests/RequestHandlers/Api/RevokeTest.php index b7a2711..8ec9358 100644 --- a/tests/RequestHandlers/Api/RevokeTest.php +++ b/tests/RequestHandlers/Api/RevokeTest.php @@ -417,6 +417,7 @@ public function testHandleFlatFormat(): void $serverHpke->encapsKey, $serverHpke->cs ); + $this->assertNotInTransaction(); $protocol->addKey($encryptedForServer, $canonical); // Now, let's build a revocation token. diff --git a/tests/RequestHandlers/Api/TotpEnrollTest.php b/tests/RequestHandlers/Api/TotpEnrollTest.php index da3d72c..b628d2d 100644 --- a/tests/RequestHandlers/Api/TotpEnrollTest.php +++ b/tests/RequestHandlers/Api/TotpEnrollTest.php @@ -586,6 +586,7 @@ public function testInvalidSignature(): void $this->clearOldTransaction($config); $protocol = new Protocol($config); + $this->assertNotInTransaction(); $result = $this->addKeyForActor($canonical, $keypair, $protocol, $config); $this->assertNotInTransaction(); $this->ensureMerkleStateUnlocked(); @@ -732,6 +733,7 @@ public function testInvalidTotpCodesIndividually(): void $this->clearOldTransaction($config); $protocol = new Protocol($config); + $this->assertNotInTransaction(); $result = $this->addKeyForActor($canonical, $keypair, $protocol, $config); $this->assertNotInTransaction(); $this->ensureMerkleStateUnlocked(); diff --git a/tests/RequestHandlers/Api/TotpRotateTest.php b/tests/RequestHandlers/Api/TotpRotateTest.php index a001063..71891c8 100644 --- a/tests/RequestHandlers/Api/TotpRotateTest.php +++ b/tests/RequestHandlers/Api/TotpRotateTest.php @@ -166,6 +166,7 @@ public static function timeOffsetProvider(): array #[DataProvider("timeOffsetProvider")] public function testHandle(int $timeOffset): void { + $this->assertNotInTransaction(); $sk = SecretKey::generate(); $pk = $sk->getPublicKey(); $hash = hash('sha256', pack('q', $timeOffset)); @@ -204,8 +205,10 @@ public function testHandle(int $timeOffset): void $serverHpke->encapsKey, $serverHpke->cs, ); + $this->assertNotInTransaction(); $addKeyResult = $protocol->addKey($encryptedForServer, $canonical); $keyId = $addKeyResult->keyID; + $this->assertNotInTransaction(); $domain = parse_url($canonical)['host']; $this->assertSame($rotatedomain, $domain); @@ -636,6 +639,7 @@ public function testInvalidSignature(): void $serverHpke->encapsKey, $serverHpke->cs, ); + $this->assertNotInTransaction(); $addKeyResult = $protocol->addKey($encryptedForServer, $canonical); $keyId = $addKeyResult->keyID; @@ -736,6 +740,7 @@ public function testEqualTimestepsRejected(): void $this->clearOldTransaction($config); $protocol = new Protocol($config); + $this->assertNotInTransaction(); $addKeyResult = $this->addKeyForActor($canonical, $sk, $protocol, $config); $keyId = $addKeyResult->keyID; @@ -806,6 +811,7 @@ public function testInvalidNewOtpCodes(): void $this->clearOldTransaction($config); $protocol = new Protocol($config); + $this->assertNotInTransaction(); $addKeyResult = $this->addKeyForActor($canonical, $sk, $protocol, $config); $keyId = $addKeyResult->keyID; diff --git a/tests/Traits/NetworkTraitTest.php b/tests/Traits/NetworkTraitTest.php index fbec8a0..79a1a32 100644 --- a/tests/Traits/NetworkTraitTest.php +++ b/tests/Traits/NetworkTraitTest.php @@ -3,6 +3,7 @@ namespace FediE2EE\PKDServer\Tests\Traits; use FediE2EE\PKDServer\Traits\NetworkTrait; +use GuzzleHttp\Psr7\ServerRequest; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -121,4 +122,23 @@ public function testIpv6Mask(string $input, int $maskBits, string $expected): vo $dummy = $this->getDummyClass(); $this->assertSame($expected, $dummy->ipv6Mask($input, $maskBits)); } + public function testGetRequestActor(): void + { + $dummy = $this->getDummyClass(); + $json = json_encode(['actor' => 'https://example.com/users/alice']); + $this->assertSame( + 'https://example.com/users/alice', + $dummy->getRequestActor(new ServerRequest('GET', '/', [], $json, '1.1')) + ); + } + + public function testGetRequestDomain(): void + { + $dummy = $this->getDummyClass(); + $json = json_encode(['actor' => 'https://example.com/users/alice']); + $this->assertSame( + 'example.com', + $dummy->getRequestDomain(new ServerRequest('GET', '/', [], $json, '1.1')) + ); + } }