-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
146 lines (134 loc) · 4.56 KB
/
server.js
File metadata and controls
146 lines (134 loc) · 4.56 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
"use strict";
const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = process.env.PORT || 3000;
const PUBLIC_DIR = path.join(__dirname, "public");
// Simple MIME type map
const MIME_TYPES = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".txt": "text/plain; charset=utf-8",
};
function sendFile(res, filePath, method, statusCode = 200) {
// Determine MIME type from file extension
const ext = path.extname(filePath).toLowerCase();
const mimeType = MIME_TYPES[ext] || "application/octet-stream";
// Add caching headers for successful static file responses (not for error pages)
const headers = {
"Content-Type": mimeType,
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src 'self'",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
};
// Only cache 200 OK responses for static files, not error pages
if (statusCode === 200 && !filePath.endsWith("404.html")) {
headers["Cache-Control"] = "public, max-age=86400, immutable"; // 1 day cache
// Add ETag for cache validation
try {
const stat = fs.statSync(filePath);
headers["ETag"] = `W/\"${stat.size}-${stat.mtime.getTime()}\"`;
} catch (e) {
// Ignore ETag if stat fails
}
} else {
headers["Cache-Control"] = "no-store";
}
res.writeHead(statusCode, headers);
if (method === "HEAD") return res.end();
const stream = fs.createReadStream(filePath);
stream.on("error", (err) => {
console.error(
`[${new Date().toISOString()}] Error streaming file: ${filePath} - ${err}`
);
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Internal Server Error");
});
stream.pipe(res);
}
const server = http.createServer((req, res) => {
// Log incoming request
console.log(
`[${new Date().toISOString()}] ${req.method} ${req.url} from ${
req.socket.remoteAddress
}`
);
// Allow only GET and HEAD
if (!["GET", "HEAD"].includes(req.method)) {
console.warn(
`[${new Date().toISOString()}] Blocked method: ${req.method} ${req.url}`
);
res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
return res.end("Method Not Allowed");
}
// Decode and normalize path
let decodedPath;
try {
decodedPath = decodeURIComponent(req.url.split("?")[0].split("#")[0]);
} catch (e) {
console.error(
`[${new Date().toISOString()}] Decode error for URL: ${req.url}`
);
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
return res.end("Bad Request");
}
// Always serve index.html for root or index.html requests
let safePath = path.normalize(decodedPath).replace(/^[/\\]+/, "");
let filePath =
safePath === "" ||
safePath === "/" ||
safePath.toLowerCase() === "index.html"
? path.join(PUBLIC_DIR, "index.html")
: path.join(PUBLIC_DIR, safePath);
// Prevent serving dotfiles and sensitive files
const basename = path.basename(filePath);
// Block dotfiles and common sensitive files
const sensitiveFiles = [
".env",
".git",
".DS_Store",
"package.json",
"server.js",
"start.sh",
];
if (basename.startsWith(".") || sensitiveFiles.includes(basename)) {
console.warn(
`[${new Date().toISOString()}] Forbidden file request: ${req.url}`
);
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
return res.end("Forbidden");
}
// Prevent directory traversal
const relative = path.relative(PUBLIC_DIR, filePath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
console.warn(
`[${new Date().toISOString()}] Directory traversal attempt: ${req.url}`
);
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
return res.end("Forbidden");
}
// Check if file exists and is a file
fs.stat(filePath, (err, stats) => {
if (!err && stats.isFile()) {
return sendFile(res, filePath, req.method);
}
// Log 404 errors
console.warn(
`[${new Date().toISOString()}] File not found: ${filePath} for ${req.url}`
);
// File not found or not a file, send 404
sendFile(res, path.join(PUBLIC_DIR, "404.html"), req.method, 404);
});
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});