fix(dev): proxy local API and restrict frontend upstream

This commit is contained in:
2026-08-04 15:58:13 +02:00
parent 8ffcaf3db9
commit 87b0fda3f6
7 changed files with 114 additions and 9 deletions
+4 -7
View File
@@ -3,13 +3,10 @@ set -eu
THT_FRONTEND_API_UPSTREAM=${THT_FRONTEND_API_UPSTREAM:-http://core:8787} THT_FRONTEND_API_UPSTREAM=${THT_FRONTEND_API_UPSTREAM:-http://core:8787}
THT_FRONTEND_API_UPSTREAM=${THT_FRONTEND_API_UPSTREAM%/} THT_FRONTEND_API_UPSTREAM=${THT_FRONTEND_API_UPSTREAM%/}
case "$THT_FRONTEND_API_UPSTREAM" in if ! /usr/local/bin/validate-frontend-api-upstream "$THT_FRONTEND_API_UPSTREAM"; then
http://*|https://*) ;; echo "Invalid THT_FRONTEND_API_UPSTREAM: expected internal http://core:8787" >&2
*) exit 2
echo "Invalid THT_FRONTEND_API_UPSTREAM: use an internal http(s) upstream" >&2 fi
exit 2
;;
esac
export THT_FRONTEND_API_UPSTREAM export THT_FRONTEND_API_UPSTREAM
if ! envsubst '${THT_FRONTEND_API_UPSTREAM}' \ if ! envsubst '${THT_FRONTEND_API_UPSTREAM}' \
+1
View File
@@ -12,6 +12,7 @@ FROM nginxinc/nginx-unprivileged:1.27-alpine AS runtime
COPY --from=build /src/dist /usr/share/nginx/html COPY --from=build /src/dist /usr/share/nginx/html
COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template
COPY --chmod=755 docker/frontend-entrypoint.sh /usr/local/bin/frontend-entrypoint COPY --chmod=755 docker/frontend-entrypoint.sh /usr/local/bin/frontend-entrypoint
COPY --chmod=755 docker/validate-frontend-api-upstream.sh /usr/local/bin/validate-frontend-api-upstream
ENTRYPOINT ["/usr/local/bin/frontend-entrypoint"] ENTRYPOINT ["/usr/local/bin/frontend-entrypoint"]
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
EXPOSE 8080 EXPOSE 8080
+23
View File
@@ -31,4 +31,27 @@ if rg -n 'BACKEND_BASE_URL|VITE_BACKEND_URL' docker/frontend-entrypoint.sh docke
exit 1 exit 1
fi fi
upstream_validator=docker/validate-frontend-api-upstream.sh
for upstream in http://core:8787 http://core:8787/; do
if ! "$upstream_validator" "$upstream"; then
echo "frontend upstream validator rejected $upstream" >&2
exit 1
fi
done
for upstream in \
https://core:8787 \
http://core:8080 \
http://core:8787/api \
http://user:pass@core:8787 \
'http://core:8787?next=evil' \
'http://core:8787#fragment' \
'http://core:8787 injected' \
'http://core:8787;proxy_pass http://evil'; do
if "$upstream_validator" "$upstream" >/dev/null 2>&1; then
echo "frontend upstream validator accepted unsafe upstream: $upstream" >&2
exit 1
fi
done
echo "frontend same-origin proxy policy: ok" echo "frontend same-origin proxy policy: ok"
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
case "${1-}" in
http://core:8787|http://core:8787/) exit 0 ;;
*) exit 2 ;;
esac
+10
View File
@@ -4,6 +4,7 @@ import path from "path";
export default defineConfig(() => { export default defineConfig(() => {
const embedBase = process.env.VITE_BASE; // "/datamart-builder/assets/" in embedded; undefined = standalone const embedBase = process.env.VITE_BASE; // "/datamart-builder/assets/" in embedded; undefined = standalone
const apiUpstream = process.env.THT_FRONTEND_API_UPSTREAM ?? "http://localhost:8787";
return { return {
plugins: [react()], plugins: [react()],
// base: prefisso pubblico degli asset. Default "/" (standalone). // base: prefisso pubblico degli asset. Default "/" (standalone).
@@ -11,6 +12,15 @@ export default defineConfig(() => {
// /datamart-builder/assets/ → frontend-root mappa 1:1 (niente /assets/assets/). // /datamart-builder/assets/ → frontend-root mappa 1:1 (niente /assets/assets/).
base: embedBase ?? "/", base: embedBase ?? "/",
build: { manifest: true, outDir: "dist", assetsDir: embedBase ? "" : "assets" }, build: { manifest: true, outDir: "dist", assetsDir: embedBase ? "" : "assets" },
server: {
proxy: {
"/api": {
target: apiUpstream,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api(?=\/|$)/, ""),
},
},
},
resolve: { alias: { "@": path.resolve(__dirname, "./src") } }, resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
}; };
}); });
+2 -2
View File
@@ -55,10 +55,10 @@ trap cleanup EXIT INT TERM
) & ) &
pids+=($!) pids+=($!)
# Frontend: punta al backend reale. # Frontend: il browser usa sempre /api; Vite lo inoltra al backend locale.
( (
cd "$FRONTEND" cd "$FRONTEND"
VITE_BACKEND_URL="http://localhost:$BACKEND_PORT" \ THT_FRONTEND_API_UPSTREAM="http://localhost:$BACKEND_PORT" \
npm run dev -- --port "$FRONTEND_PORT" npm run dev -- --port "$FRONTEND_PORT"
) & ) &
pids+=($!) pids+=($!)
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
TMPDIR_TEST="$(mktemp -d)"
PIDS=()
cleanup() {
for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null || true; done
rm -rf "$TMPDIR_TEST"
}
trap cleanup EXIT INT TERM
free_port() {
node -e 'const server = require("node:net").createServer(); server.listen(0, "127.0.0.1", () => { console.log(server.address().port); server.close(); });'
}
backend_port_file="$TMPDIR_TEST/backend-port"
node -e '
const Fastify = require(process.argv[1]);
const fs = require("node:fs");
const app = Fastify();
app.get("/health", async (request) => ({ backend: "fastify", path: request.raw.url }));
app.listen({ port: 0, host: "127.0.0.1" }).then((address) => {
fs.writeFileSync(process.argv[2], String(new URL(address).port));
});
' "$ROOT/backend/node_modules/fastify" "$backend_port_file" >"$TMPDIR_TEST/backend.log" 2>&1 &
PIDS+=("$!")
for _ in {1..50}; do
[ -s "$backend_port_file" ] && break
sleep 0.1
done
[ -s "$backend_port_file" ] || { cat "$TMPDIR_TEST/backend.log" >&2; exit 1; }
backend_port="$(<"$backend_port_file")"
frontend_port="$(free_port)"
THT_FRONTEND_API_UPSTREAM="http://127.0.0.1:$backend_port" \
npm --prefix "$ROOT/frontend" run dev -- --host 127.0.0.1 --port "$frontend_port" \
>"$TMPDIR_TEST/vite.log" 2>&1 &
PIDS+=("$!")
response=""
for _ in {1..50}; do
if response="$(curl -fsS "http://127.0.0.1:$frontend_port/api/health" 2>/dev/null)"; then
break
fi
sleep 0.1
done
if [ "$response" != '{"backend":"fastify","path":"/health"}' ]; then
cat "$TMPDIR_TEST/vite.log" >&2
printf 'expected Vite /api/health to reach Fastify /health, got: %s\n' "$response" >&2
exit 1
fi
if ! rg -Fq 'THT_FRONTEND_API_UPSTREAM="http://localhost:$BACKEND_PORT"' "$ROOT/scripts/run-stack.sh"; then
echo "run-stack.sh must configure the Vite internal upstream from BACKEND_PORT" >&2
exit 1
fi
if rg -q 'VITE_BACKEND_URL' "$ROOT/scripts/run-stack.sh"; then
echo "run-stack.sh must keep the browser base on /api" >&2
exit 1
fi
echo "local browser /api routes to Fastify through the Vite proxy: ok"