diff --git a/src/storage/mapping/ExtensionBasedMapper.ts b/src/storage/mapping/ExtensionBasedMapper.ts index 7127fa2b5..fe54af7db 100644 --- a/src/storage/mapping/ExtensionBasedMapper.ts +++ b/src/storage/mapping/ExtensionBasedMapper.ts @@ -7,13 +7,13 @@ import { APPLICATION_OCTET_STREAM, TEXT_TURTLE } from '../../util/ContentTypes'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { - decodeUriPathComponents, encodeUriPathComponents, ensureTrailingSlash, isContainerIdentifier, trimTrailingSlashes, } from '../../util/PathUtil'; import type { FileIdentifierMapper, ResourceLink } from '../FileIdentifierMapper'; +import { getAbsolutePath, getRelativePath, validateRelativePath } from './MapperUtil'; const { join: joinPath, normalize: normalizePath } = posix; @@ -60,19 +60,10 @@ export class ExtensionBasedMapper implements FileIdentifierMapper { * @returns A ResourceLink with all the necessary metadata. */ public async mapUrlToFilePath(identifier: ResourceIdentifier, contentType?: string): Promise { - const path = this.getRelativePath(identifier); + const path = getRelativePath(this.baseRequestURI, identifier, this.logger); + validateRelativePath(path, identifier, this.logger); - if (!path.startsWith('/')) { - this.logger.warn(`URL ${identifier.path} needs a / after the base`); - throw new UnsupportedHttpError('URL needs a / after the base'); - } - - if (path.includes('/..')) { - this.logger.warn(`Disallowed /.. segment in URL ${identifier.path}.`); - throw new UnsupportedHttpError('Disallowed /.. segment in URL'); - } - - let filePath = this.getAbsolutePath(path); + let filePath = getAbsolutePath(this.rootFilepath, path); // Container if (isContainerIdentifier(identifier)) { @@ -200,32 +191,4 @@ export class ExtensionBasedMapper implements FileIdentifierMapper { const extension = /\.([^./]+)$/u.exec(path); return extension && extension[1]; } - - /** - * Get the absolute file path based on the rootFilepath of the store. - * @param path - The relative file path. - * @param identifier - Optional identifier to add to the path. - * - * @returns Absolute path of the file. - */ - private getAbsolutePath(path: string, identifier = ''): string { - return joinPath(this.rootFilepath, path, identifier); - } - - /** - * Strips the baseRequestURI from the identifier and checks if the stripped base URI matches the store's one. - * @param identifier - Incoming identifier. - * - * @throws {@link NotFoundHttpError} - * If the identifier does not match the baseRequestURI path of the store. - * - * @returns A string representing the relative path. - */ - 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)); - } } diff --git a/src/storage/mapping/MapperUtil.ts b/src/storage/mapping/MapperUtil.ts new file mode 100644 index 000000000..e30697364 --- /dev/null +++ b/src/storage/mapping/MapperUtil.ts @@ -0,0 +1,60 @@ +import { posix } from 'path'; +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import type { Logger } from '../../logging/Logger'; +import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; +import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; +import { decodeUriPathComponents } from '../../util/Util'; + +const { join: joinPath } = posix; + +/** + * Get the absolute file path based on the rootFilepath of the store. + * @param rootFilepath - The root file path. + * @param path - The relative file path. + * @param identifier - Optional identifier to add to the path. + * + * @returns Absolute path of the file. + */ +export const getAbsolutePath = (rootFilepath: string, path: string, identifier = ''): string => + joinPath(rootFilepath, path, identifier); + +/** + * Strips the baseRequestURI from the identifier and checks if the stripped base URI matches the store's one. + * @param baseRequestURI - Base URL for requests. + * @param identifier - Incoming identifier. + * @param logger - A logger instance. + * + * @throws {@link NotFoundHttpError} + * If the identifier does not match the baseRequestURI path of the store. + * + * @returns A string representing the relative path. + */ +export const getRelativePath = (baseRequestURI: string, identifier: ResourceIdentifier, logger: Logger): string => { + if (!identifier.path.startsWith(baseRequestURI)) { + logger.warn(`The URL ${identifier.path} is outside of the scope ${baseRequestURI}`); + throw new NotFoundHttpError(); + } + return decodeUriPathComponents(identifier.path.slice(baseRequestURI.length)); +}; + +/** + * Check if the given relative path is valid. + * + * @throws {@link UnsupportedHttpError} + * If the relative path is invalid. + * + * @param path - A relative path, as generated by {@link getRelativePath}. + * @param identifier - A resource identifier. + * @param logger - A logger instance. + */ +export const validateRelativePath = (path: string, identifier: ResourceIdentifier, logger: Logger): void => { + if (!path.startsWith('/')) { + logger.warn(`URL ${identifier.path} needs a / after the base`); + throw new UnsupportedHttpError('URL needs a / after the base'); + } + + if (path.includes('/..')) { + logger.warn(`Disallowed /.. segment in URL ${identifier.path}.`); + throw new UnsupportedHttpError('Disallowed /.. segment in URL'); + } +};