security
This commit is contained in:
+98
@@ -0,0 +1,98 @@
|
||||
import { makeAuthToken, parseAuthToken } from "./security";
|
||||
|
||||
export const AUTH_COOKIE = "fh_auth";
|
||||
export const AUTH_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
export function parseCookie(header: string | null, name: string): string | null {
|
||||
if (!header) return null;
|
||||
for (const part of header.split(";")) {
|
||||
const idx = part.indexOf("=");
|
||||
if (idx === -1) continue;
|
||||
if (part.slice(0, idx).trim() === name) return part.slice(idx + 1).trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function hasValidAuth(req: Request, secret: string, key: string): boolean {
|
||||
const token = parseCookie(req.headers.get("cookie"), AUTH_COOKIE);
|
||||
if (!token) return false;
|
||||
const parsed = parseAuthToken(token, secret);
|
||||
return parsed !== null && parsed.key === key;
|
||||
}
|
||||
|
||||
export function authCookieValue(key: string, secret: string): string {
|
||||
const token = makeAuthToken(key, secret, AUTH_TTL_MS);
|
||||
return `${AUTH_COOKIE}=${token}`;
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export function renderPasswordForm(
|
||||
key: string,
|
||||
filename: string,
|
||||
error: boolean,
|
||||
): Response {
|
||||
const message = error
|
||||
? '<p class="err">Incorrect password. Please try again.</p>'
|
||||
: "";
|
||||
const html = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<title>Protected file</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
background: #0f1115; color: #e6e8ee; min-height: 100vh;
|
||||
display: flex; align-items: center; justify-content: center; padding: 16px;
|
||||
}
|
||||
.card {
|
||||
width: 100%; max-width: 380px; background: #171a21; border: 1px solid #2a2f3a;
|
||||
border-radius: 14px; padding: 26px; box-shadow: 0 10px 40px rgba(0,0,0,.35);
|
||||
}
|
||||
h1 { margin: 0 0 4px; font-size: 19px; }
|
||||
p { margin: 6px 0 16px; color: #9aa3b2; font-size: 14px; word-break: break-all; }
|
||||
.err { margin: 0 0 14px; color: #e05252; background: rgba(224,82,82,.08);
|
||||
border: 1px solid rgba(224,82,82,.3); border-radius: 8px; padding: 9px 12px; }
|
||||
input[type="password"] {
|
||||
width: 100%; background: #0f1115; border: 1px solid #2a2f3a; color: #e6e8ee;
|
||||
border-radius: 8px; padding: 11px 12px; font-size: 14px; margin-bottom: 12px;
|
||||
}
|
||||
button { width: 100%; font: inherit; cursor: pointer; border: none; border-radius: 8px;
|
||||
padding: 11px 14px; font-size: 14px; background: #4f8cff; color: #fff; }
|
||||
button:hover { background: #3a74e0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
<h1>Protected file</h1>
|
||||
<p>Enter the password to download <strong>${escapeHtml(filename)}</strong>.</p>
|
||||
${message}
|
||||
<form method="post" action="/api/auth/${key}">
|
||||
<input type="password" name="password" placeholder="Password" autofocus autocomplete="off" />
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
return new Response(html, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"cache-control": "private, no-store",
|
||||
"x-content-type-options": "nosniff",
|
||||
"referrer-policy": "no-referrer",
|
||||
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user