-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathindex.ts
More file actions
137 lines (119 loc) Β· 4.64 KB
/
index.ts
File metadata and controls
137 lines (119 loc) Β· 4.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/** Proxy requests for files. */
export default {
async fetch(request: Request) {
switch (request.method) {
case "OPTIONS":
return handleOPTIONS(request);
case "GET":
return handleGET(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);
return new Response("", {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers":
"X-Auth-Token, X-Client-Package, X-Client-Version, Range",
"Access-Control-Max-Age": "86400",
},
});
};
const isAllowedOrigin = (origin: string | null) => {
if (!origin) return false;
try {
const url = new URL(origin);
const hostname = url.hostname;
return (
origin == "ente://app" /* desktop app */ ||
hostname.endsWith("ente.io") ||
hostname.endsWith("ente.sh") ||
hostname == "localhost"
);
} catch {
// `origin` is likely an invalid URL.
return false;
}
};
const handleGET = async (request: Request) => {
const url = new URL(request.url);
// Random bots keep trying to pentest causing noise in the logs. If the
// request doesn't have a fileID, we can just safely ignore it thereafter.
const fileID = url.searchParams.get("fileID");
if (!fileID) return new Response(null, { status: 400 });
let token = request.headers.get("X-Auth-Token");
if (!token) {
console.warn("Using deprecated token query param");
token = url.searchParams.get("token");
}
if (!token) {
console.error("No token provided");
// return new Response(null, { status: 400 });
}
// We forward the auth token as a query parameter to museum. This is so that
// it does not get preserved when museum does a redirect to the presigned S3
// URL that serves the actual thumbnail.
//
// See: [Note: Passing credentials for self-hosted file fetches]
const params = new URLSearchParams();
if (token) params.set("token", token);
const headers = {
"X-Client-Package": request.headers.get("X-Client-Package") ?? "",
"X-Client-Version": request.headers.get("X-Client-Version") ?? "",
"User-Agent": request.headers.get("User-Agent") ?? "",
"Range": request.headers.get("Range") ?? "",
"X-Forwarded-For": request.headers.get("CF-Connecting-IP") ?? "",
"CF-IPCountry": request.headers.get("CF-IPCountry") ?? "",
};
let response = await fetch(
`https://api.ente.io/files/download/${fileID}?${params.toString()}`,
{ headers },
);
if (!response.ok) console.log("Upstream error", response.status);
response = new Response(response.body, response);
response.headers.set("Access-Control-Allow-Origin", "*");
hardenResponseHeaders(response.headers);
return response;
};
const hardenResponseHeaders = (headers: Headers) => {
headers.set("X-Content-Type-Options", "nosniff");
headers.set("Content-Security-Policy", "sandbox allow-downloads");
headers.set("X-Frame-Options", "DENY");
headers.set("Referrer-Policy", "no-referrer");
const contentType = headers.get("Content-Type");
if (contentType && isHTMLLikeContentType(contentType)) {
enforceAttachmentDisposition(headers);
}
};
const isHTMLLikeContentType = (contentType: string) => {
const normalized = contentType.split(";")[0]?.trim().toLowerCase();
if (!normalized) return false;
return (
normalized === "text/html" ||
normalized === "application/xhtml+xml" ||
normalized === "image/svg+xml" ||
normalized === "text/xml" ||
normalized === "application/xml"
);
};
const enforceAttachmentDisposition = (headers: Headers) => {
const contentDisposition = headers.get("Content-Disposition");
if (contentDisposition && /attachment/i.test(contentDisposition)) {
return;
}
const filenameParams = contentDisposition
?.split(";")
.map((part) => part.trim())
.filter((part, index) => {
if (index === 0) return false;
return /^filename/i.test(part);
});
const disposition = ["attachment", ...(filenameParams ?? [])].join("; ");
headers.set("Content-Disposition", disposition);
};