From 8dac82dfb2decbf75d9c787a8359399096262bae Mon Sep 17 00:00:00 2001 From: RaizeTheLimit Date: Wed, 12 Nov 2025 18:08:56 +0700 Subject: [PATCH 1/3] Docker support and simple web authentication --- .dockerignore | 55 +++++++++++ Dockerfile | 59 ++++++++++++ docker-compose.yml | 20 ++++ src/config/example.config.json | 1 + src/index.ts | 132 +++++++++++++++++++++++++- src/views/login.html | 165 +++++++++++++++++++++++++++++++++ src/views/print-protos.html | 30 ++++++ 7 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/views/login.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d450231 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,55 @@ +# Dependencies +node_modules/ +npm-debug.log +yarn-error.log + +# Build output +dist/ + +# Proto samples (will be mounted as volume) +proto_samples/ + +# Git +.git/ +.gitignore +.gitattributes + +# Documentation +*.md +README* +CHANGELOG* +LICENSE* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Environment +.env +.env.* + +# Testing +coverage/ +.nyc_output/ + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml + +# Misc +*.log +tmp/ +temp/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81a08b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +# Stage 1: Build +FROM node:20-alpine AS builder + +# Install Yarn globally +RUN corepack enable && corepack prepare yarn@1.22.22 --activate + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock ./ + +# Install all dependencies (including devDependencies for build) +# Skip scripts to prevent prepare hook from running before source is copied +RUN yarn install --frozen-lockfile --ignore-scripts + +# Copy source code +COPY . . + +# Build the application (compile TypeScript + copy static assets) +RUN yarn build + +# Stage 2: Production +FROM node:20-alpine + +# Install Yarn globally +RUN corepack enable && corepack prepare yarn@1.22.22 --activate + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock ./ + +# Install only production dependencies +# Skip scripts since we're only copying pre-built artifacts +RUN yarn install --frozen-lockfile --production --ignore-scripts + +# Copy built application from builder stage +COPY --from=builder /app/dist ./dist + +# Copy source config directory structure (for config.json mount point) +COPY --from=builder /app/src/config/example.config.json ./src/config/ + +# Create proto_samples directory +RUN mkdir -p proto_samples + +# Expose the default port +EXPOSE 8081 + +# Set NODE_ENV to production +ENV NODE_ENV=production + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:8081/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start the application +CMD ["node", "dist/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3bb333b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + protodecoder-ui: + build: + context: . + dockerfile: Dockerfile + container_name: protodecoder-ui + ports: + - "8081:8081" + volumes: + - ./src/config/config.json:/app/src/config/config.json:ro + - ./proto_samples:/app/proto_samples + restart: unless-stopped + environment: + - NODE_ENV=production + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:8081/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 10s + start_period: 5s + retries: 3 diff --git a/src/config/example.config.json b/src/config/example.config.json index 36c5ba5..c6f31ce 100644 --- a/src/config/example.config.json +++ b/src/config/example.config.json @@ -1,5 +1,6 @@ { "default_port": 8081, + "web_password": null, "trafficlight_identifier": "AwesomeProtoSender", "redirect_to_golbat_url": null, "redirect_to_golbat_token": null, diff --git a/src/index.ts b/src/index.ts index 41764fb..1595048 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import http from "http"; import fs from "fs"; +import crypto from "crypto"; import { WebStreamBuffer, getIPAddress, handleData, moduleConfigIsAvailable, redirect_post_golbat } from "./utils"; import { decodePayload, decodePayloadTraffic } from "./parser/proto-parser"; import SampleSaver from "./utils/sample-saver"; @@ -18,9 +19,114 @@ const portBind = config["default_port"]; // Initialize sample saver const sampleSaver = config.sample_saving ? new SampleSaver(config.sample_saving) : null; +// Authentication setup +const WEB_PASSWORD = config["web_password"]; +const AUTH_REQUIRED = WEB_PASSWORD !== null && WEB_PASSWORD !== undefined && WEB_PASSWORD !== ""; +const sessions = new Set(); + +// Helper functions for authentication +function generateSessionToken(): string { + return crypto.randomBytes(32).toString("hex"); +} + +function parseCookies(cookieHeader: string | undefined): Record { + const cookies: Record = {}; + if (!cookieHeader) return cookies; + + cookieHeader.split(';').forEach(cookie => { + const parts = cookie.trim().split('='); + if (parts.length === 2) { + cookies[parts[0]] = parts[1]; + } + }); + return cookies; +} + +function isAuthenticated(req: http.IncomingMessage): boolean { + if (!AUTH_REQUIRED) return true; + + const cookies = parseCookies(req.headers.cookie); + const sessionToken = cookies['session_token']; + return !!(sessionToken && sessions.has(sessionToken)); +} + +function requireAuth(req: http.IncomingMessage, res: http.ServerResponse): boolean { + if (!isAuthenticated(req)) { + res.writeHead(302, { Location: '/login' }); + res.end(); + return false; + } + return true; +} + // server const httpServer = http.createServer(function (req, res) { let incomingData: Array = []; + + // Authentication routes + if (req.url === "/login" && req.method === "GET") { + if (isAuthenticated(req)) { + res.writeHead(302, { Location: '/' }); + res.end(); + return; + } + res.writeHead(200, { "Content-Type": "text/html" }); + const loginHTML = fs.readFileSync("./dist/views/login.html"); + res.end(loginHTML); + return; + } + + if (req.url === "/auth/login" && req.method === "POST") { + req.on("data", function (chunk) { + incomingData.push(chunk); + }); + req.on("end", function () { + try { + const requestData = incomingData.join(""); + const parsedData = JSON.parse(requestData); + + if (parsedData.password === WEB_PASSWORD) { + const sessionToken = generateSessionToken(); + sessions.add(sessionToken); + + res.writeHead(200, { + "Content-Type": "application/json", + "Set-Cookie": `session_token=${sessionToken}; HttpOnly; Path=/; Max-Age=86400` + }); + res.end(JSON.stringify({ success: true })); + } else { + res.writeHead(401, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, message: "Invalid password" })); + } + } catch (error) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, message: "Invalid request" })); + } + }); + return; + } + + if (req.url === "/auth/logout" && req.method === "POST") { + const cookies = parseCookies(req.headers.cookie); + const sessionToken = cookies['session_token']; + if (sessionToken) { + sessions.delete(sessionToken); + } + + res.writeHead(200, { + "Content-Type": "application/json", + "Set-Cookie": "session_token=; HttpOnly; Path=/; Max-Age=0" + }); + res.end(JSON.stringify({ success: true })); + return; + } + + if (req.url === "/auth/status" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ authRequired: AUTH_REQUIRED })); + return; + } + switch (req.url) { case "/golbat": req.on("data", function (chunk) { @@ -158,26 +264,31 @@ const httpServer = http.createServer(function (req, res) { }); break; case "/images/favicon.png": + if (!requireAuth(req, res)) break; res.writeHead(200, { "Content-Type": "image/png" }); const favicon = fs.readFileSync("./dist/views/images/favicon.png"); res.end(favicon); break; case "/css/style.css": + if (!requireAuth(req, res)) break; res.writeHead(200, { "Content-Type": "text/css" }); const pageCssL = fs.readFileSync("./dist/views/css/style.css"); res.end(pageCssL); break; case "/json-viewer/jquery.json-viewer.css": + if (!requireAuth(req, res)) break; res.writeHead(200, { "Content-Type": "text/css" }); const pageCss = fs.readFileSync("node_modules/jquery.json-viewer/json-viewer/jquery.json-viewer.css"); res.end(pageCss); break; case "/json-viewer/jquery.json-viewer.js": + if (!requireAuth(req, res)) break; res.writeHead(200, { "Content-Type": "text/javascript" }); const pageJs = fs.readFileSync("node_modules/jquery.json-viewer/json-viewer/jquery.json-viewer.js"); res.end(pageJs); break; case "/": + if (!requireAuth(req, res)) break; res.writeHead(200, { "Content-Type": "text/html" }); const pageHTML = fs.readFileSync("./dist/views/print-protos.html"); res.end(pageHTML); @@ -189,6 +300,22 @@ const httpServer = http.createServer(function (req, res) { }); var io = require("socket.io")(httpServer); + +// Socket.IO authentication middleware +if (AUTH_REQUIRED) { + io.use((socket, next) => { + const cookieHeader = socket.handshake.headers.cookie; + const cookies = parseCookies(cookieHeader); + const sessionToken = cookies['session_token']; + + if (sessionToken && sessions.has(sessionToken)) { + next(); + } else { + next(new Error('Authentication required')); + } + }); +} + var incoming = io.of("/incoming").on("connection", function (socket) { const reader = { read: function (data: object) { @@ -221,9 +348,12 @@ var outgoing = io.of("/outgoing").on("connection", function (socket) { httpServer.keepAliveTimeout = 0; httpServer.listen(portBind, function () { + const authStatus = AUTH_REQUIRED ? "ENABLED - Password required to access web UI" : "DISABLED"; const welcome = ` Server start access of this in urls: http://localhost:${portBind} or WLAN mode http://${getIPAddress()}:${portBind}. - + + - Web Authentication: ${authStatus} + - Clients MITM: 1) --=FurtiF™=- Tools EndPoints: http://${getIPAddress()}:${portBind}/traffic or http://${getIPAddress()}:${portBind}/golbat (depending on the modes chosen) 2) If Other set here... diff --git a/src/views/login.html b/src/views/login.html new file mode 100644 index 0000000..3f6982b --- /dev/null +++ b/src/views/login.html @@ -0,0 +1,165 @@ + + + + + + + + ProtoDecoderUI - Login + + + + + + + + + diff --git a/src/views/print-protos.html b/src/views/print-protos.html index 9f588e4..58c926d 100644 --- a/src/views/print-protos.html +++ b/src/views/print-protos.html @@ -45,6 +45,7 @@

+

@@ -484,6 +485,22 @@