From a8e9225fe8a6d05e1cda3b11765f5e8f4b15ef7d Mon Sep 17 00:00:00 2001 From: Erik van Velzen Date: Tue, 4 Oct 2022 17:55:53 +0200 Subject: [PATCH] Add RegisterAccess (#39) Add new bindings to register access for the S3-compatible gateway and to construct a linksharing URL. The RegisterAccess happy flow test isn't currently run in CI because there is no auth service in storj-sim. Solves https://github.com/storj-thirdparty/uplink-php/issues/37 --- src/Access.php | 8 ++ src/Edge/Config.php | 65 ++++++++++++ src/Edge/Credentials.php | 46 ++++++++ src/Edge/Edge.php | 112 ++++++++++++++++++++ src/Exception/Edge/DialFailed.php | 8 ++ src/Exception/Edge/EdgeException.php | 9 ++ src/Exception/Edge/RegisterAccessFailed.php | 9 ++ src/Exception/UplinkException.php | 3 + src/SharePrefix.php | 5 +- src/Uplink.php | 6 ++ test/Edge/RegisterAccessTest.php | 50 +++++++++ test/Edge/ShareUrlTest.php | 51 +++++++++ 12 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 src/Edge/Config.php create mode 100644 src/Edge/Credentials.php create mode 100644 src/Edge/Edge.php create mode 100644 src/Exception/Edge/DialFailed.php create mode 100644 src/Exception/Edge/EdgeException.php create mode 100644 src/Exception/Edge/RegisterAccessFailed.php create mode 100644 test/Edge/RegisterAccessTest.php create mode 100644 test/Edge/ShareUrlTest.php diff --git a/src/Access.php b/src/Access.php index cb9d67b..4297260 100644 --- a/src/Access.php +++ b/src/Access.php @@ -156,4 +156,12 @@ public function overrideEncryptionKey(string $bucket, string $prefix, Encryption $scope = Scope::exit(fn() => $this->ffi->uplink_free_error($pError)); Util::throwIfError($pError); } + + /** + * @internal + */ + public function getNativeAccess(): FFI\CData + { + return $this->cAccess; + } } diff --git a/src/Edge/Config.php b/src/Edge/Config.php new file mode 100644 index 0000000..2f29a4e --- /dev/null +++ b/src/Edge/Config.php @@ -0,0 +1,65 @@ +authServiceAddress = $authServiceAddress; + return $self; + } + + public function withCertificatePem(string $certificatePem): self + { + $self = clone $this; + $self->certificatePem = $certificatePem; + return $self; + } + + public function getAuthServiceAddress(): string + { + return $this->authServiceAddress; + } + + public function getCertificatePem(): string + { + return $this->certificatePem; + } + + /** + * @internal + */ + public function toCStruct(FFI $ffi, Scope $scope): FFI\CData + { + $cAuthServiceAddress = Util::createCString($this->authServiceAddress, $scope); + $cCertificatePem = Util::createCString($this->certificatePem, $scope); + + $cConfig = $ffi->new('EdgeConfig'); + $cConfig->auth_service_address = $cAuthServiceAddress; + $cConfig->certificate_pem = $cCertificatePem; + + return $cConfig; + } +} diff --git a/src/Edge/Credentials.php b/src/Edge/Credentials.php new file mode 100644 index 0000000..94ed453 --- /dev/null +++ b/src/Edge/Credentials.php @@ -0,0 +1,46 @@ +accessKeyId = $accessKeyId; + $this->secretKey = $secretKey; + $this->endpoint = $endpoint; + } + + public function getAccessKeyId(): string + { + return $this->accessKeyId; + } + + public function getSecretKey(): string + { + return $this->secretKey; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } +} diff --git a/src/Edge/Edge.php b/src/Edge/Edge.php new file mode 100644 index 0000000..659c83d --- /dev/null +++ b/src/Edge/Edge.php @@ -0,0 +1,112 @@ +ffi = $ffi; + } + + /** + * Get credentials for the Storj-hosted Gateway and linkshare service. + * + * All files accessible under the Access are then also accessible via those services. + * + * If you call this function a lot, and the use case allows it, + * please limit the lifetime of the credentials + * by setting @see Permission::$notAfter when creating the Access. + * + * @throws DialFailed in case of network errors + * @throws RegisterAccessFailed in case of server errors + */ + public function registerAccess( + Config $config, + Access $access, + bool $isPublic = false + ): Credentials + { + $scope = new Scope(); + + $cOptions = $this->ffi->new('EdgeRegisterAccessOptions'); + $cOptions->is_public = $isPublic; + + $cCredentialsResult = $this->ffi->edge_register_access( + $config->toCStruct($this->ffi, $scope), + $access->getNativeAccess(), + FFI::addr($cOptions), + ); + + $scope->onExit( + fn() => $this->ffi->edge_free_credentials_result($cCredentialsResult) + ); + + Util::throwIfErrorResult($cCredentialsResult); + + $cCredentials = $cCredentialsResult->credentials; + + return new Credentials( + FFI::string($cCredentials->access_key_id), + FFI::string($cCredentials->secret_key), + FFI::string($cCredentials->endpoint), + ); + } + + /** + * JoinShareURL creates a linksharing URL from parts. + * The existence or accessibility of the target is not checked, it might not exist or be inaccessible. + * + * @param string $baseUrl Linksharing service, e.g. https://link.storjshare.io + * @param string $accessKeyId Can be obtained by calling RegisterAccess. It must be associated with public visibility. + * @param string $bucket Optional, leave it blank to share the entire project. + * @param string $key Optional, if empty shares the entire bucket. It can also be a prefix, in which case it must end with a "/". + * @param bool $raw Whether to get a direct link to the data instead of a landing page + * + * @return string example https://link.storjshare.io/s/l5pucy3dmvzxgs3fpfewix27l5pq/mybucket/myprefix/myobject + * + * @throws UplinkException + */ + public function joinShareUrl( + string $baseUrl, + string $accessKeyId, + string $bucket = "", + string $key = "", + bool $raw = false + ): string + { + $cOptions = $this->ffi->new('EdgeShareURLOptions'); + $cOptions->raw = $raw; + + $scope = new Scope(); + $cStringResult = $this->ffi->edge_join_share_url( + $baseUrl, + $accessKeyId, + $bucket, + $key, + FFI::addr($cOptions) + ); + + $scope->onExit( + fn() => $this->ffi->uplink_free_string_result($cStringResult) + ); + + Util::throwIfErrorResult($cStringResult); + + return FFI::string($cStringResult->string); + } +} diff --git a/src/Exception/Edge/DialFailed.php b/src/Exception/Edge/DialFailed.php new file mode 100644 index 0000000..87efc76 --- /dev/null +++ b/src/Exception/Edge/DialFailed.php @@ -0,0 +1,8 @@ + Object\InvalidObjectKey::class, 0x21 => Object\ObjectNotFound::class, 0x22 => Object\UploadDone::class, + + 0x30 => Edge\DialFailed::class, + 0x31 => Edge\RegisterAccessFailed::class, ]; /** diff --git a/src/SharePrefix.php b/src/SharePrefix.php index 596ca8d..4d15f4b 100644 --- a/src/SharePrefix.php +++ b/src/SharePrefix.php @@ -43,9 +43,12 @@ public function getPrefix(): string * @param FFI $ffi * @param SharePrefix[] $sharePrefixes */ - public static function toCStructArray(FFI $ffi, array $sharePrefixes, Scope $scope): FFI\CData + public static function toCStructArray(FFI $ffi, array $sharePrefixes, Scope $scope): ?FFI\CData { $count = count($sharePrefixes); + if ($count === 0) { + return null; + } $cSharePrefixesType = FFI::arrayType($ffi->type('UplinkSharePrefix'), [$count]); $cSharePrefixes = $ffi->new($cSharePrefixesType); diff --git a/src/Uplink.php b/src/Uplink.php index 4faec23..361f49a 100644 --- a/src/Uplink.php +++ b/src/Uplink.php @@ -3,6 +3,7 @@ namespace Storj\Uplink; use FFI; +use Storj\Uplink\Edge\Edge; use Storj\Uplink\Exception\UplinkException; use Storj\Uplink\Internal\Scope; use Storj\Uplink\Internal\Util; @@ -170,4 +171,9 @@ public function deriveEncryptionKey(string $passphrase, string $salt): Encryptio $scope ); } + + public function edgeServices(): Edge + { + return new Edge($this->ffi); + } } diff --git a/test/Edge/RegisterAccessTest.php b/test/Edge/RegisterAccessTest.php new file mode 100644 index 0000000..1dee7cb --- /dev/null +++ b/test/Edge/RegisterAccessTest.php @@ -0,0 +1,50 @@ +markTestSkipped('No auth service address set'); + } + + $edgeConfig = (new Config())->withAuthServiceAddress($authService); + $edge = Util::uplink()->edgeServices(); + + // set expiry so we don't pollute the Auth service prod datebase when running tests against prod + $tomorrow = (new DateTimeImmutable())->add(new DateInterval('P1D')); + $access = Util::access()->share( + Permission::readOnlyPermission() + ->notAfter($tomorrow) + ); + + $credentials = $edge->registerAccess($edgeConfig, $access); + + // just to check it isn't empty or garbage + self::assertMatchesRegularExpression('~\w{10,200}~', $credentials->getAccessKeyId()); + self::assertMatchesRegularExpression('~\w{10,200}~', $credentials->getSecretKey()); + self::assertMatchesRegularExpression('~https://[\w.]{10,200}~', $credentials->getEndpoint()); + } + + public function testRegisterAccessInvalidAddress(): void + { + // No DRPC auth service is running at this address. + $edgeConfig = (new Config())->withAuthServiceAddress('storj.io:33463'); + $uplink = Util::uplink(); + $edge = $uplink->edgeServices(); + + $this->expectException(DialFailed::class); + $edge->registerAccess($edgeConfig, Util::access()); + } +} diff --git a/test/Edge/ShareUrlTest.php b/test/Edge/ShareUrlTest.php new file mode 100644 index 0000000..8a6703d --- /dev/null +++ b/test/Edge/ShareUrlTest.php @@ -0,0 +1,51 @@ +edgeServices(); + + self::assertEquals( + 'https://link.storjshare.io/s/l5pucy3dmvzxgs3fpfewix27l5pq', + $edge->joinShareUrl( + 'https://link.storjshare.io', + 'l5pucy3dmvzxgs3fpfewix27l5pq') + ); + + self::assertEquals( + 'https://link.storjshare.io/s/l5pucy3dmvzxgs3fpfewix27l5pq/mybucket/myprefix/myobject', + $edge->joinShareUrl( + 'https://link.storjshare.io', + 'l5pucy3dmvzxgs3fpfewix27l5pq', + 'mybucket', + 'myprefix/myobject' + ) + ); + + self::assertEquals( + 'https://link.storjshare.io/raw/l5pucy3dmvzxgs3fpfewix27l5pq/mybucket/myprefix/myobject', + $edge->joinShareUrl( + 'https://link.storjshare.io', + 'l5pucy3dmvzxgs3fpfewix27l5pq', + 'mybucket', + 'myprefix/myobject', + true + ) + ); + + $this->expectExceptionMessage('uplink: bucket is required if key is specified'); + $edge->joinShareUrl( + 'https://link.storjshare.io', + 'l5pucy3dmvzxgs3fpfewix27l5pq', + '', + 'myprefix/myobject' + ); + } +}