WIP storage API.

This commit is contained in:
flash 2025-01-14 01:45:03 +00:00
parent aac8911b4b
commit add738b492
14 changed files with 617 additions and 61 deletions

10
composer.lock generated
View file

@ -47,11 +47,11 @@
},
{
"name": "flashwave/index",
"version": "v0.2410.211811",
"version": "v0.2410.630140",
"source": {
"type": "git",
"url": "https://patchii.net/flash/index.git",
"reference": "40cbd35ba3855056987d2f7647f669e66f938979"
"reference": "469391f9b601bf30553252470f175588744d4c18"
},
"require": {
"ext-mbstring": "*",
@ -60,8 +60,8 @@
"twig/twig": "^3.14"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^11.2"
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.4"
},
"suggest": {
"ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).",
@ -98,7 +98,7 @@
],
"description": "Composer package for the common library for my projects.",
"homepage": "https://railgun.sh/index",
"time": "2024-10-21T18:15:09+00:00"
"time": "2024-12-02T01:41:44+00:00"
},
{
"name": "guzzlehttp/psr7",

View file

@ -1,5 +1,5 @@
parameters:
level: 5
level: 9
treatPhpDocTypesAsCertain: false
paths:
- src

View file

@ -3,6 +3,7 @@ namespace Syokuhou;
use RuntimeException;
use RPCii\Client\RpcClient;
use Index\Http\{HttpRequest,HttpResponseBuilder};
class AuthzContext {
public private(set) bool $authed = false;
@ -11,8 +12,9 @@ class AuthzContext {
public private(set) string $type = '';
public private(set) string $userId = '';
public private(set) string $appId = '';
/** @var ?mixed[] */
private ?array $scopeRaw = null;
private ?int $expiresAtRaw = null;
private ?int $expiresAtRaw = null; // @phpstan-ignore-line: property hook skill issue
public function __construct(
private RpcClient $rpcClient
@ -68,13 +70,13 @@ class AuthzContext {
get => $this->scopeRaw === null ? '' : implode(' ', $this->scopeRaw);
}
/** @var string[] */
public array $scope {
get => $this->scopeRaw ?? [];
}
public function hasScope(string $scope): bool {
return $this->scopeRaw === null
|| in_array($scope, $this->scopeRaw);
return $this->isUser && ($this->hasWildcardScope || in_array($scope, $this->scopeRaw));
}
public int $expiresAt {
@ -88,7 +90,9 @@ class AuthzContext {
private function handleRpcResponse(mixed $info): void {
if(!is_array($info)) {
$this->error = json_encode($info);
$data = json_encode($info);
if(!$data) $data = 'data';
$this->error = $data;
return;
}
@ -133,7 +137,7 @@ class AuthzContext {
$this->attemptBasicAppAuthInternal($remoteAddr, $clientId, $clientSecret);
}
public function basicAppAuthMiddleware($response, $request): void {
public function basicAppAuthMiddleware(HttpResponseBuilder $response, HttpRequest $request): void {
if($this->authed)
return;
@ -177,7 +181,7 @@ class AuthzContext {
$this->attemptBearerTokenAuthInternal($remoteAddr, $bearerToken);
}
public function bearerTokenAuthMiddleware($response, $request): void {
public function bearerTokenAuthMiddleware(HttpResponseBuilder $response, HttpRequest $request): void {
if($this->authed)
return;
@ -219,7 +223,7 @@ class AuthzContext {
$this->attemptMisuzuTokenAuthInternal($remoteAddr, $misuzuToken);
}
public function misuzuTokenAuthMiddleware($response, $request) {
public function misuzuTokenAuthMiddleware(HttpResponseBuilder $response, HttpRequest $request): void {
if($this->authed)
return;
@ -241,4 +245,14 @@ class AuthzContext {
$misuzuToken
);
}
public function misuzuCookieAuthMiddleware(HttpResponseBuilder $response, HttpRequest $request): void {
if($this->authed || !$request->hasCookie('msz_auth'))
return;
$this->attemptMisuzuTokenAuthInternal(
(string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'),
$request->getCookie('msz_auth')
);
}
}

View file

@ -4,6 +4,7 @@ namespace Syokuhou;
use Index\{MediaType,XString};
class HttpAcceptHeader {
/** @param MediaType[] $accept */
public function __construct(
private array $accept,
) {
@ -22,6 +23,7 @@ class HttpAcceptHeader {
return false;
}
/** @param array<MediaType|string> $options */
public function getMostPreferred(array $options = []): ?string {
if(empty($options))
return null;

View file

@ -2,9 +2,11 @@
namespace Syokuhou\OAuth2;
use RuntimeException;
use Stringable;
use Syokuhou\SyokuhouContext;
use Syokuhou\RpcModels\Hanyuu\OAuth2RfcModel;
use Index\Config\Config;
use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HandlerAttribute,HttpGet,HttpOptions,HttpPost,Router,RouteHandler,RouteHandlerTrait};
class OAuth2Routes implements RouteHandler {
@ -25,12 +27,17 @@ class OAuth2Routes implements RouteHandler {
HandlerAttribute::register($router, $this);
}
private static function filter($response, array $result, bool $authzHeader = false): array {
/**
* @param array<string, mixed> $result
* @return array<string, mixed>
*/
private static function filter(HttpResponseBuilder $response, array $result, bool $authzHeader = false): array {
if(array_key_exists('error', $result)) {
if($authzHeader) {
$wwwAuth = sprintf('Basic realm="%s"', $_SERVER['HTTP_HOST']);
$wwwAuth = sprintf('Basic realm="%s"', (string)filter_input(INPUT_SERVER, 'HTTP_HOST'));
foreach($result as $name => $value)
$wwwAuth .= sprintf(', %s="%s"', $name, rawurlencode($value));
if(is_scalar($value) || $value instanceof Stringable)
$wwwAuth .= sprintf(', %s="%s"', $name, rawurlencode((string)$value));
$response->setStatusCode(401);
$response->setHeader('WWW-Authenticate', $wwwAuth);
@ -41,9 +48,10 @@ class OAuth2Routes implements RouteHandler {
return $result;
}
/** @return void|int */
#[HttpGet('/authorise')]
#[HttpGet('/authorize')]
public function getAuthorise($response, $request) {
public function getAuthorise(HttpResponseBuilder $response, HttpRequest $request) {
$url = trim($this->config->getString('authorise'));
if($url === '')
return 404;
@ -57,9 +65,12 @@ class OAuth2Routes implements RouteHandler {
$response->redirect($url);
}
/**
* @return array<string, mixed>
*/
#[HttpPost('/request-authorise')]
#[HttpPost('/request-authorize')]
public function postRequestAuthorise($response, $request) {
public function postRequestAuthorise(HttpResponseBuilder $response, HttpRequest $request): array {
$response->setHeader('Cache-Control', 'no-store');
if($this->ctx->authz->error === 'secret')
@ -68,20 +79,21 @@ class OAuth2Routes implements RouteHandler {
'error_description' => 'Provided client secret is not correct for this application.',
], authzHeader: true);
if(!$request->isFormContent())
$content = $request->getContent();
if(!($content instanceof FormHttpContent))
return self::filter($response, [
'error' => 'invalid_request',
'error_description' => 'Your request must use content type application/x-www-form-urlencoded.',
]);
$rpc = $this->ctx->rpc;
$content = $request->getContent();
if(!$this->ctx->authz->authed)
if(!$this->ctx->authz->authed) {
$this->ctx->authz->attemptBasicAppAuth(
(string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'),
(string)$content->getParam('client_id')
);
}
if(!$this->ctx->authz->isApp)
return self::filter($response, [
@ -90,10 +102,17 @@ class OAuth2Routes implements RouteHandler {
]);
try {
$reqInfo = new OAuth2RfcModel($rpc->action('hanyuu:oauth2:createAuthoriseRequest', [
$reqInfo = $rpc->action('hanyuu:oauth2:createAuthoriseRequest', [
'appId' => $this->ctx->authz->appId,
'scope' => (string)$content->getParam('scope'),
]));
]);
if(!is_array($reqInfo))
return self::filter($response, [
'error' => 'server_error',
'error_description' => 'Unable to create authorisation request.',
]);
$reqInfo = new OAuth2RfcModel($reqInfo);
} catch(RuntimeException $ex) {
$reqInfo = null;
}
@ -107,9 +126,12 @@ class OAuth2Routes implements RouteHandler {
return self::filter($response, $reqInfo->raw);
}
/**
* @return int|array<string, mixed>
*/
#[HttpOptions('/token')]
#[HttpPost('/token')]
public function postToken($response, $request) {
public function postToken(HttpResponseBuilder $response, HttpRequest $request): int|array {
$response->setHeader('Cache-Control', 'no-store');
$originHeaders = ['Origin', 'X-Origin', 'Referer'];
@ -125,7 +147,7 @@ class OAuth2Routes implements RouteHandler {
// different origins being specified for each header should probably also be considered suspect...
$response->setHeader('Access-Control-Allow-Origin', $origins[0]);
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST');
$response->setHeader('Access-Control-Allow-Methods', 'POST');
$response->setHeader('Access-Control-Allow-Headers', 'Authorization');
$response->setHeader('Access-Control-Expose-Headers', 'Vary');
foreach($originHeaders as $originHeader)
@ -135,14 +157,14 @@ class OAuth2Routes implements RouteHandler {
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$request->isFormContent())
$content = $request->getContent();
if(!($content instanceof FormHttpContent))
return self::filter($response, [
'error' => 'invalid_request',
'error_description' => 'Your request must use content type application/x-www-form-urlencoded.',
]);
$rpc = $this->ctx->rpc;
$content = $request->getContent();
$authzHeader = true;
if(!$this->ctx->authz->authed) {

View file

@ -6,6 +6,7 @@ use Index\Config\Config;
use RPCii\Client\{RpcClient,HttpRpcClient,ScopedRpcClient};
class RpcClientWrapper implements RpcClient {
/** @var array<string, RpcClient> */
private array $clients = [];
public function createHmacConfig(Config $config, string $prefix): void {
@ -17,6 +18,7 @@ class RpcClientWrapper implements RpcClient {
);
}
/** @param callable(): string $getSecretKey */
public function createHmac(string $prefix, string $url, $getSecretKey): void {
$this->register($prefix, HttpRpcClient::createHmac($url, $getSecretKey));
}

View file

@ -2,11 +2,10 @@
namespace Syokuhou\RpcModels;
abstract class RpcModel {
protected array $info;
public function __construct(array $info) {
$this->info = $info;
}
/** @param array<mixed, mixed> $raw */
public function __construct(
public private(set) array $raw
) {}
public bool $hasError {
get => $this->hasValue('error');
@ -16,40 +15,37 @@ abstract class RpcModel {
get => $this->getString('error');
}
public array $raw {
get => $this->info;
}
protected function hasValue(string $name): bool {
return array_key_exists($name, $this->info);
return array_key_exists($name, $this->raw);
}
protected function getValue(string $name, mixed $fallback = null): mixed {
return array_key_exists($name, $this->info) ? $this->info[$name] : $fallback;
return array_key_exists($name, $this->raw) ? $this->raw[$name] : $fallback;
}
/** @return mixed[] */
protected function getArray(string $name): array {
return array_key_exists($name, $this->info) && is_array($this->info[$name])
? $this->info[$name] : [];
return array_key_exists($name, $this->raw) && is_array($this->raw[$name])
? $this->raw[$name] : [];
}
protected function getBoolean(string $name): bool {
return array_key_exists($name, $this->info) && is_bool($this->info[$name])
&& $this->info[$name];
return array_key_exists($name, $this->raw) && is_bool($this->raw[$name])
&& $this->raw[$name];
}
protected function getInteger(string $name): int {
return array_key_exists($name, $this->info) && is_int($this->info[$name])
? $this->info[$name] : 0;
return array_key_exists($name, $this->raw) && is_int($this->raw[$name])
? $this->raw[$name] : 0;
}
protected function getFloat(string $name): float {
return array_key_exists($name, $this->info) && is_float($this->info[$name])
? $this->info[$name] : 0.0;
return array_key_exists($name, $this->raw) && is_float($this->raw[$name])
? $this->raw[$name] : 0.0;
}
protected function getString(string $name): string {
return array_key_exists($name, $this->info) && is_string($this->info[$name])
? $this->info[$name] : '';
return array_key_exists($name, $this->raw) && is_string($this->raw[$name])
? $this->raw[$name] : '';
}
}

View file

@ -3,6 +3,7 @@ namespace Syokuhou;
use RuntimeException;
use Index\Config\Config;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpRouter,Router};
class SyokuhouContext {
@ -14,6 +15,7 @@ class SyokuhouContext {
$this->rpc = new RpcClientWrapper;
$this->rpc->createHmacConfig($config, 'hanyuu');
$this->rpc->createHmacConfig($config, 'misuzu');
$this->rpc->createHmacConfig($config, 'eeprom');
$this->authz = new AuthzContext($this->rpc);
@ -26,10 +28,11 @@ class SyokuhouContext {
$this->router->get('/', fn() => '<!doctype html>Hello! Someday this page will probably redirect to documentation, but none exists yet.');
$this->router->scopeTo('/oauth2')->register(new OAuth2\OAuth2Routes($this, $config->scopeTo('oauth2')));
$this->router->scopeTo('/v1')->register(new V1\V1Routes(new V1\V1Context($this)));
$this->router->scopeTo('/v1')->register(new V1\V1Routes(new V1\V1Context($this, $config->scopeTo('v1'))));
}
public function handleAcceptHeader($response, $request) {
/** @return void|int */
public function handleAcceptHeader(HttpResponseBuilder $response, HttpRequest $request) {
$accept = HttpAcceptHeader::parse($request->getHeaderLine('Accept'));
$contentHandler = new SyokuhouContentHandler($accept);

View file

@ -10,7 +10,7 @@ class SyokuhouErrorHandler implements HttpErrorHandler {
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
$this->contentHandler->handle($response, [
'code' => sprintf('http:%s', $code),
'error' => sprintf('http:%s', $code),
'message' => $message,
]);
}

View file

@ -1,12 +1,14 @@
<?php
namespace Syokuhou\V1;
use Syokuhou\{SyokuhouContext,AuthzContext};
use Index\Config\Config;
use RPCii\Client\RpcClient;
use Syokuhou\{SyokuhouContext,AuthzContext};
class V1Context {
public function __construct(
private SyokuhouContext $ctx
private SyokuhouContext $ctx, // @phpstan-ignore-line: property hook issue
private Config $config
) {}
public RpcClient $rpc {
@ -16,4 +18,8 @@ class V1Context {
public AuthzContext $authz {
get => $this->ctx->authz;
}
public array $allowCookieOrigins {
get => $this->config->getArray('allowCookie', []);
}
}

View file

@ -1,6 +1,7 @@
<?php
namespace Syokuhou\V1;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,HttpOptions,RouteHandler,RouteHandlerTrait};
use RPCii\Client\RpcClient;
@ -11,20 +12,24 @@ class V1EmotesRoutes implements RouteHandler {
private RpcClient $rpc
) {}
/** @return int|mixed[] */
#[HttpOptions('/')]
#[HttpGet('/')]
public function getEmotes($response, $request) {
$response->setHeader('Access-Control-Allow-Origin', '*');
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
$response->setHeader('Access-Control-Allow-Headers', 'Cache-Control');
public function getEmotes(HttpResponseBuilder $response, HttpRequest $request): int|array {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->addHeader('Access-Control-Allow-Headers', 'Cache-Control');
$response->setHeader('Cache-Control', 'public, max-age=3600');
if($request->getMethod() === 'OPTIONS')
return 204;
return $this->rpc->query('all', [
$result = $this->rpc->query('all', [
'includeId' => !empty($request->getParam('include_id')),
'includeOrder' => !empty($request->getParam('include_order')),
]);
if(!is_array($result))
return 500;
return $result;
}
}

View file

@ -1,6 +1,7 @@
<?php
namespace Syokuhou\V1;
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{Router,RouteHandler};
class V1Routes implements RouteHandler {
@ -9,8 +10,27 @@ class V1Routes implements RouteHandler {
) {}
public function registerRoutes(Router $router): void {
$router->use('/', function(HttpResponseBuilder $response, HttpRequest $request) {
$response->setHeader('Access-Control-Allow-Origin', '*');
$response->setHeader('Access-Control-Allow-Headers', 'Authorization');
if($request->hasHeader('Origin')) {
$host = parse_url($request->getHeaderLine('Origin'), PHP_URL_HOST);
if(is_string($host)) {
$host = '.' . $host;
$allowCookieOrigins = $this->ctx->allowCookieOrigins;
foreach($allowCookieOrigins as $allowCookieOrigin)
if(str_ends_with($host, $allowCookieOrigin)) {
$response->setHeader('Access-Control-Allow-Credentials', 'true');
break;
}
}
}
});
$router->use('/', $this->ctx->authz->bearerTokenAuthMiddleware(...));
$router->use('/', $this->ctx->authz->misuzuTokenAuthMiddleware(...));
$router->use('/', $this->ctx->authz->misuzuCookieAuthMiddleware(...));
$router->get('/', fn() => ['status' => 'operational']);
@ -18,6 +38,10 @@ class V1Routes implements RouteHandler {
new V1EmotesRoutes($this->ctx->rpc->scopeTo('misuzu:emotes:'))
);
$router->scopeTo('/storage')->register(
new V1StorageRoutes($this->ctx, $this->ctx->rpc->scopeTo('eeprom:'))
);
$usersRoutes = new V1UsersRoutes($this->ctx, $this->ctx->rpc->scopeTo('misuzu:users:'));
$router->options('/me', $usersRoutes->getMe(...));
$router->get('/me', $usersRoutes->getMe(...));

481
src/V1/V1StorageRoutes.php Normal file
View file

@ -0,0 +1,481 @@
<?php
namespace Syokuhou\V1;
use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerTrait};
use RPCii\Client\RpcClient;
class V1StorageRoutes implements RouteHandler {
use RouteHandlerTrait;
public function __construct(
private V1Context $ctx,
private RpcClient $rpc
) {}
#[HttpOptions('/')]
#[HttpGet('/')]
public function getIndex(HttpResponseBuilder $response, HttpRequest $request) {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
return [
'meow' => 'return stats n junk here ig',
];
}
private function handleUploadsAllResponse(HttpResponseBuilder $response, array|string $result): array {
if(is_array($result) && array_key_exists('result', $result))
return $result['result'];
$result = match($result) {
'pip' => [403, 'storage:pool_protected'],
'pnf' => [404, 'storage:pool_not_found'],
'biv' => [400, 'storage:uploads_limit'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpOptions('/uploads')]
#[HttpGet('/uploads')]
public function getUploads(HttpResponseBuilder $response, HttpRequest $request) {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$limit = null;
if($request->hasParam('take')) {
$limit = (int)$request->getParam('take');
if($limit < 1 || $limit > 100) {
$response->setStatusCode(400);
return [
'error' => 'storage:limit',
];
}
}
$beforeId = null;
if($request->hasParam('before'))
$beforeId = $request->getParam('before');
return $this->handleUploadsAllResponse($response, $this->rpc->query('uploads:all', [
'userId' => $this->ctx->authz->userId, // null for mod?
'limit' => $limit,
'beforeId' => $beforeId,
'revealProtectedPool' => false, // reveal for mod?
]));
}
#[HttpOptions('/uploads/([A-Za-z0-9]+)')]
#[HttpGet('/uploads/([A-Za-z0-9]+)')]
public function getUpload(HttpResponseBuilder $response, HttpRequest $request, string $uploadId) {
$response->setHeader('Access-Control-Allow-Methods', 'GET, DELETE');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$result = $this->rpc->query('uploads:get', [
'uploadId' => $uploadId,
]);
if(is_array($result) && array_key_exists('result', $result))
return $result['result'];
$result = match($result) {
'unf' => [404, 'storage:upload_not_found'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpDelete('/uploads/([A-Za-z0-9]+)')]
public function deleteUpload(HttpResponseBuilder $response, HttpRequest $request, string $uploadId) {
$response->setHeader('Access-Control-Allow-Methods', 'GET, DELETE');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage:delete')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$result = $this->rpc->action('uploads:delete', [
'uploadId' => $uploadId,
'userId' => $this->ctx->authz->userId, // null for mod?
]);
if($result === '')
return 204;
$result = match($result) {
'unf' => [404, 'storage:upload_not_found'],
'dna' => [403, 'storage:delete_not_allowed'],
'duf' => [500, 'storage:delete_failed'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpOptions('/uploads/([A-Za-z0-9]+)/variants')]
#[HttpGet('/uploads/([A-Za-z0-9]+)/variants')]
public function getUploadVariants(HttpResponseBuilder $response, HttpRequest $request, string $uploadId) {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$result = $this->rpc->query('uploads:variants:all', [
'uploadId' => $uploadId,
]);
if(is_array($result) && array_key_exists('result', $result))
return $result['result'];
$result = match($result) {
'unf' => [404, 'storage:upload_not_found'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpOptions('/uploads/([A-Za-z0-9]+)/variants/([a-z0-9]+)')]
#[HttpGet('/uploads/([A-Za-z0-9]+)/variants/([a-z0-9]+)')]
public function getUploadVariant(HttpResponseBuilder $response, HttpRequest $request, string $uploadId, string $variant) {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$result = $this->rpc->query('uploads:variants:get', [
'uploadId' => $uploadId,
'variant' => $variant
]);
if(is_array($result) && array_key_exists('result', $result))
return $result['result'];
$result = match($result) {
'unf' => [404, 'storage:upload_not_found'],
'vnf' => [404, 'storage:variant_not_found'],
'fnf' => [500, 'storage:file_not_found'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpOptions('/pools')]
#[HttpGet('/pools')]
public function getPools(HttpResponseBuilder $response, HttpRequest $request) {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$result = $this->rpc->query('pools:all', [
'protected' => !empty($request->getParam('with_protected')),
'deprecated' => !empty($request->getParam('with_deprecated')),
]);
if(is_array($result) && array_key_exists('result', $result))
return $result['result'];
$response->setStatusCode(500);
return [
'error' => 'storage:unknown',
];
}
#[HttpOptions('/pools/([a-z0-9\-]+)')]
#[HttpGet('/pools/([a-z0-9\-]+)')]
public function getPool(HttpResponseBuilder $response, HttpRequest $request, string $poolName) {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$result = $this->rpc->query('pools:get', [
'name' => $poolName,
]);
if(is_array($result) && array_key_exists('result', $result))
return $result['result'];
$result = match($result) {
'pnf' => [404, 'storage:pool_not_found'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpOptions('/pools/([a-z0-9\-]+)/uploads')]
#[HttpGet('/pools/([a-z0-9\-]+)/uploads')]
public function getPoolUploads(HttpResponseBuilder $response, HttpRequest $request, string $poolName) {
$response->setHeader('Access-Control-Allow-Methods', 'GET, POST');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
if(!$this->ctx->authz->hasScope('storage')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
$limit = null;
if($request->hasParam('take')) {
$limit = (int)$request->getParam('take');
if($limit < 1 || $limit > 100) {
$response->setStatusCode(400);
return [
'error' => 'storage:uploads_limit',
];
}
}
$beforeId = null;
if($request->hasParam('before'))
$beforeId = $request->getParam('before');
return $this->handleUploadsAllResponse($response, $this->rpc->query('uploads:all', [
'poolName' => $poolName,
'userId' => $this->ctx->authz->userId, // null for mod?
'limit' => $limit,
'beforeId' => $beforeId,
'revealProtectedPool' => false, // reveal for mod?
]));
}
#[HttpPost('/pools/([a-z0-9\-]+)/uploads')]
public function postUpload(HttpResponseBuilder $response, HttpRequest $request, string $poolName) {
$response->setHeader('Access-Control-Allow-Methods', 'GET, POST');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
$content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
if(!$this->ctx->authz->hasScope('storage:upload')
&& !$this->ctx->authz->hasScope('beans'))
return 403;
if(!$content->hasParam('hash')) {
$response->setStatusCode(400);
return [
'error' => 'missing_param',
'name' => 'hash',
];
}
if(!$content->hasParam('size')) {
$response->setStatusCode(400);
return [
'error' => 'missing_param',
'name' => 'size',
];
}
if(!$content->hasParam('type')) {
$response->setStatusCode(400);
return [
'error' => 'missing_param',
'name' => 'type',
];
}
$args = [
'remoteAddr' => $request->getRemoteAddress(),
'userId' => $this->ctx->authz->userId,
'poolName' => $poolName,
'fileName' => (string)$content->getParam('name'),
'fileSize' => (int)$content->getParam('size'),
'fileType' => (string)$content->getParam('type'),
'fileHash' => (string)$content->getParam('hash'),
];
if($content->hasParam('signature'))
$args['signature'] = (string)$content->getParam('signature');
$result = $this->rpc->action('tasks:create', $args);
if(is_array($result)) {
if(array_key_exists('result', $result)) {
$response->setStatusCode(
array_key_exists('state', $result['result']) && $result['result']['state'] === 'pending'
? 201 : 200
);
return $result['result'];
}
if(array_key_exists('error', $result))
switch($result['error']) {
case 'fhdl':
$response->setStatusCode(451);
return [
'error' => 'storage:task_denied',
'reason' => $result['reason'] ?? 'other',
];
case 'prcf':
$response->setStatusCode(400);
// this needs to do better error conversion
return [
'error' => 'storage:task_rule_failed',
'rule' => $result['rule'] ?? '',
'args' => $result['args'] ?? '',
];
default:
$result = $result['error'];
break;
}
}
$result = match($result) {
'fsts' => [400, 'storage:task_size_too_small'],
'fttl' => [400, 'storage:task_type_too_long'],
'ftiv' => [400, 'storage:task_type_not_valid'],
'fntl' => [400, 'storage:task_name_too_long'],
'uinn' => [500, 'storage:task_user_id_not_valid'],
'raiv' => [500, 'storage:task_remote_address_not_valid'],
'fhiv' => [400, 'storage:task_hash_not_valid'],
'pnnf' => [404, 'storage:pool_not_found'],
'srpp' => [403, 'storage:task_signature_required'],
'sans' => [400, 'storage:task_signature_algo_missing'],
'shns' => [400, 'storage:task_signature_hash_missing'],
'stns' => [400, 'storage:task_signature_time_missing'],
'ssns' => [400, 'storage:task_signature_salt_missing'],
'sanv' => [400, 'storage:task_signature_algo_not_supported'],
'stnv' => [403, 'storage:task_signature_time_not_valid'],
'ssts' => [400, 'storage:task_signature_salt_too_short'],
'shnv' => [400, 'storage:task_signature_hash_not_valid'],
'shvf' => [403, 'storage:task_signature_verification_failed'],
'smbn' => [400, 'storage:task_signature_not_required'],
'fsnm' => [400, 'storage:task_file_size_hash_not_match'],
'wfcf' => [500, 'storage:task_data_create_failed'],
'wfrf' => [500, 'storage:task_data_reserve_failed'],
'wfff' => [500, 'storage:task_data_flush_failed'],
'utcf' => [500, 'storage:task_create_failed'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpOptions('/tasks/([A-Za-z0-9]+)')]
#[HttpGet('/tasks/([A-Za-z0-9]+)')]
public function getTask(HttpResponseBuilder $response, HttpRequest $request, string $taskId) {
$response->setHeader('Access-Control-Allow-Methods', 'GET, POST');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
$result = $this->rpc->query('tasks:get', [
'taskId' => $taskId,
]);
if(is_array($result) && array_key_exists('result', $result))
return $result['result'];
$result = match($result) {
'tnf' => [404, 'storage:task_not_found'],
'svf' => [404, 'storage:task_not_found'],
default => [500, 'storage:unknown'],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
#[HttpPost('/tasks/([A-Za-z0-9]+)')]
public function postTask(HttpResponseBuilder $response, HttpRequest $request, string $taskId) {
$response->setHeader('Access-Control-Allow-Methods', 'GET, POST');
$response->setHeader('Cache-Control', 'no-store');
if($request->getMethod() === 'OPTIONS')
return 204;
$result = $this->rpc->action('tasks:finish', [
'taskId' => $taskId,
]);
if(is_array($result) && array_key_exists('result', $result)) {
$response->setStatusCode(201);
return $result['result'];
}
$result = match($result) {
'tnf' => [404, 'storage:task_not_found'],
'svf' => [404, 'storage:task_not_found'],
'fnf' => [404, 'storage:file_not_found'],
'fne' => [500, 'storage:task_data_create_failed'],
'hnm' => [400, 'storage:task_hash_not_match'],
'tcf' => [400, 'storage:task_unknown'],
'tnp' => [403, 'storage:task_not_pending'],
default => [500, $result],
};
$response->setStatusCode($result[0]);
return [
'error' => $result[1],
];
}
}

View file

@ -1,6 +1,7 @@
<?php
namespace Syokuhou\V1;
use Index\Http\HttpResponseBuilder;
use Index\Http\Routing\{RouteHandler,RouteHandlerTrait};
use RPCii\Client\RpcClient;
@ -12,9 +13,9 @@ class V1UsersRoutes implements RouteHandler {
private RpcClient $rpc
) {}
public function getMe($response) {
$response->setHeader('Access-Control-Allow-Origin', '*');
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
/** @return int|mixed[] */
public function getMe(HttpResponseBuilder $response): array|int {
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Cache-Control', 'no-store');
if(!$this->ctx->authz->hasScope('identify')