mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/4.0.0
# Conflicts: # test/unit/util/errors/RedirectHttpError.test.ts
This commit is contained in:
@@ -260,6 +260,7 @@ export * from './server/HttpResponse';
|
||||
export * from './server/HttpServerFactory';
|
||||
export * from './server/OperationHttpHandler';
|
||||
export * from './server/ParsingHttpHandler';
|
||||
export * from './server/RedirectingHttpHandler';
|
||||
export * from './server/WebSocketHandler';
|
||||
export * from './server/WebSocketServerFactory';
|
||||
|
||||
|
||||
109
src/server/RedirectingHttpHandler.ts
Normal file
109
src/server/RedirectingHttpHandler.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { TargetExtractor } from '../http/input/identifier/TargetExtractor';
|
||||
import { RedirectResponseDescription } from '../http/output/response/RedirectResponseDescription';
|
||||
import type { ResponseWriter } from '../http/output/ResponseWriter';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { FoundHttpError } from '../util/errors/FoundHttpError';
|
||||
import { MovedPermanentlyHttpError } from '../util/errors/MovedPermanentlyHttpError';
|
||||
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
|
||||
import { PermanentRedirectHttpError } from '../util/errors/PermanentRedirectHttpError';
|
||||
import type { RedirectHttpError } from '../util/errors/RedirectHttpError';
|
||||
import { SeeOtherHttpError } from '../util/errors/SeeOtherHttpError';
|
||||
import { TemporaryRedirectHttpError } from '../util/errors/TemporaryRedirectHttpError';
|
||||
import { getRelativeUrl, joinUrl } from '../util/PathUtil';
|
||||
import type { HttpHandlerInput } from './HttpHandler';
|
||||
import { HttpHandler } from './HttpHandler';
|
||||
import type { HttpRequest } from './HttpRequest';
|
||||
|
||||
const redirectErrorFactories: Record<301 | 302 | 303 | 307 | 308, (location: string) => RedirectHttpError> = {
|
||||
301: (location: string): RedirectHttpError => new MovedPermanentlyHttpError(location),
|
||||
302: (location: string): RedirectHttpError => new FoundHttpError(location),
|
||||
303: (location: string): RedirectHttpError => new SeeOtherHttpError(location),
|
||||
307: (location: string): RedirectHttpError => new TemporaryRedirectHttpError(location),
|
||||
308: (location: string): RedirectHttpError => new PermanentRedirectHttpError(location),
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler that redirects paths matching given patterns
|
||||
* to their corresponding URL, substituting selected groups.
|
||||
*/
|
||||
export class RedirectingHttpHandler extends HttpHandler {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
private readonly redirects: {
|
||||
regex: RegExp;
|
||||
redirectPattern: string;
|
||||
}[];
|
||||
|
||||
/**
|
||||
* Creates a handler for the provided redirects.
|
||||
* @param redirects - A mapping between URL patterns.
|
||||
* @param targetExtractor - To extract the target from the request.
|
||||
* @param responseWriter - To write the redirect to the response.
|
||||
* @param statusCode - Desired 30x redirection code (defaults to 308).
|
||||
*/
|
||||
public constructor(
|
||||
redirects: Record<string, string>,
|
||||
private readonly baseUrl: string,
|
||||
private readonly targetExtractor: TargetExtractor,
|
||||
private readonly responseWriter: ResponseWriter,
|
||||
private readonly statusCode: 301 | 302 | 303 | 307 | 308 = 308,
|
||||
) {
|
||||
super();
|
||||
|
||||
// Create an array of (regexp, redirect) pairs
|
||||
this.redirects = Object.keys(redirects).map(
|
||||
(pattern): { regex: RegExp; redirectPattern: string } => ({
|
||||
regex: new RegExp(pattern, 'u'),
|
||||
redirectPattern: redirects[pattern],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async canHandle({ request }: HttpHandlerInput): Promise<void> {
|
||||
// Try to find redirect for target URL
|
||||
await this.findRedirect(request);
|
||||
}
|
||||
|
||||
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||
// Try to find redirect for target URL
|
||||
const redirect = await this.findRedirect(request);
|
||||
|
||||
// Send redirect response
|
||||
this.logger.info(`Redirecting ${request.url} to ${redirect}`);
|
||||
const result = new RedirectResponseDescription(redirectErrorFactories[this.statusCode](redirect));
|
||||
await this.responseWriter.handleSafe({ response, result });
|
||||
}
|
||||
|
||||
private async findRedirect(request: HttpRequest): Promise<string> {
|
||||
// Retrieve target relative to base URL
|
||||
const target = await getRelativeUrl(this.baseUrl, request, this.targetExtractor);
|
||||
|
||||
// Get groups and redirect of first matching pattern
|
||||
let result;
|
||||
for (const { regex, redirectPattern } of this.redirects) {
|
||||
const match = regex.exec(target);
|
||||
if (match) {
|
||||
result = { match, redirectPattern };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only return if a redirect is configured for the requested URL
|
||||
if (!result) {
|
||||
throw new NotImplementedHttpError(`No redirect configured for ${target}`);
|
||||
}
|
||||
|
||||
// Build redirect URL from regexp result
|
||||
const { match, redirectPattern } = result;
|
||||
const redirect = match.reduce(
|
||||
(prev, param, index): string => prev.replace(`$${index}`, param),
|
||||
redirectPattern,
|
||||
);
|
||||
|
||||
// Don't redirect if target is already correct
|
||||
if (redirect === target) {
|
||||
throw new NotImplementedHttpError('Target is already correct.');
|
||||
}
|
||||
|
||||
return /^(?:[a-z]+:)?\/\//ui.test(redirect) ? redirect : joinUrl(this.baseUrl, redirect);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ const BaseHttpError = generateRedirectHttpErrorClass(302, 'FoundHttpError');
|
||||
|
||||
/**
|
||||
* Error used for resources that have been moved temporarily.
|
||||
* Methods other than GET may or may not be changed to GET in subsequent requests.
|
||||
*/
|
||||
export class FoundHttpError extends BaseHttpError {
|
||||
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
||||
|
||||
@@ -6,6 +6,7 @@ const BaseHttpError = generateRedirectHttpErrorClass(301, 'MovedPermanentlyHttpE
|
||||
|
||||
/**
|
||||
* Error used for resources that have been moved permanently.
|
||||
* Methods other than GET may or may not be changed to GET in subsequent requests.
|
||||
*/
|
||||
export class MovedPermanentlyHttpError extends BaseHttpError {
|
||||
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
||||
|
||||
15
src/util/errors/PermanentRedirectHttpError.ts
Normal file
15
src/util/errors/PermanentRedirectHttpError.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { HttpErrorOptions } from './HttpError';
|
||||
import { generateRedirectHttpErrorClass } from './RedirectHttpError';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const BaseHttpError = generateRedirectHttpErrorClass(308, 'PermanentRedirectHttpError');
|
||||
|
||||
/**
|
||||
* Error used for resources that have been moved permanently.
|
||||
* Method and body should not be changed in subsequent requests.
|
||||
*/
|
||||
export class PermanentRedirectHttpError extends BaseHttpError {
|
||||
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
||||
super(location, message, options);
|
||||
}
|
||||
}
|
||||
16
src/util/errors/SeeOtherHttpError.ts
Normal file
16
src/util/errors/SeeOtherHttpError.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { HttpErrorOptions } from './HttpError';
|
||||
import { generateRedirectHttpErrorClass } from './RedirectHttpError';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const BaseHttpError = generateRedirectHttpErrorClass(303, 'SeeOtherHttpError');
|
||||
|
||||
/**
|
||||
* Error used to redirect not to the requested resource itself, but to another page,
|
||||
* for example a representation of a real-world object.
|
||||
* The method used to display this redirected page is always GET.
|
||||
*/
|
||||
export class SeeOtherHttpError extends BaseHttpError {
|
||||
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
||||
super(location, message, options);
|
||||
}
|
||||
}
|
||||
15
src/util/errors/TemporaryRedirectHttpError.ts
Normal file
15
src/util/errors/TemporaryRedirectHttpError.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { HttpErrorOptions } from './HttpError';
|
||||
import { generateRedirectHttpErrorClass } from './RedirectHttpError';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const BaseHttpError = generateRedirectHttpErrorClass(307, 'TemporaryRedirectHttpError');
|
||||
|
||||
/**
|
||||
* Error used for resources that have been moved temporarily.
|
||||
* Method and body should not be changed in subsequent requests.
|
||||
*/
|
||||
export class TemporaryRedirectHttpError extends BaseHttpError {
|
||||
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
||||
super(location, message, options);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user