Merge pull request #24702 from nextcloud/enhancement/well-known-handler-api

Add well known handlers API
This commit is contained in:
Christoph Wurst 2020-12-18 13:34:04 +01:00 committed by GitHub
commit f37e150d1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1022 additions and 36 deletions

View File

@ -61,10 +61,6 @@
RewriteCond %{HTTP_USER_AGENT} DavClnt RewriteCond %{HTTP_USER_AGENT} DavClnt
RewriteRule ^$ /remote.php/webdav/ [L,R=302] RewriteRule ^$ /remote.php/webdav/ [L,R=302]
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteRule ^\.well-known/host-meta /public.php?service=host-meta [QSA,L]
RewriteRule ^\.well-known/host-meta\.json /public.php?service=host-meta-json [QSA,L]
RewriteRule ^\.well-known/webfinger /public.php?service=webfinger [QSA,L]
RewriteRule ^\.well-known/nodeinfo /public.php?service=nodeinfo [QSA,L]
RewriteRule ^\.well-known/carddav /remote.php/dav/ [R=301,L] RewriteRule ^\.well-known/carddav /remote.php/dav/ [R=301,L]
RewriteRule ^\.well-known/caldav /remote.php/dav/ [R=301,L] RewriteRule ^\.well-known/caldav /remote.php/dav/ [R=301,L]
RewriteRule ^remote/(.*) remote.php [QSA,L] RewriteRule ^remote/(.*) remote.php [QSA,L]

View File

@ -256,10 +256,10 @@ window.addEventListener('DOMContentLoaded', function(){
// run setup checks then gather error messages // run setup checks then gather error messages
$.when( $.when(
OC.SetupChecks.checkWebDAV(), OC.SetupChecks.checkWebDAV(),
OC.SetupChecks.checkWellKnownUrl('/.well-known/webfinger', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true && !!OC.appConfig.core.public_webfinger, [200, 404]), OC.SetupChecks.checkWellKnownUrl('GET', '/.well-known/webfinger', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true, [200, 404], true),
OC.SetupChecks.checkWellKnownUrl('/.well-known/nodeinfo', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true && !!OC.appConfig.core.public_nodeinfo, [200, 404]), OC.SetupChecks.checkWellKnownUrl('GET', '/.well-known/nodeinfo', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true, [200, 404], true),
OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true),
OC.SetupChecks.checkWellKnownUrl('/.well-known/carddav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/carddav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true),
OC.SetupChecks.checkProviderUrl(OC.getRootPath() + '/ocm-provider/', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), OC.SetupChecks.checkProviderUrl(OC.getRootPath() + '/ocm-provider/', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true),
OC.SetupChecks.checkProviderUrl(OC.getRootPath() + '/ocs-provider/', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), OC.SetupChecks.checkProviderUrl(OC.getRootPath() + '/ocs-provider/', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true),
OC.SetupChecks.checkSetup(), OC.SetupChecks.checkSetup(),

View File

@ -163,8 +163,6 @@ class Application extends App implements IBootstrap {
$groupManager->listen('\OC\Group', 'postRemoveUser', [$this, 'removeUserFromGroup']); $groupManager->listen('\OC\Group', 'postRemoveUser', [$this, 'removeUserFromGroup']);
$groupManager->listen('\OC\Group', 'postAddUser', [$this, 'addUserToGroup']); $groupManager->listen('\OC\Group', 'postAddUser', [$this, 'addUserToGroup']);
}); });
Util::connectHook('\OCP\Config', 'js', $this, 'extendJsConfig');
} }
public function addUserToGroup(IGroup $group, IUser $user): void { public function addUserToGroup(IGroup $group, IUser $user): void {
@ -209,23 +207,4 @@ class Application extends App implements IBootstrap {
$hooks = $this->getContainer()->query(Hooks::class); $hooks = $this->getContainer()->query(Hooks::class);
$hooks->onChangeEmail($parameters['user'], $parameters['old_value']); $hooks->onChangeEmail($parameters['user'], $parameters['old_value']);
} }
/**
* @param array $settings
*/
public function extendJsConfig(array $settings) {
$appConfig = json_decode($settings['array']['oc_appconfig'], true);
$publicWebFinger = \OC::$server->getConfig()->getAppValue('core', 'public_webfinger', '');
if (!empty($publicWebFinger)) {
$appConfig['core']['public_webfinger'] = $publicWebFinger;
}
$publicNodeInfo = \OC::$server->getConfig()->getAppValue('core', 'public_nodeinfo', '');
if (!empty($publicNodeInfo)) {
$appConfig['core']['public_nodeinfo'] = $publicNodeInfo;
}
$settings['array']['oc_appconfig'] = json_encode($appConfig);
}
} }

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\Core\Controller;
use OC\Http\WellKnown\RequestManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
class WellKnownController extends Controller {
/** @var RequestManager */
private $requestManager;
public function __construct(IRequest $request,
RequestManager $wellKnownManager) {
parent::__construct('core', $request);
$this->requestManager = $wellKnownManager;
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @return Response
*/
public function handle(string $service): Response {
$response = $this->requestManager->process(
$service,
$this->request
);
if ($response === null) {
$httpResponse = new JSONResponse(["message" => "$service not supported"], Http::STATUS_NOT_FOUND);
} else {
$httpResponse = $response->toHttpResponse();
}
// We add a custom header so that setup checks can detect if their requests are answered by this controller
return $httpResponse->addHeader('X-NEXTCLOUD-WELL-KNOWN', '1');
}
}

View File

@ -56,7 +56,7 @@
* @param {int|int[]} expectedStatus the expected HTTP status to be returned by the URL, 207 by default * @param {int|int[]} expectedStatus the expected HTTP status to be returned by the URL, 207 by default
* @return $.Deferred object resolved with an array of error messages * @return $.Deferred object resolved with an array of error messages
*/ */
checkWellKnownUrl: function(url, placeholderUrl, runCheck, expectedStatus) { checkWellKnownUrl: function(verb, url, placeholderUrl, runCheck, expectedStatus, checkCustomHeader) {
if (expectedStatus === undefined) { if (expectedStatus === undefined) {
expectedStatus = [207]; expectedStatus = [207];
} }
@ -73,7 +73,8 @@
} }
var afterCall = function(xhr) { var afterCall = function(xhr) {
var messages = []; var messages = [];
if (expectedStatus.indexOf(xhr.status) === -1) { var customWellKnown = xhr.getResponseHeader('X-NEXTCLOUD-WELL-KNOWN')
if (expectedStatus.indexOf(xhr.status) === -1 || (checkCustomHeader && !customWellKnown)) {
var docUrl = placeholderUrl.replace('PLACEHOLDER', 'admin-setup-well-known-URL'); var docUrl = placeholderUrl.replace('PLACEHOLDER', 'admin-setup-well-known-URL');
messages.push({ messages.push({
msg: t('core', 'Your web server is not properly set up to resolve "{url}". Further information can be found in the <a target="_blank" rel="noreferrer noopener" href="{docLink}">documentation</a>.', { docLink: docUrl, url: url }), msg: t('core', 'Your web server is not properly set up to resolve "{url}". Further information can be found in the <a target="_blank" rel="noreferrer noopener" href="{docLink}">documentation</a>.', { docLink: docUrl, url: url }),
@ -84,7 +85,7 @@
}; };
$.ajax({ $.ajax({
type: 'PROPFIND', type: verb,
url: url, url: url,
complete: afterCall, complete: afterCall,
allowAuthErrors: true allowAuthErrors: true

View File

@ -62,7 +62,7 @@ describe('OC.SetupChecks tests', function() {
describe('checkWellKnownUrl', function() { describe('checkWellKnownUrl', function() {
it('should fail with another response status code than the expected one', function(done) { it('should fail with another response status code than the expected one', function(done) {
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207); var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207);
suite.server.requests[0].respond(200); suite.server.requests[0].respond(200);
@ -76,7 +76,7 @@ describe('OC.SetupChecks tests', function() {
}); });
it('should return no error with the expected response status code', function(done) { it('should return no error with the expected response status code', function(done) {
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207); var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207);
suite.server.requests[0].respond(207); suite.server.requests[0].respond(207);
@ -87,7 +87,7 @@ describe('OC.SetupChecks tests', function() {
}); });
it('should return no error with the default expected response status code', function(done) { it('should return no error with the default expected response status code', function(done) {
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', true); var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', true);
suite.server.requests[0].respond(207); suite.server.requests[0].respond(207);
@ -98,7 +98,7 @@ describe('OC.SetupChecks tests', function() {
}); });
it('should return no error when no check should be run', function(done) { it('should return no error when no check should be run', function(done) {
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', false); var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', false);
async.done(function( data, s, x ){ async.done(function( data, s, x ){
expect(data).toEqual([]); expect(data).toEqual([]);

View File

@ -89,6 +89,9 @@ $application->registerRoutes($this, [
// Logins for passwordless auth // Logins for passwordless auth
['name' => 'WebAuthn#startAuthentication', 'url' => 'login/webauthn/start', 'verb' => 'POST'], ['name' => 'WebAuthn#startAuthentication', 'url' => 'login/webauthn/start', 'verb' => 'POST'],
['name' => 'WebAuthn#finishAuthentication', 'url' => 'login/webauthn/finish', 'verb' => 'POST'], ['name' => 'WebAuthn#finishAuthentication', 'url' => 'login/webauthn/finish', 'verb' => 'POST'],
// Well known requests https://tools.ietf.org/html/rfc5785
['name' => 'WellKnown#handle', 'url' => '.well-known/{service}'],
], ],
'ocs' => [ 'ocs' => [
['root' => '/cloud', 'name' => 'OCS#getCapabilities', 'url' => '/capabilities', 'verb' => 'GET'], ['root' => '/cloud', 'name' => 'OCS#getCapabilities', 'url' => '/capabilities', 'verb' => 'GET'],

View File

@ -359,6 +359,11 @@ return array(
'OCP\\Http\\Client\\IClientService' => $baseDir . '/lib/public/Http/Client/IClientService.php', 'OCP\\Http\\Client\\IClientService' => $baseDir . '/lib/public/Http/Client/IClientService.php',
'OCP\\Http\\Client\\IResponse' => $baseDir . '/lib/public/Http/Client/IResponse.php', 'OCP\\Http\\Client\\IResponse' => $baseDir . '/lib/public/Http/Client/IResponse.php',
'OCP\\Http\\Client\\LocalServerException' => $baseDir . '/lib/public/Http/Client/LocalServerException.php', 'OCP\\Http\\Client\\LocalServerException' => $baseDir . '/lib/public/Http/Client/LocalServerException.php',
'OCP\\Http\\WellKnown\\GenericResponse' => $baseDir . '/lib/public/Http/WellKnown/GenericResponse.php',
'OCP\\Http\\WellKnown\\IHandler' => $baseDir . '/lib/public/Http/WellKnown/IHandler.php',
'OCP\\Http\\WellKnown\\IRequestContext' => $baseDir . '/lib/public/Http/WellKnown/IRequestContext.php',
'OCP\\Http\\WellKnown\\IResponse' => $baseDir . '/lib/public/Http/WellKnown/IResponse.php',
'OCP\\Http\\WellKnown\\JrdResponse' => $baseDir . '/lib/public/Http/WellKnown/JrdResponse.php',
'OCP\\IAddressBook' => $baseDir . '/lib/public/IAddressBook.php', 'OCP\\IAddressBook' => $baseDir . '/lib/public/IAddressBook.php',
'OCP\\IAppConfig' => $baseDir . '/lib/public/IAppConfig.php', 'OCP\\IAppConfig' => $baseDir . '/lib/public/IAppConfig.php',
'OCP\\IAvatar' => $baseDir . '/lib/public/IAvatar.php', 'OCP\\IAvatar' => $baseDir . '/lib/public/IAvatar.php',
@ -892,6 +897,7 @@ return array(
'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php',
'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php', 'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php',
'OC\\Core\\Controller\\WellKnownController' => $baseDir . '/core/Controller/WellKnownController.php',
'OC\\Core\\Controller\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php', 'OC\\Core\\Controller\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php',
'OC\\Core\\Controller\\WipeController' => $baseDir . '/core/Controller/WipeController.php', 'OC\\Core\\Controller\\WipeController' => $baseDir . '/core/Controller/WipeController.php',
'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php', 'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php',
@ -1136,6 +1142,7 @@ return array(
'OC\\Http\\Client\\ClientService' => $baseDir . '/lib/private/Http/Client/ClientService.php', 'OC\\Http\\Client\\ClientService' => $baseDir . '/lib/private/Http/Client/ClientService.php',
'OC\\Http\\Client\\Response' => $baseDir . '/lib/private/Http/Client/Response.php', 'OC\\Http\\Client\\Response' => $baseDir . '/lib/private/Http/Client/Response.php',
'OC\\Http\\CookieHelper' => $baseDir . '/lib/private/Http/CookieHelper.php', 'OC\\Http\\CookieHelper' => $baseDir . '/lib/private/Http/CookieHelper.php',
'OC\\Http\\WellKnown\\RequestManager' => $baseDir . '/lib/private/Http/WellKnown/RequestManager.php',
'OC\\InitialStateService' => $baseDir . '/lib/private/InitialStateService.php', 'OC\\InitialStateService' => $baseDir . '/lib/private/InitialStateService.php',
'OC\\Installer' => $baseDir . '/lib/private/Installer.php', 'OC\\Installer' => $baseDir . '/lib/private/Installer.php',
'OC\\IntegrityCheck\\Checker' => $baseDir . '/lib/private/IntegrityCheck/Checker.php', 'OC\\IntegrityCheck\\Checker' => $baseDir . '/lib/private/IntegrityCheck/Checker.php',

View File

@ -388,6 +388,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\Http\\Client\\IClientService' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IClientService.php', 'OCP\\Http\\Client\\IClientService' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IClientService.php',
'OCP\\Http\\Client\\IResponse' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IResponse.php', 'OCP\\Http\\Client\\IResponse' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IResponse.php',
'OCP\\Http\\Client\\LocalServerException' => __DIR__ . '/../../..' . '/lib/public/Http/Client/LocalServerException.php', 'OCP\\Http\\Client\\LocalServerException' => __DIR__ . '/../../..' . '/lib/public/Http/Client/LocalServerException.php',
'OCP\\Http\\WellKnown\\GenericResponse' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/GenericResponse.php',
'OCP\\Http\\WellKnown\\IHandler' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/IHandler.php',
'OCP\\Http\\WellKnown\\IRequestContext' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/IRequestContext.php',
'OCP\\Http\\WellKnown\\IResponse' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/IResponse.php',
'OCP\\Http\\WellKnown\\JrdResponse' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/JrdResponse.php',
'OCP\\IAddressBook' => __DIR__ . '/../../..' . '/lib/public/IAddressBook.php', 'OCP\\IAddressBook' => __DIR__ . '/../../..' . '/lib/public/IAddressBook.php',
'OCP\\IAppConfig' => __DIR__ . '/../../..' . '/lib/public/IAppConfig.php', 'OCP\\IAppConfig' => __DIR__ . '/../../..' . '/lib/public/IAppConfig.php',
'OCP\\IAvatar' => __DIR__ . '/../../..' . '/lib/public/IAvatar.php', 'OCP\\IAvatar' => __DIR__ . '/../../..' . '/lib/public/IAvatar.php',
@ -921,6 +926,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php',
'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php', 'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php',
'OC\\Core\\Controller\\WellKnownController' => __DIR__ . '/../../..' . '/core/Controller/WellKnownController.php',
'OC\\Core\\Controller\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php', 'OC\\Core\\Controller\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php',
'OC\\Core\\Controller\\WipeController' => __DIR__ . '/../../..' . '/core/Controller/WipeController.php', 'OC\\Core\\Controller\\WipeController' => __DIR__ . '/../../..' . '/core/Controller/WipeController.php',
'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php', 'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php',
@ -1165,6 +1171,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Http\\Client\\ClientService' => __DIR__ . '/../../..' . '/lib/private/Http/Client/ClientService.php', 'OC\\Http\\Client\\ClientService' => __DIR__ . '/../../..' . '/lib/private/Http/Client/ClientService.php',
'OC\\Http\\Client\\Response' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Response.php', 'OC\\Http\\Client\\Response' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Response.php',
'OC\\Http\\CookieHelper' => __DIR__ . '/../../..' . '/lib/private/Http/CookieHelper.php', 'OC\\Http\\CookieHelper' => __DIR__ . '/../../..' . '/lib/private/Http/CookieHelper.php',
'OC\\Http\\WellKnown\\RequestManager' => __DIR__ . '/../../..' . '/lib/private/Http/WellKnown/RequestManager.php',
'OC\\InitialStateService' => __DIR__ . '/../../..' . '/lib/private/InitialStateService.php', 'OC\\InitialStateService' => __DIR__ . '/../../..' . '/lib/private/InitialStateService.php',
'OC\\Installer' => __DIR__ . '/../../..' . '/lib/private/Installer.php', 'OC\\Installer' => __DIR__ . '/../../..' . '/lib/private/Installer.php',
'OC\\IntegrityCheck\\Checker' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Checker.php', 'OC\\IntegrityCheck\\Checker' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Checker.php',

View File

@ -74,6 +74,9 @@ class RegistrationContext {
/** @var array[] */ /** @var array[] */
private $initialStates = []; private $initialStates = [];
/** @var array[] */
private $wellKnownHandlers = [];
/** @var ILogger */ /** @var ILogger */
private $logger; private $logger;
@ -176,6 +179,13 @@ class RegistrationContext {
$class $class
); );
} }
public function registerWellKnownHandler(string $class): void {
$this->context->registerWellKnown(
$this->appId,
$class
);
}
}; };
} }
@ -262,6 +272,13 @@ class RegistrationContext {
]; ];
} }
public function registerWellKnown(string $appId, string $class): void {
$this->wellKnownHandlers[] = [
'appId' => $appId,
'class' => $class,
];
}
/** /**
* @param App[] $apps * @param App[] $apps
*/ */
@ -439,4 +456,11 @@ class RegistrationContext {
public function getInitialStates(): array { public function getInitialStates(): array {
return $this->initialStates; return $this->initialStates;
} }
/**
* @return array[]
*/
public function getWellKnownHandlers(): array {
return $this->wellKnownHandlers;
}
} }

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\Http\WellKnown;
use OC\AppFramework\Bootstrap\Coordinator;
use OCP\AppFramework\QueryException;
use OCP\Http\WellKnown\IHandler;
use OCP\Http\WellKnown\IRequestContext;
use OCP\Http\WellKnown\IResponse;
use OCP\Http\WellKnown\JrdResponse;
use OCP\IRequest;
use OCP\IServerContainer;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function array_reduce;
class RequestManager {
/** @var Coordinator */
private $coordinator;
/** @var IServerContainer */
private $container;
/** @var LoggerInterface */
private $logger;
public function __construct(Coordinator $coordinator,
IServerContainer $container,
LoggerInterface $logger) {
$this->coordinator = $coordinator;
$this->container = $container;
$this->logger = $logger;
}
public function process(string $service, IRequest $request): ?IResponse {
$handlers = $this->loadHandlers();
$context = new class($request) implements IRequestContext {
/** @var IRequest */
private $request;
public function __construct(IRequest $request) {
$this->request = $request;
}
public function getHttpRequest(): IRequest {
return $this->request;
}
};
$subject = $request->getParam('resource');
$initialResponse = new JrdResponse($subject ?? '');
$finalResponse = array_reduce($handlers, function (?IResponse $previousResponse, IHandler $handler) use ($context, $service) {
return $handler->handle($service, $context, $previousResponse);
}, $initialResponse);
if ($finalResponse instanceof JrdResponse && $finalResponse->isEmpty()) {
return null;
}
return $finalResponse;
}
/**
* @return IHandler[]
*/
private function loadHandlers(): array {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
throw new RuntimeException("Well known handlers requested before the apps had been fully registered");
}
$registrations = $context->getWellKnownHandlers();
$this->logger->debug(count($registrations) . " well known handlers registered");
return array_filter(
array_map(function (array $registration) {
$class = $registration['class'];
try {
$handler = $this->container->get($class);
if (!($handler) instanceof IHandler) {
$this->logger->error("Well known handler $class is invalid");
return null;
}
return $handler;
} catch (QueryException $e) {
$this->logger->error("Could not load well known handler $class", [
'exception' => $e,
]);
return null;
}
}, $registrations)
);
}
}

View File

@ -181,4 +181,18 @@ interface IRegistrationContext {
* @since 21.0.0 * @since 21.0.0
*/ */
public function registerInitialStateProvider(string $class): void; public function registerInitialStateProvider(string $class): void;
/**
* Register a well known protocol handler
*
* It is allowed to register more than one handler per app.
*
* @param string $class
* @psalm-param class-string<\OCP\Http\WellKnown\IHandler> $class
*
* @return void
*
* @since 21.0.0
*/
public function registerWellKnownHandler(string $class): void;
} }

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\Http\WellKnown;
use OCP\AppFramework\Http\Response;
/**
* @since 21.0.0
*/
final class GenericResponse implements IResponse {
/** @var Response */
private $response;
/**
* @since 21.0.0
*/
public function __construct(Response $response) {
$this->response = $response;
}
/**
* @since 21.0.0
*/
public function toHttpResponse(): Response {
return $this->response;
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\Http\WellKnown;
/**
* Interface for an app handler that reacts to requests to Nextcloud's well
* known URLs, e.g. to a WebFinger
*
* @ref https://tools.ietf.org/html/rfc5785
*
* @since 21.0.0
*/
interface IHandler {
/**
* @param string $service the name of the well known service, e.g. 'webfinger'
* @param IRequestContext $context
* @param IResponse|null $previousResponse the response of the previous handler, if any
*
* @return IResponse|null a response object if the request could be handled, null otherwise
*
* @since 21.0.0
*/
public function handle(string $service, IRequestContext $context, ?IResponse $previousResponse): ?IResponse;
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\Http\WellKnown;
use OCP\IRequest;
/**
* The context object for \OCP\Http\IWellKnownHandler::handle
*
* Objects of this type will transport any optional information, e.g. the request
* object through which the app well known handler can obtain URL parameters
*
* @since 21.0.0
*/
interface IRequestContext {
/**
* @return IRequest
*
* @since 21.0.0
*/
public function getHttpRequest(): IRequest;
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\Http\WellKnown;
use OCP\AppFramework\Http\Response;
/**
* @since 21.0.0
*/
interface IResponse {
/**
* @since 21.0.0
*/
public function toHttpResponse(): Response;
}

View File

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\Http\WellKnown;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use function array_filter;
/**
* A JSON Document Format (JDF) response to a well-known request
*
* @ref https://tools.ietf.org/html/rfc6415#appendix-A
* @ref https://tools.ietf.org/html/rfc7033#section-4.4
*
* @since 21.0.0
*/
final class JrdResponse implements IResponse {
/** @var string */
private $subject;
/** @var string|null */
private $expires;
/** @var string[] */
private $aliases = [];
/** @var (string|null)[] */
private $properties = [];
/** @var mixed[] */
private $links;
/**
* @param string $subject https://tools.ietf.org/html/rfc7033#section-4.4.1
*
* @since 21.0.0
*/
public function __construct(string $subject) {
$this->subject = $subject;
}
/**
* @param string $expires
*
* @return $this
*
* @since 21.0.0
*/
public function setExpires(string $expires): self {
$this->expires = $expires;
return $this;
}
/**
* Add an alias
*
* @ref https://tools.ietf.org/html/rfc7033#section-4.4.2
*
* @param string $alias
*
* @return $this
*
* @since 21.0.0
*/
public function addAlias(string $alias): self {
$this->aliases[] = $alias;
return $this;
}
/**
* Add a property
*
* @ref https://tools.ietf.org/html/rfc7033#section-4.4.3
*
* @param string $property
* @param string|null $value
*
* @return $this
*
* @since 21.0.0
*/
public function addProperty(string $property, ?string $value): self {
$this->properties[$property] = $value;
return $this;
}
/**
* Add a link
*
* @ref https://tools.ietf.org/html/rfc7033#section-8.4
*
* @param string $rel https://tools.ietf.org/html/rfc7033#section-4.4.4.1
* @param string|null $type https://tools.ietf.org/html/rfc7033#section-4.4.4.2
* @param string|null $href https://tools.ietf.org/html/rfc7033#section-4.4.4.3
* @param string[]|null $titles https://tools.ietf.org/html/rfc7033#section-4.4.4.4
* @param string|null $properties https://tools.ietf.org/html/rfc7033#section-4.4.4.5
*
* @psalm-param array<string,(string|null)>|null $properties https://tools.ietf.org/html/rfc7033#section-4.4.4.5
*
* @return JrdResponse
* @since 21.0.0
*/
public function addLink(string $rel,
?string $type,
?string $href,
?array $titles = [],
?array $properties = []): self {
$this->links[] = array_filter([
'rel' => $rel,
'type' => $type,
'href' => $href,
'titles' => $titles,
'properties' => $properties,
]);
return $this;
}
/**
* @since 21.0.0
*/
public function toHttpResponse(): Response {
return new JSONResponse(array_filter([
'subject' => $this->subject,
'expires' => $this->expires,
'aliases' => $this->aliases,
'properties' => $this->properties,
'links' => $this->links,
]));
}
/**
* Does this response have any data attached to it?
*
* @since 21.0.0
*/
public function isEmpty(): bool {
return $this->expires === null
&& empty($this->aliases)
&& empty($this->properties)
&& empty($this->links);
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Tests\Core\Controller;
use OC\Core\Controller\WellKnownController;
use OC\Http\WellKnown\RequestManager;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Http\WellKnown\IResponse;
use OCP\IRequest;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class WellKnownControllerTest extends TestCase {
/** @var IRequest|MockObject */
private $request;
/** @var RequestManager|MockObject */
private $manager;
/** @var WellKnownController */
private $controller;
protected function setUp(): void {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
$this->manager = $this->createMock(RequestManager::class);
$this->controller = new WellKnownController(
$this->request,
$this->manager,
);
}
public function testHandleNotProcessed(): void {
$httpResponse = $this->controller->handle("nodeinfo");
self::assertInstanceOf(JSONResponse::class, $httpResponse);
self::assertArrayHasKey('X-NEXTCLOUD-WELL-KNOWN', $httpResponse->getHeaders());
}
public function testHandle(): void {
$response = $this->createMock(IResponse::class);
$jsonResponse = $this->createMock(JSONResponse::class);
$response->expects(self::once())
->method('toHttpResponse')
->willReturn($jsonResponse);
$this->manager->expects(self::once())
->method('process')
->with(
"nodeinfo",
$this->request
)->willReturn($response);
$jsonResponse->expects(self::once())
->method('addHeader')
->willReturnSelf();
$httpResponse = $this->controller->handle("nodeinfo");
self::assertInstanceOf(JSONResponse::class, $httpResponse);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Tests\Http\WellKnown;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Http\WellKnown\GenericResponse;
use Test\TestCase;
class GenericResponseTest extends TestCase {
public function testToHttpResponse(): void {
$httpResponse = $this->createMock(JSONResponse::class);
$response = new GenericResponse($httpResponse);
self::assertSame($httpResponse, $response->toHttpResponse());
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Test\Http\WellKnown;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Http\WellKnown\JrdResponse;
use Test\TestCase;
class JrdResponseTest extends TestCase {
public function testEmptyToHttpResponse(): void {
$response = new JrdResponse("subject");
$httpResponse = $response->toHttpResponse();
self::assertTrue($response->isEmpty());
self::assertInstanceOf(JSONResponse::class, $httpResponse);
/** @var JSONResponse $httpResponse */
self::assertEquals(
[
'subject' => 'subject',
],
$httpResponse->getData()
);
}
public function testComplexToHttpResponse(): void {
$response = new JrdResponse("subject");
$response->addAlias('alias');
$response->addAlias('blias');
$response->addProperty('propa', 'a');
$response->addProperty('propb', null);
$response->setExpires('tomorrow');
$response->addLink('rel', null, null);
$response->addLink('rel', 'type', null);
$response->addLink('rel', 'type', 'href', ['title' => 'titlevalue']);
$response->addLink('rel', 'type', 'href', ['title' => 'titlevalue'], ['propx' => 'valx']);
$httpResponse = $response->toHttpResponse();
self::assertFalse($response->isEmpty());
self::assertInstanceOf(JSONResponse::class, $httpResponse);
/** @var JSONResponse $httpResponse */
self::assertEquals(
[
'subject' => 'subject',
'aliases' => [
'alias',
'blias',
],
'properties' => [
'propa' => 'a',
'propb' => null,
],
'expires' => 'tomorrow',
'links' => [
[
'rel' => 'rel',
],
[
'rel' => 'rel',
'type' => 'type',
],
[
'rel' => 'rel',
'type' => 'type',
'href' => 'href',
'titles' => [
'title' => 'titlevalue',
],
],
[
'rel' => 'rel',
'type' => 'type',
'href' => 'href',
'titles' => [
'title' => 'titlevalue',
],
'properties' => [
'propx' => 'valx',
],
],
]
],
$httpResponse->getData()
);
}
}

View File

@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
/*
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Test\Http\WellKnown;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Bootstrap\RegistrationContext;
use OC\Http\WellKnown\RequestManager;
use OCP\AppFramework\QueryException;
use OCP\Http\WellKnown\IHandler;
use OCP\Http\WellKnown\IRequestContext;
use OCP\Http\WellKnown\IResponse;
use OCP\Http\WellKnown\JrdResponse;
use OCP\IRequest;
use OCP\IServerContainer;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Test\TestCase;
use function get_class;
class RequestManagerTest extends TestCase {
/** @var Coordinator|MockObject */
private $coordinator;
/** @var IServerContainer|MockObject */
private $container;
/** @var MockObject|LoggerInterface */
private $logger;
/** @var RequestManager */
private $manager;
protected function setUp(): void {
parent::setUp();
$this->coordinator = $this->createMock(Coordinator::class);
$this->container = $this->createMock(IServerContainer::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->manager = new RequestManager(
$this->coordinator,
$this->container,
$this->logger,
);
}
public function testProcessAppsNotRegistered(): void {
$request = $this->createMock(IRequest::class);
$this->expectException(RuntimeException::class);
$this->manager->process("webfinger", $request);
}
public function testProcessNoHandlersRegistered(): void {
$request = $this->createMock(IRequest::class);
$registrationContext = $this->createMock(RegistrationContext::class);
$this->coordinator->expects(self::once())
->method('getRegistrationContext')
->willReturn($registrationContext);
$registrationContext->expects(self::once())
->method('getWellKnownHandlers')
->willReturn([]);
$response = $this->manager->process("webfinger", $request);
self::assertNull($response);
}
public function testProcessHandlerNotLoadable(): void {
$request = $this->createMock(IRequest::class);
$registrationContext = $this->createMock(RegistrationContext::class);
$this->coordinator->expects(self::once())
->method('getRegistrationContext')
->willReturn($registrationContext);
$handler = new class {
};
$registrationContext->expects(self::once())
->method('getWellKnownHandlers')
->willReturn([
[
'class' => get_class($handler),
],
]);
$this->container->expects(self::once())
->method('get')
->with(get_class($handler))
->willThrowException(new QueryException(""));
$this->logger->expects(self::once())
->method('error');
$response = $this->manager->process("webfinger", $request);
self::assertNull($response);
}
public function testProcessHandlerOfWrongType(): void {
$request = $this->createMock(IRequest::class);
$registrationContext = $this->createMock(RegistrationContext::class);
$this->coordinator->expects(self::once())
->method('getRegistrationContext')
->willReturn($registrationContext);
$handler = new class {
};
$registrationContext->expects(self::once())
->method('getWellKnownHandlers')
->willReturn([
[
'class' => get_class($handler),
],
]);
$this->container->expects(self::once())
->method('get')
->with(get_class($handler))
->willReturn($handler);
$this->logger->expects(self::once())
->method('error');
$response = $this->manager->process("webfinger", $request);
self::assertNull($response);
}
public function testProcess(): void {
$request = $this->createMock(IRequest::class);
$registrationContext = $this->createMock(RegistrationContext::class);
$this->coordinator->expects(self::once())
->method('getRegistrationContext')
->willReturn($registrationContext);
$handler = new class implements IHandler {
public function handle(string $service, IRequestContext $context, ?IResponse $previousResponse): ?IResponse {
return (new JrdResponse($service))->addAlias('alias');
}
};
$registrationContext->expects(self::once())
->method('getWellKnownHandlers')
->willReturn([
[
'class' => get_class($handler),
],
]);
$this->container->expects(self::once())
->method('get')
->with(get_class($handler))
->willReturn($handler);
$response = $this->manager->process("webfinger", $request);
self::assertNotNull($response);
self::assertInstanceOf(JrdResponse::class, $response);
}
}