diff --git a/Makefile b/Makefile index 8e5c16e..2c54dda 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ UID := $(shell id -u) tmp/uplink-c: mkdir -p tmp - git clone --branch v1.5.0 https://github.com/storj/uplink-c.git ./tmp/uplink-c + git clone --branch v1.5.1 https://github.com/storj/uplink-c.git ./tmp/uplink-c .PHONY: clean clean: diff --git a/docker/go-docker/Dockerfile b/docker/go-docker/Dockerfile deleted file mode 100644 index 84e3911..0000000 --- a/docker/go-docker/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM golang:1.18.3-buster - -RUN apt update -RUN apt install -y docker.io diff --git a/docker/phpunit/Dockerfile b/docker/phpunit/Dockerfile deleted file mode 100644 index 43d2781..0000000 --- a/docker/phpunit/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# docker container with PHP and the Storj test network -FROM php:8.0.12-cli - -RUN apt-get update \ - && apt-get install -y libffi-dev postgresql git redis-server wget \ - && docker-php-ext-install ffi - -RUN wget https://golang.org/dl/go1.17.2.linux-amd64.tar.gz \ - && tar -xvf go1.17.2.linux-amd64.tar.gz \ - && mv go /usr/local - -ENV GOROOT=/usr/local/go -ENV PATH="/usr/local/go/bin:${PATH}" - -RUN rm /etc/postgresql/13/main/pg_hba.conf; \ - echo 'local all all trust' >> /etc/postgresql/13/main/pg_hba.conf; \ - echo 'host all all 127.0.0.1/8 trust' >> /etc/postgresql/13/main/pg_hba.conf; \ - echo 'host all all ::1/128 trust' >> /etc/postgresql/13/main/pg_hba.conf; \ - echo 'host all all ::0/0 trust' >> /etc/postgresql/13/main/pg_hba.conf; - -RUN git clone https://github.com/storj/storj.git /storj \ - && cd /storj \ - && make install-sim diff --git a/docker/zip/Dockerfile b/docker/zip/Dockerfile deleted file mode 100644 index cdf1890..0000000 --- a/docker/zip/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM debian:11.2 - -RUN apt-get update && apt-get install -y zip make 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..79c8bf9 --- /dev/null +++ b/src/Edge/Edge.php @@ -0,0 +1,102 @@ +ffi = $ffi; + } + + /** + * @throws \Storj\Uplink\Exception\UplinkException + */ + 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 DialFailed in case of network errors + * @throws RegisterAccessFailed in case of server errors + */ + 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..d988585 --- /dev/null +++ b/test/Edge/RegisterAccessTest.php @@ -0,0 +1,51 @@ +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..a0fb6e7 --- /dev/null +++ b/test/Edge/ShareUrlTest.php @@ -0,0 +1,56 @@ +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' + ); + } +} \ No newline at end of file