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