169 lines
4.1 KiB
JavaScript
Executable File
169 lines
4.1 KiB
JavaScript
Executable File
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const url = require('url');
|
|
|
|
const PORT = 80;
|
|
const ROOT = '/app';
|
|
|
|
function corsHeaders(extra = {}) {
|
|
return {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, HEAD',
|
|
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With',
|
|
...extra
|
|
};
|
|
}
|
|
|
|
function sendFile(req, res, filePath, contentType) {
|
|
if (!fs.existsSync(filePath)) {
|
|
res.writeHead(404);
|
|
return res.end('Not Found');
|
|
}
|
|
|
|
const stat = fs.statSync(filePath);
|
|
|
|
res.writeHead(200, {
|
|
...corsHeaders(),
|
|
'Content-Type': contentType,
|
|
'Content-Length': stat.size,
|
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0'
|
|
});
|
|
|
|
if (req.method !== 'HEAD') {
|
|
fs.createReadStream(filePath).pipe(res);
|
|
} else {
|
|
res.end();
|
|
}
|
|
}
|
|
|
|
function contentType(file) {
|
|
const ext = path.extname(file).toLowerCase();
|
|
|
|
const types = {
|
|
'.html': 'text/html',
|
|
'.css': 'text/css',
|
|
'.js': 'application/javascript',
|
|
'.json': 'application/json',
|
|
'.svg': 'image/svg+xml',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.ico': 'image/x-icon',
|
|
'.woff': 'font/woff',
|
|
'.woff2': 'font/woff2'
|
|
};
|
|
|
|
return types[ext] || 'application/octet-stream';
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
const parsed = url.parse(req.url);
|
|
const pathname = parsed.pathname;
|
|
|
|
// CORS preflight
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(200, corsHeaders());
|
|
return res.end();
|
|
}
|
|
|
|
// Get client IP
|
|
if (pathname === '/getIP') {
|
|
const ip =
|
|
req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
|
req.socket.remoteAddress ||
|
|
'';
|
|
|
|
res.writeHead(200, {
|
|
...corsHeaders(),
|
|
'Content-Type': 'text/plain',
|
|
'Cache-Control': 'no-store'
|
|
});
|
|
|
|
return res.end(ip);
|
|
}
|
|
|
|
// Upload endpoint
|
|
if (pathname === '/upload') {
|
|
if (req.method !== 'POST') {
|
|
res.writeHead(200, corsHeaders());
|
|
return res.end('OK');
|
|
}
|
|
|
|
let bytes = 0;
|
|
|
|
req.on('data', chunk => {
|
|
bytes += chunk.length;
|
|
});
|
|
|
|
req.on('end', () => {
|
|
res.writeHead(200, {
|
|
...corsHeaders(),
|
|
'Content-Type': 'text/plain',
|
|
'Cache-Control': 'no-store'
|
|
});
|
|
|
|
res.end('OK');
|
|
});
|
|
|
|
req.on('error', () => {
|
|
if (!res.headersSent) {
|
|
res.writeHead(500, corsHeaders());
|
|
}
|
|
res.end('ERROR');
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// Download endpoint
|
|
if (pathname === '/downloading') {
|
|
const file = path.join(ROOT, 'downloading');
|
|
|
|
if (!fs.existsSync(file)) {
|
|
res.writeHead(404);
|
|
return res.end('Not Found');
|
|
}
|
|
|
|
const stat = fs.statSync(file);
|
|
|
|
res.writeHead(200, {
|
|
...corsHeaders(),
|
|
'Content-Type': 'application/octet-stream',
|
|
'Content-Length': stat.size,
|
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
|
'Content-Disposition': 'inline'
|
|
});
|
|
|
|
if (req.method !== 'HEAD') {
|
|
fs.createReadStream(file).pipe(res);
|
|
} else {
|
|
res.end();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Static files
|
|
let requested = pathname === '/' ? '/index.html' : pathname;
|
|
const filePath = path.normalize(path.join(ROOT, requested));
|
|
|
|
// Prevent path traversal
|
|
if (!filePath.startsWith(ROOT)) {
|
|
res.writeHead(403);
|
|
return res.end('Forbidden');
|
|
}
|
|
|
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
res.writeHead(404);
|
|
return res.end('Not Found');
|
|
}
|
|
|
|
sendFile(req, res, filePath, contentType(filePath));
|
|
});
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Speedtest server listening on port ${PORT}`);
|
|
});
|