- XSS: escape serviceName in waking page HTML - Session TTL: 24h expiration with periodic cleanup - Rate limit: 5 login attempts / 15 min per IP - CORS: restrict to same-origin + localhost - SSRF: block localhost/metadata in service targets - UpSnap: log response bodies on auth/wake failures Co-Authored-By: Claude <noreply@anthropic.com>
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
function escapeHtml(s: string): string {
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
export function getWakingPageHtml(serviceName: string): string {
|
|
const safe = escapeHtml(serviceName);
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Waking up — ${safe}</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
background: #0f172a;
|
|
color: #e2e8f0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
}
|
|
.container {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
.spinner {
|
|
width: 48px; height: 48px;
|
|
border: 4px solid #334155;
|
|
border-top-color: #3b82f6;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 1.5rem;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
p { color: #94a3b8; font-size: 0.95rem; }
|
|
.status { margin-top: 1rem; font-size: 0.85rem; color: #64748b; }
|
|
.status.ready { color: #22c55e; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="spinner" id="spinner"></div>
|
|
<h1>Waking up ${safe}...</h1>
|
|
<p>The server is starting. This page will reload automatically.</p>
|
|
<p class="status" id="status">Waiting for response...</p>
|
|
</div>
|
|
<script>
|
|
const CHECK_INTERVAL = 3000;
|
|
let attempt = 0;
|
|
async function check() {
|
|
attempt++;
|
|
const el = document.getElementById('status');
|
|
el.textContent = 'Checking... (attempt ' + attempt + ')';
|
|
try {
|
|
const res = await fetch(window.location.href, { redirect: 'follow' });
|
|
const text = await res.text();
|
|
if (!text.includes('Waking up')) {
|
|
el.textContent = 'Ready! Reloading...';
|
|
el.className = 'status ready';
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
} catch (e) {}
|
|
setTimeout(check, CHECK_INTERVAL);
|
|
}
|
|
setTimeout(check, CHECK_INTERVAL);
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|