Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/app-runtime/test/runtime/AppStringProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ describe("AppStringProcessor", function () {
const fileResult = await runtime1Session.transportServices.files.uploadOwnFile({
filename: "aFileName",
content: new TextEncoder().encode("aFileContent"),
mimetype: "aMimetype",
mimetype: "text/plain",
expiresAt: CoreDate.utc().add({ minutes: 10 }).toISOString()
});
const file = fileResult.value;
Expand Down Expand Up @@ -502,7 +502,7 @@ describe("AppStringProcessor", function () {
const fileResult = await runtime1Session.transportServices.files.uploadOwnFile({
filename: "aFileName",
content: new TextEncoder().encode("aFileContent"),
mimetype: "aMimetype",
mimetype: "text/plain",
expiresAt: CoreDate.utc().add({ minutes: 10 }).toISOString()
});
const file = fileResult.value;
Expand Down
2 changes: 1 addition & 1 deletion packages/consumption/test/core/TestUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ export class TestUtil {
description: "aDescription",
filename: "aFilename",
filemodified: CoreDate.from("2019-09-30T00:00:00.000Z"),
mimetype: "aMimetype",
mimetype: "text/plain",
expiresAt: parameters?.expiresAt ?? CoreDate.utc().add({ minutes: 5 }),
tags: parameters?.tags
};
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"json-stringify-safe": "^5.0.1",
"lodash": "^4.18.1",
"luxon": "^3.7.2",
"media-typer": "^1.1.1",
"reflect-metadata": "^0.2.2",
"ts-simple-nameof": "^1.3.3"
},
Expand All @@ -105,6 +106,7 @@
"@types/json-stringify-safe": "5.0.3",
"@types/lodash": "4.17.25",
"@types/luxon": "3.7.5",
"@types/media-typer": "0.3.4",
"jwt-decode": "4.0.0",
"openid-client": "6.8.8",
"ts-json-schema-generator": "2.9.0",
Expand Down
47 changes: 47 additions & 0 deletions packages/runtime/src/useCases/transport/files/UploadOwnFile.ts
Comment thread
Milena-Czierlinski marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CoreBuffer } from "@nmshd/crypto";
import { FileDTO } from "@nmshd/runtime-types";
import { AccountController, FileController } from "@nmshd/transport";
import { Inject } from "@nmshd/typescript-ioc";
import { parse as parseMediaType } from "media-typer";
import { nameof } from "ts-simple-nameof";
import { ISO8601DateTimeString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase, ValidationFailure, ValidationResult } from "../../common";
import { FileMapper } from "./FileMapper";
Expand Down Expand Up @@ -40,6 +41,26 @@ class Validator extends SchemaValidator<UploadOwnFileValidatableRequest> {
const validationResult = super.validate(input);
if (!validationResult.isValid()) return validationResult;

const filenameValidationError = this.validateFilename(input.filename);
if (filenameValidationError) {
validationResult.addFailure(
new ValidationFailure(
RuntimeErrors.general.invalidPropertyValue(filenameValidationError),
nameof<UploadOwnFileValidatableRequest>((r) => r.filename)
)
);
}

const mimetypeValidationError = this.validateMimetype(input.mimetype);
if (mimetypeValidationError) {
validationResult.addFailure(
new ValidationFailure(
RuntimeErrors.general.invalidPropertyValue(mimetypeValidationError),
nameof<UploadOwnFileValidatableRequest>((r) => r.mimetype)
)
);
}

if (input.content.byteLength > this._maxFileSize) {
validationResult.addFailure(
new ValidationFailure(
Expand All @@ -60,6 +81,32 @@ class Validator extends SchemaValidator<UploadOwnFileValidatableRequest> {

return validationResult;
}

private validateFilename(filename: string): string | undefined {
const propertyName = nameof<UploadOwnFileValidatableRequest>((r) => r.filename);

if (filename.trim().length === 0) return `'${propertyName}' must not be empty or consist only of whitespace`;
if (filename === "." || filename === "..") return `'${propertyName}' must not be '.' or '..'`;
if (/[\\/]/.test(filename)) return `'${propertyName}' must not contain path separators`;
if (/\p{Cc}/u.test(filename)) return `'${propertyName}' must not contain Unicode control characters`;
if (new TextEncoder().encode(filename).byteLength > 255) return `'${propertyName}' must not exceed 255 UTF-8 bytes`;

return undefined;
}

private validateMimetype(mimetype: string): string | undefined {
const propertyName = nameof<UploadOwnFileValidatableRequest>((r) => r.mimetype);

if (mimetype.trim().length === 0) return `'${propertyName}' must not be empty or consist only of whitespace`;
if (mimetype.trim() !== mimetype) return `'${propertyName}' must not contain leading or trailing whitespace`;

try {
parseMediaType(mimetype);
return undefined;
} catch {
return `'${propertyName}' must be a concrete media type in the form 'type/subtype' without parameters or wildcards`;
}
}
}

export class UploadOwnFileUseCase extends UseCase<UploadOwnFileRequest, FileDTO> {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/test/lib/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export async function makeUploadRequest(values: object = {}): Promise<UploadOwnF
title: "aTitle",
filename: "aFileName",
content: await fs.promises.readFile(fileURLToPath(new URL("../__assets__/test.txt", import.meta.url))),
mimetype: "aMimetype",
mimetype: "text/plain",
description: "aDescription",
expiresAt: DateTime.utc().plus({ minutes: 5 }).toString(),
...values
Expand Down
39 changes: 39 additions & 0 deletions packages/runtime/test/transport/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,45 @@ describe("File upload", () => {
expect(response).toBeSuccessful();
});

test.each(["test.txt", "a file.txt", "ÄÖÜ.txt"])("can upload a file named '%s'", async (filename) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ filename }));

expect(response).toBeSuccessful();
expect(response.value.filename).toBe(filename);
});

test.each([
["an empty filename", ""],
["a filename consisting only of whitespace", " \t "],
["'.' as filename", "."],
["'..' as filename", ".."],
["a forward slash in the filename", "directory/file.txt"],
["a backslash in the filename", "directory\\file.txt"],
["a NUL character in the filename", "file\0.txt"],
["another control character in the filename", "file\u0085.txt"],
["a filename longer than 255 UTF-8 bytes", "ä".repeat(128)]
])("cannot upload a file with %s", async (_description, filename) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ filename }));

expect(response).toBeAnError(/filename/, "error.runtime.validation.invalidPropertyValue");
});

test.each(["text/plain", "application/pdf", "image/svg+xml", "application/vnd.api+json", "IMAGE/PNG"])("can upload a file with MIME type '%s'", async (mimetype) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ mimetype }));

expect(response).toBeSuccessful();
expect(response.value.mimetype).toBe(mimetype);
});

test.each(["", " \t ", "text", "text/", "/plain", "text/pla in", "text/plain; charset=utf-8", "text/*", "*/*", "text/plain\r\n"])(
"cannot upload a file with invalid MIME type '%s'",
async (mimetype) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ mimetype }));

expect(response).toBeAnError(/mimetype/, "error.runtime.validation.invalidPropertyValue");
}
);

test("uploaded files can be accessed under /Files", async () => {
const uploadResponse = await transportServices1.files.uploadOwnFile(await makeUploadRequest());
expect(uploadResponse).toBeSuccessful();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe("FileSync", function () {
description: "aDescription",
expiresAt: CoreDate.utc().add({ minutes: 2 }),
filename: "aFilename.txt",
mimetype: "aMimetype",
mimetype: "text/plain",
title: "aTitle",
buffer: CoreBuffer.fromUtf8("test")
});
Expand Down