mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Make the RouterHandler more robust
It now extracts paths based on the base URL and allows catching all methods.
This commit is contained in:
@@ -1,41 +1,57 @@
|
||||
import { parse } from 'url';
|
||||
import type { TargetExtractor } from '../../ldp/http/TargetExtractor';
|
||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
|
||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||
import { ensureTrailingSlash, getRelativeUrl } from '../../util/PathUtil';
|
||||
import type { HttpHandlerInput } from '../HttpHandler';
|
||||
import { HttpHandler } from '../HttpHandler';
|
||||
|
||||
export interface RouterHandlerArgs {
|
||||
baseUrl: string;
|
||||
targetExtractor: TargetExtractor;
|
||||
handler: HttpHandler;
|
||||
allowedMethods: string[];
|
||||
allowedPathNames: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An HttpHandler that checks if a given method and path are satisfied
|
||||
* and allows its handler to be executed if so.
|
||||
*
|
||||
* If `allowedMethods` contains '*' it will match all methods.
|
||||
*/
|
||||
export class RouterHandler extends HttpHandler {
|
||||
protected readonly handler: HttpHandler;
|
||||
protected readonly allowedMethods: string[];
|
||||
protected readonly allowedPathNamesRegEx: RegExp[];
|
||||
private readonly baseUrl: string;
|
||||
private readonly targetExtractor: TargetExtractor;
|
||||
private readonly handler: HttpHandler;
|
||||
private readonly allowedMethods: string[];
|
||||
private readonly allMethods: boolean;
|
||||
private readonly allowedPathNamesRegEx: RegExp[];
|
||||
|
||||
public constructor(handler: HttpHandler, allowedMethods: string[], allowedPathNames: string[]) {
|
||||
public constructor(args: RouterHandlerArgs) {
|
||||
super();
|
||||
this.handler = handler;
|
||||
this.allowedMethods = allowedMethods;
|
||||
this.allowedPathNamesRegEx = allowedPathNames.map((pn): RegExp => new RegExp(pn, 'u'));
|
||||
this.baseUrl = ensureTrailingSlash(args.baseUrl);
|
||||
this.targetExtractor = args.targetExtractor;
|
||||
this.handler = args.handler;
|
||||
this.allowedMethods = args.allowedMethods;
|
||||
this.allMethods = args.allowedMethods.includes('*');
|
||||
this.allowedPathNamesRegEx = args.allowedPathNames.map((pn): RegExp => new RegExp(pn, 'u'));
|
||||
}
|
||||
|
||||
public async canHandle(input: HttpHandlerInput): Promise<void> {
|
||||
if (!input.request.url) {
|
||||
throw new Error('Cannot handle request without a url');
|
||||
const { request } = input;
|
||||
if (!request.url) {
|
||||
throw new BadRequestHttpError('Cannot handle request without a url');
|
||||
}
|
||||
if (!input.request.method) {
|
||||
throw new Error('Cannot handle request without a method');
|
||||
if (!request.method) {
|
||||
throw new BadRequestHttpError('Cannot handle request without a method');
|
||||
}
|
||||
if (!this.allowedMethods.includes(input.request.method)) {
|
||||
throw new MethodNotAllowedHttpError(`${input.request.method} is not allowed.`);
|
||||
if (!this.allMethods && !this.allowedMethods.includes(request.method)) {
|
||||
throw new MethodNotAllowedHttpError(`${request.method} is not allowed.`);
|
||||
}
|
||||
const { pathname } = parse(input.request.url);
|
||||
if (!pathname) {
|
||||
throw new Error('Cannot handle request without pathname');
|
||||
}
|
||||
if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathname))) {
|
||||
throw new NotFoundHttpError(`Cannot handle route ${pathname}`);
|
||||
const pathName = await getRelativeUrl(this.baseUrl, request, this.targetExtractor);
|
||||
if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) {
|
||||
throw new NotFoundHttpError(`Cannot handle route ${pathName}`);
|
||||
}
|
||||
await this.handler.canHandle(input);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { posix, win32 } from 'path';
|
||||
import urljoin from 'url-join';
|
||||
import type { TargetExtractor } from '../ldp/http/TargetExtractor';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import type { HttpRequest } from '../server/HttpRequest';
|
||||
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
||||
|
||||
/**
|
||||
* Changes a potential Windows path into a POSIX path.
|
||||
@@ -150,6 +153,24 @@ export function extractScheme(url: string): { scheme: string; rest: string } {
|
||||
return { scheme: match[1], rest: match[2] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a relative URL by removing the base URL.
|
||||
* Will throw an error in case the resulting target is not withing the base URL scope.
|
||||
* @param baseUrl - Base URL.
|
||||
* @param request - Incoming request of which the target needs to be extracted.
|
||||
* @param targetExtractor - Will extract the target from the request.
|
||||
*/
|
||||
export async function getRelativeUrl(baseUrl: string, request: HttpRequest, targetExtractor: TargetExtractor):
|
||||
Promise<string> {
|
||||
baseUrl = ensureTrailingSlash(baseUrl);
|
||||
const target = await targetExtractor.handleSafe({ request });
|
||||
if (!target.path.startsWith(baseUrl)) {
|
||||
throw new BadRequestHttpError(`The identifier ${target.path} is outside the configured identifier space.`,
|
||||
{ errorCode: 'E0001', details: { path: target.path }});
|
||||
}
|
||||
return target.path.slice(baseUrl.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a regular expression that matches URLs containing the given baseUrl, or a subdomain of the given baseUrl.
|
||||
* In case there is a subdomain, the first match of the regular expression will be that subdomain.
|
||||
|
||||
Reference in New Issue
Block a user