MyAgent / space-access-proxy.mjs
cjovs's picture
Add external password gate and fix custom model config
adc74c4 verified
Raw
History Blame Contribute Delete
8.96 kB
import http from 'node:http'
import https from 'node:https'
import { createHmac, timingSafeEqual } from 'node:crypto'
const publicPort = Number(process.env.PORT || '7860')
const upstreamUrl = new URL(
process.env.WORKSPACE_UPSTREAM_URL ||
`http://127.0.0.1:${process.env.WORKSPACE_INTERNAL_PORT || '7861'}`,
)
const accessPassword = process.env.SPACE_ACCESS_PASSWORD || ''
const sessionSecret = process.env.SPACE_SESSION_SECRET || accessPassword
const cookieName = 'hermes-space-access'
const loginPath = '/__space_auth/login'
const logoutPath = '/__space_auth/logout'
const healthPath = '/__space_auth/health'
const authorizedToken = signToken('authorized')
if (!accessPassword) {
console.error(
'[space-access] Missing SPACE_ACCESS_PASSWORD. Configure it as a Hugging Face Space secret.',
)
process.exit(1)
}
function signToken(scope) {
return createHmac('sha256', sessionSecret)
.update(`${scope}:${accessPassword}`)
.digest('base64url')
}
function safeEqual(left, right) {
const leftBuffer = Buffer.from(left)
const rightBuffer = Buffer.from(right)
if (leftBuffer.length !== rightBuffer.length) {
return false
}
return timingSafeEqual(leftBuffer, rightBuffer)
}
function parseCookies(cookieHeader = '') {
const cookies = {}
for (const chunk of cookieHeader.split(';')) {
const trimmed = chunk.trim()
if (!trimmed) continue
const separatorIndex = trimmed.indexOf('=')
if (separatorIndex === -1) continue
const key = trimmed.slice(0, separatorIndex)
const value = trimmed.slice(separatorIndex + 1)
cookies[key] = value
}
return cookies
}
function isAuthorized(request) {
const cookies = parseCookies(request.headers.cookie)
const token = cookies[cookieName]
if (!token) return false
return safeEqual(token, authorizedToken)
}
function wantsHtml(request) {
const accept = request.headers.accept || ''
return accept.includes('text/html')
}
function readJsonBody(request) {
return new Promise((resolve, reject) => {
const chunks = []
let size = 0
request.on('data', (chunk) => {
size += chunk.length
if (size > 16 * 1024) {
reject(new Error('Body too large'))
request.destroy()
return
}
chunks.push(chunk)
})
request.on('end', () => {
try {
const raw = Buffer.concat(chunks).toString('utf8')
resolve(raw ? JSON.parse(raw) : {})
} catch (error) {
reject(error)
}
})
request.on('error', reject)
})
}
function sendJson(response, statusCode, payload, extraHeaders = {}) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store',
...extraHeaders,
})
response.end(JSON.stringify(payload))
}
function sendPromptPage(response) {
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Protected Workspace</title>
<style>
:root {
color-scheme: light dark;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background:
radial-gradient(circle at top, rgba(97, 126, 255, 0.18), transparent 40%),
linear-gradient(180deg, #f5f7fb 0%, #e9eef7 100%);
color: #182131;
}
.card {
width: min(460px, calc(100vw - 32px));
border: 1px solid rgba(24, 33, 49, 0.12);
border-radius: 20px;
padding: 28px;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 20px 60px rgba(24, 33, 49, 0.14);
}
h1 {
margin: 0 0 10px;
font-size: 1.2rem;
}
p {
margin: 0;
line-height: 1.6;
color: #4c5b73;
}
button {
margin-top: 18px;
border: 0;
border-radius: 999px;
padding: 10px 16px;
background: #1f4bff;
color: white;
font: inherit;
cursor: pointer;
}
button:hover {
background: #173ae0;
}
#status {
margin-top: 14px;
font-size: 0.95rem;
}
</style>
</head>
<body>
<main class="card">
<h1>Workspace Locked</h1>
<p>This Space is protected. Enter the access password in the browser prompt to continue.</p>
<button id="retry" type="button">Enter Password</button>
<p id="status">Nothing inside the workspace will load until the correct password is entered.</p>
</main>
<script>
const status = document.getElementById('status')
async function requestPassword(message) {
const password = window.prompt(message || 'Enter the access password to continue.')
if (password === null) {
status.textContent = 'Access is still locked. Press "Enter Password" when you are ready.'
return
}
const response = await fetch('${loginPath}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
})
if (response.ok) {
status.textContent = 'Password accepted. Loading workspace...'
window.location.reload()
return
}
status.textContent = 'Incorrect password. Try again.'
window.setTimeout(() => {
void requestPassword('Incorrect password. Enter the access password to continue.')
}, 50)
}
document.getElementById('retry').addEventListener('click', () => {
void requestPassword('Enter the access password to continue.')
})
window.addEventListener('load', () => {
void requestPassword('Enter the access password to continue.')
})
</script>
</body>
</html>`
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
})
response.end(html)
}
function proxyRequest(clientRequest, clientResponse) {
const proxyModule = upstreamUrl.protocol === 'https:' ? https : http
const headers = { ...clientRequest.headers }
headers.host = upstreamUrl.host
headers['x-forwarded-host'] = clientRequest.headers.host || ''
headers['x-forwarded-proto'] = 'https'
headers['x-forwarded-port'] = String(publicPort)
const upstreamRequest = proxyModule.request(
{
protocol: upstreamUrl.protocol,
hostname: upstreamUrl.hostname,
port: upstreamUrl.port,
method: clientRequest.method,
path: clientRequest.url,
headers,
},
(upstreamResponse) => {
const responseHeaders = { ...upstreamResponse.headers }
clientResponse.writeHead(upstreamResponse.statusCode || 502, responseHeaders)
upstreamResponse.pipe(clientResponse)
},
)
upstreamRequest.on('error', (error) => {
console.error('[space-access] Upstream request failed:', error)
sendJson(clientResponse, 502, {
ok: false,
error: 'Workspace upstream is unavailable.',
})
})
clientRequest.pipe(upstreamRequest)
}
const server = http.createServer(async (request, response) => {
const url = new URL(request.url || '/', `http://${request.headers.host || '127.0.0.1'}`)
if (url.pathname === healthPath) {
sendJson(response, 200, {
ok: true,
upstream: upstreamUrl.toString(),
})
return
}
if (url.pathname === logoutPath && request.method === 'POST') {
sendJson(
response,
200,
{ ok: true },
{
'Set-Cookie': `${cookieName}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`,
},
)
return
}
if (url.pathname === loginPath && request.method === 'POST') {
try {
const body = await readJsonBody(request)
const submittedPassword =
body && typeof body.password === 'string' ? body.password : ''
if (safeEqual(submittedPassword, accessPassword)) {
sendJson(
response,
200,
{ ok: true },
{
'Set-Cookie': `${cookieName}=${authorizedToken}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${30 * 24 * 60 * 60}`,
},
)
return
}
sendJson(response, 401, { ok: false, error: 'Invalid password' })
return
} catch (error) {
sendJson(response, 400, {
ok: false,
error: error instanceof Error ? error.message : 'Invalid request',
})
return
}
}
if (!isAuthorized(request)) {
if (wantsHtml(request)) {
sendPromptPage(response)
return
}
sendJson(response, 401, {
ok: false,
error: 'Authentication required',
})
return
}
proxyRequest(request, response)
})
server.listen(publicPort, '0.0.0.0', () => {
console.log(
`[space-access] Password gate listening on http://0.0.0.0:${publicPort} -> ${upstreamUrl.toString()}`,
)
})