mirror of
https://github.com/C4illin/ConvertX.git
synced 2026-10-28 07:44:21 +01:00
Disable url upload feature by default
This commit is contained in:
@@ -94,6 +94,7 @@ All are optional, JWT_SECRET is recommended to be set.
|
||||
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
|
||||
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
|
||||
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
|
||||
| ALLOW_URL_UPLOAD | false | Allow uploading/converting files from URLs |
|
||||
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
|
||||
| WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
|
||||
| FFMPEG_ARGS | | Arguments to pass to the input file of ffmpeg, e.g. `-hwaccel vaapi`. See https://github.com/C4illin/ConvertX/issues/190 for more info about hw-acceleration. |
|
||||
|
||||
@@ -6,6 +6,8 @@ export const HTTP_ALLOWED = process.env.HTTP_ALLOWED?.toLowerCase() === "true" |
|
||||
export const ALLOW_UNAUTHENTICATED =
|
||||
process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
|
||||
|
||||
export const ALLOW_URL_UPLOAD = process.env.ALLOW_URL_UPLOAD?.toLowerCase() === "true" || false;
|
||||
|
||||
export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
|
||||
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
|
||||
: 24;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import mime from "mime";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export const getFilename = (urlStr: string, headers: Headers): string => {
|
||||
const contentType = headers.get("content-type")?.split(";")[0]?.trim();
|
||||
const extension = contentType ? mime.getExtension(contentType) : null;
|
||||
|
||||
let candidate = "";
|
||||
const contentDisposition = headers.get("Content-Disposition");
|
||||
if (contentDisposition) {
|
||||
const utf8Match = /filename\*=UTF-8''([^;\s]+)/i.exec(contentDisposition);
|
||||
if (utf8Match && utf8Match[1]) {
|
||||
try {
|
||||
candidate = decodeURIComponent(utf8Match[1]);
|
||||
} catch {
|
||||
// ignore malformed URI
|
||||
}
|
||||
}
|
||||
if (!candidate) {
|
||||
const match = /filename=(?:"([^"]+)"|([^;\s]+))/i.exec(contentDisposition);
|
||||
if (match) {
|
||||
candidate = match[1] || match[2] || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!candidate) {
|
||||
try {
|
||||
const path = new URL(urlStr).pathname;
|
||||
const lastPart = path.split("/").filter(Boolean).at(-1);
|
||||
if (lastPart) {
|
||||
candidate = decodeURIComponent(lastPart);
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid URL path
|
||||
}
|
||||
}
|
||||
|
||||
const sanitized = sanitize(candidate).trim().replace(/^\.+/, "");
|
||||
|
||||
if (!sanitized) {
|
||||
return extension ? `${randomUUID()}.${extension}` : randomUUID();
|
||||
}
|
||||
|
||||
if (!sanitized.includes(".") && extension) {
|
||||
return `${sanitized}.${extension}`;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export function validateSafeUrl(urlString: string): URL {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(urlString);
|
||||
} catch {
|
||||
throw new Error("Invalid URL format");
|
||||
}
|
||||
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
throw new Error("Only HTTP and HTTPS URLs are supported");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { getFilename } from "../../src/helpers/getFilename";
|
||||
|
||||
describe("getFilename", () => {
|
||||
test("extracts filename from simple URL", () => {
|
||||
const headers = new Headers();
|
||||
const result = getFilename("https://example.com/cat.jpg", headers);
|
||||
expect(result).toBe("cat.jpg");
|
||||
});
|
||||
|
||||
test("extracts filename from Content-Disposition header", () => {
|
||||
const headers = new Headers({
|
||||
"Content-Disposition": 'attachment; filename="cute-dog.png"',
|
||||
});
|
||||
const result = getFilename("https://example.com/download?id=123", headers);
|
||||
expect(result).toBe("cute-dog.png");
|
||||
});
|
||||
|
||||
test("extracts RFC 5987 encoded filename from Content-Disposition header", () => {
|
||||
const headers = new Headers({
|
||||
"Content-Disposition": "attachment; filename*=UTF-8''my%20document.pdf",
|
||||
});
|
||||
const result = getFilename("https://example.com/file", headers);
|
||||
expect(result).toBe("my document.pdf");
|
||||
});
|
||||
|
||||
test("appends MIME extension when filename has no extension", () => {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "image/png",
|
||||
});
|
||||
const result = getFilename("https://example.com/myimage", headers);
|
||||
expect(result).toBe("myimage.png");
|
||||
});
|
||||
|
||||
test("generates fallback filename when remote supplies empty or all-dot filename", () => {
|
||||
const headers = new Headers({
|
||||
"Content-Disposition": 'attachment; filename="...."',
|
||||
"Content-Type": "application/pdf",
|
||||
});
|
||||
const result = getFilename("https://example.com/...", headers);
|
||||
expect(result.endsWith(".pdf")).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(4);
|
||||
expect(result).not.toContain("..");
|
||||
});
|
||||
|
||||
test("generates fallback UUID when URL has no filename and no Content-Disposition", () => {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "text/plain",
|
||||
});
|
||||
const result = getFilename("https://example.com/", headers);
|
||||
expect(result.endsWith(".txt")).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { validateSafeUrl } from "../src/helpers/validateUrl";
|
||||
|
||||
describe("validateSafeUrl", () => {
|
||||
test("allows valid HTTP and HTTPS URLs", () => {
|
||||
expect(() => validateSafeUrl("https://example.com/cat.jpg")).not.toThrow();
|
||||
expect(() => validateSafeUrl("http://example.com/image.png")).not.toThrow();
|
||||
});
|
||||
|
||||
test("allows local IP and LAN addresses for self-hosted usage", () => {
|
||||
expect(() => validateSafeUrl("http://localhost:3000/test.jpg")).not.toThrow();
|
||||
expect(() => validateSafeUrl("http://192.168.1.50/file.png")).not.toThrow();
|
||||
expect(() => validateSafeUrl("http://10.0.0.5/doc.pdf")).not.toThrow();
|
||||
expect(() => validateSafeUrl("http://nas.local/data.zip")).not.toThrow();
|
||||
});
|
||||
|
||||
test("rejects non-http/https protocols", () => {
|
||||
expect(() => validateSafeUrl("file:///etc/passwd")).toThrow(
|
||||
"Only HTTP and HTTPS URLs are supported",
|
||||
);
|
||||
expect(() => validateSafeUrl("ftp://example.com/file")).toThrow(
|
||||
"Only HTTP and HTTPS URLs are supported",
|
||||
);
|
||||
expect(() => validateSafeUrl("gopher://example.com")).toThrow(
|
||||
"Only HTTP and HTTPS URLs are supported",
|
||||
);
|
||||
expect(() => validateSafeUrl("javascript:alert(1)")).toThrow(
|
||||
"Only HTTP and HTTPS URLs are supported",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid URL formats", () => {
|
||||
expect(() => validateSafeUrl("not-a-url")).toThrow("Invalid URL format");
|
||||
expect(() => validateSafeUrl("://bad.url")).toThrow("Invalid URL format");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user