mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Generalize HTTP handler behaviour in BaseHttpHandler
This commit is contained in:
parent
4f1a86dfa0
commit
20197ea1de
@ -18,18 +18,18 @@
|
|||||||
{
|
{
|
||||||
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||||
"@type": "IdentityProviderHttpHandler",
|
"@type": "IdentityProviderHttpHandler",
|
||||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"idpPath": "/idp",
|
"args_idpPath": "/idp",
|
||||||
"requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
||||||
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
"args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
||||||
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
||||||
"interactionCompleter": {
|
"args_interactionCompleter": {
|
||||||
"comment": "Responsible for finishing OIDC interactions.",
|
"comment": "Responsible for finishing OIDC interactions.",
|
||||||
"@type": "InteractionCompleter",
|
"@type": "InteractionCompleter",
|
||||||
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
|
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
|
||||||
},
|
},
|
||||||
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||||
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||||
"IdentityProviderHttpHandler:_interactionRoutes": [
|
"IdentityProviderHttpHandler:_args_interactionRoutes": [
|
||||||
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" },
|
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" },
|
||||||
{ "@id": "urn:solid-server:auth:password:LoginRoute" },
|
{ "@id": "urn:solid-server:auth:password:LoginRoute" },
|
||||||
{ "@id": "urn:solid-server:auth:password:ResetPasswordRoute" },
|
{ "@id": "urn:solid-server:auth:password:ResetPasswordRoute" },
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{
|
{
|
||||||
"comment": "Enable registration by adding a registration handler to the list of interaction routes.",
|
"comment": "Enable registration by adding a registration handler to the list of interaction routes.",
|
||||||
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||||
"IdentityProviderHttpHandler:_interactionRoutes": [
|
"IdentityProviderHttpHandler:_args_interactionRoutes": [
|
||||||
{ "@id": "urn:solid-server:auth:password:RegistrationRoute" }
|
{ "@id": "urn:solid-server:auth:password:RegistrationRoute" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,15 @@ import type { ResponseDescription } from '../ldp/http/response/ResponseDescripti
|
|||||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
||||||
import type { Operation } from '../ldp/operations/Operation';
|
import type { Operation } from '../ldp/operations/Operation';
|
||||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
||||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
||||||
import { HttpHandler } from '../server/HttpHandler';
|
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
||||||
import type { HttpRequest } from '../server/HttpRequest';
|
import type { HttpRequest } from '../server/HttpRequest';
|
||||||
import type { HttpResponse } from '../server/HttpResponse';
|
import type { HttpResponse } from '../server/HttpResponse';
|
||||||
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
|
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
|
||||||
import { APPLICATION_JSON } from '../util/ContentTypes';
|
import { APPLICATION_JSON } from '../util/ContentTypes';
|
||||||
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
|
||||||
import { assertError, createErrorMessage } from '../util/errors/ErrorUtil';
|
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||||
import { InternalServerError } from '../util/errors/InternalServerError';
|
import { InternalServerError } from '../util/errors/InternalServerError';
|
||||||
import { joinUrl, trimTrailingSlashes } from '../util/PathUtil';
|
import { joinUrl, trimTrailingSlashes } from '../util/PathUtil';
|
||||||
import { addTemplateMetadata } from '../util/ResourceUtil';
|
import { addTemplateMetadata } from '../util/ResourceUtil';
|
||||||
@ -64,6 +63,37 @@ export class InteractionRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||||
|
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
|
||||||
|
requestParser: RequestParser;
|
||||||
|
errorHandler: ErrorHandler;
|
||||||
|
responseWriter: ResponseWriter;
|
||||||
|
/**
|
||||||
|
* Base URL of the server.
|
||||||
|
*/
|
||||||
|
baseUrl: string;
|
||||||
|
/**
|
||||||
|
* Relative path of the IDP entry point.
|
||||||
|
*/
|
||||||
|
idpPath: string;
|
||||||
|
/**
|
||||||
|
* Used to generate the OIDC provider.
|
||||||
|
*/
|
||||||
|
providerFactory: ProviderFactory;
|
||||||
|
/**
|
||||||
|
* All routes handling the custom IDP behaviour.
|
||||||
|
*/
|
||||||
|
interactionRoutes: InteractionRoute[];
|
||||||
|
/**
|
||||||
|
* Used for content negotiation.
|
||||||
|
*/
|
||||||
|
converter: RepresentationConverter;
|
||||||
|
/**
|
||||||
|
* Used for POST requests that need to be handled by the OIDC library.
|
||||||
|
*/
|
||||||
|
interactionCompleter: InteractionCompleter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all requests relevant for the entire IDP interaction,
|
* Handles all requests relevant for the entire IDP interaction,
|
||||||
* by sending them to either a matching {@link InteractionRoute},
|
* by sending them to either a matching {@link InteractionRoute},
|
||||||
@ -76,71 +106,32 @@ export class InteractionRoute {
|
|||||||
* This handler handles all requests since it assumes all those requests are relevant for the IDP interaction.
|
* 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.
|
* A {@link RouterHandler} should be used to filter out other requests.
|
||||||
*/
|
*/
|
||||||
export class IdentityProviderHttpHandler extends HttpHandler {
|
export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
private readonly requestParser: RequestParser;
|
|
||||||
private readonly providerFactory: ProviderFactory;
|
private readonly providerFactory: ProviderFactory;
|
||||||
private readonly interactionRoutes: InteractionRoute[];
|
private readonly interactionRoutes: InteractionRoute[];
|
||||||
private readonly converter: RepresentationConverter;
|
private readonly converter: RepresentationConverter;
|
||||||
private readonly interactionCompleter: InteractionCompleter;
|
private readonly interactionCompleter: InteractionCompleter;
|
||||||
private readonly errorHandler: ErrorHandler;
|
|
||||||
private readonly responseWriter: ResponseWriter;
|
|
||||||
|
|
||||||
/**
|
public constructor(args: IdentityProviderHttpHandlerArgs) {
|
||||||
* @param baseUrl - Base URL of the server.
|
// It is important that the RequestParser does not read out the Request body stream.
|
||||||
* @param idpPath - Relative path of the IDP entry point.
|
// Otherwise we can't pass it anymore to the OIDC library when needed.
|
||||||
* @param requestParser - Used for parsing requests.
|
super(args);
|
||||||
* @param providerFactory - Used to generate the OIDC provider.
|
|
||||||
* @param interactionRoutes - All routes handling the custom IDP behaviour.
|
|
||||||
* @param converter - Used for content negotiation..
|
|
||||||
* @param interactionCompleter - Used for POST requests that need to be handled by the OIDC library.
|
|
||||||
* @param errorHandler - Converts errors to responses.
|
|
||||||
* @param responseWriter - Renders error responses.
|
|
||||||
*/
|
|
||||||
public constructor(
|
|
||||||
baseUrl: string,
|
|
||||||
idpPath: string,
|
|
||||||
requestParser: RequestParser,
|
|
||||||
providerFactory: ProviderFactory,
|
|
||||||
interactionRoutes: InteractionRoute[],
|
|
||||||
converter: RepresentationConverter,
|
|
||||||
interactionCompleter: InteractionCompleter,
|
|
||||||
errorHandler: ErrorHandler,
|
|
||||||
responseWriter: ResponseWriter,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off
|
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off
|
||||||
this.baseUrl = trimTrailingSlashes(joinUrl(baseUrl, idpPath));
|
this.baseUrl = trimTrailingSlashes(joinUrl(args.baseUrl, args.idpPath));
|
||||||
this.requestParser = requestParser;
|
this.providerFactory = args.providerFactory;
|
||||||
this.providerFactory = providerFactory;
|
this.interactionRoutes = args.interactionRoutes;
|
||||||
this.interactionRoutes = interactionRoutes;
|
this.converter = args.converter;
|
||||||
this.converter = converter;
|
this.interactionCompleter = args.interactionCompleter;
|
||||||
this.interactionCompleter = interactionCompleter;
|
|
||||||
this.errorHandler = errorHandler;
|
|
||||||
this.responseWriter = responseWriter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
|
||||||
let preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
|
||||||
try {
|
|
||||||
// It is important that this RequestParser does not read out the Request body stream.
|
|
||||||
// Otherwise we can't pass it anymore to the OIDC library when needed.
|
|
||||||
const operation = await this.requestParser.handleSafe(request);
|
|
||||||
({ preferences } = operation);
|
|
||||||
await this.handleOperation(operation, request, response);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
assertError(error);
|
|
||||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
|
||||||
await this.responseWriter.handleSafe({ response, result });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the matching route and resolves the operation.
|
* Finds the matching route and resolves the operation.
|
||||||
*/
|
*/
|
||||||
private async handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse): Promise<void> {
|
protected async handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse):
|
||||||
|
Promise<ResponseDescription | undefined> {
|
||||||
// This being defined means we're in an OIDC session
|
// This being defined means we're in an OIDC session
|
||||||
let oidcInteraction: Interaction | undefined;
|
let oidcInteraction: Interaction | undefined;
|
||||||
try {
|
try {
|
||||||
@ -155,7 +146,11 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
|||||||
if (!route) {
|
if (!route) {
|
||||||
const provider = await this.providerFactory.getProvider();
|
const provider = await this.providerFactory.getProvider();
|
||||||
this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
|
this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
|
||||||
return provider.callback(request, response);
|
// Even though the typings do not indicate this, this is a Promise that needs to be awaited.
|
||||||
|
// Otherwise the `BaseHttpServerFactory` will write a 404 before the OIDC library could handle the response.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||||
|
await provider.callback(request, response);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDP handlers expect JSON data
|
// IDP handlers expect JSON data
|
||||||
@ -169,9 +164,7 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { result, templateFiles } = await this.resolveRoute(operation, route, oidcInteraction);
|
const { result, templateFiles } = await this.resolveRoute(operation, route, oidcInteraction);
|
||||||
const responseDescription =
|
return this.handleInteractionResult(operation, request, result, templateFiles, oidcInteraction);
|
||||||
await this.handleInteractionResult(operation, request, result, templateFiles, oidcInteraction);
|
|
||||||
await this.responseWriter.handleSafe({ response, result: responseDescription });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,6 +197,7 @@ export * from './pods/GeneratedPodManager';
|
|||||||
export * from './pods/PodManager';
|
export * from './pods/PodManager';
|
||||||
|
|
||||||
// Server
|
// Server
|
||||||
|
export * from './server/BaseHttpHandler';
|
||||||
export * from './server/BaseHttpServerFactory';
|
export * from './server/BaseHttpServerFactory';
|
||||||
export * from './server/HttpHandler';
|
export * from './server/HttpHandler';
|
||||||
export * from './server/HttpRequest';
|
export * from './server/HttpRequest';
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import type { Credentials } from '../authentication/Credentials';
|
import type { Credentials } from '../authentication/Credentials';
|
||||||
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
|
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
|
||||||
import type { Authorizer } from '../authorization/Authorizer';
|
import type { Authorizer } from '../authorization/Authorizer';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
||||||
|
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
||||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||||
import { HttpHandler } from '../server/HttpHandler';
|
|
||||||
import type { HttpRequest } from '../server/HttpRequest';
|
import type { HttpRequest } from '../server/HttpRequest';
|
||||||
import type { HttpResponse } from '../server/HttpResponse';
|
|
||||||
import { assertError } from '../util/errors/ErrorUtil';
|
|
||||||
import type { ErrorHandler } from './http/ErrorHandler';
|
import type { ErrorHandler } from './http/ErrorHandler';
|
||||||
import type { RequestParser } from './http/RequestParser';
|
import type { RequestParser } from './http/RequestParser';
|
||||||
import type { ResponseDescription } from './http/response/ResponseDescription';
|
import type { ResponseDescription } from './http/response/ResponseDescription';
|
||||||
@ -15,16 +13,12 @@ import type { Operation } from './operations/Operation';
|
|||||||
import type { OperationHandler } from './operations/OperationHandler';
|
import type { OperationHandler } from './operations/OperationHandler';
|
||||||
import type { PermissionSet } from './permissions/PermissionSet';
|
import type { PermissionSet } from './permissions/PermissionSet';
|
||||||
import type { PermissionsExtractor } from './permissions/PermissionsExtractor';
|
import type { PermissionsExtractor } from './permissions/PermissionsExtractor';
|
||||||
import type { RepresentationPreferences } from './representation/RepresentationPreferences';
|
|
||||||
|
|
||||||
/**
|
export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
||||||
* Collection of handlers needed for {@link AuthenticatedLdpHandler} to function.
|
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
|
||||||
*/
|
|
||||||
export interface AuthenticatedLdpHandlerArgs {
|
|
||||||
/**
|
|
||||||
* Parses the incoming requests.
|
|
||||||
*/
|
|
||||||
requestParser: RequestParser;
|
requestParser: RequestParser;
|
||||||
|
errorHandler: ErrorHandler;
|
||||||
|
responseWriter: ResponseWriter;
|
||||||
/**
|
/**
|
||||||
* Extracts the credentials from the incoming request.
|
* Extracts the credentials from the incoming request.
|
||||||
*/
|
*/
|
||||||
@ -41,36 +35,27 @@ export interface AuthenticatedLdpHandlerArgs {
|
|||||||
* Executed the operation.
|
* Executed the operation.
|
||||||
*/
|
*/
|
||||||
operationHandler: OperationHandler;
|
operationHandler: OperationHandler;
|
||||||
/**
|
|
||||||
* Converts errors to a serializable format.
|
|
||||||
*/
|
|
||||||
errorHandler: ErrorHandler;
|
|
||||||
/**
|
|
||||||
* Writes out the response of the operation.
|
|
||||||
*/
|
|
||||||
responseWriter: ResponseWriter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The central manager that connects all the necessary handlers to go from an incoming request to an executed operation.
|
* The central manager that connects all the necessary handlers to go from an incoming request to an executed operation.
|
||||||
*/
|
*/
|
||||||
export class AuthenticatedLdpHandler extends HttpHandler {
|
export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
||||||
private readonly requestParser!: RequestParser;
|
private readonly credentialsExtractor: CredentialsExtractor;
|
||||||
private readonly credentialsExtractor!: CredentialsExtractor;
|
private readonly permissionsExtractor: PermissionsExtractor;
|
||||||
private readonly permissionsExtractor!: PermissionsExtractor;
|
private readonly authorizer: Authorizer;
|
||||||
private readonly authorizer!: Authorizer;
|
private readonly operationHandler: OperationHandler;
|
||||||
private readonly operationHandler!: OperationHandler;
|
|
||||||
private readonly errorHandler!: ErrorHandler;
|
|
||||||
private readonly responseWriter!: ResponseWriter;
|
|
||||||
private readonly logger = getLoggerFor(this);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the handler.
|
* Creates the handler.
|
||||||
* @param args - The handlers required. None of them are optional.
|
* @param args - The handlers required. None of them are optional.
|
||||||
*/
|
*/
|
||||||
public constructor(args: AuthenticatedLdpHandlerArgs) {
|
public constructor(args: AuthenticatedLdpHandlerArgs) {
|
||||||
super();
|
super(args);
|
||||||
Object.assign(this, args);
|
this.credentialsExtractor = args.credentialsExtractor;
|
||||||
|
this.permissionsExtractor = args.permissionsExtractor;
|
||||||
|
this.authorizer = args.authorizer;
|
||||||
|
this.operationHandler = args.operationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,61 +70,14 @@ export class AuthenticatedLdpHandler extends HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the incoming request and writes out the response.
|
* Handles the incoming operation and generates a response.
|
||||||
* This includes the following steps:
|
* This includes the following steps:
|
||||||
* - Parsing the request to an Operation.
|
|
||||||
* - Extracting credentials from the request.
|
* - Extracting credentials from the request.
|
||||||
* - Extracting the required permissions.
|
* - Extracting the required permissions.
|
||||||
* - Validating if this operation is allowed.
|
* - Validating if this operation is allowed.
|
||||||
* - Executing the operation.
|
* - Executing the operation.
|
||||||
* - Writing out the response.
|
|
||||||
* @param input - The incoming request and response object to write to.
|
|
||||||
*
|
|
||||||
* @returns A promise resolving when the handling is finished.
|
|
||||||
*/
|
*/
|
||||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
protected async handleOperation(operation: Operation, request: HttpRequest): Promise<ResponseDescription> {
|
||||||
let writeData: { response: HttpResponse; result: ResponseDescription };
|
|
||||||
|
|
||||||
try {
|
|
||||||
writeData = { response: input.response, result: await this.runHandlers(input.request) };
|
|
||||||
} catch (error: unknown) {
|
|
||||||
assertError(error);
|
|
||||||
// We don't know the preferences yet at this point
|
|
||||||
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
|
||||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
|
||||||
writeData = { response: input.response, result };
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.responseWriter.handleSafe(writeData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs all handlers except writing the output to the response.
|
|
||||||
* This because any errors thrown here have an impact on the response.
|
|
||||||
* @param request - Incoming request.
|
|
||||||
*
|
|
||||||
* @returns A promise resolving to the generated Operation.
|
|
||||||
*/
|
|
||||||
private async runHandlers(request: HttpRequest): Promise<ResponseDescription> {
|
|
||||||
this.logger.verbose(`Handling LDP request for ${request.url}`);
|
|
||||||
|
|
||||||
const operation: Operation = await this.requestParser.handleSafe(request);
|
|
||||||
this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.handleOperation(request, operation);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
assertError(error);
|
|
||||||
return await this.errorHandler.handleSafe({ error, preferences: operation.preferences });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the operation object.
|
|
||||||
* Runs all non-RequestParser handlers.
|
|
||||||
* This way the preferences can be used in case an error needs to be written.
|
|
||||||
*/
|
|
||||||
private async handleOperation(request: HttpRequest, operation: Operation): Promise<ResponseDescription> {
|
|
||||||
const credentials: Credentials = await this.credentialsExtractor.handleSafe(request);
|
const credentials: Credentials = await this.credentialsExtractor.handleSafe(request);
|
||||||
this.logger.verbose(`Extracted credentials: ${credentials.webId}`);
|
this.logger.verbose(`Extracted credentials: ${credentials.webId}`);
|
||||||
|
|
||||||
|
72
src/server/BaseHttpHandler.ts
Normal file
72
src/server/BaseHttpHandler.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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 { 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';
|
||||||
|
|
||||||
|
export interface BaseHttpHandlerArgs {
|
||||||
|
/**
|
||||||
|
* Parses the incoming requests.
|
||||||
|
*/
|
||||||
|
requestParser: RequestParser;
|
||||||
|
/**
|
||||||
|
* Converts errors to a serializable format.
|
||||||
|
*/
|
||||||
|
errorHandler: ErrorHandler;
|
||||||
|
/**
|
||||||
|
* Writes out the response of the operation.
|
||||||
|
*/
|
||||||
|
responseWriter: ResponseWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses requests and sends the resulting Operation to the abstract `handleOperation` function.
|
||||||
|
* Errors are caught and handled by the Errorhandler.
|
||||||
|
* In case the `handleOperation` function returns a result it will be sent to the ResponseWriter.
|
||||||
|
*/
|
||||||
|
export abstract class BaseHttpHandler extends HttpHandler {
|
||||||
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
|
protected readonly requestParser: RequestParser;
|
||||||
|
protected readonly errorHandler: ErrorHandler;
|
||||||
|
protected readonly responseWriter: ResponseWriter;
|
||||||
|
|
||||||
|
protected constructor(args: BaseHttpHandlerArgs) {
|
||||||
|
super();
|
||||||
|
this.requestParser = args.requestParser;
|
||||||
|
this.errorHandler = args.errorHandler;
|
||||||
|
this.responseWriter = args.responseWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||||
|
let result: ResponseDescription | undefined;
|
||||||
|
let preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const operation = await this.requestParser.handleSafe(request);
|
||||||
|
({ preferences } = operation);
|
||||||
|
result = await this.handleOperation(operation, request, response);
|
||||||
|
this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
assertError(error);
|
||||||
|
result = await this.errorHandler.handleSafe({ error, preferences });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
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>;
|
||||||
|
}
|
@ -1,6 +1,12 @@
|
|||||||
import type { Provider } from 'oidc-provider';
|
import type { Provider } from 'oidc-provider';
|
||||||
import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory';
|
import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory';
|
||||||
import { InteractionRoute, IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler';
|
import type {
|
||||||
|
IdentityProviderHttpHandlerArgs,
|
||||||
|
} from '../../../src/identity/IdentityProviderHttpHandler';
|
||||||
|
import {
|
||||||
|
InteractionRoute,
|
||||||
|
IdentityProviderHttpHandler,
|
||||||
|
} from '../../../src/identity/IdentityProviderHttpHandler';
|
||||||
import type { InteractionHandler } from '../../../src/identity/interaction/email-password/handler/InteractionHandler';
|
import type { InteractionHandler } from '../../../src/identity/interaction/email-password/handler/InteractionHandler';
|
||||||
import { IdpInteractionError } from '../../../src/identity/interaction/util/IdpInteractionError';
|
import { IdpInteractionError } from '../../../src/identity/interaction/util/IdpInteractionError';
|
||||||
import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter';
|
import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter';
|
||||||
@ -95,17 +101,18 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
|
|
||||||
responseWriter = { handleSafe: jest.fn() } as any;
|
responseWriter = { handleSafe: jest.fn() } as any;
|
||||||
|
|
||||||
handler = new IdentityProviderHttpHandler(
|
const args: IdentityProviderHttpHandlerArgs = {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
idpPath,
|
idpPath,
|
||||||
requestParser,
|
requestParser,
|
||||||
providerFactory,
|
providerFactory,
|
||||||
Object.values(routes),
|
interactionRoutes: Object.values(routes),
|
||||||
converter,
|
converter,
|
||||||
interactionCompleter,
|
interactionCompleter,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
responseWriter,
|
responseWriter,
|
||||||
);
|
};
|
||||||
|
handler = new IdentityProviderHttpHandler(args);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the provider if there is no matching route.', async(): Promise<void> => {
|
it('calls the provider if there is no matching route.', async(): Promise<void> => {
|
||||||
@ -284,17 +291,18 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('errors if no route is configured for the default prompt.', async(): Promise<void> => {
|
it('errors if no route is configured for the default prompt.', async(): Promise<void> => {
|
||||||
handler = new IdentityProviderHttpHandler(
|
const args: IdentityProviderHttpHandlerArgs = {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
idpPath,
|
idpPath,
|
||||||
requestParser,
|
requestParser,
|
||||||
providerFactory,
|
providerFactory,
|
||||||
[],
|
interactionRoutes: [],
|
||||||
converter,
|
converter,
|
||||||
interactionCompleter,
|
interactionCompleter,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
responseWriter,
|
responseWriter,
|
||||||
);
|
};
|
||||||
|
handler = new IdentityProviderHttpHandler(args);
|
||||||
request.url = '/idp';
|
request.url = '/idp';
|
||||||
provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any);
|
provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any);
|
||||||
const error = new InternalServerError('No handler for the default session prompt has been configured.');
|
const error = new InternalServerError('No handler for the default session prompt has been configured.');
|
||||||
|
69
test/unit/server/BaseHttpHandler.test.ts
Normal file
69
test/unit/server/BaseHttpHandler.test.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
|
||||||
|
import type { RequestParser } from '../../../src/ldp/http/RequestParser';
|
||||||
|
import { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||||
|
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
||||||
|
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||||
|
import type { BaseHttpHandlerArgs } from '../../../src/server/BaseHttpHandler';
|
||||||
|
import { BaseHttpHandler } from '../../../src/server/BaseHttpHandler';
|
||||||
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
|
|
||||||
|
class DummyHttpHandler extends BaseHttpHandler {
|
||||||
|
public constructor(args: BaseHttpHandlerArgs) {
|
||||||
|
super(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleOperation(): Promise<ResponseDescription | undefined> {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('A BaseHttpHandler', (): void => {
|
||||||
|
const request: HttpRequest = {} as any;
|
||||||
|
const response: HttpResponse = {} as any;
|
||||||
|
const preferences = { type: { 'text/html': 1 }};
|
||||||
|
const operation: Operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences };
|
||||||
|
const errorResponse = new ResponseDescription(400);
|
||||||
|
let requestParser: jest.Mocked<RequestParser>;
|
||||||
|
let errorHandler: jest.Mocked<ErrorHandler>;
|
||||||
|
let responseWriter: jest.Mocked<ResponseWriter>;
|
||||||
|
let handler: jest.Mocked<DummyHttpHandler>;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
requestParser = { handleSafe: jest.fn().mockResolvedValue(operation) } as any;
|
||||||
|
errorHandler = { handleSafe: jest.fn().mockResolvedValue(errorResponse) } as any;
|
||||||
|
responseWriter = { handleSafe: jest.fn() } as any;
|
||||||
|
|
||||||
|
handler = new DummyHttpHandler({ requestParser, errorHandler, responseWriter }) as any;
|
||||||
|
handler.handleOperation = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the handleOperation function with the generated operation.', async(): Promise<void> => {
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(handler.handleOperation).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response);
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the responseWriter if there is a response.', async(): Promise<void> => {
|
||||||
|
const result = new ResponseDescription(200);
|
||||||
|
handler.handleOperation.mockResolvedValueOnce(result);
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(handler.handleOperation).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response);
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the error handler if something goes wrong.', async(): Promise<void> => {
|
||||||
|
const error = new Error('bad data');
|
||||||
|
handler.handleOperation.mockRejectedValueOnce(error);
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user