fix(dev): proxy local API and restrict frontend upstream
This commit is contained in:
@@ -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%/}
|
||||
case "$THT_FRONTEND_API_UPSTREAM" in
|
||||
http://*|https://*) ;;
|
||||
*)
|
||||
echo "Invalid THT_FRONTEND_API_UPSTREAM: use an internal http(s) upstream" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
if ! /usr/local/bin/validate-frontend-api-upstream "$THT_FRONTEND_API_UPSTREAM"; then
|
||||
echo "Invalid THT_FRONTEND_API_UPSTREAM: expected internal http://core:8787" >&2
|
||||
exit 2
|
||||
fi
|
||||
export THT_FRONTEND_API_UPSTREAM
|
||||
|
||||
if ! envsubst '${THT_FRONTEND_API_UPSTREAM}' \
|
||||
|
||||
@@ -12,6 +12,7 @@ FROM nginxinc/nginx-unprivileged:1.27-alpine AS runtime
|
||||
COPY --from=build /src/dist /usr/share/nginx/html
|
||||
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/validate-frontend-api-upstream.sh /usr/local/bin/validate-frontend-api-upstream
|
||||
ENTRYPOINT ["/usr/local/bin/frontend-entrypoint"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -31,4 +31,27 @@ if rg -n 'BACKEND_BASE_URL|VITE_BACKEND_URL' docker/frontend-entrypoint.sh docke
|
||||
exit 1
|
||||
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"
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
case "${1-}" in
|
||||
http://core:8787|http://core:8787/) exit 0 ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
@@ -4,6 +4,7 @@ import path from "path";
|
||||
|
||||
export default defineConfig(() => {
|
||||
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 {
|
||||
plugins: [react()],
|
||||
// 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/).
|
||||
base: embedBase ?? "/",
|
||||
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") } },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -55,10 +55,10 @@ trap cleanup EXIT INT TERM
|
||||
) &
|
||||
pids+=($!)
|
||||
|
||||
# Frontend: punta al backend reale.
|
||||
# Frontend: il browser usa sempre /api; Vite lo inoltra al backend locale.
|
||||
(
|
||||
cd "$FRONTEND"
|
||||
VITE_BACKEND_URL="http://localhost:$BACKEND_PORT" \
|
||||
THT_FRONTEND_API_UPSTREAM="http://localhost:$BACKEND_PORT" \
|
||||
npm run dev -- --port "$FRONTEND_PORT"
|
||||
) &
|
||||
pids+=($!)
|
||||
|
||||
Executable
+67
@@ -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"
|
||||
Reference in New Issue
Block a user