diff --git a/infra/workers/uploader/package.json b/infra/workers/uploader/package.json new file mode 100644 index 0000000000..e22b4eb1fc --- /dev/null +++ b/infra/workers/uploader/package.json @@ -0,0 +1,10 @@ +{ + "name": "uploader", + "private": true, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240614.0", + "typescript": "^5", + "wrangler": "^3" + }, + "packageManager": "yarn@1.22.22" +} diff --git a/infra/workers/uploader/src/index.ts b/infra/workers/uploader/src/index.ts new file mode 100644 index 0000000000..d94aeb048c --- /dev/null +++ b/infra/workers/uploader/src/index.ts @@ -0,0 +1,123 @@ +/** + * Proxy file uploads. + * + * See: https://ente.io/blog/tech/making-uploads-faster/ + */ + +export default { + async fetch(request: Request) { + switch (request.method) { + case "OPTIONS": + return handleOPTIONS(request); + case "POST": + return handlePOSTOrPUT(request); + case "PUT": + return handlePOSTOrPUT(request); + default: + console.log(`Unsupported HTTP method ${request.method}`); + return new Response(null, { status: 405 }); + } + }, +} satisfies ExportedHandler; + +const handleOPTIONS = (request: Request) => { + const origin = request.headers.get("Origin"); + if (!isAllowedOrigin(origin)) console.warn("Unknown origin", origin); + const headers = request.headers.get("Access-Control-Request-Headers"); + if (!areAllowedHeaders(headers)) + console.warn("Unknown header in list", headers); + return new Response("", { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, PUT, OPTIONS", + "Access-Control-Max-Age": "86400", + // "Access-Control-Allow-Headers": "Content-Type", "UPLOAD-URL, X-Client-Package", + "Access-Control-Allow-Headers": "*", + "Access-Control-Expose-Headers": "X-Request-Id, CF-Ray", + }, + }); +}; + +const isAllowedOrigin = (origin: string | null) => { + const desktopApp = "ente://app"; + const allowedHostnames = [ + "web.ente.io", + "photos.ente.io", + "photos.ente.sh", + "localhost", + ]; + + if (!origin) return false; + try { + const url = new URL(origin); + return origin == desktopApp || allowedHostnames.includes(url.hostname); + } catch { + // origin is likely an invalid URL + return false; + } +}; + +const areAllowedHeaders = (headers: string | null) => { + const allowed = ["Content-Type", "UPLOAD-URL", "X-Client-Package"]; + + if (!headers) return true; + for (const header of headers.split(",")) { + if (!allowed.includes(header.trim().toLowerCase())) return false; + } + return true; +}; + +const handlePOSTOrPUT = async (request: Request) => { + const url = new URL(request.url); + + const uploadURL = request.headers.get("UPLOAD-URL"); + if (!uploadURL) { + console.error("No uploadURL provided"); + return new Response(null, { status: 400 }); + } + + let response: Response; + switch (url.pathname) { + case "/file-upload": + response = await fetch(uploadURL, { + method: request.method, + body: request.body, + }); + break; + case "/multipart-upload": + response = await fetch(uploadURL, { + method: request.method, + body: request.body, + }); + if (response.ok) { + const etag = response.headers.get("etag"); + if (etag === null) { + console.log("No etag in response", response); + response = new Response(null, { status: 500 }); + } else { + response = new Response(JSON.stringify({ etag })); + } + } + break; + case "/multipart-complete": + response = await fetch(uploadURL, { + method: request.method, + body: request.body, + headers: { + "Content-Type": "text/xml", + }, + }); + break; + default: + response = new Response(null, { status: 404 }); + break; + } + + response = new Response(response.body, response); + response.headers.set("Access-Control-Allow-Origin", "*"); + response.headers.set( + "Access-Control-Expose-Headers", + "X-Request-Id, CF-Ray" + ); + return response; +}; diff --git a/infra/workers/uploader/tsconfig.json b/infra/workers/uploader/tsconfig.json new file mode 100644 index 0000000000..a65b752070 --- /dev/null +++ b/infra/workers/uploader/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../tsconfig.base.json", "include": ["src"] } diff --git a/infra/workers/uploader/wrangler.toml b/infra/workers/uploader/wrangler.toml new file mode 100644 index 0000000000..9a03d8c6d5 --- /dev/null +++ b/infra/workers/uploader/wrangler.toml @@ -0,0 +1,7 @@ +name = "uploader" +main = "src/index.ts" +compatibility_date = "2024-06-14" + +routes = [{ pattern = "uploader.ente.io", custom_domain = true }] + +tail_consumers = [{ service = "tail" }]