mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add logging
* feat: Add logging * refactor: Configure the logger for the tests once globally * feat: Add logging * fix: Fix ESLint errors due to merge conflicts * Review log and error messages. * refactor: Cleanup a bit * refactor: Change to logger info calls Co-authored-by: Ruben Verborgh <ruben@verborgh.org>
This commit is contained in:
parent
1ef75126ee
commit
99464d9a95
@ -13,7 +13,7 @@ module.exports = {
|
||||
"js"
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"setupFilesAfterEnv": ["jest-rdf"],
|
||||
"setupFilesAfterEnv": ["jest-rdf", "<rootDir>/test/util/SetupTests.ts"],
|
||||
"collectCoverage": true,
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
|
@ -4,6 +4,7 @@ import type { Credentials } from '../authentication/Credentials';
|
||||
import type { PermissionSet } from '../ldp/permissions/PermissionSet';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { ContainerManager } from '../storage/ContainerManager';
|
||||
import type { ResourceStore } from '../storage/ResourceStore';
|
||||
import { INTERNAL_QUADS } from '../util/ContentTypes';
|
||||
@ -21,6 +22,8 @@ import { Authorizer } from './Authorizer';
|
||||
* Does not support `acl:agentGroup`, `acl:origin` and `acl:trustedApp` yet.
|
||||
*/
|
||||
export class WebAclAuthorizer extends Authorizer {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly aclManager: AclManager;
|
||||
private readonly containerManager: ContainerManager;
|
||||
private readonly resourceStore: ResourceStore;
|
||||
@ -66,7 +69,14 @@ export class WebAclAuthorizer extends Authorizer {
|
||||
const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control'];
|
||||
const auths = store.getQuads(null, ACL.mode, modeString, null).map((quad: Quad): Term => quad.subject);
|
||||
if (!auths.some((term): boolean => this.hasAccess(agent, term, store))) {
|
||||
throw typeof agent.webID === 'string' ? new ForbiddenHttpError() : new UnauthorizedHttpError();
|
||||
const isLoggedIn = typeof agent.webID === 'string';
|
||||
if (isLoggedIn) {
|
||||
this.logger.warn(`Agent ${agent.webID} has no ${mode} permissions`);
|
||||
throw new ForbiddenHttpError();
|
||||
} else {
|
||||
this.logger.warn(`Unauthenticated agent has no ${mode} permissions`);
|
||||
throw new UnauthorizedHttpError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,18 +121,25 @@ export class WebAclAuthorizer extends Authorizer {
|
||||
* @returns A store containing the relevant acl triples.
|
||||
*/
|
||||
private async getAclRecursive(id: ResourceIdentifier, recurse?: boolean): Promise<Store> {
|
||||
this.logger.debug(`Trying to read the direct ACL document of ${id.path}`);
|
||||
try {
|
||||
const acl = await this.aclManager.getAcl(id);
|
||||
this.logger.debug(`Trying to read the ACL document ${acl.path}`);
|
||||
const data = await this.resourceStore.getRepresentation(acl, { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
|
||||
this.logger.info(`Reading ACL statements from ${acl.path}`);
|
||||
return this.filterData(data, recurse ? ACL.default : ACL.accessTo, id.path);
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof NotFoundHttpError)) {
|
||||
if (error instanceof NotFoundHttpError) {
|
||||
this.logger.debug(`No direct ACL document found for ${id.path}`);
|
||||
} else {
|
||||
this.logger.error(`Error reading ACL for ${id.path}`, { error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const parent = await this.containerManager.getContainer(id);
|
||||
return this.getAclRecursive(parent, true);
|
||||
}
|
||||
|
||||
this.logger.debug(`Traversing to the parent of ${id.path}`);
|
||||
const parent = await this.containerManager.getContainer(id);
|
||||
return this.getAclRecursive(parent, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,15 +35,16 @@ export class BasicRequestParser extends RequestParser {
|
||||
// Can handle all requests
|
||||
}
|
||||
|
||||
public async handle(input: HttpRequest): Promise<Operation> {
|
||||
if (!input.method) {
|
||||
throw new Error('Missing method.');
|
||||
public async handle(request: HttpRequest): Promise<Operation> {
|
||||
const { method } = request;
|
||||
if (!method) {
|
||||
throw new Error('No method specified on the HTTP request');
|
||||
}
|
||||
const target = await this.targetExtractor.handleSafe(input);
|
||||
const preferences = await this.preferenceParser.handleSafe(input);
|
||||
const metadata = await this.metadataExtractor.handleSafe(input);
|
||||
const body = await this.bodyParser.handleSafe({ request: input, metadata });
|
||||
const target = await this.targetExtractor.handleSafe(request);
|
||||
const preferences = await this.preferenceParser.handleSafe(request);
|
||||
const metadata = await this.metadataExtractor.handleSafe(request);
|
||||
const body = await this.bodyParser.handleSafe({ request, metadata });
|
||||
|
||||
return { method: input.method, target, preferences, body };
|
||||
return { method, target, preferences, body };
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { HttpResponse } from '../../server/HttpResponse';
|
||||
import { HttpError } from '../../util/errors/HttpError';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
@ -9,9 +10,12 @@ import { ResponseWriter } from './ResponseWriter';
|
||||
* Still needs a way to write correct status codes for successful operations.
|
||||
*/
|
||||
export class BasicResponseWriter extends ResponseWriter {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise<void> {
|
||||
if (!(input.result instanceof Error) && input.result.body && !input.result.body.binary) {
|
||||
throw new UnsupportedHttpError('Only binary results and errors are supported.');
|
||||
this.logger.warn('This writer can only write binary bodies and errors');
|
||||
throw new UnsupportedHttpError('Only binary results and errors are supported');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { TLSSocket } from 'tls';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { HttpRequest } from '../../server/HttpRequest';
|
||||
import { toCanonicalUriPath } from '../../util/Util';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
@ -10,22 +11,28 @@ import { TargetExtractor } from './TargetExtractor';
|
||||
* TODO: input requires more extensive cleaning/parsing based on headers (see #22).
|
||||
*/
|
||||
export class BasicTargetExtractor extends TargetExtractor {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async canHandle(): Promise<void> {
|
||||
// Can handle all URLs
|
||||
}
|
||||
|
||||
public async handle(input: HttpRequest): Promise<ResourceIdentifier> {
|
||||
if (!input.url) {
|
||||
throw new Error('Missing URL.');
|
||||
public async handle(request: HttpRequest): Promise<ResourceIdentifier> {
|
||||
if (!request.url) {
|
||||
this.logger.error('The request has no URL');
|
||||
throw new Error('Missing URL');
|
||||
}
|
||||
if (!input.headers.host) {
|
||||
throw new Error('Missing host.');
|
||||
if (!request.headers.host) {
|
||||
this.logger.error('The request has no Host header');
|
||||
throw new Error('Missing Host header');
|
||||
}
|
||||
const isHttps = input.connection && (input.connection as TLSSocket).encrypted;
|
||||
|
||||
const isHttps = request.connection && (request.connection as TLSSocket).encrypted;
|
||||
this.logger.debug(`Request is using HTTPS: ${isHttps}`);
|
||||
|
||||
// URL object applies punycode encoding to domain
|
||||
const base = `http${isHttps ? 's' : ''}://${input.headers.host}`;
|
||||
const url = toCanonicalUriPath(input.url);
|
||||
const base = `http${isHttps ? 's' : ''}://${request.headers.host}`;
|
||||
const url = toCanonicalUriPath(request.url);
|
||||
const path = new URL(url, base).href;
|
||||
|
||||
return { path };
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import type { BodyParserArgs } from './BodyParser';
|
||||
@ -7,6 +8,8 @@ import { BodyParser } from './BodyParser';
|
||||
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
|
||||
*/
|
||||
export class RawBodyParser extends BodyParser {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async canHandle(): Promise<void> {
|
||||
// All content-types are supported
|
||||
}
|
||||
@ -17,13 +20,15 @@ export class RawBodyParser extends BodyParser {
|
||||
// RFC7230, §3.3: The presence of a message body in a request
|
||||
// is signaled by a Content-Length or Transfer-Encoding header field.
|
||||
if (!request.headers['content-length'] && !request.headers['transfer-encoding']) {
|
||||
this.logger.debug('HTTP request appears to not have a body, so nothing to parse');
|
||||
return;
|
||||
}
|
||||
|
||||
// While RFC7231 allows treating a body without content type as an octet stream,
|
||||
// such an omission likely signals a mistake, so force clients to make this explicit.
|
||||
if (!request.headers['content-type']) {
|
||||
throw new UnsupportedHttpError('An HTTP request body was passed without Content-Type header');
|
||||
this.logger.warn('A body was passed, but the content length was not specified');
|
||||
throw new UnsupportedHttpError('HTTP request body was passed without Content-Type header');
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { PassThrough } from 'stream';
|
||||
import type { Algebra } from 'sparqlalgebrajs';
|
||||
import { translate } from 'sparqlalgebrajs';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
||||
@ -14,8 +15,12 @@ import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
|
||||
* Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}.
|
||||
*/
|
||||
export class SparqlUpdateBodyParser extends BodyParser {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async canHandle({ request }: BodyParserArgs): Promise<void> {
|
||||
if (request.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) {
|
||||
const contentType = request.headers['content-type'];
|
||||
if (contentType !== APPLICATION_SPARQL_UPDATE) {
|
||||
this.logger.debug(`Unsupported content type: ${contentType}`);
|
||||
throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
|
||||
}
|
||||
}
|
||||
@ -33,6 +38,7 @@ export class SparqlUpdateBodyParser extends BodyParser {
|
||||
const sparql = await readableToString(toAlgebraStream);
|
||||
algebra = translate(sparql, { quads: true });
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error });
|
||||
if (error instanceof Error) {
|
||||
throw new UnsupportedHttpError(error.message);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||
import type { HttpRequest } from '../../../server/HttpRequest';
|
||||
import { UnsupportedHttpError } from '../../../util/errors/UnsupportedHttpError';
|
||||
import { HTTP } from '../../../util/UriConstants';
|
||||
@ -8,12 +9,16 @@ import type { MetadataParser } from './MetadataParser';
|
||||
* Converts the contents of the slug header to metadata.
|
||||
*/
|
||||
export class SlugParser implements MetadataParser {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async parse(request: HttpRequest, metadata: RepresentationMetadata): Promise<void> {
|
||||
const { slug } = request.headers;
|
||||
if (slug) {
|
||||
if (Array.isArray(slug)) {
|
||||
throw new UnsupportedHttpError('At most 1 slug header is allowed.');
|
||||
this.logger.warn(`Expected 0 or 1 Slug headers but received ${slug.length}`);
|
||||
throw new UnsupportedHttpError('Request has multiple Slug headers');
|
||||
}
|
||||
this.logger.debug(`Request Slug is '${slug}'.`);
|
||||
metadata.set(HTTP.slug, slug);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export class DeleteOperationHandler extends OperationHandler {
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'DELETE') {
|
||||
throw new UnsupportedHttpError('This handler only supports DELETE operations.');
|
||||
throw new UnsupportedHttpError('This handler only supports DELETE operations');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ export class GetOperationHandler extends OperationHandler {
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'GET') {
|
||||
throw new UnsupportedHttpError('This handler only supports GET operations.');
|
||||
throw new UnsupportedHttpError('This handler only supports GET operations');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ export class HeadOperationHandler extends OperationHandler {
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'HEAD') {
|
||||
throw new UnsupportedHttpError('This handler only supports HEAD operations.');
|
||||
throw new UnsupportedHttpError('This handler only supports HEAD operations');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import type { Operation } from './Operation';
|
||||
@ -9,6 +10,8 @@ import type { ResponseDescription } from './ResponseDescription';
|
||||
* Calls the addResource function from a {@link ResourceStore}.
|
||||
*/
|
||||
export class PostOperationHandler extends OperationHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly store: ResourceStore;
|
||||
|
||||
public constructor(store: ResourceStore) {
|
||||
@ -18,13 +21,14 @@ export class PostOperationHandler extends OperationHandler {
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'POST') {
|
||||
throw new UnsupportedHttpError('This handler only supports POST operations.');
|
||||
throw new UnsupportedHttpError('This handler only supports POST operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
if (!input.body) {
|
||||
throw new UnsupportedHttpError('POST operations require a body.');
|
||||
this.logger.warn('POST operations require a body');
|
||||
throw new UnsupportedHttpError('POST operations require a body');
|
||||
}
|
||||
const identifier = await this.store.addResource(input.target, input.body);
|
||||
return { identifier };
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import type { Operation } from './Operation';
|
||||
@ -9,6 +10,8 @@ import type { ResponseDescription } from './ResponseDescription';
|
||||
* Calls the setRepresentation function from a {@link ResourceStore}.
|
||||
*/
|
||||
export class PutOperationHandler extends OperationHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly store: ResourceStore;
|
||||
|
||||
public constructor(store: ResourceStore) {
|
||||
@ -18,13 +21,14 @@ export class PutOperationHandler extends OperationHandler {
|
||||
|
||||
public async canHandle(input: Operation): Promise<void> {
|
||||
if (input.method !== 'PUT') {
|
||||
throw new UnsupportedHttpError('This handler only supports PUT operations.');
|
||||
throw new UnsupportedHttpError('This handler only supports PUT operations');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
if (typeof input.body !== 'object') {
|
||||
throw new UnsupportedHttpError('PUT operations require a body.');
|
||||
this.logger.warn('No body specified on PUT request');
|
||||
throw new UnsupportedHttpError('PUT operations require a body');
|
||||
}
|
||||
await this.store.setRepresentation(input.target, input.body);
|
||||
return { identifier: input.target };
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import type { Operation } from '../operations/Operation';
|
||||
import type { PermissionSet } from './PermissionSet';
|
||||
@ -12,8 +13,11 @@ const SUPPORTED_METHODS = new Set([ ...READ_METHODS, ...WRITE_METHODS ]);
|
||||
* Specifically: GET, HEAD, POST, PUT and DELETE.
|
||||
*/
|
||||
export class MethodPermissionsExtractor extends PermissionsExtractor {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async canHandle({ method }: Operation): Promise<void> {
|
||||
if (!SUPPORTED_METHODS.has(method)) {
|
||||
this.logger.warn(`Unrecognized method ${method}`);
|
||||
throw new UnsupportedHttpError(`Cannot determine permissions of ${method}`);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { DataFactory, Store } from 'n3';
|
||||
import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { toObjectTerm, toNamedNode, isTerm } from '../../util/UriUtil';
|
||||
|
||||
export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode | Literal | string)[];
|
||||
@ -9,6 +10,8 @@ export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode |
|
||||
* Most functions return the metadata object to allow for chaining.
|
||||
*/
|
||||
export class RepresentationMetadata {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private store: Store;
|
||||
private id: NamedNode | BlankNode;
|
||||
|
||||
@ -184,6 +187,7 @@ export class RepresentationMetadata {
|
||||
return;
|
||||
}
|
||||
if (terms.length > 1) {
|
||||
this.logger.error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`);
|
||||
throw new Error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`);
|
||||
}
|
||||
return terms[0];
|
||||
|
@ -2,6 +2,7 @@ import { promises as fsPromises } from 'fs';
|
||||
import { posix } from 'path';
|
||||
import * as mime from 'mime-types';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { APPLICATION_OCTET_STREAM, TEXT_TURTLE } from '../util/ContentTypes';
|
||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
|
||||
@ -36,6 +37,8 @@ export interface ResourcePath {
|
||||
* This new extension is stripped again when generating an identifier.
|
||||
*/
|
||||
export class ExtensionBasedMapper implements FileIdentifierMapper {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly baseRequestURI: string;
|
||||
private readonly rootFilepath: string;
|
||||
private readonly types: Record<string, any>;
|
||||
@ -56,34 +59,38 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
||||
* @returns A ResourceLink with all the necessary metadata.
|
||||
*/
|
||||
public async mapUrlToFilePath(identifier: ResourceIdentifier, contentType?: string): Promise<ResourceLink> {
|
||||
let path = this.getRelativePath(identifier);
|
||||
const path = this.getRelativePath(identifier);
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
throw new UnsupportedHttpError('URL needs a / after the base.');
|
||||
this.logger.warn(`URL ${identifier.path} needs a / after the base`);
|
||||
throw new UnsupportedHttpError('URL needs a / after the base');
|
||||
}
|
||||
|
||||
if (path.includes('/..')) {
|
||||
throw new UnsupportedHttpError('Disallowed /.. segment in URL.');
|
||||
this.logger.warn(`Disallowed /.. segment in URL ${identifier.path}.`);
|
||||
throw new UnsupportedHttpError('Disallowed /.. segment in URL');
|
||||
}
|
||||
|
||||
path = this.getAbsolutePath(path);
|
||||
let filePath = this.getAbsolutePath(path);
|
||||
|
||||
// Container
|
||||
if (identifier.path.endsWith('/')) {
|
||||
this.logger.debug(`URL ${identifier.path} points to the container ${filePath}`);
|
||||
return {
|
||||
identifier,
|
||||
filePath: path,
|
||||
filePath,
|
||||
};
|
||||
}
|
||||
|
||||
// Would conflict with how new extensions get stored
|
||||
if (/\$\.\w+$/u.test(path)) {
|
||||
throw new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension.');
|
||||
// Would conflict with how new extensions are stored
|
||||
if (/\$\.\w+$/u.test(filePath)) {
|
||||
this.logger.warn(`Identifier ${identifier.path} contains a dollar sign before its extension`);
|
||||
throw new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension');
|
||||
}
|
||||
|
||||
// Existing file
|
||||
if (!contentType) {
|
||||
const [ , folder, documentName ] = /^(.*\/)(.*)$/u.exec(path)!;
|
||||
const [ , folder, documentName ] = /^(.*\/)(.*)$/u.exec(filePath)!;
|
||||
|
||||
let fileName: string | undefined;
|
||||
try {
|
||||
@ -94,34 +101,40 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
||||
);
|
||||
} catch {
|
||||
// Parent folder does not exist (or is not a folder)
|
||||
this.logger.warn(`No parent folder for ${identifier.path} found at ${folder}`);
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
|
||||
// File doesn't exist
|
||||
if (!fileName) {
|
||||
this.logger.warn(`File for URL ${identifier.path} does not exist in ${folder}`);
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
|
||||
filePath = joinPath(folder, fileName);
|
||||
this.logger.info(`The path for ${identifier.path} is ${filePath}`);
|
||||
return {
|
||||
identifier,
|
||||
filePath: joinPath(folder, fileName),
|
||||
filePath,
|
||||
contentType: this.getContentTypeFromExtension(fileName),
|
||||
};
|
||||
}
|
||||
|
||||
// If the extension of the identifier matches a different content-type than the one that is given,
|
||||
// we need to add a new extension to match the correct type.
|
||||
if (contentType !== this.getContentTypeFromExtension(path)) {
|
||||
if (contentType !== this.getContentTypeFromExtension(filePath)) {
|
||||
const extension = mime.extension(contentType);
|
||||
if (!extension) {
|
||||
throw new UnsupportedHttpError(`Unsupported content-type ${contentType}.`);
|
||||
this.logger.warn(`No extension found for ${contentType}`);
|
||||
throw new UnsupportedHttpError(`Unsupported content type ${contentType}`);
|
||||
}
|
||||
path = `${path}$.${extension}`;
|
||||
filePath += `$.${extension}`;
|
||||
}
|
||||
|
||||
this.logger.info(`The path for ${identifier.path} is ${filePath}`);
|
||||
return {
|
||||
identifier,
|
||||
filePath: path,
|
||||
filePath,
|
||||
contentType,
|
||||
};
|
||||
}
|
||||
@ -135,13 +148,16 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
||||
*/
|
||||
public async mapFilePathToUrl(filePath: string, isContainer: boolean): Promise<ResourceLink> {
|
||||
if (!filePath.startsWith(this.rootFilepath)) {
|
||||
throw new Error(`File ${filePath} is not part of the file storage at ${this.rootFilepath}.`);
|
||||
this.logger.error(`Trying to access file ${filePath} outside of ${this.rootFilepath}`);
|
||||
throw new Error(`File ${filePath} is not part of the file storage at ${this.rootFilepath}`);
|
||||
}
|
||||
|
||||
let relative = filePath.slice(this.rootFilepath.length);
|
||||
if (isContainer) {
|
||||
const path = ensureTrailingSlash(this.baseRequestURI + encodeUriPathComponents(relative));
|
||||
this.logger.info(`Container filepath ${filePath} maps to URL ${path}`);
|
||||
return {
|
||||
identifier: { path: ensureTrailingSlash(this.baseRequestURI + encodeUriPathComponents(relative)) },
|
||||
identifier: { path },
|
||||
filePath,
|
||||
};
|
||||
}
|
||||
@ -153,8 +169,11 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
||||
relative = relative.slice(0, -(extension.length + 2));
|
||||
}
|
||||
|
||||
const path = trimTrailingSlashes(this.baseRequestURI + encodeUriPathComponents(relative));
|
||||
this.logger.info(`File ${filePath} (${contentType}) maps to URL ${path}`);
|
||||
|
||||
return {
|
||||
identifier: { path: trimTrailingSlashes(this.baseRequestURI + encodeUriPathComponents(relative)) },
|
||||
identifier: { path },
|
||||
filePath,
|
||||
contentType,
|
||||
};
|
||||
@ -203,6 +222,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
||||
*/
|
||||
private getRelativePath(identifier: ResourceIdentifier): string {
|
||||
if (!identifier.path.startsWith(this.baseRequestURI)) {
|
||||
this.logger.warn(`The URL ${identifier.path} is outside of the scope ${this.baseRequestURI}`);
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
return decodeUriPathComponents(identifier.path.slice(this.baseRequestURI.length));
|
||||
|
@ -3,6 +3,7 @@ import type { Patch } from '../ldp/http/Patch';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { AtomicResourceStore } from './AtomicResourceStore';
|
||||
import type { Conditions } from './Conditions';
|
||||
import type { ResourceLocker } from './ResourceLocker';
|
||||
@ -16,6 +17,8 @@ const READ_TIMEOUT = 1000;
|
||||
* and releases it afterwards.
|
||||
*/
|
||||
export class LockingResourceStore implements AtomicResourceStore {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly source: ResourceStore;
|
||||
private readonly locks: ResourceLocker;
|
||||
|
||||
@ -117,8 +120,10 @@ export class LockingResourceStore implements AtomicResourceStore {
|
||||
*/
|
||||
protected createExpiringReadable(source: Readable): Readable {
|
||||
// Destroy the source when a timeout occurs.
|
||||
const destroySource = (): void =>
|
||||
const destroySource = (): void => {
|
||||
this.logger.info(`Stream reading timout of ${READ_TIMEOUT}ms exceeded; destroying source`);
|
||||
source.destroy(new Error(`Stream reading timout of ${READ_TIMEOUT}ms exceeded`));
|
||||
};
|
||||
let timeout = setTimeout(destroySource, READ_TIMEOUT);
|
||||
|
||||
// Cancel the timeout when the source terminates by itself.
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { matchingMediaType } from '../util/Util';
|
||||
import type { Conditions } from './Conditions';
|
||||
import type { RepresentationConverter } from './conversion/RepresentationConverter';
|
||||
@ -17,6 +18,8 @@ import type { ResourceStore } from './ResourceStore';
|
||||
* if there is a low weight for that type conversions might still be preferred.
|
||||
*/
|
||||
export class RepresentationConvertingStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly converter: RepresentationConverter;
|
||||
|
||||
public constructor(source: T, converter: RepresentationConverter) {
|
||||
@ -30,6 +33,7 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
|
||||
if (this.matchesPreferences(representation, preferences)) {
|
||||
return representation;
|
||||
}
|
||||
this.logger.info(`Convert ${identifier.path} from ${representation.metadata.contentType} to ${preferences.type}`);
|
||||
return this.converter.handleSafe({ identifier, representation, preferences });
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import AsyncLock from 'async-lock';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { Lock } from './Lock';
|
||||
import type { ResourceLocker } from './ResourceLocker';
|
||||
|
||||
@ -7,6 +8,8 @@ import type { ResourceLocker } from './ResourceLocker';
|
||||
* A resource locker making use of the `async-lock` library.
|
||||
*/
|
||||
export class SingleThreadedResourceLocker implements ResourceLocker {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly locks: AsyncLock;
|
||||
|
||||
public constructor() {
|
||||
@ -21,9 +24,14 @@ export class SingleThreadedResourceLocker implements ResourceLocker {
|
||||
* @returns The {@link Lock} when it's available. Its release function needs to be called when finished.
|
||||
*/
|
||||
public async acquire(identifier: ResourceIdentifier): Promise<Lock> {
|
||||
this.logger.verbose(`Acquiring lock for ${identifier.path}`);
|
||||
return new Promise(async(resolve): Promise<Lock> =>
|
||||
this.locks.acquire(identifier.path, (done): void => {
|
||||
resolve({ release: async(): Promise<void> => done() });
|
||||
this.logger.verbose(`Acquired lock for ${identifier.path}`);
|
||||
resolve({ release: async(): Promise<void> => {
|
||||
this.logger.verbose(`Released lock for ${identifier.path}`);
|
||||
done();
|
||||
} });
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ export class UrlContainerManager implements ContainerManager {
|
||||
public async getContainer(id: ResourceIdentifier): Promise<ResourceIdentifier> {
|
||||
const path = this.canonicalUrl(id.path);
|
||||
if (this.base === path) {
|
||||
throw new Error('Root does not have a container.');
|
||||
throw new Error('Root does not have a container');
|
||||
}
|
||||
|
||||
const parentPath = new URL('..', path).toString();
|
||||
|
||||
// This probably means there is an issue with the root
|
||||
if (parentPath === path) {
|
||||
throw new Error('URL root reached.');
|
||||
throw new Error('URL root reached');
|
||||
}
|
||||
|
||||
return { path: parentPath };
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { matchingMediaType } from '../../util/Util';
|
||||
import { checkRequest } from './ConversionUtil';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
@ -9,6 +10,8 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
* It chains these converters by finding intermediate types that are supported by converters on either side.
|
||||
*/
|
||||
export class ChainedConverter extends TypedRepresentationConverter {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly converters: TypedRepresentationConverter[];
|
||||
|
||||
/**
|
||||
@ -86,6 +89,7 @@ export class ChainedConverter extends TypedRepresentationConverter {
|
||||
if (weight > bestMatch.weight && matchingMediaType(leftType, rightType)) {
|
||||
bestMatch = { type: leftType, weight };
|
||||
if (weight === 1) {
|
||||
this.logger.info(`${bestMatch.type} is an exact match between ${leftKeys} and ${rightKeys}`);
|
||||
return bestMatch.type;
|
||||
}
|
||||
}
|
||||
@ -93,9 +97,11 @@ export class ChainedConverter extends TypedRepresentationConverter {
|
||||
}
|
||||
|
||||
if (bestMatch.weight === 0) {
|
||||
this.logger.error(`No match found between ${leftKeys} and ${rightKeys}`);
|
||||
throw new Error(`No match found between ${leftKeys} and ${rightKeys}`);
|
||||
}
|
||||
|
||||
this.logger.info(`${bestMatch.type} is the best match between ${leftKeys} and ${rightKeys}`);
|
||||
return bestMatch.type;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import type { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch';
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { CONTENT_TYPE } from '../../util/UriConstants';
|
||||
@ -20,6 +21,8 @@ import { PatchHandler } from './PatchHandler';
|
||||
* Currently all DELETE/INSERT types are supported that have empty where bodies and no variables.
|
||||
*/
|
||||
export class SparqlUpdatePatchHandler extends PatchHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly source: ResourceStore;
|
||||
private readonly locker: ResourceLocker;
|
||||
|
||||
@ -31,14 +34,15 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
|
||||
|
||||
public async canHandle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> {
|
||||
if (typeof input.patch.algebra !== 'object') {
|
||||
throw new UnsupportedHttpError('Only SPARQL update patch requests are supported.');
|
||||
throw new UnsupportedHttpError('Only SPARQL update patch requests are supported');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> {
|
||||
const op = input.patch.algebra;
|
||||
if (!this.isDeleteInsert(op)) {
|
||||
throw new UnsupportedHttpError('Only DELETE/INSERT SPARQL update operations are supported.');
|
||||
this.logger.warn(`Unsupported operation: ${op.type}`);
|
||||
throw new UnsupportedHttpError('Only DELETE/INSERT SPARQL update operations are supported');
|
||||
}
|
||||
|
||||
const def = defaultGraph();
|
||||
@ -46,14 +50,17 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
|
||||
const inserts = op.insert ?? [];
|
||||
|
||||
if (!deletes.every((pattern): boolean => pattern.graph.equals(def))) {
|
||||
throw new UnsupportedHttpError('GRAPH statements are not supported.');
|
||||
this.logger.warn('GRAPH statement in DELETE clause');
|
||||
throw new UnsupportedHttpError('GRAPH statements are not supported');
|
||||
}
|
||||
if (!inserts.every((pattern): boolean => pattern.graph.equals(def))) {
|
||||
throw new UnsupportedHttpError('GRAPH statements are not supported.');
|
||||
this.logger.warn('GRAPH statement in INSERT clause');
|
||||
throw new UnsupportedHttpError('GRAPH statements are not supported');
|
||||
}
|
||||
if (op.where ?? deletes.some((pattern): boolean =>
|
||||
someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
|
||||
throw new UnsupportedHttpError('WHERE statements are not supported.');
|
||||
this.logger.warn('WHERE statements are not supported');
|
||||
throw new UnsupportedHttpError('WHERE statements are not supported');
|
||||
}
|
||||
|
||||
const lock = await this.locker.acquire(input.identifier);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { AsyncHandler } from './AsyncHandler';
|
||||
import { HttpError } from './errors/HttpError';
|
||||
import { InternalServerError } from './errors/InternalServerError';
|
||||
@ -10,6 +11,8 @@ import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
|
||||
* allowing for more fine-grained handlers to check before catch-all handlers.
|
||||
*/
|
||||
export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly handlers: AsyncHandler<TIn, TOut>[];
|
||||
|
||||
/**
|
||||
@ -43,7 +46,8 @@ export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut>
|
||||
try {
|
||||
handler = await this.findHandler(input);
|
||||
} catch {
|
||||
throw new Error('All handlers failed. This might be the consequence of calling handle before canHandle.');
|
||||
this.logger.warn('All handlers failed. This might be the consequence of calling handle before canHandle.');
|
||||
throw new Error('All handlers failed');
|
||||
}
|
||||
|
||||
return handler.handle(input);
|
||||
@ -85,13 +89,13 @@ export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut>
|
||||
} else if (error instanceof Error) {
|
||||
errors.push(new InternalServerError(error.message));
|
||||
} else {
|
||||
errors.push(new InternalServerError('Unknown error.'));
|
||||
errors.push(new InternalServerError('Unknown error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const joined = errors.map((error: Error): string => error.message).join(', ');
|
||||
const message = `No handler supports the given input: [${joined}].`;
|
||||
const message = `No handler supports the given input: [${joined}]`;
|
||||
|
||||
// Check if all errors have the same status code
|
||||
if (errors.every((error): boolean => error.statusCode === errors[0].statusCode)) {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
|
||||
|
||||
const logger = getLoggerFor('HeaderUtil');
|
||||
|
||||
// BNF based on https://tools.ietf.org/html/rfc7231
|
||||
//
|
||||
// Accept = #( media-range [ accept-params ] )
|
||||
@ -105,9 +108,8 @@ export const transformQuotedStrings = (input: string): { result: string; replace
|
||||
const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
|
||||
// Not all characters allowed in quoted strings, see BNF above
|
||||
if (!/^"(?:[\t !\u0023-\u005B\u005D-\u007E\u0080-\u00FF]|(?:\\[\t\u0020-\u007E\u0080-\u00FF]))*"$/u.test(match)) {
|
||||
throw new UnsupportedHttpError(
|
||||
`Invalid quoted string in header: ${match}. Check which characters are allowed`,
|
||||
);
|
||||
logger.warn(`Invalid quoted string in header: ${match}`);
|
||||
throw new UnsupportedHttpError(`Invalid quoted string in header: ${match}`);
|
||||
}
|
||||
const replacement = `"${idx}"`;
|
||||
replacements[replacement] = match.slice(1, -1);
|
||||
@ -137,6 +139,7 @@ export const splitAndClean = (input: string): string[] =>
|
||||
*/
|
||||
const testQValue = (qvalue: string): void => {
|
||||
if (!/^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue)) {
|
||||
logger.warn(`Invalid q value: ${qvalue}`);
|
||||
throw new UnsupportedHttpError(
|
||||
`Invalid q value: ${qvalue} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`,
|
||||
);
|
||||
@ -163,6 +166,7 @@ export const parseParameters = (parameters: string[], replacements: { [id: strin
|
||||
// parameter = token "=" ( token / quoted-string )
|
||||
// second part is optional for certain parameters
|
||||
if (!(token.test(name) && (!rawValue || /^"\d+"$/u.test(rawValue) || token.test(rawValue)))) {
|
||||
logger.warn(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue}`);
|
||||
throw new UnsupportedHttpError(
|
||||
`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` +
|
||||
`does not match (token ( "=" ( token / quoted-string ))?). `,
|
||||
@ -197,6 +201,7 @@ const parseAcceptPart = (part: string, replacements: { [id: string]: string }):
|
||||
// No reason to test differently for * since we don't check if the type exists
|
||||
const [ type, subtype ] = range.split('/');
|
||||
if (!type || !subtype || !token.test(type) || !token.test(subtype)) {
|
||||
logger.warn(`Invalid Accept range: ${range}`);
|
||||
throw new UnsupportedHttpError(
|
||||
`Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`,
|
||||
);
|
||||
@ -215,8 +220,9 @@ const parseAcceptPart = (part: string, replacements: { [id: string]: string }):
|
||||
weight = Number.parseFloat(value);
|
||||
} else {
|
||||
if (!value && map !== extensionParams) {
|
||||
logger.warn(`Invalid Accept parameter ${name}`);
|
||||
throw new UnsupportedHttpError(`Invalid Accept parameter ${name}: ` +
|
||||
`Accept parameter values are not optional when preceding the q value.`);
|
||||
`Accept parameter values are not optional when preceding the q value`);
|
||||
}
|
||||
map[name] = value || '';
|
||||
}
|
||||
@ -249,7 +255,8 @@ const parseNoParameters = (input: string): { range: string; weight: number }[] =
|
||||
const result = { range, weight: 1 };
|
||||
if (qvalue) {
|
||||
if (!qvalue.startsWith('q=')) {
|
||||
throw new UnsupportedHttpError(`Only q parameters are allowed in ${input}.`);
|
||||
logger.warn(`Only q parameters are allowed in ${input}`);
|
||||
throw new UnsupportedHttpError(`Only q parameters are allowed in ${input}`);
|
||||
}
|
||||
const val = qvalue.slice(2);
|
||||
testQValue(val);
|
||||
@ -293,6 +300,7 @@ export const parseAcceptCharset = (input: string): AcceptCharset[] => {
|
||||
const results = parseNoParameters(input);
|
||||
results.forEach((result): void => {
|
||||
if (!token.test(result.range)) {
|
||||
logger.warn(`Invalid Accept-Charset range: ${result.range}`);
|
||||
throw new UnsupportedHttpError(
|
||||
`Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`,
|
||||
);
|
||||
@ -315,6 +323,7 @@ export const parseAcceptEncoding = (input: string): AcceptEncoding[] => {
|
||||
const results = parseNoParameters(input);
|
||||
results.forEach((result): void => {
|
||||
if (!token.test(result.range)) {
|
||||
logger.warn(`Invalid Accept-Encoding range: ${result.range}`);
|
||||
throw new UnsupportedHttpError(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`);
|
||||
}
|
||||
});
|
||||
@ -336,6 +345,9 @@ export const parseAcceptLanguage = (input: string): AcceptLanguage[] => {
|
||||
results.forEach((result): void => {
|
||||
// (1*8ALPHA *("-" 1*8alphanum)) / "*"
|
||||
if (result.range !== '*' && !/^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$/u.test(result.range)) {
|
||||
logger.warn(
|
||||
`Invalid Accept-Language range: ${result.range}`,
|
||||
);
|
||||
throw new UnsupportedHttpError(
|
||||
`Invalid Accept-Language range: ${result.range} does not match ((1*8ALPHA *("-" 1*8alphanum)) / "*")`,
|
||||
);
|
||||
|
@ -2,6 +2,9 @@ import type { Readable, Writable } from 'stream';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory } from 'n3';
|
||||
import type { Literal, NamedNode, Quad } from 'rdf-js';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
|
||||
const logger = getLoggerFor('Util');
|
||||
|
||||
/**
|
||||
* Makes sure the input path has exactly 1 slash at the end.
|
||||
@ -66,7 +69,10 @@ export const matchingMediaType = (mediaA: string, mediaB: string): boolean => {
|
||||
export const pipeStreamsAndErrors = <T extends Writable>(readable: Readable, destination: T,
|
||||
mapError?: (error: Error) => Error): T => {
|
||||
readable.pipe(destination);
|
||||
readable.on('error', (error): boolean => destination.emit('error', mapError ? mapError(error) : error));
|
||||
readable.on('error', (error): boolean => {
|
||||
logger.warn(`Piped stream errored with ${error.message}`);
|
||||
return destination.emit('error', mapError ? mapError(error) : error);
|
||||
});
|
||||
return destination;
|
||||
};
|
||||
|
||||
|
@ -2,8 +2,6 @@ import * as Path from 'path';
|
||||
import type { Loader } from 'componentsjs';
|
||||
import { runCli } from '../../../src/init/CliRunner';
|
||||
import type { Setup } from '../../../src/init/Setup';
|
||||
import { setGlobalLoggerFactory } from '../../../src/logging/LogUtil';
|
||||
import { VoidLoggerFactory } from '../../../src/logging/VoidLoggerFactory';
|
||||
|
||||
let calledInstantiateFromUrl: boolean;
|
||||
let calledRegisterAvailableModuleResources: boolean;
|
||||
@ -56,8 +54,6 @@ jest.mock('yargs', (): any => ({
|
||||
|
||||
describe('CliRunner', (): void => {
|
||||
beforeAll(async(): Promise<void> => {
|
||||
setGlobalLoggerFactory(new VoidLoggerFactory());
|
||||
|
||||
mockSetup.setup.mockImplementation(async(): Promise<any> => {
|
||||
// The info method will be called when all other code has been executed, so end the waiting function.
|
||||
outsideResolve();
|
||||
|
@ -25,7 +25,8 @@ describe('A BasicRequestParser', (): void => {
|
||||
});
|
||||
|
||||
it('errors if there is no input.', async(): Promise<void> => {
|
||||
await expect(requestParser.handle({ url: 'url' } as any)).rejects.toThrow('Missing method.');
|
||||
await expect(requestParser.handle({ url: 'url' } as any))
|
||||
.rejects.toThrow('No method specified on the HTTP request');
|
||||
});
|
||||
|
||||
it('returns the output of all input parsers after calling handle.', async(): Promise<void> => {
|
||||
|
@ -8,11 +8,11 @@ describe('A BasicTargetExtractor', (): void => {
|
||||
});
|
||||
|
||||
it('errors if there is no URL.', async(): Promise<void> => {
|
||||
await expect(extractor.handle({ headers: { host: 'test.com' }} as any)).rejects.toThrow('Missing URL.');
|
||||
await expect(extractor.handle({ headers: { host: 'test.com' }} as any)).rejects.toThrow('Missing URL');
|
||||
});
|
||||
|
||||
it('errors if there is no host.', async(): Promise<void> => {
|
||||
await expect(extractor.handle({ url: 'url', headers: {}} as any)).rejects.toThrow('Missing host.');
|
||||
await expect(extractor.handle({ url: 'url', headers: {}} as any)).rejects.toThrow('Missing Host header');
|
||||
});
|
||||
|
||||
it('returns the input URL.', async(): Promise<void> => {
|
||||
|
@ -3,8 +3,8 @@ import streamifyArray from 'streamify-array';
|
||||
import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser';
|
||||
import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
||||
import 'jest-rdf';
|
||||
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
||||
|
||||
describe('A RawBodyparser', (): void => {
|
||||
const bodyParser = new RawBodyParser();
|
||||
@ -28,14 +28,14 @@ describe('A RawBodyparser', (): void => {
|
||||
input.request = streamifyArray([ 'abc' ]) as HttpRequest;
|
||||
input.request.headers = { 'content-length': '0' };
|
||||
await expect(bodyParser.handle(input)).rejects
|
||||
.toThrow('An HTTP request body was passed without Content-Type header');
|
||||
.toThrow('HTTP request body was passed without Content-Type header');
|
||||
});
|
||||
|
||||
it('errors when a transfer encoding was specified without content type.', async(): Promise<void> => {
|
||||
input.request = streamifyArray([ 'abc' ]) as HttpRequest;
|
||||
input.request.headers = { 'transfer-encoding': 'chunked' };
|
||||
await expect(bodyParser.handle(input)).rejects
|
||||
.toThrow('An HTTP request body was passed without Content-Type header');
|
||||
.toThrow('HTTP request body was passed without Content-Type header');
|
||||
});
|
||||
|
||||
it('returns a Representation if there was data.', async(): Promise<void> => {
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { LinkTypeParser } from '../../../../../src/ldp/http/metadata/LinkTypeParser';
|
||||
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import { setGlobalLoggerFactory } from '../../../../../src/logging/LogUtil';
|
||||
import { VoidLoggerFactory } from '../../../../../src/logging/VoidLoggerFactory';
|
||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
||||
import { RDF } from '../../../../../src/util/UriConstants';
|
||||
|
||||
@ -10,10 +8,6 @@ describe('A LinkTypeParser', (): void => {
|
||||
let request: HttpRequest;
|
||||
let metadata: RepresentationMetadata;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
setGlobalLoggerFactory(new VoidLoggerFactory());
|
||||
});
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
request = { headers: {}} as HttpRequest;
|
||||
metadata = new RepresentationMetadata();
|
||||
|
@ -22,7 +22,7 @@ describe('A SlugParser', (): void => {
|
||||
it('errors if there are multiple slug headers.', async(): Promise<void> => {
|
||||
request.headers.slug = [ 'slugA', 'slugB' ];
|
||||
await expect(parser.parse(request, metadata))
|
||||
.rejects.toThrow(new UnsupportedHttpError('At most 1 slug header is allowed.'));
|
||||
.rejects.toThrow(new UnsupportedHttpError('Request has multiple Slug headers'));
|
||||
});
|
||||
|
||||
it('stores the slug metadata.', async(): Promise<void> => {
|
||||
|
@ -1,7 +1,5 @@
|
||||
import type { Server } from 'http';
|
||||
import request from 'supertest';
|
||||
import { setGlobalLoggerFactory } from '../../../src/logging/LogUtil';
|
||||
import { VoidLoggerFactory } from '../../../src/logging/VoidLoggerFactory';
|
||||
import { ExpressHttpServer } from '../../../src/server/ExpressHttpServer';
|
||||
import { HttpHandler } from '../../../src/server/HttpHandler';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
@ -33,7 +31,6 @@ describe('ExpressHttpServer', (): void => {
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// Prevent test from writing to stderr
|
||||
mock = jest.spyOn(process.stderr, 'write').mockImplementation((): boolean => true);
|
||||
setGlobalLoggerFactory(new VoidLoggerFactory());
|
||||
});
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
|
@ -27,12 +27,12 @@ describe('An ExtensionBasedMapper', (): void => {
|
||||
|
||||
it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => {
|
||||
await expect(mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }))
|
||||
.rejects.toThrow(new UnsupportedHttpError('URL needs a / after the base.'));
|
||||
.rejects.toThrow(new UnsupportedHttpError('URL needs a / after the base'));
|
||||
});
|
||||
|
||||
it('throws 400 if the input path contains relative parts.', async(): Promise<void> => {
|
||||
await expect(mapper.mapUrlToFilePath({ path: `${base}test/../test2` }))
|
||||
.rejects.toThrow(new UnsupportedHttpError('Disallowed /.. segment in URL.'));
|
||||
.rejects.toThrow(new UnsupportedHttpError('Disallowed /.. segment in URL'));
|
||||
});
|
||||
|
||||
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
|
||||
@ -44,7 +44,7 @@ describe('An ExtensionBasedMapper', (): void => {
|
||||
|
||||
it('rejects URLs that end with "$.{extension}".', async(): Promise<void> => {
|
||||
await expect(mapper.mapUrlToFilePath({ path: `${base}test$.txt` }))
|
||||
.rejects.toThrow(new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension.'));
|
||||
.rejects.toThrow(new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension'));
|
||||
});
|
||||
|
||||
it('throws 404 when looking in a folder that does not exist.', async(): Promise<void> => {
|
||||
@ -95,7 +95,7 @@ describe('An ExtensionBasedMapper', (): void => {
|
||||
|
||||
it('throws 400 if the given content-type is not recognized.', async(): Promise<void> => {
|
||||
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, 'fake/data'))
|
||||
.rejects.toThrow(new UnsupportedHttpError(`Unsupported content-type fake/data.`));
|
||||
.rejects.toThrow(new UnsupportedHttpError(`Unsupported content type fake/data`));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -12,20 +12,20 @@ describe('An UrlContainerManager', (): void => {
|
||||
it('errors when getting the container of root.', async(): Promise<void> => {
|
||||
let manager = new UrlContainerManager('http://test.com/foo/');
|
||||
await expect(manager.getContainer({ path: 'http://test.com/foo/' }))
|
||||
.rejects.toThrow('Root does not have a container.');
|
||||
.rejects.toThrow('Root does not have a container');
|
||||
await expect(manager.getContainer({ path: 'http://test.com/foo' }))
|
||||
.rejects.toThrow('Root does not have a container.');
|
||||
.rejects.toThrow('Root does not have a container');
|
||||
|
||||
manager = new UrlContainerManager('http://test.com/foo/');
|
||||
await expect(manager.getContainer({ path: 'http://test.com/foo/' }))
|
||||
.rejects.toThrow('Root does not have a container.');
|
||||
.rejects.toThrow('Root does not have a container');
|
||||
await expect(manager.getContainer({ path: 'http://test.com/foo' }))
|
||||
.rejects.toThrow('Root does not have a container.');
|
||||
.rejects.toThrow('Root does not have a container');
|
||||
});
|
||||
|
||||
it('errors when the root of an URl is reached that does not match the input root.', async(): Promise<void> => {
|
||||
const manager = new UrlContainerManager('http://test.com/foo/');
|
||||
await expect(manager.getContainer({ path: 'http://test.com/' }))
|
||||
.rejects.toThrow('URL root reached.');
|
||||
.rejects.toThrow('URL root reached');
|
||||
});
|
||||
});
|
||||
|
@ -152,7 +152,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
|
||||
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
|
||||
{ quads: true },
|
||||
) } as SparqlUpdatePatch });
|
||||
await expect(handle).rejects.toThrow('GRAPH statements are not supported.');
|
||||
await expect(handle).rejects.toThrow('GRAPH statements are not supported');
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
|
||||
@ -163,7 +163,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
|
||||
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
|
||||
{ quads: true },
|
||||
) } as SparqlUpdatePatch });
|
||||
await expect(handle).rejects.toThrow('GRAPH statements are not supported.');
|
||||
await expect(handle).rejects.toThrow('GRAPH statements are not supported');
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
|
||||
@ -175,7 +175,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
|
||||
'WHERE { ?s ?p ?o }',
|
||||
{ quads: true },
|
||||
) } as SparqlUpdatePatch });
|
||||
await expect(handle).rejects.toThrow('WHERE statements are not supported.');
|
||||
await expect(handle).rejects.toThrow('WHERE statements are not supported');
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
|
||||
@ -185,7 +185,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
|
||||
'DELETE WHERE { ?v <http://test.com/startP1> <http://test.com/startO1> }',
|
||||
{ quads: true },
|
||||
) } as SparqlUpdatePatch });
|
||||
await expect(handle).rejects.toThrow('WHERE statements are not supported.');
|
||||
await expect(handle).rejects.toThrow('WHERE statements are not supported');
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
|
||||
@ -195,7 +195,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
|
||||
'MOVE DEFAULT TO GRAPH <http://test.com/newGraph>',
|
||||
{ quads: true },
|
||||
) } as SparqlUpdatePatch });
|
||||
await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported.');
|
||||
await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported');
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
@ -44,7 +44,7 @@ describe('A CompositeAsyncHandler', (): void => {
|
||||
it('can not handle data if no handler supports it.', async(): Promise<void> => {
|
||||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
|
||||
|
||||
await expect(handler.canHandle(null)).rejects.toThrow('[Not supported., Not supported.]');
|
||||
await expect(handler.canHandle(null)).rejects.toThrow('[Not supported, Not supported]');
|
||||
});
|
||||
|
||||
it('throws unknown errors if no Error objects are thrown.', async(): Promise<void> => {
|
||||
@ -53,7 +53,7 @@ describe('A CompositeAsyncHandler', (): void => {
|
||||
};
|
||||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
|
||||
|
||||
await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error., Unknown error.]');
|
||||
await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error, Unknown error]');
|
||||
});
|
||||
|
||||
it('handles data if a handler supports it.', async(): Promise<void> => {
|
||||
@ -67,7 +67,7 @@ describe('A CompositeAsyncHandler', (): void => {
|
||||
it('errors if the handle function is called but no handler supports the data.', async(): Promise<void> => {
|
||||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
|
||||
|
||||
await expect(handler.handle('test')).rejects.toThrow('All handlers failed.');
|
||||
await expect(handler.handle('test')).rejects.toThrow('All handlers failed');
|
||||
});
|
||||
|
||||
it('only calls the canHandle function once of its handlers when handleSafe is called.', async(): Promise<void> => {
|
||||
@ -81,7 +81,7 @@ describe('A CompositeAsyncHandler', (): void => {
|
||||
it('throws the canHandle error when calling handleSafe if the data is not supported.', async(): Promise<void> => {
|
||||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
|
||||
|
||||
await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported., Not supported.]');
|
||||
await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported, Not supported]');
|
||||
});
|
||||
|
||||
it('throws an error with matching status code if all handlers threw the same.', async(): Promise<void> => {
|
||||
|
4
test/util/SetupTests.ts
Normal file
4
test/util/SetupTests.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { setGlobalLoggerFactory } from '../../src/logging/LogUtil';
|
||||
import { VoidLoggerFactory } from '../../src/logging/VoidLoggerFactory';
|
||||
|
||||
setGlobalLoggerFactory(new VoidLoggerFactory());
|
@ -14,7 +14,7 @@ export class StaticAsyncHandler<TOut> extends AsyncHandler<any, TOut> {
|
||||
if (this.canHandleStatic) {
|
||||
return;
|
||||
}
|
||||
throw new Error('Not supported.');
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
public async handle(): Promise<TOut> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user