Spaces:
Paused
Paused
| import express from 'express'; | |
| import http from 'http'; | |
| import { spawn } from 'child_process'; | |
| import url from 'url'; | |
| import fetch from 'node-fetch'; | |
| const PROXY_PORT = 3000; | |
| const SECONDARY_SERVER_PORT = 7860; | |
| let activeTunnelUrl = null; // Переменная для хранения URL туннеля | |
| const proxyApp = express(); | |
| proxyApp.use(express.raw({ | |
| type: '*/*', | |
| limit: '100mb' | |
| })); | |
| function addCorsHeaders(res, clientRequestOrigin) { | |
| if (clientRequestOrigin) { | |
| res.setHeader('Access-Control-Allow-Origin', clientRequestOrigin); | |
| res.setHeader('Access-Control-Allow-Credentials', 'true'); | |
| res.setHeader('Vary', 'Origin'); | |
| } else { | |
| res.setHeader('Access-Control-Allow-Origin', '*'); | |
| } | |
| } | |
| proxyApp.options('*', (req, res) => { | |
| const origin = req.headers.origin; | |
| addCorsHeaders(res, origin); | |
| res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'); | |
| const requestedHeaders = req.headers['access-control-request-headers']; | |
| if (requestedHeaders) { | |
| res.setHeader('Access-Control-Allow-Headers', requestedHeaders); | |
| } else { | |
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-CSRF-Token, Accept, Origin, User-Agent'); | |
| } | |
| res.setHeader('Access-Control-Max-Age', '86400'); | |
| res.status(204).end(); | |
| }); | |
| proxyApp.get('/', (req, res) => { | |
| const origin = req.headers.origin; | |
| addCorsHeaders(res, origin); | |
| res.setHeader('Content-Type', 'text/plain; charset=utf-8'); | |
| res.send('Hello world'); | |
| }); | |
| proxyApp.all('*', async (req, res) => { | |
| if (req.path === '/') { | |
| return; | |
| } | |
| const clientRequestOrigin = req.headers.origin; | |
| try { | |
| let targetUrlString = req.url.substring(1); | |
| try { | |
| const decoded = decodeURIComponent(targetUrlString); | |
| if (decoded.startsWith('http://') || decoded.startsWith('https://') || decoded.match(/^[a-zA-Z]+:\/[^/]/)) { | |
| targetUrlString = decoded; | |
| } | |
| } catch (e) { } | |
| const mangledSchemeMatch = targetUrlString.match(/^([a-zA-Z]+):\/([^\/].*)$/); | |
| if (mangledSchemeMatch) { | |
| targetUrlString = `${mangledSchemeMatch[1]}://${mangledSchemeMatch[2]}`; | |
| } | |
| if (!targetUrlString) { | |
| addCorsHeaders(res, clientRequestOrigin); | |
| res.status(400).send('Target URL is missing in the path.'); | |
| return; | |
| } | |
| if (!targetUrlString.match(/^https?:\/\//i)) { | |
| targetUrlString = 'http://' + targetUrlString; | |
| } | |
| let targetUrl; | |
| try { | |
| targetUrl = new URL(targetUrlString); | |
| } catch (e) { | |
| addCorsHeaders(res, clientRequestOrigin); | |
| res.status(400).send(`Invalid target URL provided: "${targetUrlString}".`); | |
| return; | |
| } | |
| const requestHeaders = {...req.headers}; | |
| delete requestHeaders['host']; | |
| delete requestHeaders['content-length']; | |
| delete requestHeaders['connection']; | |
| const proxyResponse = await fetch(targetUrl.toString(), { | |
| method: req.method, | |
| headers: requestHeaders, | |
| body: (req.method !== 'GET' && req.method !== 'HEAD' && req.body && req.body.length > 0) ? req.body : undefined, | |
| redirect: 'manual', | |
| compress: false | |
| }); | |
| proxyResponse.headers.forEach((value, key) => { | |
| const lowerKey = key.toLowerCase(); | |
| if (!lowerKey.startsWith('access-control-') && | |
| !['strict-transport-security', 'content-security-policy', 'public-key-pins', | |
| 'transfer-encoding', 'connection', 'keep-alive', 'proxy-authenticate', | |
| 'proxy-authorization', 'te', 'trailers', 'upgrade'].includes(lowerKey) | |
| ) { | |
| res.setHeader(key, value); | |
| } | |
| }); | |
| addCorsHeaders(res, clientRequestOrigin); | |
| const exposedHeaders = Array.from(proxyResponse.headers.keys()).filter(key => | |
| !key.toLowerCase().startsWith('access-control-') && | |
| key.toLowerCase() !== 'transfer-encoding' && | |
| key.toLowerCase() !== 'connection' | |
| ).join(', '); | |
| if (exposedHeaders) { | |
| res.setHeader('Access-Control-Expose-Headers', exposedHeaders); | |
| } else { | |
| res.setHeader('Access-Control-Expose-Headers', '*'); | |
| } | |
| res.status(proxyResponse.status); | |
| if (proxyResponse.body) { | |
| proxyResponse.body.pipe(res); | |
| } else { | |
| res.end(); | |
| } | |
| } catch (error) { | |
| if (!res.headersSent) { | |
| addCorsHeaders(res, clientRequestOrigin); | |
| let statusCode = 500; | |
| let message = 'Proxy error occurred.'; | |
| if (error.code === 'ENOTFOUND' || error.cause?.code === 'ENOTFOUND') { | |
| statusCode = 404; message = `Target host not found.`; | |
| } else if (error.message?.includes('Invalid URL') || (error.name === 'TypeError' && error.message?.includes('Invalid URL'))) { | |
| statusCode = 400; message = `Invalid target URL in path.`; | |
| } else if (error.code === 'ECONNREFUSED' || error.cause?.code === 'ECONNREFUSED') { | |
| statusCode = 502; message = `Bad Gateway: Could not connect to target server.`; | |
| } else if (error.code === 'ERR_INVALID_URL') { | |
| statusCode = 400; message = `Invalid target URL format in path.`; | |
| } | |
| res.status(statusCode).send(message); | |
| } else { | |
| res.end(); | |
| } | |
| } | |
| }); | |
| proxyApp.listen(PROXY_PORT, '0.0.0.0', () => { | |
| const tunnel = spawn('ssh', [ | |
| '-R', `80:localhost:${PROXY_PORT}`, | |
| '-o', 'StrictHostKeyChecking=no', | |
| '-o', 'UserKnownHostsFile=/dev/null', | |
| '-o', 'ServerAliveInterval=60', | |
| '-o', 'ExitOnForwardFailure=yes', | |
| '-o', 'ConnectTimeout=20', | |
| 'serveo.net' | |
| ]); | |
| tunnel.stdout.on('data', (data) => { | |
| const output = data.toString(); | |
| const urlMatch = output.match(/https?:\/\/[a-zA-Z0-9-]+\.serveo\.net/); | |
| if (urlMatch) { | |
| activeTunnelUrl = urlMatch[0]; | |
| } | |
| }); | |
| tunnel.stderr.on('data', (data) => {}); | |
| tunnel.on('close', (code) => { | |
| activeTunnelUrl = null; // Сбрасываем URL, если туннель закрылся | |
| }); | |
| tunnel.on('error', (err) => { | |
| activeTunnelUrl = null; // Сбрасываем URL при ошибке запуска | |
| }); | |
| }); | |
| const secondaryServer = http.createServer((req, res) => { | |
| if (req.url === '/' && req.method === 'GET') { | |
| res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); | |
| if (activeTunnelUrl) { | |
| res.end(activeTunnelUrl); | |
| } else { | |
| res.end('Serveo.net tunnel URL not available yet.'); | |
| } | |
| } else { | |
| res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); | |
| res.end('Not Found'); | |
| } | |
| }); | |
| secondaryServer.listen(SECONDARY_SERVER_PORT, '0.0.0.0', () => {}); | |
| process.on('SIGINT', () => { | |
| process.exit(0); | |
| }); | |
| process.on('SIGTERM', () => { | |
| process.exit(0); | |
| }); |