2020-06-26 09:54:37 +00:00
< ? php
2024-10-08 07:54:40 +00:00
require_once __DIR__ . '/../vendor/autoload.php' ;
2026-02-13 13:29:54 +00:00
require_once __DIR__ . '/init/span.php' ;
2020-06-26 09:54:37 +00:00
2024-03-06 17:34:21 +00:00
use Appwrite\Utopia\Request ;
2020-10-29 13:07:56 +00:00
use Appwrite\Utopia\Response ;
2024-10-08 07:54:40 +00:00
use Swoole\Constant ;
use Swoole\Http\Request as SwooleRequest ;
use Swoole\Http\Response as SwooleResponse ;
use Swoole\Http\Server ;
use Swoole\Process ;
2024-11-22 13:57:42 +00:00
use Swoole\Table ;
2025-09-15 14:15:56 +00:00
use Swoole\Timer ;
2025-12-14 01:43:35 +00:00
use Utopia\Audit\Adapter\Database as AdapterDatabase ;
2025-12-14 02:33:17 +00:00
use Utopia\Audit\Adapter\SQL as AuditAdapterSQL ;
2024-03-06 17:34:21 +00:00
use Utopia\Audit\Audit ;
2025-02-15 00:19:21 +00:00
use Utopia\Compression\Compression ;
2021-05-04 10:24:08 +00:00
use Utopia\Config\Config ;
2026-02-10 05:04:24 +00:00
use Utopia\Console ;
2025-05-14 06:14:07 +00:00
use Utopia\Database\Adapter\Pool as DatabasePool ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Database ;
2024-11-22 13:57:42 +00:00
use Utopia\Database\DateTime ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Document ;
2025-05-14 06:14:07 +00:00
use Utopia\Database\Exception\Duplicate as DuplicateException ;
2022-12-14 15:42:25 +00:00
use Utopia\Database\Helpers\ID ;
2022-12-14 16:04:06 +00:00
use Utopia\Database\Helpers\Permission ;
use Utopia\Database\Helpers\Role ;
2024-11-22 13:57:42 +00:00
use Utopia\Database\Query ;
2026-02-10 05:04:24 +00:00
use Utopia\Http\Files ;
use Utopia\Http\Http ;
2024-10-08 07:54:40 +00:00
use Utopia\Logger\Log ;
use Utopia\Logger\Log\User ;
use Utopia\Pools\Group ;
2026-02-13 13:29:54 +00:00
use Utopia\Span\Span ;
2024-04-01 11:02:47 +00:00
use Utopia\System\System ;
2020-06-26 09:54:37 +00:00
2024-11-22 13:57:42 +00:00
const DOMAIN_SYNC_TIMER = 30 ; // 30 seconds
2026-02-12 07:03:58 +00:00
$files = null ;
2024-11-22 13:57:42 +00:00
$domains = new Table ( 1_000_000 ); // 1 million rows
$domains -> column ( 'value' , Table :: TYPE_INT , 1 );
$domains -> create ();
2024-10-08 07:54:40 +00:00
$http = new Server (
host : " 0.0.0.0 " ,
port : System :: getEnv ( 'PORT' , 80 ),
mode : SWOOLE_PROCESS ,
);
2020-07-01 08:55:14 +00:00
2024-05-09 09:41:02 +00:00
$payloadSize = 12 * ( 1024 * 1024 ); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
2025-01-06 12:48:01 +00:00
$totalWorkers = intval ( System :: getEnv ( '_APP_CPU_NUM' , swoole_cpu_num ())) * intval ( System :: getEnv ( '_APP_WORKER_PER_CORE' , 6 ));
2022-09-30 10:32:58 +00:00
2024-11-22 13:57:42 +00:00
/**
* Assigns HTTP requests to worker threads by analyzing its payload / content .
*
* Routes requests as 'safe' or 'risky' based on specific content patterns ( like POST actions or certain domains )
* to optimize load distribution between the workers . Utilizes `$safeThreadsPercent` to manage risk by assigning
* riskier tasks to a dedicated worker subset . Prefers idle workers , with fallback to random selection if necessary .
* doc : https :// openswoole . com / docs / modules / swoole - server / configuration #dispatch_func
*
* @ param Server $server Swoole server instance .
* @ param int $fd client ID
* @ param int $type the type of data and its current state
* @ param string | null $data Request content for categorization .
* @ global int $totalThreads Total number of workers .
* @ return int Chosen worker ID for the request .
*/
function dispatch ( Server $server , int $fd , int $type , $data = null ) : int
{
2025-04-24 21:11:52 +00:00
$resolveWorkerId = function ( Server $server , $data = null ) {
global $totalWorkers , $domains ;
// If data is not set we can send request to any worker
// first we try to pick idle worker, if not we randomly pick a worker
if ( $data === null ) {
for ( $i = 0 ; $i < $totalWorkers ; $i ++ ) {
if ( $server -> getWorkerStatus ( $i ) === SWOOLE_WORKER_IDLE ) {
return $i ;
}
}
return rand ( 0 , $totalWorkers - 1 );
}
$riskyWorkersPercent = intval ( System :: getEnv ( '_APP_RISKY_WORKERS_PERCENT' , 80 )) / 100 ; // Decimal form 0 to 1
// Each worker has numeric ID, starting from 0 and incrementing
// From 0 to riskyWorkers, we consider safe workers
// From riskyWorkers to totalWorkers, we consider risky workers
$riskyWorkers = ( int ) floor ( $totalWorkers * $riskyWorkersPercent ); // Absolute amount of risky workers
$domain = '' ;
// max up to 3 as first line has request details and second line has host
$lines = explode ( " \n " , $data , 3 );
$request = $lines [ 0 ];
if ( count ( $lines ) > 1 ) {
$domain = trim ( explode ( 'Host: ' , $lines [ 1 ])[ 1 ]);
}
// Sync executions are considered risky
$risky = false ;
if ( str_starts_with ( $request , 'POST' ) && str_contains ( $request , '/executions' )) {
$risky = true ;
} elseif ( $domains -> get ( md5 ( $domain ), 'value' ) === 1 ) {
// executions request coming from custom domain
$risky = true ;
2026-01-30 15:48:40 +00:00
} else {
foreach ( \explode ( ',' , System :: getEnv ( '_APP_DOMAIN_FUNCTIONS' )) as $riskyDomain ) {
2026-01-30 20:30:00 +00:00
if ( empty ( $riskyDomain )) {
continue ;
}
2026-01-30 15:48:40 +00:00
if ( str_ends_with ( $domain , $riskyDomain )) {
$risky = true ;
break ;
}
}
2025-04-24 21:11:52 +00:00
}
2024-11-22 13:57:42 +00:00
2025-04-24 21:11:52 +00:00
if ( $risky ) {
// If risky request, only consider risky workers
for ( $j = $riskyWorkers ; $j < $totalWorkers ; $j ++ ) {
/** Reference https://openswoole.com/docs/modules/swoole-server-getWorkerStatus#description */
if ( $server -> getWorkerStatus ( $j ) === SWOOLE_WORKER_IDLE ) {
// If idle worker found, give to him
return $j ;
}
}
// If no idle workers, give to random risky worker
$worker = rand ( $riskyWorkers , $totalWorkers - 1 );
Console :: warning ( " swoole_dispatch: Risky branch: did not find a idle worker, picking random worker { $worker } " );
return $worker ;
}
// If safe request, give to any idle worker
// Its fine to pick risky worker here, because it's idle. Idle is never actually risky
2024-11-22 13:57:42 +00:00
for ( $i = 0 ; $i < $totalWorkers ; $i ++ ) {
if ( $server -> getWorkerStatus ( $i ) === SWOOLE_WORKER_IDLE ) {
return $i ;
}
}
2025-04-24 21:11:52 +00:00
// If no idle worker found, give to random safe worker
// We avoid risky workers here, as it could be in work - not idle. Thats exactly when they are risky.
$worker = rand ( 0 , $riskyWorkers - 1 );
Console :: warning ( " swoole_dispatch: Non-risky branch: did not find a idle worker, picking random worker { $worker } " );
2024-11-22 13:57:42 +00:00
return $worker ;
2025-04-24 21:11:52 +00:00
};
$workerId = $resolveWorkerId ( $server , $data );
$server -> bind ( $fd , $workerId );
return $workerId ;
}
2024-11-22 13:57:42 +00:00
2025-04-24 21:11:52 +00:00
$http
-> set ([
Constant :: OPTION_WORKER_NUM => $totalWorkers ,
Constant :: OPTION_DISPATCH_FUNC => dispatch ( ... ),
Constant :: OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD ,
Constant :: OPTION_HTTP_COMPRESSION => false ,
Constant :: OPTION_PACKAGE_MAX_LENGTH => $payloadSize ,
Constant :: OPTION_OUTPUT_BUFFER_SIZE => $payloadSize ,
Constant :: OPTION_TASK_WORKER_NUM => 1 , // required for the task to fetch domains background
]);
2026-02-12 07:03:58 +00:00
$http -> on ( Constant :: EVENT_WORKER_START , function ( $server , $workerId ) use ( & $files ) {
2026-02-12 07:11:36 +00:00
if ( ! $server -> taskworker ) {
$files = new Files ();
$files -> load ( __DIR__ . '/../public' );
}
2025-04-24 21:11:52 +00:00
});
2025-09-15 14:15:56 +00:00
$http -> on ( Constant :: EVENT_WORKER_STOP , function ( $server , $workerId ) {
Timer :: clearAll ();
Console :: success ( 'Worker ' . ++ $workerId . ' stopped successfully' );
});
$http -> on ( Constant :: EVENT_BEFORE_RELOAD , function ( $server ) {
2025-04-24 21:11:52 +00:00
Console :: success ( 'Starting reload...' );
});
2025-09-15 14:15:56 +00:00
$http -> on ( Constant :: EVENT_AFTER_RELOAD , function ( $server ) {
2025-04-24 21:11:52 +00:00
Console :: success ( 'Reload completed...' );
});
2024-11-22 13:57:42 +00:00
2024-10-08 07:54:40 +00:00
include __DIR__ . '/controllers/general.php' ;
2026-02-06 09:34:29 +00:00
function createDatabase ( Http $app , string $resourceKey , string $dbName , array $collections , mixed $pools , ? callable $extraSetup = null ) : void
2025-01-27 02:51:01 +00:00
{
$max = 10 ;
$sleep = 1 ;
$attempts = 0 ;
2022-08-02 18:56:45 +00:00
2025-05-14 06:14:07 +00:00
while ( true ) {
2025-01-27 02:51:01 +00:00
try {
$attempts ++ ;
$resource = $app -> getResource ( $resourceKey );
/* @var $database Database */
$database = is_callable ( $resource ) ? $resource () : $resource ;
break ; // exit loop on success
} catch ( \Exception $e ) {
Console :: warning ( " └── Database not ready. Retrying connection ( { $attempts } )... " );
if ( $attempts >= $max ) {
throw new \Exception ( ' └── Failed to connect to database: ' . $e -> getMessage ());
2022-01-10 06:31:33 +00:00
}
2025-01-27 02:51:01 +00:00
sleep ( $sleep );
}
2025-05-14 06:14:07 +00:00
}
2022-06-22 21:11:42 +00:00
2026-02-13 13:29:54 +00:00
Span :: init ( " database.setup " );
Span :: add ( 'database.name' , $dbName );
2021-07-04 15:14:39 +00:00
2025-01-27 02:51:01 +00:00
// Attempt to create the database
try {
$database -> create ();
} catch ( \Exception $e ) {
2026-02-13 13:29:54 +00:00
Span :: add ( 'database.exists' , true );
2025-01-27 02:51:01 +00:00
}
2021-07-04 12:05:46 +00:00
2025-01-27 02:51:01 +00:00
// Process collections
2026-02-13 13:29:54 +00:00
$collectionsCreated = 0 ;
2025-01-27 02:51:01 +00:00
foreach ( $collections as $key => $collection ) {
if (( $collection [ '$collection' ] ? ? '' ) !== Database :: METADATA ) {
continue ;
2024-10-08 07:54:40 +00:00
}
2022-01-10 06:31:33 +00:00
2025-01-27 02:51:01 +00:00
if ( ! $database -> getCollection ( $key ) -> isEmpty ()) {
continue ;
}
2022-05-23 14:54:50 +00:00
2025-01-27 02:51:01 +00:00
$attributes = array_map ( fn ( $attr ) => new Document ([
'$id' => ID :: custom ( $attr [ '$id' ]),
'type' => $attr [ 'type' ],
'size' => $attr [ 'size' ],
'required' => $attr [ 'required' ],
'signed' => $attr [ 'signed' ],
'array' => $attr [ 'array' ],
'filters' => $attr [ 'filters' ],
'default' => $attr [ 'default' ] ? ? null ,
'format' => $attr [ 'format' ] ? ? ''
]), $collection [ 'attributes' ]);
$indexes = array_map ( fn ( $index ) => new Document ([
'$id' => ID :: custom ( $index [ '$id' ]),
'type' => $index [ 'type' ],
'attributes' => $index [ 'attributes' ],
'lengths' => $index [ 'lengths' ],
'orders' => $index [ 'orders' ],
]), $collection [ 'indexes' ]);
$database -> createCollection ( $key , $attributes , $indexes );
2026-02-13 13:29:54 +00:00
$collectionsCreated ++ ;
2025-01-27 02:51:01 +00:00
}
2022-05-23 14:54:50 +00:00
2026-02-13 13:29:54 +00:00
Span :: add ( 'database.collections_created' , $collectionsCreated );
2025-01-27 02:51:01 +00:00
if ( $extraSetup ) {
$extraSetup ( $database );
}
2026-02-13 13:29:54 +00:00
Span :: current () ? -> finish ();
2025-01-27 02:51:01 +00:00
}
2021-12-27 10:35:51 +00:00
2026-02-13 13:29:54 +00:00
$http -> on ( Constant :: EVENT_START , function ( Server $http ) use ( $payloadSize , $totalWorkers , $register ) {
2026-02-04 05:30:22 +00:00
$app = new Http ( 'UTC' );
2021-12-21 15:01:40 +00:00
2025-01-27 02:51:01 +00:00
go ( function () use ( $register , $app ) {
$pools = $register -> get ( 'pools' );
/** @var Group $pools */
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'pools' , fn () => $pools );
2021-12-21 15:01:40 +00:00
2025-01-27 02:51:01 +00:00
/** @var array $collections */
$collections = Config :: getParam ( 'collections' , []);
2024-11-21 03:46:10 +00:00
2025-01-27 02:51:01 +00:00
// create logs database first, `getLogsDB` is a callable.
createDatabase ( $app , 'getLogsDB' , 'logs' , $collections [ 'logs' ], $pools );
2024-11-21 03:46:10 +00:00
2025-01-27 02:51:01 +00:00
// create appwrite database, `dbForPlatform` is a direct access call.
2026-01-14 15:08:00 +00:00
createDatabase ( $app , 'dbForPlatform' , 'appwrite' , $collections [ 'console' ], $pools , function ( Database $dbForPlatform ) use ( $collections , $app ) {
$authorization = $app -> getResource ( 'authorization' );
2025-12-14 02:34:33 +00:00
if ( $dbForPlatform -> getCollection ( AuditAdapterSQL :: COLLECTION ) -> isEmpty ()) {
2025-12-14 01:43:35 +00:00
$adapter = new AdapterDatabase ( $dbForPlatform );
$audit = new Audit ( $adapter );
2025-01-27 02:51:01 +00:00
$audit -> setup ();
2024-10-08 07:54:40 +00:00
}
2021-12-21 15:01:40 +00:00
2025-02-21 19:21:13 +00:00
if ( $dbForPlatform -> getDocument ( 'buckets' , 'default' ) -> isEmpty ()) {
2025-01-27 02:51:01 +00:00
$dbForPlatform -> createDocument ( 'buckets' , new Document ([
'$id' => ID :: custom ( 'default' ),
'$collection' => ID :: custom ( 'buckets' ),
'name' => 'Default' ,
'maximumFileSize' => ( int ) System :: getEnv ( '_APP_STORAGE_LIMIT' , 0 ),
'allowedFileExtensions' => [],
'enabled' => true ,
'compression' => 'gzip' ,
'encryption' => true ,
'antivirus' => true ,
'fileSecurity' => true ,
'$permissions' => [
Permission :: create ( Role :: any ()),
Permission :: read ( Role :: any ()),
Permission :: update ( Role :: any ()),
Permission :: delete ( Role :: any ()),
],
'search' => 'buckets Default' ,
]));
$bucket = $dbForPlatform -> getDocument ( 'buckets' , 'default' );
$files = $collections [ 'buckets' ][ 'files' ] ? ? [];
if ( empty ( $files )) {
throw new Exception ( 'Files collection is not configured.' );
2024-11-21 03:46:10 +00:00
}
2025-01-27 02:51:01 +00:00
$attributes = array_map ( fn ( $attr ) => new Document ([
'$id' => ID :: custom ( $attr [ '$id' ]),
'type' => $attr [ 'type' ],
'size' => $attr [ 'size' ],
'required' => $attr [ 'required' ],
'signed' => $attr [ 'signed' ],
'array' => $attr [ 'array' ],
'filters' => $attr [ 'filters' ],
'default' => $attr [ 'default' ] ? ? null ,
'format' => $attr [ 'format' ] ? ? ''
]), $files [ 'attributes' ]);
$indexes = array_map ( fn ( $index ) => new Document ([
'$id' => ID :: custom ( $index [ '$id' ]),
'type' => $index [ 'type' ],
'attributes' => $index [ 'attributes' ],
'lengths' => $index [ 'lengths' ],
'orders' => $index [ 'orders' ],
]), $files [ 'indexes' ]);
2025-05-26 05:42:11 +00:00
$dbForPlatform -> createCollection ( 'bucket_' . $bucket -> getSequence (), $attributes , $indexes );
2024-11-21 03:46:10 +00:00
}
2026-01-14 15:08:00 +00:00
if ( $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'buckets' , 'screenshots' ) -> isEmpty ())) {
$authorization -> skip ( fn () => $dbForPlatform -> createDocument ( 'buckets' , new Document ([
2025-02-20 12:17:23 +00:00
'$id' => ID :: custom ( 'screenshots' ),
'$collection' => ID :: custom ( 'buckets' ),
'name' => 'Screenshots' ,
2025-05-12 14:52:13 +00:00
'maximumFileSize' => 20000000 , // ~20MB
2025-02-20 12:17:23 +00:00
'allowedFileExtensions' => [ 'png' ],
'enabled' => true ,
'compression' => Compression :: GZIP ,
'encryption' => false ,
'antivirus' => false ,
'fileSecurity' => true ,
2025-02-27 11:44:22 +00:00
'$permissions' => [],
2025-02-20 12:17:23 +00:00
'search' => 'buckets Screenshots' ,
2025-02-27 11:44:22 +00:00
])));
2025-02-20 12:17:23 +00:00
2026-01-14 15:08:00 +00:00
$bucket = $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'buckets' , 'screenshots' ));
2025-02-20 12:44:22 +00:00
2025-02-20 12:17:23 +00:00
$files = $collections [ 'buckets' ][ 'files' ] ? ? [];
if ( empty ( $files )) {
throw new Exception ( 'Files collection is not configured.' );
}
2024-11-21 03:46:10 +00:00
2025-02-20 12:17:23 +00:00
$attributes = array_map ( fn ( $attr ) => new Document ([
'$id' => ID :: custom ( $attr [ '$id' ]),
'type' => $attr [ 'type' ],
'size' => $attr [ 'size' ],
'required' => $attr [ 'required' ],
'signed' => $attr [ 'signed' ],
'array' => $attr [ 'array' ],
2025-01-27 02:51:01 +00:00
'filters' => $attr [ 'filters' ],
'default' => $attr [ 'default' ] ? ? null ,
'format' => $attr [ 'format' ] ? ? ''
]), $files [ 'attributes' ]);
$indexes = array_map ( fn ( $index ) => new Document ([
'$id' => ID :: custom ( $index [ '$id' ]),
'type' => $index [ 'type' ],
'attributes' => $index [ 'attributes' ],
'lengths' => $index [ 'lengths' ],
'orders' => $index [ 'orders' ],
]), $files [ 'indexes' ]);
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForPlatform -> createCollection ( 'bucket_' . $bucket -> getSequence (), $attributes , $indexes ));
2024-11-21 03:46:10 +00:00
}
2025-01-27 02:51:01 +00:00
});
2024-10-08 07:54:40 +00:00
2025-02-20 11:51:29 +00:00
$projectCollections = $collections [ 'projects' ];
$sharedTables = \explode ( ',' , System :: getEnv ( '_APP_DATABASE_SHARED_TABLES' , '' ));
$sharedTablesV1 = \explode ( ',' , System :: getEnv ( '_APP_DATABASE_SHARED_TABLES_V1' , '' ));
$sharedTablesV2 = \array_diff ( $sharedTables , $sharedTablesV1 );
$cache = $app -> getResource ( 'cache' );
foreach ( $sharedTablesV2 as $hostname ) {
2026-02-13 13:29:54 +00:00
Span :: init ( 'database.setup' );
Span :: add ( 'database.hostname' , $hostname );
2025-05-14 06:14:07 +00:00
$adapter = new DatabasePool ( $pools -> get ( $hostname ));
2025-02-20 11:51:29 +00:00
$dbForProject = ( new Database ( $adapter , $cache ))
-> setDatabase ( 'appwrite' )
-> setSharedTables ( true )
-> setTenant ( null )
-> setNamespace ( System :: getEnv ( '_APP_DATABASE_SHARED_NAMESPACE' , '' ));
try {
$dbForProject -> create ();
2025-05-14 06:14:07 +00:00
} catch ( DuplicateException ) {
2026-02-13 13:29:54 +00:00
Span :: add ( 'database.exists' , true );
2025-02-20 11:51:29 +00:00
}
2025-12-14 02:33:17 +00:00
if ( $dbForProject -> getCollection ( AuditAdapterSQL :: COLLECTION ) -> isEmpty ()) {
2025-12-14 01:43:35 +00:00
$adapter = new AdapterDatabase ( $dbForProject );
$audit = new Audit ( $adapter );
2025-02-20 11:51:29 +00:00
$audit -> setup ();
}
2026-02-13 13:29:54 +00:00
$collectionsCreated = 0 ;
2025-02-20 11:51:29 +00:00
foreach ( $projectCollections as $key => $collection ) {
if (( $collection [ '$collection' ] ? ? '' ) !== Database :: METADATA ) {
continue ;
}
if ( ! $dbForProject -> getCollection ( $key ) -> isEmpty ()) {
continue ;
}
$attributes = \array_map ( fn ( $attribute ) => new Document ( $attribute ), $collection [ 'attributes' ]);
$indexes = \array_map ( fn ( array $index ) => new Document ( $index ), $collection [ 'indexes' ]);
$dbForProject -> createCollection ( $key , $attributes , $indexes );
2026-02-13 13:29:54 +00:00
$collectionsCreated ++ ;
2025-02-20 11:51:29 +00:00
}
2026-02-13 13:29:54 +00:00
Span :: add ( 'database.collections_created' , $collectionsCreated );
Span :: current () ? -> finish ();
}
2024-10-01 14:30:47 +00:00
});
2021-12-21 15:01:40 +00:00
2026-02-13 13:29:54 +00:00
Span :: init ( 'http.server.start' );
Span :: add ( 'server.workers' , $totalWorkers );
Span :: add ( 'server.payload_size' , $payloadSize );
Span :: add ( 'server.master_pid' , $http -> master_pid );
Span :: add ( 'server.manager_pid' , $http -> manager_pid );
Span :: current () ? -> finish ();
2024-10-08 07:54:40 +00:00
2024-11-22 13:57:42 +00:00
// Start the task that starts fetching custom domains
$http -> task ([], 0 );
2024-10-08 07:54:40 +00:00
// listen ctrl + c
Process :: signal ( 2 , function () use ( $http ) {
Console :: log ( 'Stop by Ctrl+C' );
$http -> shutdown ();
2024-10-01 14:30:47 +00:00
});
2024-10-08 07:54:40 +00:00
});
2026-02-12 07:03:58 +00:00
$http -> on ( Constant :: EVENT_REQUEST , function ( SwooleRequest $swooleRequest , SwooleResponse $swooleResponse ) use ( $register , & $files ) {
2026-02-13 13:29:54 +00:00
Span :: init ( 'http.request' );
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'swooleRequest' , fn () => $swooleRequest );
Http :: setResource ( 'swooleResponse' , fn () => $swooleResponse );
2024-10-08 07:54:40 +00:00
$request = new Request ( $swooleRequest );
$response = new Response ( $swooleResponse );
2026-02-13 13:29:54 +00:00
Span :: add ( 'http.method' , $request -> getMethod ());
2026-02-12 07:03:58 +00:00
if ( $files instanceof Files && $files -> isFileLoaded ( $request -> getURI ())) {
2026-02-12 07:11:36 +00:00
$time = ( 60 * 60 * 24 * 45 ); // 45 days cache
2024-10-08 07:54:40 +00:00
$response
2026-02-10 05:04:24 +00:00
-> setContentType ( $files -> getFileMimeType ( $request -> getURI ()))
2024-10-08 07:54:40 +00:00
-> addHeader ( 'Cache-Control' , 'public, max-age=' . $time )
-> addHeader ( 'Expires' , \date ( 'D, d M Y H:i:s' , \time () + $time ) . ' GMT' ) // 45 days cache
2026-02-10 05:04:24 +00:00
-> send ( $files -> getFileContents ( $request -> getURI ()));
2024-10-08 07:54:40 +00:00
return ;
}
2026-02-04 05:30:22 +00:00
$app = new Http ( 'UTC' );
2025-01-16 17:23:22 +00:00
$app -> setCompression ( System :: getEnv ( '_APP_COMPRESSION_ENABLED' , 'enabled' ) === 'enabled' );
2024-11-08 18:48:32 +00:00
$app -> setCompressionMinSize ( intval ( System :: getEnv ( '_APP_COMPRESSION_MIN_SIZE_BYTES' , '1024' ))); // 1KB
2024-10-08 07:54:40 +00:00
$pools = $register -> get ( 'pools' );
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'pools' , fn () => $pools );
2024-10-08 07:54:40 +00:00
try {
2026-01-14 15:08:00 +00:00
$authorization = $app -> getResource ( 'authorization' );
$request -> setAuthorization ( $authorization );
$response -> setAuthorization ( $authorization );
$authorization -> cleanRoles ();
$authorization -> addRole ( Role :: any () -> toString ());
2024-10-08 07:54:40 +00:00
$app -> run ( $request , $response );
2026-02-13 13:29:54 +00:00
$route = $app -> getRoute ();
Span :: add ( 'http.path' , $route ? -> getPath () ? ? 'unknown' );
2024-10-08 07:54:40 +00:00
} catch ( \Throwable $th ) {
2026-02-13 13:29:54 +00:00
Span :: error ( $th );
2024-10-08 07:54:40 +00:00
$version = System :: getEnv ( '_APP_VERSION' , 'UNKNOWN' );
$logger = $app -> getResource ( " logger " );
if ( $logger ) {
try {
/** @var Utopia\Database\Document $user */
$user = $app -> getResource ( 'user' );
} catch ( \Throwable $_th ) {
// All good, user is optional information for logger
}
$route = $app -> getRoute ();
$log = $app -> getResource ( " log " );
if ( isset ( $user ) && ! $user -> isEmpty ()) {
$log -> setUser ( new User ( $user -> getId ()));
2024-11-26 13:54:27 +00:00
} else {
$log -> setUser ( new User ( 'guest-' . hash ( 'sha256' , $request -> getIP ())));
2024-10-08 07:54:40 +00:00
}
$log -> setNamespace ( " http " );
2024-11-26 13:54:27 +00:00
$log -> setServer ( System :: getEnv ( '_APP_LOGGING_SERVICE_IDENTIFIER' , \gethostname ()));
2024-10-08 07:54:40 +00:00
$log -> setVersion ( $version );
$log -> setType ( Log :: TYPE_ERROR );
$log -> setMessage ( $th -> getMessage ());
$log -> addTag ( 'method' , $route -> getMethod ());
$log -> addTag ( 'url' , $route -> getPath ());
$log -> addTag ( 'verboseType' , get_class ( $th ));
$log -> addTag ( 'code' , $th -> getCode ());
// $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant
$log -> addTag ( 'hostname' , $request -> getHostname ());
$log -> addTag ( 'locale' , ( string ) $request -> getParam ( 'locale' , $request -> getHeader ( 'x-appwrite-locale' , '' )));
$log -> addExtra ( 'file' , $th -> getFile ());
$log -> addExtra ( 'line' , $th -> getLine ());
$log -> addExtra ( 'trace' , $th -> getTraceAsString ());
2026-01-14 15:08:00 +00:00
$log -> addExtra ( 'roles' , isset ( $authorization ) ? $authorization -> getRoles () : []);
2024-10-08 07:54:40 +00:00
2025-01-17 04:38:38 +00:00
$sdk = $route -> getLabel ( " sdk " , false );
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD' ;
if ( ! empty ( $sdk )) {
/** @var Appwrite\SDK\Method $sdk */
$action = $sdk -> getNamespace () . '.' . $sdk -> getMethodName ();
}
2024-10-08 07:54:40 +00:00
$log -> setAction ( $action );
2024-11-26 13:54:27 +00:00
$log -> addTag ( 'service' , $action );
2024-10-08 07:54:40 +00:00
$isProduction = System :: getEnv ( '_APP_ENV' , 'development' ) === 'production' ;
$log -> setEnvironment ( $isProduction ? Log :: ENVIRONMENT_PRODUCTION : Log :: ENVIRONMENT_STAGING );
try {
$responseCode = $logger -> addLog ( $log );
Console :: info ( 'Error log pushed with status code: ' . $responseCode );
} catch ( Throwable $th ) {
Console :: error ( 'Error pushing log: ' . $th -> getMessage ());
}
}
$swooleResponse -> setStatusCode ( 500 );
2026-02-04 05:30:22 +00:00
$output = (( Http :: isDevelopment ())) ? [
2024-10-08 07:54:40 +00:00
'message' => 'Error: ' . $th -> getMessage (),
'code' => 500 ,
'file' => $th -> getFile (),
'line' => $th -> getLine (),
'trace' => $th -> getTrace (),
'version' => $version ,
] : [
'message' => 'Error: Server Error' ,
'code' => 500 ,
'version' => $version ,
];
$swooleResponse -> end ( \json_encode ( $output ));
2026-02-13 13:29:54 +00:00
} finally {
Span :: add ( 'http.response.code' , $response -> getStatusCode ());
Span :: current () ? -> finish ();
2024-10-08 07:54:40 +00:00
}
});
2020-06-26 09:54:37 +00:00
2024-11-22 13:57:42 +00:00
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
2025-05-06 01:50:38 +00:00
$http -> on ( Constant :: EVENT_TASK , function () use ( $register , $domains ) {
2024-11-22 13:57:42 +00:00
$lastSyncUpdate = null ;
$pools = $register -> get ( 'pools' );
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'pools' , fn () => $pools );
$app = new Http ( 'UTC' );
2024-11-22 13:57:42 +00:00
2024-12-12 10:30:26 +00:00
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app -> getResource ( 'dbForPlatform' );
2024-11-22 13:57:42 +00:00
2026-01-14 15:08:00 +00:00
Timer :: tick ( DOMAIN_SYNC_TIMER * 1000 , function () use ( $dbForPlatform , $domains , & $lastSyncUpdate , $app ) {
2024-11-22 13:57:42 +00:00
try {
$time = DateTime :: now ();
$limit = 1000 ;
$sum = $limit ;
$latestDocument = null ;
while ( $sum === $limit ) {
$queries = [ Query :: limit ( $limit )];
if ( $latestDocument !== null ) {
$queries [] = Query :: cursorAfter ( $latestDocument );
}
if ( $lastSyncUpdate != null ) {
$queries [] = Query :: greaterThanEqual ( '$updatedAt' , $lastSyncUpdate );
}
$results = [];
try {
2026-01-14 15:08:00 +00:00
$authorization = $app -> getResource ( 'authorization' );
$results = $authorization -> skip ( fn () => $dbForPlatform -> find ( 'rules' , $queries ));
2024-11-22 13:57:42 +00:00
} catch ( Throwable $th ) {
Console :: error ( $th -> getMessage ());
}
$sum = count ( $results );
foreach ( $results as $document ) {
$domain = $document -> getAttribute ( 'domain' );
2026-01-30 15:20:46 +00:00
$denyDomains = [];
$denyEnvVars = [
System :: getEnv ( '_APP_DOMAIN_FUNCTIONS_FALLBACK' , '' ),
System :: getEnv ( '_APP_DOMAIN_FUNCTIONS' , '' ),
System :: getEnv ( '_APP_DOMAIN_SITES' , '' ),
];
foreach ( $denyEnvVars as $denyEnvVar ) {
foreach ( \explode ( ',' , $denyEnvVar ) as $denyDomain ) {
if ( empty ( $denyDomain )) {
continue ;
}
$denyDomains [] = $denyDomain ;
}
}
$isDenyDomain = false ;
foreach ( $denyDomains as $denyDomain ) {
if ( str_ends_with ( $domain , $denyDomain )) {
$isDenyDomain = true ;
}
}
if ( $isDenyDomain ) {
2024-11-22 13:57:42 +00:00
continue ;
}
2026-01-30 15:20:46 +00:00
2024-11-22 13:57:42 +00:00
$domains -> set ( md5 ( $domain ), [ 'value' => 1 ]);
}
$latestDocument = ! empty ( array_key_last ( $results )) ? $results [ array_key_last ( $results )] : null ;
}
$lastSyncUpdate = $time ;
if ( $sum > 0 ) {
Console :: log ( " Sync domains tick: { $sum } domains were updated " );
}
} catch ( Throwable $th ) {
Console :: error ( $th -> getMessage ());
}
});
});
2022-05-23 14:54:50 +00:00
$http -> start ();