portfolio-page/backend/server.js

81 lines
2.8 KiB
JavaScript

import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import http from 'node:http'
import 'dotenv/config'
import { auditRouter } from './routes/audit.js'
import { env } from './services/env.js'
const app = express()
// Behind a reverse proxy, so trust exactly one proxy hop. Without this the
// rate limiter would see the proxy's IP for everyone and throttle all
// visitors together.
app.set('trust proxy', 1)
app.use(helmet())
// An allowlist, never '*'. ALLOWED_ORIGINS is comma separated.
const allowedOrigins = (env('ALLOWED_ORIGINS', '') ?? '')
.split(',')
.map((origin) => origin.trim().replace(/\/$/, ''))
.filter(Boolean)
// An empty allowlist refuses every browser request, which looks exactly like
// a broken endpoint. Say so at startup instead of at 3am.
if (allowedOrigins.length === 0) {
console.warn(
'WARNING: ALLOWED_ORIGINS is empty - all cross-origin requests will be refused.'
)
}
app.use(
cors({
origin(origin, callback) {
// Same-origin and curl send no Origin header at all.
if (!origin) return callback(null, true)
if (allowedOrigins.includes(origin)) return callback(null, true)
callback(new Error('Origin not allowed'))
},
methods: ['POST'],
allowedHeaders: ['Content-Type'],
})
)
// The form payload is a few hundred bytes. Anything near this cap is abuse.
app.use(express.json({ limit: '10kb' }))
app.get('/health', (_req, res) => res.json({ status: 'ok' }))
app.use('/api/audit-request', auditRouter)
// Keep internals out of the response body.
app.use((error, _req, res, _next) => {
console.error('Unhandled error:', error.message)
res.status(500).json({ success: false, message: 'Internal server error.' })
})
const port = Number(env('PORT', '3001'))
app.listen(port, '127.0.0.1', () => {
console.log(`Audit endpoint listening on 127.0.0.1:${port}`)
})
// Optional second listener for a reverse proxy that itself runs in Docker
// (e.g. Caddy). 127.0.0.1 inside that proxy's container is the container's
// own loopback, not this host's, so it cannot reach the 127.0.0.1 listener
// above at all. The fix is NOT to bind 0.0.0.0 - on a VPS with a public IP
// and no confirmed firewall, that would also accept connections arriving on
// the public interface, bypassing the proxy's TLS termination entirely.
// Binding this second, additional listener to the Docker bridge's gateway
// address instead only accepts traffic arriving over that specific virtual
// network. Find the right value with:
// docker inspect <proxy-container> --format \
// '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}: {{$v.Gateway}}{{"\n"}}{{end}}'
const dockerBridgeHost = env('DOCKER_BRIDGE_HOST')
if (dockerBridgeHost) {
http.createServer(app).listen(port, dockerBridgeHost, () => {
console.log(`Audit endpoint also listening on ${dockerBridgeHost}:${port} (Docker bridge)`)
})
}