-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
178 lines (160 loc) · 4.97 KB
/
index.js
File metadata and controls
178 lines (160 loc) · 4.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
const express = require("express");
const bodyParser = require("body-parser");
const helmet = require("helmet"); // New import
const { v4: uuidv4 } = require("uuid");
const path = require("path");
const http = require("http");
const rateLimit = require("express-rate-limit"); // New import
const { getConfig, setConfig } = require("./db");
const app = express();
const server = http.createServer(app); // wrap express in http server
const { Server } = require("socket.io");
const io = new Server(server, {
cors: {
origin: process.env.NODE_ENV === "development" ? true : false,
methods: ["GET", "POST"],
},
transports: ["polling", "websocket"],
});
const port = process.env.PORT || 3000;
// Enhanced Helmet configuration with custom CSP
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"https://cdn.jsdelivr.net",
"https://cdnjs.cloudflare.com",
"'unsafe-inline'",
],
styleSrc: [
"'self'",
"https://cdn.jsdelivr.net",
"https://cdnjs.cloudflare.com",
"'unsafe-inline'",
],
connectSrc: ["'self'"],
// Allow data: URIs for background images
imgSrc: ["'self'", "data:"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
frameAncestors: ["'none'"],
},
})
);
// Add static middleware to serve files from /Users/tech/Projects/requester/public
app.use(express.static(path.join(__dirname, "public")));
// Setup EJS for templating.
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
// Middleware to capture text and json bodies.
app.use(bodyParser.json({ limit: "1mb" }));
app.use(bodyParser.urlencoded({ extended: true, limit: "1mb" }));
// Add text parser for 'text/plain'
app.use(bodyParser.text({ type: "text/plain" }));
// Apply rate limiter to prevent endpoint abuse.
const requestLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 60, // limit each IP to 60 requests per window
message: "Too many requests, please try again later.",
// Disable all validations (JUST FOR DEVELOPMENT)
validate: process.env.VALIDATE_REQ_LIMITER || true,
});
// Remove activeRooms tracking
// Socket.IO connection handling.
io.on("connection", (socket) => {
socket.on("join_room", (room) => {
// Validate room ID (UUID v4 format)
if (!/^[0-9a-fA-F\-]{36}$/.test(room)) {
return;
}
socket.join(room);
});
});
// Home page: generate a new unique URL.
app.get("/", (req, res) => {
const uniqueId = uuidv4();
const origin = `${req.protocol}://${req.get("host")}`; // Compute full origin
res.render("index", { uniqueId, origin });
});
// New route to view the explorer/request logs
app.get("/explorer/:id", (req, res) => {
const id = req.params.id;
res.render("explorer", { id, requests: [] });
});
// Provide config by ID
app.get("/config/:id", async (req, res) => {
const { id } = req.params;
try {
let row = await getConfig(id);
if (!row) {
// Default config on first usage
row = { id, status: 200, body: "Request logged", contentType: "text" };
}
res.render("config", { responseConfig: row, configId: id });
} catch (err) {
console.info(err);
res.status(500).send("Error retrieving configuration.");
}
});
app.post("/config/:id", async (req, res) => {
const { id } = req.params;
const { status, body, contentType } = req.body;
try {
const statusCode = parseInt(status);
if (isNaN(statusCode) || statusCode < 100 || statusCode > 599) {
return res
.status(400)
.send("Invalid status code. Must be between 100 and 599.");
}
await setConfig(id, {
status: statusCode,
body: body || "Request logged",
contentType: contentType || "text",
});
res.redirect(`/config/${id}?status=ok`);
} catch (err) {
console.info(err);
res.status(500).send("Error saving configuration.");
}
});
app.all("/r/:id", requestLimiter, async (req, res) => {
const { id } = req.params;
const reqInfo = {
id: uuidv4(), // assign a unique id for this request
method: req.method,
headers: req.headers,
query: req.query,
body: req.body,
timestamp: new Date(),
};
io.to(id).emit("new_request", reqInfo);
try {
let row = await getConfig(id);
if (!row) {
// Fallback if none is configured for that ID
row = { status: 200, body: "Request logged", contentType: "text" };
}
res.setHeader("Content-Type", getMimeType(row.contentType));
res.status(row.status).send(row.body);
} catch (err) {
console.info(err);
res.status(500).send("Server error.");
}
});
// Helper to get MIME type by config.
function getMimeType(type) {
switch (type) {
case "json":
return "application/json";
case "xml":
return "application/xml";
default:
return "text/plain";
}
}
// Remove the DELETE endpoint since deletion is handled client-side.
server.listen(port, () => {
console.info(`Server listening on port ${port}`);
});