feat: Add frankenphp worker support for more endpoints

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
pull/61115/head
Côme Chilliet 2 months ago
parent 41b1fe1fe1
commit 27f45934ca
No known key found for this signature in database
GPG Key ID: A3E2F658B28C760A
  1. 24
      Caddyfile
  2. 39
      index.php
  3. 37
      lib/OC.php
  4. 136
      ocs/v1.php
  5. 126
      remote.php

@ -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
}
}

@ -10,7 +10,6 @@ declare(strict_types=1);
require_once __DIR__ . '/lib/versioncheck.php';
use OC\Files\Filesystem;
use OC\ServiceUnavailableException;
use OC\User\LoginException;
use OCP\HintException;
@ -24,23 +23,11 @@ require_once __DIR__ . '/lib/OC.php';
\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 (): void {
\OC::handleRequests(static function (): void {
try {
resetStaticProperties();
OC::init();
OC::handleRequest();
\OC::resetStaticProperties();
\OC::init();
\OC::handleRequest();
} catch (ServiceUnavailableException $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
@ -124,20 +111,4 @@ $handler = static function (): 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();
}
});

@ -7,6 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-only
*/
use OC\Files\Filesystem;
use OC\NavigationManager;
use OC\Profiler\BuiltInProfiler;
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
@ -1332,4 +1333,40 @@ class OC {
return false;
}
}
/**
* @internal
*/
public 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();
}
}
}

@ -8,9 +8,6 @@ declare(strict_types=1);
* 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,75 +25,84 @@ use Psr\Log\LoggerInterface;
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';
\OC::boot();
$serveAppApiDuringMaintenance = false;
if (!Util::needUpgrade() && Server::get(IConfig::class)->getSystemValueBool('maintenance')) {
$pathInfo = $request->getPathInfo();
// AppAPI must keep serving HaRP traffic (signed OCS calls and ExApp callbacks)
$serveAppApiDuringMaintenance
= ($pathInfo === '/apps/app_api' || str_starts_with($pathInfo, '/apps/app_api/'))
&& Server::get(IAppManager::class)->isEnabledForAnyone('app_api');
}
\OC::handleRequests(static function () {
\OC::resetStaticProperties();
\OC::init();
$request = Server::get(IRequest::class);
if ((Util::needUpgrade()
|| (Server::get(IConfig::class)->getSystemValueBool('maintenance') && !$serveAppApiDuringMaintenance))
&& $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;
}
$serveAppApiDuringMaintenance = false;
if (!Util::needUpgrade() && Server::get(IConfig::class)->getSystemValueBool('maintenance')) {
$pathInfo = $request->getPathInfo();
// AppAPI must keep serving HaRP traffic (signed OCS calls and ExApp callbacks)
$serveAppApiDuringMaintenance
= ($pathInfo === '/apps/app_api' || str_starts_with($pathInfo, '/apps/app_api/'))
&& Server::get(IAppManager::class)->isEnabledForAnyone('app_api');
}
/*
* Try the appframework routes
*/
try {
$appManager = Server::get(IAppManager::class);
$appManager->loadApps(['session']);
$appManager->loadApps(['authentication']);
$appManager->loadApps(['extended_authentication']);
if ((Util::needUpgrade()
|| (Server::get(IConfig::class)->getSystemValueBool('maintenance') && !$serveAppApiDuringMaintenance))
&& $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;
}
$request->throwDecodingExceptionIfAny();
/*
* Try the appframework routes
*/
try {
$appManager = Server::get(IAppManager::class);
$appManager->loadApps(['session']);
$appManager->loadApps(['authentication']);
$appManager->loadApps(['extended_authentication']);
if ($request->getPathInfo() !== '/core/update') {
if ($serveAppApiDuringMaintenance) {
// loadApps() below is a no-op during maintenance, load app_api explicitly
$appManager->loadApp('app_api');
}
// 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);
$request->throwDecodingExceptionIfAny();
if ($request->getPathInfo() !== '/core/update') {
if ($serveAppApiDuringMaintenance) {
// loadApps() below is a no-op during maintenance, load app_api explicitly
$appManager->loadApp('app_api');
}
// 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);
}
});

@ -1,24 +1,27 @@
<?php
use OC\ServiceUnavailableException;
use OCP\IConfig;
use OCP\Util;
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
require_once __DIR__ . '/lib/versioncheck.php';
use OC\ServiceUnavailableException;
use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
use OCP\App\IAppManager;
use OCP\IConfig;
use OCP\IRequest;
use OCP\Template\ITemplateManager;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\Server;
require_once __DIR__ . '/lib/versioncheck.php';
require_once __DIR__ . '/lib/OC.php';
/**
* Class RemoteException
* Dummy exception class to be use locally to identify certain conditions
@ -94,64 +97,71 @@ function resolveService($service) {
return \OCP\Server::get(IConfig::class)->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::resetStaticProperties();
\OC::init();
// 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';");
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);
}
$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);
$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 = resolveService($service);
$file = resolveService($service);
if (is_null($file)) {
throw new RemoteException('Path not found', 404);
}
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;
$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);
}
});

Loading…
Cancel
Save