156 lines
4.1 KiB
JavaScript
156 lines
4.1 KiB
JavaScript
'use strict';
|
|
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { URL } = require('url');
|
|
|
|
const PREFERRED_PORT = Number(process.env.PORT) || 3000;
|
|
const MAX_PORT_ATTEMPTS = process.env.PORT ? 1 : 10;
|
|
const ROOT = __dirname;
|
|
const FETCH_TIMEOUT_MS = 25000;
|
|
|
|
const MIME_TYPES = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'application/javascript; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.svg': 'image/svg+xml',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.webp': 'image/webp',
|
|
'.ico': 'image/x-icon',
|
|
'.woff2': 'font/woff2'
|
|
};
|
|
|
|
function send(res, status, body, headers) {
|
|
res.writeHead(status, headers || {});
|
|
res.end(body);
|
|
}
|
|
|
|
function isValidFetchUrl(raw) {
|
|
try {
|
|
const parsed = new URL(raw);
|
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function fetchRemotePage(url) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(function () {
|
|
controller.abort();
|
|
}, FETCH_TIMEOUT_MS);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
signal: controller.signal,
|
|
redirect: 'follow',
|
|
headers: {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 FeepPricePage/1.0',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language': 'fa,en-US;q=0.9,en;q=0.8'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('HTTP ' + response.status);
|
|
}
|
|
|
|
return response.text();
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function serveStatic(req, res) {
|
|
const reqUrl = new URL(req.url, 'http://localhost');
|
|
let pathname = decodeURIComponent(reqUrl.pathname);
|
|
if (pathname === '/') pathname = '/index.html';
|
|
|
|
const filePath = path.normalize(path.join(ROOT, pathname));
|
|
if (!filePath.startsWith(ROOT)) {
|
|
send(res, 403, 'Forbidden');
|
|
return;
|
|
}
|
|
|
|
fs.readFile(filePath, function (err, data) {
|
|
if (err) {
|
|
send(res, err.code === 'ENOENT' ? 404 : 500, err.code === 'ENOENT' ? 'Not Found' : 'Server Error');
|
|
return;
|
|
}
|
|
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
send(res, 200, data, {
|
|
'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
|
|
'Cache-Control': 'no-cache'
|
|
});
|
|
});
|
|
}
|
|
|
|
const server = http.createServer(async function (req, res) {
|
|
const reqUrl = new URL(req.url, 'http://localhost');
|
|
|
|
if (reqUrl.pathname === '/api/fetch-page' && req.method === 'GET') {
|
|
const target = reqUrl.searchParams.get('url');
|
|
if (!target || !isValidFetchUrl(target)) {
|
|
send(res, 400, JSON.stringify({ error: 'Invalid URL' }), {
|
|
'Content-Type': 'application/json; charset=utf-8'
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const html = await fetchRemotePage(target);
|
|
send(res, 200, html, {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Cache-Control': 'no-store'
|
|
});
|
|
} catch (err) {
|
|
send(res, 502, JSON.stringify({
|
|
error: err && err.message ? err.message : 'Fetch failed'
|
|
}), {
|
|
'Content-Type': 'application/json; charset=utf-8'
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
send(res, 405, 'Method Not Allowed');
|
|
return;
|
|
}
|
|
|
|
serveStatic(req, res);
|
|
});
|
|
|
|
function startServer(port, attemptsLeft) {
|
|
function onError(err) {
|
|
server.removeListener('listening', onListening);
|
|
|
|
if (err.code === 'EADDRINUSE' && attemptsLeft > 1) {
|
|
startServer(port + 1, attemptsLeft - 1);
|
|
return;
|
|
}
|
|
|
|
console.error('Failed to start server:', err.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
function onListening() {
|
|
server.removeListener('error', onError);
|
|
console.log('Feep price page → http://localhost:' + port);
|
|
if (port !== PREFERRED_PORT) {
|
|
console.log('Port ' + PREFERRED_PORT + ' was busy; using ' + port + ' instead.');
|
|
}
|
|
}
|
|
|
|
server.once('error', onError);
|
|
server.once('listening', onListening);
|
|
server.listen(port);
|
|
}
|
|
|
|
startServer(PREFERRED_PORT, MAX_PORT_ATTEMPTS);
|