mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Split HttpHandler behaviour over multiple classes
This allows easier reuse of certain reoccurring behaviours, such as authorization. The AuthenticatedLdpHandler is no longer required since it is a combination of parsing and authorization. This did require a small change to the OperationHandler interface.
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||
import type { RequestParser } from '../ldp/http/RequestParser';
|
||||
import { RedirectResponseDescription } from '../ldp/http/response/RedirectResponseDescription';
|
||||
import { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
||||
import type { Operation } from '../ldp/operations/Operation';
|
||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
||||
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
||||
import type { HttpRequest } from '../server/HttpRequest';
|
||||
import type { HttpResponse } from '../server/HttpResponse';
|
||||
import type { OperationHttpHandlerInput } from '../server/OperationHttpHandler';
|
||||
import { OperationHttpHandler } from '../server/OperationHttpHandler';
|
||||
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
|
||||
import { APPLICATION_JSON } from '../util/ContentTypes';
|
||||
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
|
||||
@@ -25,11 +22,7 @@ import type { InteractionCompleter } from './interaction/util/InteractionComplet
|
||||
// Registration is not standardized within Solid yet, so we use a custom versioned API for now
|
||||
const API_VERSION = '0.2';
|
||||
|
||||
export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
|
||||
requestParser: RequestParser;
|
||||
errorHandler: ErrorHandler;
|
||||
responseWriter: ResponseWriter;
|
||||
export interface IdentityProviderHttpHandlerArgs {
|
||||
/**
|
||||
* Base URL of the server.
|
||||
*/
|
||||
@@ -54,6 +47,10 @@ export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
* Used for POST requests that need to be handled by the OIDC library.
|
||||
*/
|
||||
interactionCompleter: InteractionCompleter;
|
||||
/**
|
||||
* Used for converting output errors.
|
||||
*/
|
||||
errorHandler: ErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +65,7 @@ export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
* This handler handles all requests since it assumes all those requests are relevant for the IDP interaction.
|
||||
* A {@link RouterHandler} should be used to filter out other requests.
|
||||
*/
|
||||
export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
export class IdentityProviderHttpHandler extends OperationHttpHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly baseUrl: string;
|
||||
@@ -76,19 +73,21 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
private readonly interactionRoutes: InteractionRoute[];
|
||||
private readonly converter: RepresentationConverter;
|
||||
private readonly interactionCompleter: InteractionCompleter;
|
||||
private readonly errorHandler: ErrorHandler;
|
||||
|
||||
private readonly controls: Record<string, string>;
|
||||
|
||||
public constructor(args: IdentityProviderHttpHandlerArgs) {
|
||||
// It is important that the RequestParser does not read out the Request body stream.
|
||||
// Otherwise we can't pass it anymore to the OIDC library when needed.
|
||||
super(args);
|
||||
super();
|
||||
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off
|
||||
this.baseUrl = trimTrailingSlashes(joinUrl(args.baseUrl, args.idpPath));
|
||||
this.providerFactory = args.providerFactory;
|
||||
this.interactionRoutes = args.interactionRoutes;
|
||||
this.converter = args.converter;
|
||||
this.interactionCompleter = args.interactionCompleter;
|
||||
this.errorHandler = args.errorHandler;
|
||||
|
||||
this.controls = Object.assign(
|
||||
{},
|
||||
@@ -99,7 +98,7 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
/**
|
||||
* Finds the matching route and resolves the operation.
|
||||
*/
|
||||
protected async handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse):
|
||||
public async handle({ operation, request, response }: OperationHttpHandlerInput):
|
||||
Promise<ResponseDescription | undefined> {
|
||||
// This being defined means we're in an OIDC session
|
||||
let oidcInteraction: Interaction | undefined;
|
||||
|
||||
@@ -168,7 +168,6 @@ export * from './ldp/representation/RepresentationPreferences';
|
||||
export * from './ldp/representation/ResourceIdentifier';
|
||||
|
||||
// LDP
|
||||
export * from './ldp/AuthenticatedLdpHandler';
|
||||
export * from './ldp/UnsecureWebSocketsProtocol';
|
||||
|
||||
// Logging
|
||||
@@ -211,14 +210,16 @@ export * from './pods/GeneratedPodManager';
|
||||
export * from './pods/PodManager';
|
||||
|
||||
// Server
|
||||
export * from './server/BaseHttpHandler';
|
||||
export * from './server/AuthorizingHttpHandler';
|
||||
export * from './server/BaseHttpServerFactory';
|
||||
export * from './server/HttpHandler';
|
||||
export * from './server/HttpRequest';
|
||||
export * from './server/HttpResponse';
|
||||
export * from './server/HttpServerFactory';
|
||||
export * from './server/WebSocketServerFactory';
|
||||
export * from './server/OperationHttpHandler';
|
||||
export * from './server/ParsingHttpHandler';
|
||||
export * from './server/WebSocketHandler';
|
||||
export * from './server/WebSocketServerFactory';
|
||||
|
||||
// Server/Middleware
|
||||
export * from './server/middleware/CorsHandler';
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { RegistrationParams,
|
||||
RegistrationManager } from '../../identity/interaction/email-password/util/RegistrationManager';
|
||||
import type { ErrorHandler } from '../../ldp/http/ErrorHandler';
|
||||
import type { RequestParser } from '../../ldp/http/RequestParser';
|
||||
import { ResponseDescription } from '../../ldp/http/response/ResponseDescription';
|
||||
import type { ResponseWriter } from '../../ldp/http/ResponseWriter';
|
||||
import type { Operation } from '../../ldp/operations/Operation';
|
||||
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { BaseHttpHandlerArgs } from '../../server/BaseHttpHandler';
|
||||
import { BaseHttpHandler } from '../../server/BaseHttpHandler';
|
||||
import type { OperationHttpHandlerInput } from '../../server/OperationHttpHandler';
|
||||
import { OperationHttpHandler } from '../../server/OperationHttpHandler';
|
||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||
import { APPLICATION_JSON, TEXT_HTML } from '../../util/ContentTypes';
|
||||
@@ -38,12 +36,7 @@ export interface SetupInput extends Record<string, any>{
|
||||
registration?: boolean;
|
||||
}
|
||||
|
||||
export interface SetupHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
// BaseHttpHandler args
|
||||
requestParser: RequestParser;
|
||||
errorHandler: ErrorHandler;
|
||||
responseWriter: ResponseWriter;
|
||||
|
||||
export interface SetupHttpHandlerArgs {
|
||||
/**
|
||||
* Used for registering a pod during setup.
|
||||
*/
|
||||
@@ -73,6 +66,10 @@ export interface SetupHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
* Template to show when setup was completed successfully.
|
||||
*/
|
||||
responseTemplate: string;
|
||||
/**
|
||||
* Used for converting output errors.
|
||||
*/
|
||||
errorHandler: ErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +82,7 @@ export interface SetupHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
* After successfully completing a POST request this handler will disable itself and become unreachable.
|
||||
* All other methods will be rejected.
|
||||
*/
|
||||
export class SetupHttpHandler extends BaseHttpHandler {
|
||||
export class SetupHttpHandler extends OperationHttpHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly registrationManager?: RegistrationManager;
|
||||
@@ -95,11 +92,12 @@ export class SetupHttpHandler extends BaseHttpHandler {
|
||||
private readonly storage: KeyValueStorage<string, boolean>;
|
||||
private readonly viewTemplate: string;
|
||||
private readonly responseTemplate: string;
|
||||
private readonly errorHandler: ErrorHandler;
|
||||
|
||||
private finished: boolean;
|
||||
|
||||
public constructor(args: SetupHttpHandlerArgs) {
|
||||
super(args);
|
||||
super();
|
||||
this.finished = false;
|
||||
|
||||
this.registrationManager = args.registrationManager;
|
||||
@@ -109,9 +107,10 @@ export class SetupHttpHandler extends BaseHttpHandler {
|
||||
this.storage = args.storage;
|
||||
this.viewTemplate = args.viewTemplate;
|
||||
this.responseTemplate = args.responseTemplate;
|
||||
this.errorHandler = args.errorHandler;
|
||||
}
|
||||
|
||||
public async handleOperation(operation: Operation): Promise<ResponseDescription> {
|
||||
public async handle({ operation }: OperationHttpHandlerInput): Promise<ResponseDescription> {
|
||||
let json: Record<string, any>;
|
||||
let template: string;
|
||||
let success = false;
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import type { CredentialSet } from '../authentication/Credentials';
|
||||
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
|
||||
import type { Authorizer } from '../authorization/Authorizer';
|
||||
import type { PermissionReader } from '../authorization/PermissionReader';
|
||||
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
||||
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||
import type { HttpRequest } from '../server/HttpRequest';
|
||||
import type { ErrorHandler } from './http/ErrorHandler';
|
||||
import type { RequestParser } from './http/RequestParser';
|
||||
import type { ResponseDescription } from './http/response/ResponseDescription';
|
||||
import type { ResponseWriter } from './http/ResponseWriter';
|
||||
import type { OperationMetadataCollector } from './operations/metadata/OperationMetadataCollector';
|
||||
import type { Operation } from './operations/Operation';
|
||||
import type { OperationHandler } from './operations/OperationHandler';
|
||||
import type { ModesExtractor } from './permissions/ModesExtractor';
|
||||
|
||||
export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
|
||||
requestParser: RequestParser;
|
||||
errorHandler: ErrorHandler;
|
||||
responseWriter: ResponseWriter;
|
||||
/**
|
||||
* Extracts the credentials from the incoming request.
|
||||
*/
|
||||
credentialsExtractor: CredentialsExtractor;
|
||||
/**
|
||||
* Extracts the required modes from the generated Operation.
|
||||
*/
|
||||
modesExtractor: ModesExtractor;
|
||||
/**
|
||||
* Reads the permissions available for the Operation.
|
||||
*/
|
||||
permissionReader: PermissionReader;
|
||||
/**
|
||||
* Verifies if the requested operation is allowed.
|
||||
*/
|
||||
authorizer: Authorizer;
|
||||
/**
|
||||
* Executed the operation.
|
||||
*/
|
||||
operationHandler: OperationHandler;
|
||||
/**
|
||||
* Generates generic operation metadata that is required for a response.
|
||||
*/
|
||||
operationMetadataCollector: OperationMetadataCollector;
|
||||
}
|
||||
|
||||
/**
|
||||
* The central manager that connects all the necessary handlers to go from an incoming request to an executed operation.
|
||||
*/
|
||||
export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
||||
private readonly credentialsExtractor: CredentialsExtractor;
|
||||
private readonly modesExtractor: ModesExtractor;
|
||||
private readonly permissionReader: PermissionReader;
|
||||
private readonly authorizer: Authorizer;
|
||||
private readonly operationHandler: OperationHandler;
|
||||
private readonly operationMetadataCollector: OperationMetadataCollector;
|
||||
|
||||
/**
|
||||
* Creates the handler.
|
||||
* @param args - The handlers required. None of them are optional.
|
||||
*/
|
||||
public constructor(args: AuthenticatedLdpHandlerArgs) {
|
||||
super(args);
|
||||
this.credentialsExtractor = args.credentialsExtractor;
|
||||
this.modesExtractor = args.modesExtractor;
|
||||
this.permissionReader = args.permissionReader;
|
||||
this.authorizer = args.authorizer;
|
||||
this.operationHandler = args.operationHandler;
|
||||
this.operationMetadataCollector = args.operationMetadataCollector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the incoming request can be handled. The check is very non-restrictive and will usually be true.
|
||||
* It is based on whether the incoming request can be parsed to an operation.
|
||||
* @param input - Incoming request and response. Only the request will be used.
|
||||
*
|
||||
* @returns A promise resolving if this request can be handled, otherwise rejecting with an Error.
|
||||
*/
|
||||
public async canHandle(input: HttpHandlerInput): Promise<void> {
|
||||
return this.requestParser.canHandle(input.request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the incoming operation and generates a response.
|
||||
* This includes the following steps:
|
||||
* - Extracting credentials from the request.
|
||||
* - Extracting the required permissions.
|
||||
* - Validating if this operation is allowed.
|
||||
* - Executing the operation.
|
||||
*/
|
||||
protected async handleOperation(operation: Operation, request: HttpRequest): Promise<ResponseDescription> {
|
||||
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
|
||||
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
|
||||
|
||||
const modes = await this.modesExtractor.handleSafe(operation);
|
||||
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
|
||||
|
||||
const permissionSet = await this.permissionReader.handleSafe({ credentials, identifier: operation.target });
|
||||
this.logger.verbose(`Available permissions are ${JSON.stringify(permissionSet)}`);
|
||||
|
||||
try {
|
||||
await this.authorizer.handleSafe({ credentials, identifier: operation.target, modes, permissionSet });
|
||||
operation.permissionSet = permissionSet;
|
||||
} catch (error: unknown) {
|
||||
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Authorization succeeded, performing operation`);
|
||||
const response = await this.operationHandler.handleSafe(operation);
|
||||
|
||||
if (response.metadata) {
|
||||
await this.operationMetadataCollector.handleSafe({ operation, metadata: response.metadata });
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { ResourceStore } from '../../storage/ResourceStore';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||
import type { Operation } from './Operation';
|
||||
import type { OperationHandlerInput } from './OperationHandler';
|
||||
import { OperationHandler } from './OperationHandler';
|
||||
|
||||
/**
|
||||
@@ -17,14 +17,14 @@ export class DeleteOperationHandler extends OperationHandler {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'DELETE') {
|
||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||
if (operation.method !== 'DELETE') {
|
||||
throw new NotImplementedHttpError('This handler only supports DELETE operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
await this.store.deleteResource(input.target, input.conditions);
|
||||
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||
await this.store.deleteResource(operation.target, operation.conditions);
|
||||
return new ResetResponseDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ResourceStore } from '../../storage/ResourceStore';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { OkResponseDescription } from '../http/response/OkResponseDescription';
|
||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||
import type { Operation } from './Operation';
|
||||
import type { OperationHandlerInput } from './OperationHandler';
|
||||
import { OperationHandler } from './OperationHandler';
|
||||
|
||||
/**
|
||||
@@ -17,14 +17,14 @@ export class GetOperationHandler extends OperationHandler {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'GET') {
|
||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||
if (operation.method !== 'GET') {
|
||||
throw new NotImplementedHttpError('This handler only supports GET operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
|
||||
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||
const body = await this.store.getRepresentation(operation.target, operation.preferences, operation.conditions);
|
||||
|
||||
return new OkResponseDescription(body.metadata, body.data);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ResourceStore } from '../../storage/ResourceStore';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { OkResponseDescription } from '../http/response/OkResponseDescription';
|
||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||
import type { Operation } from './Operation';
|
||||
import type { OperationHandlerInput } from './OperationHandler';
|
||||
import { OperationHandler } from './OperationHandler';
|
||||
|
||||
/**
|
||||
@@ -17,14 +17,14 @@ export class HeadOperationHandler extends OperationHandler {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'HEAD') {
|
||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||
if (operation.method !== 'HEAD') {
|
||||
throw new NotImplementedHttpError('This handler only supports HEAD operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
|
||||
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||
const body = await this.store.getRepresentation(operation.target, operation.preferences, operation.conditions);
|
||||
|
||||
// Close the Readable as we will not return it.
|
||||
body.data.destroy();
|
||||
|
||||
@@ -2,7 +2,11 @@ import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||
import type { Operation } from './Operation';
|
||||
|
||||
export interface OperationHandlerInput {
|
||||
operation: Operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for a specific operation type.
|
||||
*/
|
||||
export abstract class OperationHandler extends AsyncHandler<Operation, ResponseDescription> {}
|
||||
export abstract class OperationHandler extends AsyncHandler<OperationHandlerInput, ResponseDescription> {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpErr
|
||||
import type { Patch } from '../http/Patch';
|
||||
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||
import type { Operation } from './Operation';
|
||||
import type { OperationHandlerInput } from './OperationHandler';
|
||||
import { OperationHandler } from './OperationHandler';
|
||||
|
||||
/**
|
||||
@@ -22,21 +22,21 @@ export class PatchOperationHandler extends OperationHandler {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'PATCH') {
|
||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||
if (operation.method !== 'PATCH') {
|
||||
throw new NotImplementedHttpError('This handler only supports PATCH operations.');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
||||
// without the Content-Type header with a status code of 400."
|
||||
// https://solid.github.io/specification/protocol#http-server
|
||||
if (!input.body?.metadata.contentType) {
|
||||
if (!operation.body?.metadata.contentType) {
|
||||
this.logger.warn('No Content-Type header specified on PATCH request');
|
||||
throw new BadRequestHttpError('No Content-Type header specified on PATCH request');
|
||||
}
|
||||
await this.store.modifyResource(input.target, input.body as Patch, input.conditions);
|
||||
await this.store.modifyResource(operation.target, operation.body as Patch, operation.conditions);
|
||||
return new ResetResponseDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { CreatedResponseDescription } from '../http/response/CreatedResponseDescription';
|
||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||
import type { Operation } from './Operation';
|
||||
import type { OperationHandlerInput } from './OperationHandler';
|
||||
import { OperationHandler } from './OperationHandler';
|
||||
|
||||
/**
|
||||
@@ -21,21 +21,21 @@ export class PostOperationHandler extends OperationHandler {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'POST') {
|
||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||
if (operation.method !== 'POST') {
|
||||
throw new NotImplementedHttpError('This handler only supports POST operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
||||
// without the Content-Type header with a status code of 400."
|
||||
// https://solid.github.io/specification/protocol#http-server
|
||||
if (!input.body?.metadata.contentType) {
|
||||
if (!operation.body?.metadata.contentType) {
|
||||
this.logger.warn('No Content-Type header specified on POST request');
|
||||
throw new BadRequestHttpError('No Content-Type header specified on POST request');
|
||||
}
|
||||
const identifier = await this.store.addResource(input.target, input.body, input.conditions);
|
||||
const identifier = await this.store.addResource(operation.target, operation.body, operation.conditions);
|
||||
return new CreatedResponseDescription(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||
import type { Operation } from './Operation';
|
||||
import type { OperationHandlerInput } from './OperationHandler';
|
||||
import { OperationHandler } from './OperationHandler';
|
||||
|
||||
/**
|
||||
@@ -21,21 +21,21 @@ export class PutOperationHandler extends OperationHandler {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'PUT') {
|
||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||
if (operation.method !== 'PUT') {
|
||||
throw new NotImplementedHttpError('This handler only supports PUT operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
||||
// without the Content-Type header with a status code of 400."
|
||||
// https://solid.github.io/specification/protocol#http-server
|
||||
if (!input.body?.metadata.contentType) {
|
||||
if (!operation.body?.metadata.contentType) {
|
||||
this.logger.warn('No Content-Type header specified on PUT request');
|
||||
throw new BadRequestHttpError('No Content-Type header specified on PUT request');
|
||||
}
|
||||
await this.store.setRepresentation(input.target, input.body, input.conditions);
|
||||
await this.store.setRepresentation(operation.target, operation.body, operation.conditions);
|
||||
return new ResetResponseDescription();
|
||||
}
|
||||
}
|
||||
|
||||
84
src/server/AuthorizingHttpHandler.ts
Normal file
84
src/server/AuthorizingHttpHandler.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { CredentialSet } from '../authentication/Credentials';
|
||||
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
|
||||
import type { Authorizer } from '../authorization/Authorizer';
|
||||
import type { PermissionReader } from '../authorization/PermissionReader';
|
||||
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
||||
import type { ModesExtractor } from '../ldp/permissions/ModesExtractor';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { OperationHttpHandlerInput } from './OperationHttpHandler';
|
||||
import { OperationHttpHandler } from './OperationHttpHandler';
|
||||
|
||||
export interface AuthorizingHttpHandlerArgs {
|
||||
/**
|
||||
* Extracts the credentials from the incoming request.
|
||||
*/
|
||||
credentialsExtractor: CredentialsExtractor;
|
||||
/**
|
||||
* Extracts the required modes from the generated Operation.
|
||||
*/
|
||||
modesExtractor: ModesExtractor;
|
||||
/**
|
||||
* Reads the permissions available for the Operation.
|
||||
*/
|
||||
permissionReader: PermissionReader;
|
||||
/**
|
||||
* Verifies if the requested operation is allowed.
|
||||
*/
|
||||
authorizer: Authorizer;
|
||||
/**
|
||||
* Handler to call if the operation is authorized.
|
||||
*/
|
||||
operationHandler: OperationHttpHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all the necessary steps for an authorization.
|
||||
* Errors if authorization fails, otherwise passes the parameter to the operationHandler handler.
|
||||
* The following steps are executed:
|
||||
* - Extracting credentials from the request.
|
||||
* - Extracting the required permissions.
|
||||
* - Reading the allowed permissions for the credentials.
|
||||
* - Validating if this operation is allowed.
|
||||
*/
|
||||
export class AuthorizingHttpHandler extends OperationHttpHandler {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly credentialsExtractor: CredentialsExtractor;
|
||||
private readonly modesExtractor: ModesExtractor;
|
||||
private readonly permissionReader: PermissionReader;
|
||||
private readonly authorizer: Authorizer;
|
||||
private readonly operationHandler: OperationHttpHandler;
|
||||
|
||||
public constructor(args: AuthorizingHttpHandlerArgs) {
|
||||
super();
|
||||
this.credentialsExtractor = args.credentialsExtractor;
|
||||
this.modesExtractor = args.modesExtractor;
|
||||
this.permissionReader = args.permissionReader;
|
||||
this.authorizer = args.authorizer;
|
||||
this.operationHandler = args.operationHandler;
|
||||
}
|
||||
|
||||
public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription | undefined> {
|
||||
const { request, operation } = input;
|
||||
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
|
||||
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
|
||||
|
||||
const modes = await this.modesExtractor.handleSafe(operation);
|
||||
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
|
||||
|
||||
const permissionSet = await this.permissionReader.handleSafe({ credentials, identifier: operation.target });
|
||||
this.logger.verbose(`Available permissions are ${JSON.stringify(permissionSet)}`);
|
||||
|
||||
try {
|
||||
await this.authorizer.handleSafe({ credentials, identifier: operation.target, modes, permissionSet });
|
||||
operation.permissionSet = permissionSet;
|
||||
} catch (error: unknown) {
|
||||
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Authorization succeeded, calling source handler`);
|
||||
|
||||
return this.operationHandler.handleSafe(input);
|
||||
}
|
||||
}
|
||||
16
src/server/OperationHttpHandler.ts
Normal file
16
src/server/OperationHttpHandler.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
||||
import type { Operation } from '../ldp/operations/Operation';
|
||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||
import type { HttpHandlerInput } from './HttpHandler';
|
||||
|
||||
export interface OperationHttpHandlerInput extends HttpHandlerInput {
|
||||
operation: Operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* An HTTP handler that makes use of an already parsed Operation.
|
||||
* Can either return a ResponseDescription to be resolved by the calling class,
|
||||
* or undefined if this class handles the response itself.
|
||||
*/
|
||||
export abstract class OperationHttpHandler
|
||||
extends AsyncHandler<OperationHttpHandlerInput, ResponseDescription | undefined> {}
|
||||
@@ -2,20 +2,23 @@ import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||
import type { RequestParser } from '../ldp/http/RequestParser';
|
||||
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
||||
import type { Operation } from '../ldp/operations/Operation';
|
||||
import type { OperationMetadataCollector } from '../ldp/operations/metadata/OperationMetadataCollector';
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { assertError } from '../util/errors/ErrorUtil';
|
||||
import type { HttpHandlerInput } from './HttpHandler';
|
||||
import { HttpHandler } from './HttpHandler';
|
||||
import type { HttpRequest } from './HttpRequest';
|
||||
import type { HttpResponse } from './HttpResponse';
|
||||
import type { OperationHttpHandler } from './OperationHttpHandler';
|
||||
|
||||
export interface BaseHttpHandlerArgs {
|
||||
export interface ParsingHttpHandlerArgs {
|
||||
/**
|
||||
* Parses the incoming requests.
|
||||
*/
|
||||
requestParser: RequestParser;
|
||||
/**
|
||||
* Generates generic operation metadata that is required for a response.
|
||||
*/
|
||||
metadataCollector: OperationMetadataCollector;
|
||||
/**
|
||||
* Converts errors to a serializable format.
|
||||
*/
|
||||
@@ -24,25 +27,33 @@ export interface BaseHttpHandlerArgs {
|
||||
* Writes out the response of the operation.
|
||||
*/
|
||||
responseWriter: ResponseWriter;
|
||||
/**
|
||||
* Handler to send the operation to.
|
||||
*/
|
||||
operationHandler: OperationHttpHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses requests and sends the resulting Operation to the abstract `handleOperation` function.
|
||||
* Parses requests and sends the resulting Operation to wrapped operationHandler.
|
||||
* Errors are caught and handled by the Errorhandler.
|
||||
* In case the `handleOperation` function returns a result it will be sent to the ResponseWriter.
|
||||
* In case the operationHandler returns a result it will be sent to the ResponseWriter.
|
||||
*/
|
||||
export abstract class BaseHttpHandler extends HttpHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
export class ParsingHttpHandler extends HttpHandler {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
protected readonly requestParser: RequestParser;
|
||||
protected readonly errorHandler: ErrorHandler;
|
||||
protected readonly responseWriter: ResponseWriter;
|
||||
private readonly requestParser: RequestParser;
|
||||
private readonly errorHandler: ErrorHandler;
|
||||
private readonly responseWriter: ResponseWriter;
|
||||
private readonly metadataCollector: OperationMetadataCollector;
|
||||
private readonly operationHandler: OperationHttpHandler;
|
||||
|
||||
protected constructor(args: BaseHttpHandlerArgs) {
|
||||
public constructor(args: ParsingHttpHandlerArgs) {
|
||||
super();
|
||||
this.requestParser = args.requestParser;
|
||||
this.errorHandler = args.errorHandler;
|
||||
this.responseWriter = args.responseWriter;
|
||||
this.metadataCollector = args.metadataCollector;
|
||||
this.operationHandler = args.operationHandler;
|
||||
}
|
||||
|
||||
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||
@@ -52,7 +63,12 @@ export abstract class BaseHttpHandler extends HttpHandler {
|
||||
try {
|
||||
const operation = await this.requestParser.handleSafe(request);
|
||||
({ preferences } = operation);
|
||||
result = await this.handleOperation(operation, request, response);
|
||||
result = await this.operationHandler.handleSafe({ operation, request, response });
|
||||
|
||||
if (result?.metadata) {
|
||||
await this.metadataCollector.handleSafe({ operation, metadata: result.metadata });
|
||||
}
|
||||
|
||||
this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
|
||||
} catch (error: unknown) {
|
||||
assertError(error);
|
||||
@@ -63,10 +79,4 @@ export abstract class BaseHttpHandler extends HttpHandler {
|
||||
await this.responseWriter.handleSafe({ response, result });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the operation. Should return a ResponseDescription if it does not handle the response itself.
|
||||
*/
|
||||
protected abstract handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse):
|
||||
Promise<ResponseDescription | undefined>;
|
||||
}
|
||||
Reference in New Issue
Block a user