WIP storage API.
This commit is contained in:
parent
aac8911b4b
commit
add738b492
14 changed files with 617 additions and 61 deletions
10
composer.lock
generated
10
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
parameters:
|
||||
level: 5
|
||||
level: 9
|
||||
treatPhpDocTypesAsCertain: false
|
||||
paths:
|
||||
- src
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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] : '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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', []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
481
src/V1/V1StorageRoutes.php
Normal 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],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue