Skip to content

Commit de9b665

Browse files
committed
fix: docker sandbox execution, CSP iframe allowlist, and stable test pipeline Docker handling
1 parent 4b53c76 commit de9b665

7 files changed

Lines changed: 102 additions & 21 deletions

File tree

Dockerfile

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Pull Docker CLI binary from official image to avoid apt repo complexity
2+
FROM docker:27-cli AS docker-cli
3+
14
# Multi-stage build using Node 25.2.1
25
FROM node:25.2.1 AS builder
36
WORKDIR /app
@@ -17,25 +20,22 @@ WORKDIR /app
1720
ENV NODE_ENV=production
1821
ENV ARDUINO_CACHE_DIR=/app/server/arduino-cache
1922

20-
# 1. Installiere System-Tools UND Docker-CLI
23+
# 1. Copy Docker CLI binary from official image (no apt repo setup needed)
24+
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
25+
26+
# 2. Install system tools and Arduino CLI
2127
RUN apt-get update && apt-get install -y --no-install-recommends \
2228
curl \
2329
ca-certificates \
2430
tar \
2531
xz-utils \
2632
g++ \
27-
gnupg \
28-
lsb-release \
29-
&& mkdir -p -m 0755 /etc/apt/keyrings \
30-
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
31-
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
32-
&& apt-get update \
33-
&& apt-get install -y docker-ce-cli \
34-
# 2. Arduino CLI Installation
3533
&& curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh \
36-
&& arduino-cli config init \
37-
&& arduino-cli core update-index \
38-
&& arduino-cli core install arduino:avr \
34+
&& mkdir -p /home/node/.arduino15 \
35+
&& chown -R node:node /home/node/.arduino15 \
36+
&& su node -c 'HOME=/home/node arduino-cli config init' \
37+
&& su node -c 'HOME=/home/node arduino-cli core update-index' \
38+
&& su node -c 'HOME=/home/node arduino-cli core install arduino:avr' \
3939
&& apt-get clean \
4040
&& rm -rf /var/lib/apt/lists/* \
4141
&& mkdir -p /app/server/arduino-cache /app/storage/binaries /app/temp
@@ -46,7 +46,8 @@ COPY --from=builder /app/dist ./dist
4646
# Copy package metadata and install dependencies
4747
COPY package.json package-lock.json ./
4848
RUN npm install --legacy-peer-deps --production=false \
49-
&& chown -R node:node /app
49+
&& chown -R node:node /app \
50+
&& usermod -aG root node
5051

5152
# Run as non-root user for production
5253
# node:slim already provides a 'node' user at UID 1000

docker-compose.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ services:
1010
restart: unless-stopped
1111
environment:
1212
- NODE_ENV=production
13-
- ARDUINO_CACHE_DIR=/app/arduino-cache
14-
- UNOSIM_SHARED_TEMP_DIR=/app/temp
13+
- ARDUINO_CACHE_DIR=${PWD}/server/arduino-cache
14+
- UNOSIM_SHARED_TEMP_DIR=${PWD}/temp
1515
- DOCKER_HOST=unix:///var/run/docker.sock
1616
- DOCKER_SANDBOX_IMAGE=unosim-sandbox:latest
1717
ports:
1818
- "3000:3000"
1919
volumes:
20-
- /var/run/docker.sock:/var/run/docker.sock:ro
21-
- ./server/arduino-cache:/app/arduino-cache
22-
- ./temp:/app/temp
20+
- /var/run/docker.sock:/var/run/docker.sock
21+
- ${PWD}/server/arduino-cache:${PWD}/server/arduino-cache
22+
- ${PWD}/temp:${PWD}/temp
2323
- ./storage:/app/storage
2424
healthcheck:
2525
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]

docs/EXTERNAL_API.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ The simulator only processes messages whose `event.origin` exactly matches the d
1212
When the simulator runs top-level (not in an iframe) the effective allowed origin is `"*"` (any).
1313
Messages from all other origins are silently discarded.
1414

15+
> Important: iframe embedding is controlled by the simulator page's own CSP header, not by the parent page.
16+
17+
For local development, the simulator allows embedding from common local test hosts by default:
18+
- `'self'`
19+
- `http://localhost:3000`
20+
- `http://127.0.0.1:3000`
21+
- `http://localhost:5173`
22+
- `http://127.0.0.1:5173`
23+
24+
If your test application runs on `http://localhost:5173`, the simulator origin must explicitly allow that host in its `frame-ancestors` CSP directive.
25+
26+
If you need to allow additional parent origins, set the environment variable `SIMULATOR_ALLOWED_PARENT_ORIGINS` with a comma-separated list of origins before starting the simulator. For example:
27+
28+
```bash
29+
SIMULATOR_ALLOWED_PARENT_ORIGINS=http://localhost:3000
30+
```
31+
32+
This is required when your test website is hosted on a different origin than the simulator.
33+
1534
---
1635

1736
## Inbound Messages (Website → Simulator)

run-tests.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ run_task "Unit-Tests" "NODE_OPTIONS='--no-warnings' npm run test:fast -- --repor
183183
parse_test_results "Tests.*passed"
184184

185185
# 3+4. Sandbox Image Build & Docker-Tests (optional, wenn Docker verfügbar)
186+
# Docker nach den Unit-Tests erneut prüfen: Heavy-Load kann den Daemon kurz einfrieren.
187+
if [ "$DOCKER_AVAILABLE" -eq 1 ] && ! docker info >/dev/null 2>&1; then
188+
echo -e " ${WARN} Docker nach Unit-Tests nicht mehr erreichbar – Docker-Tests werden übersprungen"
189+
DOCKER_AVAILABLE=0
190+
fi
191+
186192
if [ "$DOCKER_AVAILABLE" -eq 1 ]; then
187193
# Sandbox Image nur bauen wenn es noch nicht existiert
188194
if ! docker image inspect "$DOCKER_SANDBOX_IMAGE" > /dev/null 2>&1; then

server/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@ import { getCompilationPool } from "./services/compilation-worker-pool";
1010

1111
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1212

13+
function parseAllowedFrameAncestors(): string[] {
14+
const envValue = process.env.SIMULATOR_ALLOWED_PARENT_ORIGINS ?? process.env.ALLOW_EMBED_ORIGINS;
15+
const defaultOrigins = [
16+
"'self'",
17+
"http://localhost:3000",
18+
"http://127.0.0.1:3000",
19+
"http://localhost:5173",
20+
"http://127.0.0.1:5173",
21+
];
22+
23+
if (!envValue) {
24+
return defaultOrigins;
25+
}
26+
27+
const customOrigins = envValue
28+
.split(",")
29+
.map((origin) => origin.trim())
30+
.filter(Boolean);
31+
32+
return Array.from(new Set([...defaultOrigins, ...customOrigins]));
33+
}
34+
1335
// Cleanup service: Delete old .cleanup.json files and .cleanup directories (> 5 minutes old)
1436
function startCleanupService() {
1537
const CLEANUP_INTERVAL_MS = 60 * 1000; // Check every minute
@@ -61,8 +83,13 @@ function startCleanupService() {
6183
const app = express();
6284

6385
// Security: Helmet adds various HTTP headers for protection
86+
function getFrameAncestorsHeader(): string {
87+
return `frame-ancestors ${parseAllowedFrameAncestors().join(" ")}`;
88+
}
89+
6490
app.use(
6591
helmet({
92+
frameguard: false, // Deactivate X-Frame-Options; we use CSP frame-ancestors instead
6693
contentSecurityPolicy: {
6794
directives: {
6895
defaultSrc: ["'self'"],
@@ -73,11 +100,17 @@ app.use(
73100
fontSrc: ["'self'", "data:", "https://fonts.gstatic.com"],
74101
workerSrc: ["'self'", "blob:", "data:"],
75102
childSrc: ["'self'", "blob:"], // Wichtig für ältere Browser/Playwright
103+
frameAncestors: parseAllowedFrameAncestors(),
76104
},
77105
},
78106
}),
79107
);
80108

109+
app.use((_, res, next) => {
110+
res.setHeader("Content-Security-Policy", getFrameAncestorsHeader());
111+
next();
112+
});
113+
81114
// Security: Rate limiting to prevent DoS attacks
82115
// In test/development mode, use higher limits
83116
const isTestMode =

server/services/arduino-compiler.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -730,9 +730,6 @@ export class ArduinoCompiler {
730730
if (config.buildPath) {
731731
args.push("--build-path", config.buildPath);
732732
}
733-
if (config.buildCachePath) {
734-
args.push("--build-cache-path", config.buildCachePath);
735-
}
736733
args.push(sketchDir);
737734
return args;
738735
}

vite.config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ import { fileURLToPath } from "node:url";
66

77
const __dirname = path.dirname(fileURLToPath(import.meta.url));
88

9+
function getAllowedFrameAncestors(): string {
10+
const envValue = process.env.SIMULATOR_ALLOWED_PARENT_ORIGINS ?? process.env.ALLOW_EMBED_ORIGINS;
11+
const defaultOrigins = [
12+
"'self'",
13+
"http://localhost:3000",
14+
"http://127.0.0.1:3000",
15+
"http://localhost:5173",
16+
"http://127.0.0.1:5173",
17+
];
18+
const customOrigins = envValue
19+
? envValue.split(",").map((origin) => origin.trim()).filter(Boolean)
20+
: [];
21+
22+
const origins = Array.from(new Set([...defaultOrigins, ...customOrigins]));
23+
return `frame-ancestors ${origins.join(" ")}`;
24+
}
25+
926
export default defineConfig({
1027
plugins: [tsconfigPaths(), react()],
1128
resolve: {
@@ -42,6 +59,14 @@ export default defineConfig({
4259
server: {
4360
host: true,
4461
port: 3001, // Vite devserver Port
62+
hmr: {
63+
host: "localhost",
64+
port: 3001,
65+
protocol: "ws",
66+
},
67+
headers: {
68+
"Content-Security-Policy": getAllowedFrameAncestors(),
69+
},
4570
proxy: {
4671
// Leitet API-Aufrufe an Backend auf Port 3000 weiter
4772
"/api": {

0 commit comments

Comments
 (0)