diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php index 81f69927c..957413b3a 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php @@ -14,16 +14,62 @@ namespace Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage; use Chill\DocStoreBundle\AsyncUpload\SignedUrl; use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost; use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; +use Chill\DocStoreBundle\Entity\StoredObject; +use Symfony\Component\Clock\ClockInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class TempUrlLocalStorageGenerator implements TempUrlGeneratorInterface { + private const SIGNATURE_DURATION = 180; + + public function __construct( + private readonly string $secret, + private readonly ClockInterface $clock, + private readonly UrlGeneratorInterface $urlGenerator, + ) {} + public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl { - // TODO: Implement generate() method. + $expiration = $this->clock->now()->getTimestamp() + min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); + + return new SignedUrl( + $method, + $this->urlGenerator->generate('chill_docstore_stored_object_operate', [ + 'object_name' => $object_name, + 'exp' => $expiration, + 'sig' => $this->sign($method, $object_name, $expiration), + ], UrlGeneratorInterface::ABSOLUTE_URL), + \DateTimeImmutable::createFromFormat('U', (string) $expiration), + $object_name, + ); } public function generatePost(?int $expire_delay = null, ?int $submit_delay = null, int $max_file_count = 1, ?string $object_name = null): SignedUrlPost { - // TODO: Implement generatePost() method. + $submitDelayComputed = min($submit_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); + $expireDelayComputed = min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); + $objectNameComputed = $object_name ?? StoredObject::generatePrefix(); + $expiration = $this->clock->now()->getTimestamp() + $expireDelayComputed + $submitDelayComputed; + + return new SignedUrlPost( + $this->urlGenerator->generate( + 'chill_docstore_storedobject_post', + ['prefix' => $objectNameComputed], + UrlGeneratorInterface::ABSOLUTE_URL + ), + \DateTimeImmutable::createFromFormat('U', (string) $expiration), + $objectNameComputed, + 15_000_000, + 1, + $submitDelayComputed, + '', + $objectNameComputed, + $this->sign('POST', $object_name, $expiration), + ); + } + + private function sign(string $method, string $object_name, int $expiration): string + { + return hash_hmac('sha512', $method, sprintf('%s.%s.%s.%d', $method, $this->secret, $object_name, $expiration)); } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGeneratorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGeneratorTest.php new file mode 100644 index 000000000..f7170abcd --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGeneratorTest.php @@ -0,0 +1,78 @@ +prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('chill_docstore_stored_object_operate', [ + 'object_name' => 'testABC', + 'exp' => $exp = 1734307200 + 180, + 'sig' => '528a426aebea86ada86035e9f3299788a074b77d3f926c6c15492457f81e88cfa98b270ba489d5d7588612eadaeeb7717a57887b33932d6d449b5f441252e600', + ], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn($url = 'http://example.com/public/doc-store/stored-object/operate/testABC'); + + $generator = $this->buildGenerator($urlGenerator->reveal()); + + $signedUrl = $generator->generate('GET', 'testABC'); + + self::assertEquals($url, $signedUrl->url); + self::assertEquals('testABC', $signedUrl->object_name); + self::assertEquals($exp, $signedUrl->expires->getTimestamp()); + self::assertEquals('GET', $signedUrl->method); + } + + public function testGeneratePost(): void + { + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('chill_docstore_storedobject_post', [ + 'prefix' => 'prefixABC', + ], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn($url = 'http://example.com/public/doc-store/stored-object/prefixABC'); + + $generator = $this->buildGenerator($urlGenerator->reveal()); + + $signedUrl = $generator->generatePost(object_name: 'prefixABC'); + + self::assertEquals($url, $signedUrl->url); + self::assertEquals('prefixABC', $signedUrl->object_name); + self::assertEquals(1734307200 + 180 + 180, $signedUrl->expires->getTimestamp()); + self::assertEquals('POST', $signedUrl->method); + self::assertEquals('47e9271917c6c9149a47248d88eeadcbc1f804138e445f3406d39f6aa51b2d976c2ee6e5b1196d2bf88852b627b330596c2fbf6f31b06837e9f41cf56103a4e4', $signedUrl->signature); + } + + private function buildGenerator(UrlGeneratorInterface $urlGenerator): TempUrlLocalStorageGenerator + { + return new TempUrlLocalStorageGenerator( + 'abc', + new MockClock('2024-12-16T00:00:00+00:00'), + $urlGenerator, + ); + } +}