1
0
Fork 0
mirror of synced 2024-09-29 17:01:37 +13:00

Add unique url generator

This commit is contained in:
Matej Bačo 2023-02-22 16:07:34 +01:00
parent 0cf6a50c3c
commit 068acef0f6
8 changed files with 220 additions and 103 deletions

4
.env
View file

@ -13,10 +13,10 @@ _APP_SYSTEM_RESPONSE_FORMAT=
_APP_OPTIONS_ABUSE=disabled _APP_OPTIONS_ABUSE=disabled
_APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_FORCE_HTTPS=disabled
_APP_OPENSSL_KEY_V1=your-secret-key _APP_OPENSSL_KEY_V1=your-secret-key
_APP_DOMAIN=demo.appwrite.io _APP_DOMAIN=localhost
_APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_DO_TOKEN= _APP_DOMAIN_DO_TOKEN=
_APP_DOMAIN_TARGET=demo.appwrite.io _APP_DOMAIN_TARGET=localhost
_APP_REDIS_HOST=redis _APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379 _APP_REDIS_PORT=6379
_APP_REDIS_PASS= _APP_REDIS_PASS=

View file

@ -769,6 +769,96 @@ $collections = [
], ],
], ],
'routes' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('routes'),
'name' => 'Routes',
'attributes' => [
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('domain'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 100,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_domain'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['domain'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_resource_id'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
'schedules' => [ 'schedules' => [
'$collection' => ID::custom(Database::METADATA), '$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('schedules'), '$id' => ID::custom('schedules'),

View file

@ -514,14 +514,14 @@ return [
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.', 'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
'code' => 401, 'code' => 401,
], ],
Exception::ROUTER_INVALID_URL => [ Exception::ROUTER_INVALID_TYPE => [
'name' => Exception::ROUTER_INVALID_URL, 'name' => Exception::ROUTER_INVALID_TYPE,
'description' => 'Invalid preview URL for Appwrite Router.', 'description' => 'Invalid domain configuration. Route type is not supported.',
'code' => 400, 'code' => 400,
], ],
Exception::ROUTER_CONSOLE_PROJECT => [ Exception::ROUTER_UNKNOWN_HOST => [
'name' => Exception::ROUTER_CONSOLE_PROJECT, 'name' => Exception::ROUTER_UNKNOWN_HOST,
'description' => 'Use of "console" project with Appwrite Router is not allowed.', 'description' => 'Host is not trusted. Add a custom domain to your project first.',
'code' => 400, 'code' => 400,
], ],
Exception::WEBHOOK_NOT_FOUND => [ Exception::WEBHOOK_NOT_FOUND => [

View file

@ -38,7 +38,6 @@ use Utopia\Validator\Range;
use Utopia\Validator\WhiteList; use Utopia\Validator\WhiteList;
use Utopia\Config\Config; use Utopia\Config\Config;
use Executor\Executor; use Executor\Executor;
use Selective\Base32\Base32;
use Utopia\CLI\Console; use Utopia\CLI\Console;
use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\Roles;
use Utopia\Validator\Boolean; use Utopia\Validator\Boolean;
@ -105,6 +104,19 @@ App::post('/v1/functions')
])) ]))
); );
$functionsDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS');
$routeSubdomain = ID::unique();
$route = Authorization::skip(
fn() => $dbForConsole->createDocument('routes', new Document([
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => "{$routeSubdomain}.{$functionsDomain}",
'resourceType' => 'function',
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
]))
);
$function->setAttribute('scheduleId', $schedule->getId()); $function->setAttribute('scheduleId', $schedule->getId());
$function->setAttribute('scheduleInternalId', $schedule->getInternalId()); $function->setAttribute('scheduleInternalId', $schedule->getInternalId());
$dbForProject->updateDocument('functions', $function->getId(), $function); $dbForProject->updateDocument('functions', $function->getId(), $function);
@ -132,7 +144,8 @@ App::get('/v1/functions')
->inject('project') ->inject('project')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->action(function (array $queries, string $search, Document $project, Response $response, Database $dbForProject) { ->inject('dbForConsole')
->action(function (array $queries, string $search, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
$queries = Query::parseQueries($queries); $queries = Query::parseQueries($queries);
@ -157,15 +170,18 @@ App::get('/v1/functions')
$filterQueries = Query::groupByType($queries)['filters']; $filterQueries = Query::groupByType($queries)['filters'];
$functionsDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS');
$projectId = $project->getId();
$base32 = new Base32();
$projectIdHash = \rtrim($base32->encode($projectId), '=');
$functions = $dbForProject->find('functions', $queries); $functions = $dbForProject->find('functions', $queries);
$functions = \array_map(function (Document $function) use ($functionsDomain, $projectIdHash, $base32) { $functions = \array_map(function (Document $function) use ($dbForConsole, $project) {
$functionId = $function->getId(); $route = Authorization::skip(
$functionIdHash = \rtrim($base32->encode($functionId), '='); fn() => $dbForConsole->find('routes', [
$function = $function->setAttribute('url', "{$functionIdHash}-{$projectIdHash}.{$functionsDomain}"); Query::equal('resourceType', ['function']),
Query::equal('resourceInternalId', [ $function->getInternalId() ]),
Query::equal('projectInternalId', [ $project->getInternalId() ]),
Query::limit(1),
Query::orderAsc('_createdAt')
])
)[0] ?? null;
$function = $function->setAttribute('url', $route ? $route->getAttribute('domain') : '');
return $function; return $function;
}, $functions); }, $functions);
@ -217,21 +233,24 @@ App::get('/v1/functions/:functionId')
->inject('project') ->inject('project')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->action(function (string $functionId, Document $project, Response $response, Database $dbForProject) { ->inject('dbForConsole')
->action(function (string $functionId, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
$function = $dbForProject->getDocument('functions', $functionId); $function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) { if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND); throw new Exception(Exception::FUNCTION_NOT_FOUND);
} }
$functionsDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS'); $route = Authorization::skip(
$projectId = $project->getId(); fn() => $dbForConsole->find('routes', [
$functionId = $function->getId(); Query::equal('resourceType', ['function']),
$base32 = new Base32(); Query::equal('resourceInternalId', [ $function->getInternalId() ]),
$projectIdHash = \rtrim($base32->encode($projectId), '='); Query::equal('projectInternalId', [ $project->getInternalId() ]),
$functionIdHash = \rtrim($base32->encode($functionId), '='); Query::limit(1),
Query::orderAsc('_createdAt')
$function = $function->setAttribute('url', "{$functionIdHash}-{$projectIdHash}.{$functionsDomain}"); ])
)[0] ?? null;
$function = $function->setAttribute('url', $route ? $route->getAttribute('domain') : '');
$response->dynamic($function, Response::MODEL_FUNCTION); $response->dynamic($function, Response::MODEL_FUNCTION);
}); });

View file

@ -37,7 +37,6 @@ use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
use Appwrite\Utopia\Request\Filters\V13 as RequestV13; use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
use Appwrite\Utopia\Request\Filters\V14 as RequestV14; use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
use Appwrite\Utopia\Request\Filters\V15 as RequestV15; use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
use Selective\Base32\Base32;
use Utopia\Validator\Text; use Utopia\Validator\Text;
use Utopia\Validator\WhiteList; use Utopia\Validator\WhiteList;
@ -65,83 +64,82 @@ App::init()
* Appwrite Router * Appwrite Router
*/ */
$host = $swooleRequest->header['host'] ?? ''; $host = $swooleRequest->header['host'] ?? '';
$mainDomain = App::getEnv('_APP_DOMAIN', '');
// Function Preview // Only run Router when external domain
if (\str_ends_with($host, App::getEnv('_APP_DOMAIN_FUNCTIONS'))) { if($host !== $mainDomain) {
// Remove domain from host $route = Authorization::skip(
$previewDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS'); fn() => $dbForConsole->find('routes', [
$host = \substr($host, 0, -1 * (1 + \strlen($previewDomain))); // +1 for starting dot (.) Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? null;
if (empty($host) || \str_contains($host, '.')) { if($route === null) {
throw new AppwriteException(AppwriteException::ROUTER_INVALID_URL); throw new AppwriteException(AppwriteException::ROUTER_UNKNOWN_HOST);
} }
$type = $route->getAttribute('resourceType');
$ids = explode('-', $host); if($type === 'function') {
$functionId = $route->getAttribute('resourceId');
$projectId = $route->getAttribute('projectId');
if (\count($ids) !== 2) { $body = \json_encode([
throw new AppwriteException(AppwriteException::ROUTER_INVALID_URL); 'async' => false,
'body' => $swooleRequest->getContent() ?? '',
'method' => $swooleRequest->server['request_method'],
'path' => $swooleRequest->server['path_info'],
'headers' => $swooleRequest->header
]);
$headers = [
'Content-Type: application/json',
'Content-Length: ' . \strlen($body),
'X-Appwrite-Project: ' . $projectId
];
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions");
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// \curl_setopt($ch, CURLOPT_HEADER, true);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
$executionResponse = \curl_exec($ch);
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = \curl_error($ch);
$errNo = \curl_errno($ch);
\curl_close($ch);
if ($errNo !== 0) {
return $response->setStatusCode(500)->send("Internal error: " . $error);
}
if ($statusCode >= 400) {
$error = \json_decode($executionResponse, true)['message'];
return $response->setStatusCode(500)->send("Execution error: " . $error);
}
$execution = \json_decode($executionResponse, true);
foreach ($execution['headers'] as $header => $value) {
$response->setHeader($header, $value);
}
$body = $execution['body'] ?? '';
if (($execution['headers']['x-open-runtimes-encoding'] ?? '') === 'base64') {
$body = \base64_decode($body);
}
return $response->setStatusCode($execution['statusCode'] ?? 200)->send($body);
} else {
throw new AppwriteException(AppwriteException::ROUTER_INVALID_TYPE);
} }
// Active deployment preview
$functionIdHash = $ids[0];
$projectIdHash = $ids[1];
$base32 = new Base32();
$functionId = $base32->decode($functionIdHash);
$projectId = $base32->decode($projectIdHash);
$body = \json_encode([
'async' => false,
'body' => $swooleRequest->getContent() ?? '',
'method' => $swooleRequest->server['request_method'],
'path' => $swooleRequest->server['path_info'],
'headers' => $swooleRequest->header
]);
$headers = [
'Content-Type: application/json',
'Content-Length: ' . \strlen($body),
'X-Appwrite-Project: ' . $projectId
];
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions");
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// \curl_setopt($ch, CURLOPT_HEADER, true);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
$executionResponse = \curl_exec($ch);
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = \curl_error($ch);
$errNo = \curl_errno($ch);
\curl_close($ch);
if ($errNo !== 0) {
return $response->setStatusCode(500)->send("Internal error: " . $error);
}
if ($statusCode >= 400) {
$error = \json_decode($executionResponse, true)['message'];
return $response->setStatusCode(500)->send("Execution error: " . $error);
}
$execution = \json_decode($executionResponse, true);
foreach ($execution['headers'] as $header => $value) {
$response->setHeader($header, $value);
}
$body = $execution['body'] ?? '';
if (($execution['headers']['x-open-runtimes-encoding'] ?? '') === 'base64') {
$body = \base64_decode($body);
}
return $response->setStatusCode($execution['statusCode'] ?? 200)->send($body);
} }
/* /*

View file

@ -450,8 +450,19 @@ class DeletesV1 extends Worker
{ {
$projectId = $project->getId(); $projectId = $project->getId();
$dbForProject = $this->getProjectDB($project); $dbForProject = $this->getProjectDB($project);
$dbForConsole = $this->getConsoleDB();
$functionId = $document->getId(); $functionId = $document->getId();
/**
* Delete routes
*/
Console::info("Deleting routes for function " . $functionId);
$this->deleteByGroup('routes', [
Query::equal('resourceType', ['function']),
Query::equal('resourceInternalId', [$document->getInternalId()]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForConsole);
/** /**
* Delete Variables * Delete Variables
*/ */

View file

@ -73,8 +73,7 @@
"phpmailer/phpmailer": "6.6.0", "phpmailer/phpmailer": "6.6.0",
"chillerlan/php-qrcode": "4.3.3", "chillerlan/php-qrcode": "4.3.3",
"adhocore/jwt": "1.1.2", "adhocore/jwt": "1.1.2",
"slickdeals/statsd": "3.1.0", "slickdeals/statsd": "3.1.0"
"selective/base32": "^2.0"
}, },
"repositories": [ "repositories": [
{ {

View file

@ -164,8 +164,8 @@ class Exception extends \Exception
public const WEBHOOK_NOT_FOUND = 'webhook_not_found'; public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
/** Router */ /** Router */
public const ROUTER_INVALID_URL = 'router_invalid_url'; public const ROUTER_UNKNOWN_HOST = 'router_unknown_host';
public const ROUTER_CONSOLE_PROJECT = 'router_console_project'; public const ROUTER_INVALID_TYPE = 'router_unknown_type';
/** Keys */ /** Keys */
public const KEY_NOT_FOUND = 'key_not_found'; public const KEY_NOT_FOUND = 'key_not_found';