From bb73b302b95e05cdbb8fd5fb5259802405c289a7 Mon Sep 17 00:00:00 2001 From: Walker Aldridge Date: Sat, 23 Mar 2024 16:45:51 -0500 Subject: [PATCH 1/2] Work in progress --- src/engine/IErrorProcessorEngine.ts | 16 +++++++++++++ .../ProcessUploadErrorProcessorEngine.ts | 23 +++++++++++++++++++ src/factory/HttpErrorFactory.ts | 8 ++++++- src/filters/HttpExceptionFilter.ts | 4 ++++ src/model/di/tokens.ts | 1 + .../exceptions/ProcessUploadException.ts | 12 ++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/engine/IErrorProcessorEngine.ts create mode 100644 src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts create mode 100644 src/model/exceptions/ProcessUploadException.ts diff --git a/src/engine/IErrorProcessorEngine.ts b/src/engine/IErrorProcessorEngine.ts new file mode 100644 index 0000000..1021805 --- /dev/null +++ b/src/engine/IErrorProcessorEngine.ts @@ -0,0 +1,16 @@ +import type { HttpErrorRenderObj } from "../utils/typeings.js"; +import { Exception } from "@tsed/exceptions"; + +export interface IErrorProcessorEngine { + /** + * Process error + * @param obj + */ + process(obj: HttpErrorRenderObj): boolean; + + /** + * Returns true if this render engine supports the exception thrown by the system + * @param exception + */ + supportsError(exception: Exception): boolean; +} diff --git a/src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts b/src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts new file mode 100644 index 0000000..ad29745 --- /dev/null +++ b/src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts @@ -0,0 +1,23 @@ +import { ERROR_PROCESSOR_ENGINE } from "../../../model/di/tokens.js"; +import { Injectable, ProviderScope } from "@tsed/di"; +import { Exception } from "@tsed/exceptions"; +import { ProcessUploadException } from "../../../model/exceptions/ProcessUploadException.js"; +import type { IErrorProcessorEngine } from "../../IErrorProcessorEngine.js"; +import { HttpErrorRenderObj } from "../../../utils/typeings.js"; + +@Injectable({ + scope: ProviderScope.SINGLETON, + type: ERROR_PROCESSOR_ENGINE, +}) +export class ProcessUploadErrorProcessorEngine implements IErrorProcessorEngine { + public supportsError(exception: Exception): boolean { + return exception instanceof ProcessUploadException; + } + + public process(obj: HttpErrorRenderObj): boolean { + if (obj.status == 500) { + return false; + } + return true; + } +} diff --git a/src/factory/HttpErrorFactory.ts b/src/factory/HttpErrorFactory.ts index 0635160..a759cf7 100644 --- a/src/factory/HttpErrorFactory.ts +++ b/src/factory/HttpErrorFactory.ts @@ -1,7 +1,8 @@ import { Inject, Injectable, ProviderScope } from "@tsed/di"; import { Exception } from "@tsed/exceptions"; import type { IHttpErrorRenderEngine } from "../engine/IHttpErrorRenderEngine.js"; -import { HTTP_RENDER_ENGINE } from "../model/di/tokens.js"; +import type { IErrorProcessorEngine } from "../engine/IErrorProcessorEngine.js"; +import { ERROR_PROCESSOR_ENGINE, HTTP_RENDER_ENGINE } from "../model/di/tokens.js"; import { DefaultHttpRenderEngine } from "../engine/impl/index.js"; @Injectable({ @@ -12,6 +13,7 @@ export class HttpErrorFactory { public constructor( @Inject(HTTP_RENDER_ENGINE) private readonly engines: IHttpErrorRenderEngine[], + @Inject(ERROR_PROCESSOR_ENGINE) private readonly processors: IErrorProcessorEngine[], ) { this.defaultRenderEngine = engines.find(engine => engine instanceof DefaultHttpRenderEngine)!; } @@ -19,4 +21,8 @@ export class HttpErrorFactory { public getRenderEngine(exception: Exception): IHttpErrorRenderEngine { return this.engines.find(engine => engine.supportsError(exception)) ?? this.defaultRenderEngine; } + + public getErrorProcessor(exception: Exception): IErrorProcessorEngine | null { + return this.processors.find(processor => processor.supportsError(exception)) ?? null; + } } diff --git a/src/filters/HttpExceptionFilter.ts b/src/filters/HttpExceptionFilter.ts index 3765d07..5418414 100644 --- a/src/filters/HttpExceptionFilter.ts +++ b/src/filters/HttpExceptionFilter.ts @@ -14,12 +14,16 @@ export class HttpExceptionFilter implements ExceptionFilterMethods { public async catch(exception: Exception, ctx: PlatformContext): Promise { const renderEngine = this.httpErrorFactory.getRenderEngine(exception); + const processorEngine = this.httpErrorFactory.getErrorProcessor(exception); const obj: HttpErrorRenderObj = { status: exception.status, message: exception.message, internalError: exception, }; const response = ctx.response; + if (processorEngine) { + await processorEngine.process(obj); + } const template = await renderEngine.render(obj, response); response.status(exception.status).body(template); } diff --git a/src/model/di/tokens.ts b/src/model/di/tokens.ts index 868bbf4..7fb5e8a 100644 --- a/src/model/di/tokens.ts +++ b/src/model/di/tokens.ts @@ -1,4 +1,5 @@ export const SQLITE_DATA_SOURCE = Symbol.for("SqliteDataSource"); export const HTTP_RENDER_ENGINE = Symbol.for("IHttpErrorRenderEngine"); +export const ERROR_PROCESSOR_ENGINE = Symbol.for("IErrorProcessorEngine"); export const AV_ENGINE = Symbol.for("IAvEngine"); export const CAPTCHA_ENGINE = Symbol.for("ICaptchaEngine"); diff --git a/src/model/exceptions/ProcessUploadException.ts b/src/model/exceptions/ProcessUploadException.ts new file mode 100644 index 0000000..b254da5 --- /dev/null +++ b/src/model/exceptions/ProcessUploadException.ts @@ -0,0 +1,12 @@ +import { HTTPException } from "@tsed/exceptions"; + +export class ProcessUploadException extends HTTPException { + public constructor( + status: number, + message?: string, + public filePath?: string, + origin?: Error | string, + ) { + super(status, message, origin); + } +} From d4f960281bc7b37d06a64f8759392193dfa44974 Mon Sep 17 00:00:00 2001 From: Walker Aldridge Date: Sat, 23 Mar 2024 19:13:41 -0500 Subject: [PATCH 2/2] Complete Exception processor --- src/engine/IErrorProcessorEngine.ts | 2 +- .../ProcessUploadErrorProcessorEngine.ts | 8 +- src/engine/impl/index.ts | 9 +- src/services/FileService.ts | 96 ++++++++++--------- 4 files changed, 64 insertions(+), 51 deletions(-) diff --git a/src/engine/IErrorProcessorEngine.ts b/src/engine/IErrorProcessorEngine.ts index 1021805..2d7e677 100644 --- a/src/engine/IErrorProcessorEngine.ts +++ b/src/engine/IErrorProcessorEngine.ts @@ -6,7 +6,7 @@ export interface IErrorProcessorEngine { * Process error * @param obj */ - process(obj: HttpErrorRenderObj): boolean; + process(obj: HttpErrorRenderObj): Promise; /** * Returns true if this render engine supports the exception thrown by the system diff --git a/src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts b/src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts index ad29745..b866ab7 100644 --- a/src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts +++ b/src/engine/impl/ErrorProcessors/ProcessUploadErrorProcessorEngine.ts @@ -4,6 +4,8 @@ import { Exception } from "@tsed/exceptions"; import { ProcessUploadException } from "../../../model/exceptions/ProcessUploadException.js"; import type { IErrorProcessorEngine } from "../../IErrorProcessorEngine.js"; import { HttpErrorRenderObj } from "../../../utils/typeings.js"; +import { FileUtils } from "../../../utils/Utils.js"; +import path from "node:path"; @Injectable({ scope: ProviderScope.SINGLETON, @@ -14,9 +16,9 @@ export class ProcessUploadErrorProcessorEngine implements IErrorProcessorEngine< return exception instanceof ProcessUploadException; } - public process(obj: HttpErrorRenderObj): boolean { - if (obj.status == 500) { - return false; + public async process(obj: HttpErrorRenderObj): Promise { + if (obj.internalError.filePath) { + await FileUtils.deleteFile(path.basename(obj.internalError.filePath), true); } return true; } diff --git a/src/engine/impl/index.ts b/src/engine/impl/index.ts index d3d1e83..a740727 100644 --- a/src/engine/impl/index.ts +++ b/src/engine/impl/index.ts @@ -2,13 +2,14 @@ * @file Automatically generated by barrelsby. */ +export * from "./ErrorProcessors/ProcessUploadErrorProcessorEngine.js"; +export * from "./HttpErrorRenderers/AuthenticationErrorRenderEngine.js"; +export * from "./HttpErrorRenderers/DefaultHttpRenderEngine.js"; +export * from "./HttpErrorRenderers/FileProtectedRenderEngine.js"; +export * from "./HttpErrorRenderers/ReCAPTCHALoginExceptionRenderEngine.js"; export * from "./av/ClamAvEngine.js"; export * from "./av/MsDefenderEngine.js"; export * from "./captcha/AbstractCaptchaEngine.js"; export * from "./captcha/HcaptchaEngine.js"; export * from "./captcha/ReCAPTCHAEngine.js"; export * from "./captcha/TurnstileCaptchaEngine.js"; -export * from "./HttpErrorRenderers/AuthenticationErrorRenderEngine.js"; -export * from "./HttpErrorRenderers/DefaultHttpRenderEngine.js"; -export * from "./HttpErrorRenderers/FileProtectedRenderEngine.js"; -export * from "./HttpErrorRenderers/ReCAPTCHALoginExceptionRenderEngine.js"; diff --git a/src/services/FileService.ts b/src/services/FileService.ts index 5422a86..88e22d1 100644 --- a/src/services/FileService.ts +++ b/src/services/FileService.ts @@ -12,7 +12,7 @@ import { FileUploadResponseDto } from "../model/dto/FileUploadResponseDto.js"; import GlobalEnv from "../model/constants/GlobalEnv.js"; import { Logger } from "@tsed/logger"; import type { EntrySettings } from "../utils/typeings.js"; -import { BadRequest, InternalServerError, NotFound, UnsupportedMediaType } from "@tsed/exceptions"; +import { BadRequest, Exception, InternalServerError, NotFound, UnsupportedMediaType } from "@tsed/exceptions"; import { FileUtils, ObjectUtils } from "../utils/Utils.js"; import TimeUnit from "../model/constants/TimeUnit.js"; import argon2 from "argon2"; @@ -21,6 +21,7 @@ import { EncryptionService } from "./EncryptionService.js"; import { RecordInfoSocket } from "./socket/RecordInfoSocket.js"; import { EntryModificationDto } from "../model/dto/EntryModificationDto.js"; import { FileUploadParameters } from "../model/rest/FileUploadParameters.js"; +import { ProcessUploadException } from "../model/exceptions/ProcessUploadException.js"; @Service() export class FileService { @@ -46,56 +47,65 @@ export class FileService { { password, hideFilename, expires }: FileUploadParameters, secretToken?: string, ): Promise<[FileUploadResponseDto, boolean]> { - const token = crypto.randomUUID(); - const uploadEntry = Builder(FileUploadModel).ip(ip).token(token); - const [resourcePath, originalFileName] = await this.determineResourcePathAndFileName(source); - uploadEntry.fileName(path.parse(resourcePath).name); - await this.scanFile(resourcePath); - await this.checkMime(resourcePath); - const mediaType = await this.mimeService.findMimeType(resourcePath); - uploadEntry.mediaType(mediaType); - const fileSize = await FileUtils.getFileSize(path.basename(resourcePath)); - uploadEntry.fileSize(fileSize); - const checksum = await this.getFileHash(resourcePath); + let resourcePath: string | undefined; + let originalFileName: string | undefined; + try { + const token = crypto.randomUUID(); + const uploadEntry = Builder(FileUploadModel).ip(ip).token(token); + [resourcePath, originalFileName] = await this.determineResourcePathAndFileName(source); + uploadEntry.fileName(path.parse(resourcePath).name); + await this.scanFile(resourcePath); + await this.checkMime(resourcePath); + const mediaType = await this.mimeService.findMimeType(resourcePath); + uploadEntry.mediaType(mediaType); + const fileSize = await FileUtils.getFileSize(path.basename(resourcePath)); + uploadEntry.fileSize(fileSize); + const checksum = await this.getFileHash(resourcePath); - const existingFileModel = await this.handleExistingFileModel(resourcePath, checksum, ip); - if (existingFileModel) { - if (existingFileModel.hasExpired) { - await this.processDelete([existingFileModel.token], true); - } else { - return [FileUploadResponseDto.fromModel(existingFileModel, this.baseUrl, true), true]; + const existingFileModel = await this.handleExistingFileModel(resourcePath, checksum, ip); + if (existingFileModel) { + if (existingFileModel.hasExpired) { + await this.processDelete([existingFileModel.token], true); + } else { + return [FileUploadResponseDto.fromModel(existingFileModel, this.baseUrl, true), true]; + } } - } - uploadEntry.settings(await this.buildEntrySettings(hideFilename, password)); + uploadEntry.settings(await this.buildEntrySettings(hideFilename, password)); - const ext = FileUtils.getExtension(originalFileName); - if (ext) { - uploadEntry.fileExtension(ext); - } - uploadEntry.originalFileName(originalFileName); - uploadEntry.checksum(checksum); - if (expires) { - this.calculateCustomExpires(uploadEntry, expires, secretToken); - } else if (secretToken !== this.secret) { - uploadEntry.expires(FileUtils.getExpiresBySize(fileSize)); - } + const ext = FileUtils.getExtension(originalFileName); + if (ext) { + uploadEntry.fileExtension(ext); + } + uploadEntry.originalFileName(originalFileName); + uploadEntry.checksum(checksum); + if (expires) { + this.calculateCustomExpires(uploadEntry, expires, secretToken); + } else if (secretToken !== this.secret) { + uploadEntry.expires(FileUtils.getExpiresBySize(fileSize)); + } - if (password) { - try { - const didEncrypt = await this.encryptionService.encrypt(resourcePath, password); - uploadEntry.encrypted(didEncrypt !== null); - } catch (e) { - await this.deleteUploadedFile(resourcePath); - this.logger.error(e.message); - throw new InternalServerError(e.message); + if (password) { + try { + const didEncrypt = await this.encryptionService.encrypt(resourcePath, password); + uploadEntry.encrypted(didEncrypt !== null); + } catch (e) { + await this.deleteUploadedFile(resourcePath); + this.logger.error(e.message); + throw new InternalServerError(e.message); + } } - } - const savedEntry = await this.repo.saveEntry(uploadEntry.build()); + const savedEntry = await this.repo.saveEntry(uploadEntry.build()); - await this.recordInfoSocket.emit(); + await this.recordInfoSocket.emit(); - return [FileUploadResponseDto.fromModel(savedEntry, this.baseUrl, true), false]; + return [FileUploadResponseDto.fromModel(savedEntry, this.baseUrl, true), false]; + } catch (e) { + if (e instanceof Exception) { + throw new ProcessUploadException(e.status, e.message, resourcePath, e); + } + throw e; + } } private hashPassword(password: string): Promise {