Skip to content

Commit 8dac82d

Browse files
author
RaizeTheLimit
committed
Docker support and simple web authentication
1 parent e825a84 commit 8dac82d

7 files changed

Lines changed: 461 additions & 1 deletion

File tree

.dockerignore

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Dependencies
2+
node_modules/
3+
npm-debug.log
4+
yarn-error.log
5+
6+
# Build output
7+
dist/
8+
9+
# Proto samples (will be mounted as volume)
10+
proto_samples/
11+
12+
# Git
13+
.git/
14+
.gitignore
15+
.gitattributes
16+
17+
# Documentation
18+
*.md
19+
README*
20+
CHANGELOG*
21+
LICENSE*
22+
23+
# IDE
24+
.vscode/
25+
.idea/
26+
*.swp
27+
*.swo
28+
*~
29+
30+
# OS
31+
.DS_Store
32+
Thumbs.db
33+
34+
# Environment
35+
.env
36+
.env.*
37+
38+
# Testing
39+
coverage/
40+
.nyc_output/
41+
42+
# Docker
43+
Dockerfile
44+
docker-compose.yml
45+
.dockerignore
46+
47+
# CI/CD
48+
.github/
49+
.gitlab-ci.yml
50+
.travis.yml
51+
52+
# Misc
53+
*.log
54+
tmp/
55+
temp/

Dockerfile

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Stage 1: Build
2+
FROM node:20-alpine AS builder
3+
4+
# Install Yarn globally
5+
RUN corepack enable && corepack prepare yarn@1.22.22 --activate
6+
7+
# Set working directory
8+
WORKDIR /app
9+
10+
# Copy package files
11+
COPY package.json yarn.lock ./
12+
13+
# Install all dependencies (including devDependencies for build)
14+
# Skip scripts to prevent prepare hook from running before source is copied
15+
RUN yarn install --frozen-lockfile --ignore-scripts
16+
17+
# Copy source code
18+
COPY . .
19+
20+
# Build the application (compile TypeScript + copy static assets)
21+
RUN yarn build
22+
23+
# Stage 2: Production
24+
FROM node:20-alpine
25+
26+
# Install Yarn globally
27+
RUN corepack enable && corepack prepare yarn@1.22.22 --activate
28+
29+
# Set working directory
30+
WORKDIR /app
31+
32+
# Copy package files
33+
COPY package.json yarn.lock ./
34+
35+
# Install only production dependencies
36+
# Skip scripts since we're only copying pre-built artifacts
37+
RUN yarn install --frozen-lockfile --production --ignore-scripts
38+
39+
# Copy built application from builder stage
40+
COPY --from=builder /app/dist ./dist
41+
42+
# Copy source config directory structure (for config.json mount point)
43+
COPY --from=builder /app/src/config/example.config.json ./src/config/
44+
45+
# Create proto_samples directory
46+
RUN mkdir -p proto_samples
47+
48+
# Expose the default port
49+
EXPOSE 8081
50+
51+
# Set NODE_ENV to production
52+
ENV NODE_ENV=production
53+
54+
# Health check
55+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
56+
CMD node -e "require('http').get('http://localhost:8081/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
57+
58+
# Start the application
59+
CMD ["node", "dist/index.js"]

docker-compose.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
services:
2+
protodecoder-ui:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
container_name: protodecoder-ui
7+
ports:
8+
- "8081:8081"
9+
volumes:
10+
- ./src/config/config.json:/app/src/config/config.json:ro
11+
- ./proto_samples:/app/proto_samples
12+
restart: unless-stopped
13+
environment:
14+
- NODE_ENV=production
15+
healthcheck:
16+
test: ["CMD", "node", "-e", "require('http').get('http://localhost:8081/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
17+
interval: 30s
18+
timeout: 10s
19+
start_period: 5s
20+
retries: 3

src/config/example.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"default_port": 8081,
3+
"web_password": null,
34
"trafficlight_identifier": "AwesomeProtoSender",
45
"redirect_to_golbat_url": null,
56
"redirect_to_golbat_token": null,

src/index.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import http from "http";
22
import fs from "fs";
3+
import crypto from "crypto";
34
import { WebStreamBuffer, getIPAddress, handleData, moduleConfigIsAvailable, redirect_post_golbat } from "./utils";
45
import { decodePayload, decodePayloadTraffic } from "./parser/proto-parser";
56
import SampleSaver from "./utils/sample-saver";
@@ -18,9 +19,114 @@ const portBind = config["default_port"];
1819
// Initialize sample saver
1920
const sampleSaver = config.sample_saving ? new SampleSaver(config.sample_saving) : null;
2021

22+
// Authentication setup
23+
const WEB_PASSWORD = config["web_password"];
24+
const AUTH_REQUIRED = WEB_PASSWORD !== null && WEB_PASSWORD !== undefined && WEB_PASSWORD !== "";
25+
const sessions = new Set<string>();
26+
27+
// Helper functions for authentication
28+
function generateSessionToken(): string {
29+
return crypto.randomBytes(32).toString("hex");
30+
}
31+
32+
function parseCookies(cookieHeader: string | undefined): Record<string, string> {
33+
const cookies: Record<string, string> = {};
34+
if (!cookieHeader) return cookies;
35+
36+
cookieHeader.split(';').forEach(cookie => {
37+
const parts = cookie.trim().split('=');
38+
if (parts.length === 2) {
39+
cookies[parts[0]] = parts[1];
40+
}
41+
});
42+
return cookies;
43+
}
44+
45+
function isAuthenticated(req: http.IncomingMessage): boolean {
46+
if (!AUTH_REQUIRED) return true;
47+
48+
const cookies = parseCookies(req.headers.cookie);
49+
const sessionToken = cookies['session_token'];
50+
return !!(sessionToken && sessions.has(sessionToken));
51+
}
52+
53+
function requireAuth(req: http.IncomingMessage, res: http.ServerResponse): boolean {
54+
if (!isAuthenticated(req)) {
55+
res.writeHead(302, { Location: '/login' });
56+
res.end();
57+
return false;
58+
}
59+
return true;
60+
}
61+
2162
// server
2263
const httpServer = http.createServer(function (req, res) {
2364
let incomingData: Array<Buffer> = [];
65+
66+
// Authentication routes
67+
if (req.url === "/login" && req.method === "GET") {
68+
if (isAuthenticated(req)) {
69+
res.writeHead(302, { Location: '/' });
70+
res.end();
71+
return;
72+
}
73+
res.writeHead(200, { "Content-Type": "text/html" });
74+
const loginHTML = fs.readFileSync("./dist/views/login.html");
75+
res.end(loginHTML);
76+
return;
77+
}
78+
79+
if (req.url === "/auth/login" && req.method === "POST") {
80+
req.on("data", function (chunk) {
81+
incomingData.push(chunk);
82+
});
83+
req.on("end", function () {
84+
try {
85+
const requestData = incomingData.join("");
86+
const parsedData = JSON.parse(requestData);
87+
88+
if (parsedData.password === WEB_PASSWORD) {
89+
const sessionToken = generateSessionToken();
90+
sessions.add(sessionToken);
91+
92+
res.writeHead(200, {
93+
"Content-Type": "application/json",
94+
"Set-Cookie": `session_token=${sessionToken}; HttpOnly; Path=/; Max-Age=86400`
95+
});
96+
res.end(JSON.stringify({ success: true }));
97+
} else {
98+
res.writeHead(401, { "Content-Type": "application/json" });
99+
res.end(JSON.stringify({ success: false, message: "Invalid password" }));
100+
}
101+
} catch (error) {
102+
res.writeHead(400, { "Content-Type": "application/json" });
103+
res.end(JSON.stringify({ success: false, message: "Invalid request" }));
104+
}
105+
});
106+
return;
107+
}
108+
109+
if (req.url === "/auth/logout" && req.method === "POST") {
110+
const cookies = parseCookies(req.headers.cookie);
111+
const sessionToken = cookies['session_token'];
112+
if (sessionToken) {
113+
sessions.delete(sessionToken);
114+
}
115+
116+
res.writeHead(200, {
117+
"Content-Type": "application/json",
118+
"Set-Cookie": "session_token=; HttpOnly; Path=/; Max-Age=0"
119+
});
120+
res.end(JSON.stringify({ success: true }));
121+
return;
122+
}
123+
124+
if (req.url === "/auth/status" && req.method === "GET") {
125+
res.writeHead(200, { "Content-Type": "application/json" });
126+
res.end(JSON.stringify({ authRequired: AUTH_REQUIRED }));
127+
return;
128+
}
129+
24130
switch (req.url) {
25131
case "/golbat":
26132
req.on("data", function (chunk) {
@@ -158,26 +264,31 @@ const httpServer = http.createServer(function (req, res) {
158264
});
159265
break;
160266
case "/images/favicon.png":
267+
if (!requireAuth(req, res)) break;
161268
res.writeHead(200, { "Content-Type": "image/png" });
162269
const favicon = fs.readFileSync("./dist/views/images/favicon.png");
163270
res.end(favicon);
164271
break;
165272
case "/css/style.css":
273+
if (!requireAuth(req, res)) break;
166274
res.writeHead(200, { "Content-Type": "text/css" });
167275
const pageCssL = fs.readFileSync("./dist/views/css/style.css");
168276
res.end(pageCssL);
169277
break;
170278
case "/json-viewer/jquery.json-viewer.css":
279+
if (!requireAuth(req, res)) break;
171280
res.writeHead(200, { "Content-Type": "text/css" });
172281
const pageCss = fs.readFileSync("node_modules/jquery.json-viewer/json-viewer/jquery.json-viewer.css");
173282
res.end(pageCss);
174283
break;
175284
case "/json-viewer/jquery.json-viewer.js":
285+
if (!requireAuth(req, res)) break;
176286
res.writeHead(200, { "Content-Type": "text/javascript" });
177287
const pageJs = fs.readFileSync("node_modules/jquery.json-viewer/json-viewer/jquery.json-viewer.js");
178288
res.end(pageJs);
179289
break;
180290
case "/":
291+
if (!requireAuth(req, res)) break;
181292
res.writeHead(200, { "Content-Type": "text/html" });
182293
const pageHTML = fs.readFileSync("./dist/views/print-protos.html");
183294
res.end(pageHTML);
@@ -189,6 +300,22 @@ const httpServer = http.createServer(function (req, res) {
189300
});
190301

191302
var io = require("socket.io")(httpServer);
303+
304+
// Socket.IO authentication middleware
305+
if (AUTH_REQUIRED) {
306+
io.use((socket, next) => {
307+
const cookieHeader = socket.handshake.headers.cookie;
308+
const cookies = parseCookies(cookieHeader);
309+
const sessionToken = cookies['session_token'];
310+
311+
if (sessionToken && sessions.has(sessionToken)) {
312+
next();
313+
} else {
314+
next(new Error('Authentication required'));
315+
}
316+
});
317+
}
318+
192319
var incoming = io.of("/incoming").on("connection", function (socket) {
193320
const reader = {
194321
read: function (data: object) {
@@ -221,9 +348,12 @@ var outgoing = io.of("/outgoing").on("connection", function (socket) {
221348

222349
httpServer.keepAliveTimeout = 0;
223350
httpServer.listen(portBind, function () {
351+
const authStatus = AUTH_REQUIRED ? "ENABLED - Password required to access web UI" : "DISABLED";
224352
const welcome = `
225353
Server start access of this in urls: http://localhost:${portBind} or WLAN mode http://${getIPAddress()}:${portBind}.
226-
354+
355+
- Web Authentication: ${authStatus}
356+
227357
- Clients MITM:
228358
1) --=FurtiF™=- Tools EndPoints: http://${getIPAddress()}:${portBind}/traffic or http://${getIPAddress()}:${portBind}/golbat (depending on the modes chosen)
229359
2) If Other set here...

0 commit comments

Comments
 (0)