mirror of
https://github.com/RGJorge/ContainerFlow.git
synced 2026-08-03 07:21:42 +02:00
v0.0.23
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
[](https://github.com/RGJorge/containerflow/actions/workflows/ci.yml)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||

|
||||

|
||||

|
||||
[](https://github.com/RGJorge/containerflow/commits/main)
|
||||
|
||||
Real-time Docker architecture visualizer. Displays services, connections and metrics from all your Docker Compose projects in an interactive dashboard.
|
||||
|
||||
@@ -114,6 +118,12 @@ GitHub Actions ejecuta automaticamente en cada push/PR a `main`:
|
||||
|
||||
Ver `.github/workflows/ci.yml`.
|
||||
|
||||
## Seguridad
|
||||
|
||||
- **HTTPS obligatorio en produccion** — el token de autenticacion viaja en headers HTTP. Sin HTTPS, es texto plano visible en la red. Usa un reverse proxy con TLS (nginx, Caddy, Cloudflare Tunnel) delante de ContainerFlow.
|
||||
- **Rate limiting** — incluido por defecto: 5 intentos fallidos por minuto por IP. Despues del limite, retorna `429 Too Many Requests`. Aplica tanto a la API REST como a la autenticacion WebSocket.
|
||||
- **Acceso local por defecto** — sin `AUTH_TOKEN`, el servidor solo escucha en `127.0.0.1`. Con `AUTH_TOKEN`, escucha en `0.0.0.0` para acceso remoto.
|
||||
|
||||
## Stack
|
||||
|
||||
| Componente | Tecnologia |
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 8.5 MiB |
+47
-1
@@ -77,6 +77,39 @@ const HOST = AUTH_TOKEN ? "0.0.0.0" : "127.0.0.1";
|
||||
const POLL_INTERVAL_MS = 5000;
|
||||
const WS_RECONNECT_MS = 3000;
|
||||
|
||||
// ── Rate limiting (in-memory, per IP) ──
|
||||
const RATE_LIMIT_MAX = 5;
|
||||
const RATE_LIMIT_WINDOW_MS = 60_000;
|
||||
const failedAttempts = new Map<string, { count: number; resetAt: number }>();
|
||||
|
||||
function isRateLimited(ip: string): boolean {
|
||||
const entry = failedAttempts.get(ip);
|
||||
if (!entry) return false;
|
||||
if (Date.now() > entry.resetAt) {
|
||||
failedAttempts.delete(ip);
|
||||
return false;
|
||||
}
|
||||
return entry.count >= RATE_LIMIT_MAX;
|
||||
}
|
||||
|
||||
function recordFailedAttempt(ip: string): void {
|
||||
const now = Date.now();
|
||||
const entry = failedAttempts.get(ip);
|
||||
if (!entry || now > entry.resetAt) {
|
||||
failedAttempts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
||||
} else {
|
||||
entry.count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup stale entries every 5 minutes
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [ip, entry] of failedAttempts) {
|
||||
if (now > entry.resetAt) failedAttempts.delete(ip);
|
||||
}
|
||||
}, 5 * 60_000);
|
||||
|
||||
// ── Auth middleware ──
|
||||
if (AUTH_TOKEN) {
|
||||
app.use("*", async (c, next) => {
|
||||
@@ -84,8 +117,14 @@ if (AUTH_TOKEN) {
|
||||
if (c.req.path === "/" || c.req.path.startsWith("/assets") || c.req.path.endsWith(".png") || c.req.path.endsWith(".webp") || c.req.path.endsWith(".ico")) return next();
|
||||
if (c.req.path === "/api/auth") return next();
|
||||
|
||||
const ip = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
||||
if (isRateLimited(ip)) return c.json({ error: "Too many failed attempts. Try again later." }, 429);
|
||||
|
||||
const token = c.req.header("Authorization")?.replace("Bearer ", "");
|
||||
if (token !== AUTH_TOKEN) return c.json({ error: "Unauthorized" }, 401);
|
||||
if (token !== AUTH_TOKEN) {
|
||||
recordFailedAttempt(ip);
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
return next();
|
||||
});
|
||||
}
|
||||
@@ -529,6 +568,12 @@ const server = Bun.serve({
|
||||
|
||||
// Handle authentication via first message
|
||||
if (msg.type === "auth") {
|
||||
const wsIp = (ws as any).remoteAddress || "unknown";
|
||||
if (AUTH_TOKEN && isRateLimited(wsIp)) {
|
||||
native.send(JSON.stringify({ type: "auth_error", reason: "rate_limited" }));
|
||||
native.close();
|
||||
return;
|
||||
}
|
||||
if (msg.token === AUTH_TOKEN) {
|
||||
authenticatedClients.add(native);
|
||||
native.send(JSON.stringify({ type: "auth_ok" }));
|
||||
@@ -543,6 +588,7 @@ const server = Bun.serve({
|
||||
} catch {}
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
if (AUTH_TOKEN) recordFailedAttempt(wsIp);
|
||||
native.send(JSON.stringify({ type: "auth_error" }));
|
||||
native.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user