diff --git a/Caddyfile b/Caddyfile index 032e5d32529c1..7472251129e62 100644 --- a/Caddyfile +++ b/Caddyfile @@ -3,11 +3,35 @@ # # THIS IS AN EXPERIMENTAL FEATURE # DO NOT USE THIS IN PRODUCTION, YOU HAVE BEEN WARNED. +{ + metrics + frankenphp { + num_threads 192 + max_threads 256 + # max_requests 500 + } +} localhost { php_server { worker { file index.php + num 32 + watch + } + worker { + file remote.php + num 32 + watch + } + worker { + file ocs/v1.php + num 32 + watch + } + worker { + file ocs/v2.php + num 32 watch } } diff --git a/index.php b/index.php index d4bdd263f79f3..192d72327f251 100644 --- a/index.php +++ b/index.php @@ -10,7 +10,6 @@ require_once __DIR__ . '/lib/versioncheck.php'; -use OC\Files\Filesystem; use OC\ServiceUnavailableException; use OC\User\LoginException; use OCP\HintException; @@ -24,23 +23,10 @@ \OC::boot(); -function resetStaticProperties(): void { - // FIXME needed because these use a static var - \OC_Hook::clear(); - \OC_Util::$styles = []; - \OC_Util::$headers = []; - \OC_User::setIncognitoMode(false); - \OC_User::$_setupedBackends = []; - \OC_App::reset(); - \OC_Helper::reset(); - Filesystem::reset(); -} - -$handler = static function () { +\OC::handleRequests(static function () { try { - resetStaticProperties(); - OC::init(); - OC::handleRequest(); + \OC::initForRequest(); + \OC::handleRequest(); } catch (ServiceUnavailableException $ex) { Server::get(LoggerInterface::class)->error($ex->getMessage(), [ 'app' => 'index', @@ -124,20 +110,4 @@ function resetStaticProperties(): void { } Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); } -}; - -if (function_exists('frankenphp_handle_request') && isset($_SERVER['FRANKENPHP_WORKER']) && $_SERVER['FRANKENPHP_WORKER'] === '1') { - $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); - for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { - $keepRunning = \frankenphp_handle_request($handler); - - // Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation - gc_collect_cycles(); - - if (!$keepRunning) { - break; - } - } -} else { - $handler(); -} +}); diff --git a/lib/OC.php b/lib/OC.php index c7072df64b88d..b2aebd9740bd2 100644 --- a/lib/OC.php +++ b/lib/OC.php @@ -7,6 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +use OC\Files\Filesystem; use OC\Profiler\BuiltInProfiler; use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Share20\GroupDeletedListener; @@ -665,6 +666,9 @@ private static function addSecurityHeaders(): void { } } + /* + * Called only once at the beginning to setup things + */ public static function boot(): void { // prevent any XML processing from loading external entities libxml_set_external_entity_loader(static function () { @@ -725,7 +729,12 @@ public static function boot(): void { } } - public static function init(): void { + /* + * Called before each request served if the same worker serves several request + */ + public static function initForRequest(): void { + self::resetStaticProperties(); + // First handle PHP configuration and copy auth headers to the expected // $_SERVER variable before doing anything Server object related self::setRequiredIniValues(); @@ -1324,4 +1333,40 @@ protected static function tryAppAPILogin(OCP\IRequest $request): bool { return false; } } + + /** + * @internal + */ + private static function resetStaticProperties(): void { + // FIXME needed because these use a static var + \OC_Hook::clear(); + \OC_Util::$styles = []; + \OC_Util::$headers = []; + \OC_User::setIncognitoMode(false); + \OC_User::$_setupedBackends = []; + \OC_App::reset(); + \OC_Helper::reset(); + Filesystem::reset(); + } + + /** + * @internal + */ + public static function handleRequests(callable $handler): void { + if (function_exists('frankenphp_handle_request') && isset($_SERVER['FRANKENPHP_WORKER']) && $_SERVER['FRANKENPHP_WORKER'] === '1') { + $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); + for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { + $keepRunning = \frankenphp_handle_request($handler); + + // Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation + gc_collect_cycles(); + + if (!$keepRunning) { + break; + } + } + } else { + $handler(); + } + } } diff --git a/lib/base.php b/lib/base.php index d38e1fe259080..7e098be7062c0 100644 --- a/lib/base.php +++ b/lib/base.php @@ -9,4 +9,4 @@ require_once __DIR__ . '/OC.php'; \OC::boot(); -\OC::init(); +\OC::initForRequest(); diff --git a/ocs/v1.php b/ocs/v1.php index 5c4d125f9eb84..0a7920fba04f9 100644 --- a/ocs/v1.php +++ b/ocs/v1.php @@ -8,9 +8,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -require_once __DIR__ . '/../lib/versioncheck.php'; -require_once __DIR__ . '/../lib/base.php'; - use OC\OCS\ApiHelper; use OC\Route\Router; use OC\SystemConfig; @@ -28,60 +25,68 @@ use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; -$request = Server::get(IRequest::class); +require_once __DIR__ . '/../lib/versioncheck.php'; +require_once __DIR__ . '/../lib/OC.php'; -if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') { - // since the behavior of apps or remotes are unpredictable during - // an upgrade, return a 503 directly - ApiHelper::respond(503, 'Service unavailable', ['X-Nextcloud-Maintenance-Mode' => '1'], 503); - exit; -} +\OC::boot(); -/* - * Try the appframework routes - */ -try { - $appManager = Server::get(IAppManager::class); - $appManager->loadApps(['session']); - $appManager->loadApps(['authentication']); - $appManager->loadApps(['extended_authentication']); +\OC::handleRequests(static function () { + \OC::initForRequest(); + $request = Server::get(IRequest::class); + + if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') { + // since the behavior of apps or remotes are unpredictable during + // an upgrade, return a 503 directly + ApiHelper::respond(503, 'Service unavailable', ['X-Nextcloud-Maintenance-Mode' => '1'], 503); + exit; + } + + /* + * Try the appframework routes + */ + try { + $appManager = Server::get(IAppManager::class); + $appManager->loadApps(['session']); + $appManager->loadApps(['authentication']); + $appManager->loadApps(['extended_authentication']); - $request->throwDecodingExceptionIfAny(); + $request->throwDecodingExceptionIfAny(); - if ($request->getPathInfo() !== '/core/update') { - // load all apps to get all api routes properly setup - // FIXME: this should ideally appear after handleLogin but will cause - // side effects in existing apps - $appManager->loadApps(); - if (!Server::get(IUserSession::class)->isLoggedIn()) { - OC::handleLogin($request); + if ($request->getPathInfo() !== '/core/update') { + // load all apps to get all api routes properly setup + // FIXME: this should ideally appear after handleLogin but will cause + // side effects in existing apps + $appManager->loadApps(); + if (!Server::get(IUserSession::class)->isLoggedIn()) { + OC::handleLogin($request); + } + } else { + $appManager->loadApps(['core']); } - } else { - $appManager->loadApps(['core']); - } - Server::get(Router::class)->match('/ocsapp' . $request->getRawPathInfo()); -} catch (MaxDelayReached $ex) { - ApiHelper::respond(Http::STATUS_TOO_MANY_REQUESTS, $ex->getMessage()); -} catch (ResourceNotFoundException $e) { - $txt = 'Invalid query, please check the syntax. API specifications are here:' - . ' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services.' . "\n"; - ApiHelper::respond(OCSController::RESPOND_NOT_FOUND, $txt); -} catch (MethodNotAllowedException $e) { - ApiHelper::setContentType(); - http_response_code(405); -} catch (LoginException $e) { - ApiHelper::respond(OCSController::RESPOND_UNAUTHORISED, 'Unauthorised'); -} catch (\Exception $e) { - Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); + Server::get(Router::class)->match('/ocsapp' . $request->getRawPathInfo()); + } catch (MaxDelayReached $ex) { + ApiHelper::respond(Http::STATUS_TOO_MANY_REQUESTS, $ex->getMessage()); + } catch (ResourceNotFoundException $e) { + $txt = 'Invalid query, please check the syntax. API specifications are here:' + . ' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services.' . "\n"; + ApiHelper::respond(OCSController::RESPOND_NOT_FOUND, $txt); + } catch (MethodNotAllowedException $e) { + ApiHelper::setContentType(); + http_response_code(405); + } catch (LoginException $e) { + ApiHelper::respond(OCSController::RESPOND_UNAUTHORISED, 'Unauthorised'); + } catch (\Exception $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); - $txt = 'Internal Server Error' . "\n"; - try { - if (Server::get(SystemConfig::class)->getValue('debug', false)) { - $txt .= $e->getMessage(); + $txt = 'Internal Server Error' . "\n"; + try { + if (Server::get(SystemConfig::class)->getValue('debug', false)) { + $txt .= $e->getMessage(); + } + } catch (\Throwable $e) { + // Just to be save } - } catch (\Throwable $e) { - // Just to be save + ApiHelper::respond(OCSController::RESPOND_SERVER_ERROR, $txt); } - ApiHelper::respond(OCSController::RESPOND_SERVER_ERROR, $txt); -} +}); diff --git a/remote.php b/remote.php index 27fb703c87ce2..3bfa67e9ec09d 100644 --- a/remote.php +++ b/remote.php @@ -1,24 +1,27 @@ getAppValue('core', 'remote_' . $service); } -try { - require_once __DIR__ . '/lib/base.php'; +require_once __DIR__ . '/lib/OC.php'; - // All resources served via the DAV endpoint should have the strictest possible - // policy. Exempted from this is the SabreDAV browser plugin which overwrites - // this policy with a softer one if debug mode is enabled. - header("Content-Security-Policy: default-src 'none';"); +\OC::boot(); - if (Util::needUpgrade()) { - // since the behavior of apps or remotes are unpredictable during - // an upgrade, return a 503 directly - throw new RemoteException('Service unavailable', 503); - } +\OC::handleRequests(static function () { + try { + \OC::initForRequest(); - $request = \OCP\Server::get(IRequest::class); - $pathInfo = $request->getPathInfo(); - if ($pathInfo === false || $pathInfo === '') { - throw new RemoteException('Path not found', 404); - } - if (!$pos = strpos($pathInfo, '/', 1)) { - $pos = strlen($pathInfo); - } - $service = substr($pathInfo, 1, $pos - 1); + // All resources served via the DAV endpoint should have the strictest possible + // policy. Exempted from this is the SabreDAV browser plugin which overwrites + // this policy with a softer one if debug mode is enabled. + header("Content-Security-Policy: default-src 'none';"); - $file = resolveService($service); + if (Util::needUpgrade()) { + // since the behavior of apps or remotes are unpredictable during + // an upgrade, return a 503 directly + throw new RemoteException('Service unavailable', 503); + } - if (is_null($file)) { - throw new RemoteException('Path not found', 404); - } + $request = \OCP\Server::get(IRequest::class); + $pathInfo = $request->getPathInfo(); + if ($pathInfo === false || $pathInfo === '') { + throw new RemoteException('Path not found', 404); + } + if (!$pos = strpos($pathInfo, '/', 1)) { + $pos = strlen($pathInfo); + } + $service = substr($pathInfo, 1, $pos - 1); - $file = ltrim($file, '/'); - - $parts = explode('/', $file, 2); - $app = $parts[0]; - - // Load all required applications - \OC::$REQUESTEDAPP = $app; - $appManager = \OCP\Server::get(IAppManager::class); - $appManager->loadApps(['authentication']); - $appManager->loadApps(['extended_authentication']); - $appManager->loadApps(['filesystem', 'logging']); - - switch ($app) { - case 'core': - $file = OC::$SERVERROOT . '/' . $file; - break; - default: - if (!$appManager->isEnabledForUser($app)) { - throw new RemoteException('App not installed: ' . $app); - } - $appManager->loadApp($app); - $file = $appManager->getAppPath($app) . '/' . ($parts[1] ?? ''); - break; + $file = resolveService($service); + + if (is_null($file)) { + throw new RemoteException('Path not found', 404); + } + + $file = ltrim($file, '/'); + + $parts = explode('/', $file, 2); + $app = $parts[0]; + + // Load all required applications + \OC::$REQUESTEDAPP = $app; + $appManager = \OCP\Server::get(IAppManager::class); + $appManager->loadApps(['authentication']); + $appManager->loadApps(['extended_authentication']); + $appManager->loadApps(['filesystem', 'logging']); + + switch ($app) { + case 'core': + $file = OC::$SERVERROOT . '/' . $file; + break; + default: + if (!$appManager->isEnabledForUser($app)) { + throw new RemoteException('App not installed: ' . $app); + } + $appManager->loadApp($app); + $file = $appManager->getAppPath($app) . '/' . ($parts[1] ?? ''); + break; + } + $baseuri = OC::$WEBROOT . '/remote.php/' . $service . '/'; + require_once $file; + } catch (Exception $ex) { + handleException($ex); + } catch (Error $e) { + handleException($e); } - $baseuri = OC::$WEBROOT . '/remote.php/' . $service . '/'; - require_once $file; -} catch (Exception $ex) { - handleException($ex); -} catch (Error $e) { - handleException($e); -} +});