aboutsummaryrefslogtreecommitdiffstats
path: root/lib/public/Http
diff options
context:
space:
mode:
Diffstat (limited to 'lib/public/Http')
-rw-r--r--lib/public/Http/Client/IClient.php439
-rw-r--r--lib/public/Http/Client/IClientService.php22
-rw-r--r--lib/public/Http/Client/IPromise.php83
-rw-r--r--lib/public/Http/Client/IResponse.php42
-rw-r--r--lib/public/Http/Client/LocalServerException.php15
-rw-r--r--lib/public/Http/WellKnown/GenericResponse.php33
-rw-r--r--lib/public/Http/WellKnown/IHandler.php30
-rw-r--r--lib/public/Http/WellKnown/IRequestContext.php28
-rw-r--r--lib/public/Http/WellKnown/IResponse.php21
-rw-r--r--lib/public/Http/WellKnown/JrdResponse.php152
10 files changed, 865 insertions, 0 deletions
diff --git a/lib/public/Http/Client/IClient.php b/lib/public/Http/Client/IClient.php
new file mode 100644
index 00000000000..e4f46d44e4d
--- /dev/null
+++ b/lib/public/Http/Client/IClient.php
@@ -0,0 +1,439 @@
+<?php
+
+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
+ */
+namespace OCP\Http\Client;
+
+/**
+ * Interface IClient
+ *
+ * @since 8.1.0
+ */
+interface IClient {
+
+ /**
+ * Default request timeout for requests
+ *
+ * @since 31.0.0
+ */
+ public const DEFAULT_REQUEST_TIMEOUT = 30;
+
+ /**
+ * Sends a GET request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 8.1.0
+ */
+ public function get(string $uri, array $options = []): IResponse;
+
+ /**
+ * Sends a HEAD request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 8.1.0
+ */
+ public function head(string $uri, array $options = []): IResponse;
+
+ /**
+ * Sends a POST request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 8.1.0
+ */
+ public function post(string $uri, array $options = []): IResponse;
+
+ /**
+ * Sends a PUT request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 8.1.0
+ */
+ public function put(string $uri, array $options = []): IResponse;
+
+ /**
+ * Sends a PATCH request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 29.0.0
+ */
+ public function patch(string $uri, array $options = []): IResponse;
+
+ /**
+ * Sends a DELETE request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 8.1.0
+ */
+ public function delete(string $uri, array $options = []): IResponse;
+
+ /**
+ * Sends an OPTIONS request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 8.1.0
+ */
+ public function options(string $uri, array $options = []): IResponse;
+
+ /**
+ * Get the response of a Throwable thrown by the request methods when possible
+ *
+ * @param \Throwable $e
+ * @return IResponse
+ * @throws \Throwable When $e did not have a response
+ * @since 29.0.0
+ */
+ public function getResponseFromThrowable(\Throwable $e): IResponse;
+
+ /**
+ * Sends a HTTP request
+ * @param string $method The HTTP method to use
+ * @param string $uri
+ * @param array $options Array such as
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IResponse
+ * @throws \Exception If the request could not get completed
+ * @since 29.0.0
+ */
+ public function request(string $method, string $uri, array $options = []): IResponse;
+
+ /**
+ * Sends an asynchronous GET request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function getAsync(string $uri, array $options = []): IPromise;
+
+ /**
+ * Sends an asynchronous HEAD request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function headAsync(string $uri, array $options = []): IPromise;
+
+ /**
+ * Sends an asynchronous POST request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function postAsync(string $uri, array $options = []): IPromise;
+
+ /**
+ * Sends an asynchronous PUT request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function putAsync(string $uri, array $options = []): IPromise;
+
+ /**
+ * Sends an asynchronous DELETE request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function deleteAsync(string $uri, array $options = []): IPromise;
+
+ /**
+ * Sends an asynchronous OPTIONS request
+ * @param string $uri
+ * @param array $options Array such as
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function optionsAsync(string $uri, array $options = []): IPromise;
+}
diff --git a/lib/public/Http/Client/IClientService.php b/lib/public/Http/Client/IClientService.php
new file mode 100644
index 00000000000..148757e5b31
--- /dev/null
+++ b/lib/public/Http/Client/IClientService.php
@@ -0,0 +1,22 @@
+<?php
+
+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
+ */
+namespace OCP\Http\Client;
+
+/**
+ * Interface IClientService
+ *
+ * @since 8.1.0
+ */
+interface IClientService {
+ /**
+ * @return IClient
+ * @since 8.1.0
+ */
+ public function newClient(): IClient;
+}
diff --git a/lib/public/Http/Client/IPromise.php b/lib/public/Http/Client/IPromise.php
new file mode 100644
index 00000000000..8778829af0e
--- /dev/null
+++ b/lib/public/Http/Client/IPromise.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCP\Http\Client;
+
+use Exception;
+use LogicException;
+
+/**
+ * A wrapper around Guzzle's PromiseInterface
+ * @see \GuzzleHttp\Promise\PromiseInterface
+ * @since 28.0.0
+ */
+interface IPromise {
+ /**
+ * @since 28.0.0
+ */
+ public const STATE_PENDING = 'pending';
+ /**
+ * @since 28.0.0
+ */
+ public const STATE_FULFILLED = 'fulfilled';
+ /**
+ * @since 28.0.0
+ */
+ public const STATE_REJECTED = 'rejected';
+
+ /**
+ * Appends fulfillment and rejection handlers to the promise, and returns
+ * a new promise resolving to the return value of the called handler.
+ *
+ * @param ?callable(IResponse): void $onFulfilled Invoked when the promise fulfills. Gets an \OCP\Http\Client\IResponse passed in as argument
+ * @param ?callable(Exception): void $onRejected Invoked when the promise is rejected. Gets an \Exception passed in as argument
+ *
+ * @return IPromise
+ * @since 28.0.0
+ */
+ public function then(
+ ?callable $onFulfilled = null,
+ ?callable $onRejected = null,
+ ): IPromise;
+
+ /**
+ * Get the state of the promise ("pending", "rejected", or "fulfilled").
+ *
+ * The three states can be checked against the constants defined:
+ * STATE_PENDING, STATE_FULFILLED, and STATE_REJECTED.
+ *
+ * @return self::STATE_*
+ * @since 28.0.0
+ */
+ public function getState(): string;
+
+ /**
+ * Cancels the promise if possible.
+ *
+ * @link https://github.com/promises-aplus/cancellation-spec/issues/7
+ * @since 28.0.0
+ */
+ public function cancel(): void;
+
+ /**
+ * Waits until the promise completes if possible.
+ *
+ * Pass $unwrap as true to unwrap the result of the promise, either
+ * returning the resolved value or throwing the rejected exception.
+ *
+ * If the promise cannot be waited on, then the promise will be rejected.
+ *
+ * @param bool $unwrap
+ *
+ * @return mixed
+ *
+ * @throws LogicException if the promise has no wait function or if the
+ * promise does not settle after waiting.
+ * @since 28.0.0
+ */
+ public function wait(bool $unwrap = true): mixed;
+}
diff --git a/lib/public/Http/Client/IResponse.php b/lib/public/Http/Client/IResponse.php
new file mode 100644
index 00000000000..53032ef2a37
--- /dev/null
+++ b/lib/public/Http/Client/IResponse.php
@@ -0,0 +1,42 @@
+<?php
+
+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
+ */
+namespace OCP\Http\Client;
+
+/**
+ * Interface IResponse
+ *
+ * @since 8.1.0
+ */
+interface IResponse {
+ /**
+ * @return null|resource|string
+ * @since 8.1.0
+ * @sicne 8.2.0 with stream enabled, the function returns null or a resource
+ */
+ public function getBody();
+
+ /**
+ * @return int
+ * @since 8.1.0
+ */
+ public function getStatusCode(): int;
+
+ /**
+ * @param string $key
+ * @return string
+ * @since 8.1.0
+ */
+ public function getHeader(string $key): string;
+
+ /**
+ * @return array
+ * @since 8.1.0
+ */
+ public function getHeaders(): array;
+}
diff --git a/lib/public/Http/Client/LocalServerException.php b/lib/public/Http/Client/LocalServerException.php
new file mode 100644
index 00000000000..bb18da13a15
--- /dev/null
+++ b/lib/public/Http/Client/LocalServerException.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCP\Http\Client;
+
+/**
+ * @since 19.0.0
+ */
+class LocalServerException extends \RuntimeException {
+}
diff --git a/lib/public/Http/WellKnown/GenericResponse.php b/lib/public/Http/WellKnown/GenericResponse.php
new file mode 100644
index 00000000000..c5279f075c9
--- /dev/null
+++ b/lib/public/Http/WellKnown/GenericResponse.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+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;
+ }
+}
diff --git a/lib/public/Http/WellKnown/IHandler.php b/lib/public/Http/WellKnown/IHandler.php
new file mode 100644
index 00000000000..274d0241969
--- /dev/null
+++ b/lib/public/Http/WellKnown/IHandler.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+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;
+}
diff --git a/lib/public/Http/WellKnown/IRequestContext.php b/lib/public/Http/WellKnown/IRequestContext.php
new file mode 100644
index 00000000000..241a9069904
--- /dev/null
+++ b/lib/public/Http/WellKnown/IRequestContext.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+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;
+}
diff --git a/lib/public/Http/WellKnown/IResponse.php b/lib/public/Http/WellKnown/IResponse.php
new file mode 100644
index 00000000000..2cf63043b63
--- /dev/null
+++ b/lib/public/Http/WellKnown/IResponse.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCP\Http\WellKnown;
+
+use OCP\AppFramework\Http\Response;
+
+/**
+ * @since 21.0.0
+ */
+interface IResponse {
+ /**
+ * @since 21.0.0
+ */
+ public function toHttpResponse(): Response;
+}
diff --git a/lib/public/Http/WellKnown/JrdResponse.php b/lib/public/Http/WellKnown/JrdResponse.php
new file mode 100644
index 00000000000..21ad948d148
--- /dev/null
+++ b/lib/public/Http/WellKnown/JrdResponse.php
@@ -0,0 +1,152 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+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);
+ }
+}