Files

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);