-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworker.js
More file actions
130 lines (113 loc) · 4.94 KB
/
Copy pathworker.js
File metadata and controls
130 lines (113 loc) · 4.94 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
// --- SECURITY UTILITIES ---
async function sign(message, secret) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const key = await crypto.subtle.importKey(
"raw", encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false, ["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
async function getVerifiedRole(request, secret) {
const cookieHeader = request.headers.get("Cookie") || "";
const match = cookieHeader.match(/auth=([^;]+)/);
if (!match) return null;
const value = decodeURIComponent(match[1]);
const [role, signature] = value.split('.');
if (!role || !signature) return null;
const expectedSig = await sign(role, secret);
return (signature === expectedSig) ? role : null;
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const path = url.pathname;
const secret = env.SESSION_SECRET || "SUPER_SECRET_FALLBACK_CHANGE_THIS";
// Auth Status
const role = await getVerifiedRole(request, secret);
const isAdmin = role === "admin";
const isGuest = role === "guest";
const authed = isAdmin || isGuest;
// 1. ROUTE: Index
if (path === "/" || path === "/index.html") {
const obj = await env.R2.get("index.html");
if (!obj) return new Response("index.html not found", { status: 404 });
return new Response(obj.body, { headers: { "Content-Type": "text/html; charset=utf-8" } });
}
// 2. ROUTE: Login
if (path === "/login" && request.method === "POST") {
const body = await request.json();
let userRole = "";
if (body.password === env.SITE_PASSWORD) userRole = "admin";
else if (body.password === env.FRIEND_PASSWORD) userRole = "guest";
if (userRole) {
const signature = await sign(userRole, secret);
const cookieValue = `${userRole}.${signature}`;
return new Response("ok", {
headers: {
"Set-Cookie": `auth=${cookieValue}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=604800`
}
});
}
return new Response("wrong", { status: 401 });
}
// 3. ROUTE: Logout (Clears the HttpOnly cookie)
if (path === "/logout") {
return new Response("ok", {
headers: { "Set-Cookie": "auth=; Path=/; HttpOnly; Secure; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:00 GMT" }
});
}
// 4. ROUTE: List
if (path === "/list.json") {
if (!authed) return new Response("unauthorized", { status: 401 });
let files = [];
if (isAdmin) {
const a = await env.R2.list({ prefix: "admin/" });
const g = await env.R2.list({ prefix: "guest/" });
files = [...a.objects, ...g.objects];
} else {
const g = await env.R2.list({ prefix: "guest/" });
files = g.objects;
}
const data = files.map(o => ({ name: o.key, size: o.size, display: o.key.split('/').pop() }));
return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } });
}
// 5. ROUTE: Upload
if (path === "/upload" && request.method === "POST") {
if (!authed) return new Response("unauthorized", { status: 401 });
const form = await request.formData();
const file = form.get("file");
if (!file || typeof file === "string") return new Response("no file", { status: 400 });
if (isGuest) {
const list = await env.R2.list({ prefix: "guest/" });
const total = list.objects.reduce((acc, o) => acc + o.size, 0);
if (total + file.size > 5368709120) return new Response("limit", { status: 403 });
}
const prefix = isAdmin ? "admin/" : "guest/";
await env.R2.put(prefix + file.name, file.stream(), {
httpMetadata: { contentType: file.type || "application/octet-stream" }
});
return new Response("ok");
}
// 6. ROUTE: Delete
if (path === "/delete" && request.method === "POST") {
if (!isAdmin) return new Response("unauthorized", { status: 401 });
const body = await request.json();
await env.R2.delete(body.name);
return new Response("ok");
}
// 7. ROUTE: Serve Files (With Permission Check)
if (path.length > 1) {
const key = decodeURIComponent(path.slice(1));
// Security: Prevent guests from accessing admin folder via direct URL
if (!isAdmin && key.startsWith("admin/")) return new Response("unauthorized", { status: 401 });
if (!authed) return new Response("unauthorized", { status: 401 });
const obj = await env.R2.get(key);
if (!obj) return new Response("not found", { status: 404 });
return new Response(obj.body, { headers: { "Content-Type": obj.httpMetadata?.contentType || "application/octet-stream" } });
}
return new Response("not found", { status: 404 });
}
}