prox / server.js
recycleactor's picture
Upload server.js
24275c8 verified
Raw
History Blame Contribute Delete
27 kB
const express = require('express')
const fs = require('fs')
const path = require('path')
const { Readable } = require('stream')
const babel = require('@babel/core')
const regeneratorRuntimeCode = fs.readFileSync(require.resolve('regenerator-runtime/runtime'), 'utf8')
const app = express()
app.disable('x-powered-by')
app.set('trust proxy', true)
const MAX_BYTES = 2 * 1024 * 1024
const MAX_CSS_DEPTH = 6
function getSelfBase(req) {
const protoRaw = (req.headers['x-forwarded-proto'] || req.protocol || 'https') + ''
const proto = protoRaw.split(',')[0].trim() || 'https'
const host = (req.headers['x-forwarded-host'] || req.get('host') || '').split(',')[0].trim()
return proto + '://' + host
}
function normalizeUrl(u) {
if (!u) return null
let s = String(u).trim()
if (!s) return null
// Если URL уже закодирован, раскодируем его один раз перед проверкой
if (s.indexOf('%3A') !== -1 || s.indexOf('%3a') !== -1) {
try { s = decodeURIComponent(s) } catch(e) {}
}
if (!/^https?:\/\//i.test(s)) {
s = 'https://' + s
}
try {
const url = new URL(s)
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
url.hash = ''
return url.toString()
} catch (e) {
return null
}
}
async function fetchText(url, timeoutMs, clientUA) {
const controller = new AbortController()
const t = setTimeout(() => controller.abort(), timeoutMs)
try {
const res = await fetch(url, {
redirect: 'follow',
signal: controller.signal,
headers: {
'user-agent': clientUA || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
})
const status = res.status
if (!res.ok) {
const txt = await res.text().catch(() => '')
const e = new Error('Fetch failed: ' + status)
e.status = status
e.body = txt
throw e
}
const buf = await res.arrayBuffer()
if (buf.byteLength > MAX_BYTES) {
const e = new Error('Too large')
e.status = 413
throw e
}
const text = Buffer.from(buf).toString('utf8')
const contentType = res.headers.get('content-type') || ''
return { text, contentType, status }
} finally {
clearTimeout(t)
}
}
function jsWrapper(serverBase, baseUrl, transpiledCode, needRegenerator, useVpn, useEs5, useStream) {
const safeServer = JSON.stringify(String(serverBase || ''))
const safeBaseUrl = JSON.stringify(String(baseUrl || ''))
const fetchPolyfill =
'(function(){' +
"if(typeof window.fetch==='function')return;" +
"function H(h){this.map={};if(!h)return;for(var k in h)if(h.hasOwnProperty(k))this.map[k.toLowerCase()]=String(h[k]);}" +
"H.prototype.get=function(k){return this.map[String(k).toLowerCase()]||null;};" +
"H.prototype.has=function(k){return this.map.hasOwnProperty(String(k).toLowerCase());};" +
"function R(b,o){this._body=b||'';this.status=o&&o.status||200;this.statusText=o&&o.statusText||'OK';this.headers=new H(o&&o.headers||{});this.ok=this.status>=200&&this.status<300;}" +
"R.prototype.text=function(){return Promise.resolve(String(this._body));};" +
"R.prototype.json=function(){return Promise.resolve(String(this._body)).then(JSON.parse);};" +
"window.fetch=function(u,opt){opt=opt||{};return new Promise(function(resolve,reject){try{var x=new XMLHttpRequest();x.open((opt.method||'GET'),u,true);var h=opt.headers||{};for(var k in h)if(h.hasOwnProperty(k))x.setRequestHeader(k,h[k]);x.onreadystatechange=function(){if(x.readyState===4){resolve(new R(x.responseText,{status:x.status||0,statusText:x.statusText||'',headers:{}}));}};x.onerror=function(){reject(new Error('Network error'));};x.send(opt.body||null);}catch(e){reject(e);}});};" +
'})();'
const cssVarsPonyfill =
'(function(){' +
'function t(v){return (v===null||v===undefined)?\'\':String(v);}' +
'function gOrigin(u){try{var a=document.createElement("a");a.href=u;return (a.protocol? a.protocol:"") + "//" + (a.host||"");}catch(e){return "";}}' +
'function collect(){var map={};var styles=document.getElementsByTagName("style");for(var i=0;i<styles.length;i++){var css=t(styles[i].textContent||styles[i].innerText||"");var m=css.match(/:root\\s*\\{[\\s\\S]*?\\}/g);if(!m)continue;for(var j=0;j<m.length;j++){var block=m[j];var r=/--([A-Za-z0-9_-]+)\\s*:\\s*([^;]+);/g;var mm;while((mm=r.exec(block))){map["--"+mm[1]]=t(mm[2]).replace(/^\\s+|\\s+$/g,"");}}}return map;}' +
'function applyVars(map){var styles=document.getElementsByTagName("style");for(var i=0;i<styles.length;i++){var css=t(styles[i].textContent||styles[i].innerText||"");if(css.indexOf("var(")===-1)continue;var out=css.replace(/var\\(\\s*(--[A-Za-z0-9_-]+)\\s*(?:,\\s*([^\\)]+))?\\)/g,function(_,name,fb){var v=map[name];if(v)return v;return fb?String(fb).replace(/^\\s+|\\s+$/g,""):"";});if(out!==css){try{styles[i].textContent=out;}catch(e){try{styles[i].innerText=out;}catch(e2){}}}}}' +
'window.__es5CssVarsApply=function(){try{var map=collect();applyVars(map);}catch(e){}};' +
'})();'
const patchLoaders =
'(function(){' +
'var __es5_server=' +
safeServer +
';' +
'var __es5_base=' +
safeBaseUrl +
';' +
'var __es5_active=true;' +
'var Lampa=window.Lampa;' +
'function t(v){return (v===null||v===undefined)?\'\':String(v);}' +
'function baseDir(u){u=t(u);u=u.replace(/[?#].*$/,"");return u.replace(/[^\\/]*$/,"");}' +
'var __es5_base_dir=baseDir(__es5_base);' +
'function abs(u){u=t(u);if(!u)return u;if(u.indexOf("/p?url=")!==-1||u.indexOf("/s?url=")!==-1)return u;if(u.indexOf("http://")===0||u.indexOf("https://")===0)return u;if(u.indexOf("//")===0)return (location.protocol||"https:")+u;if(u.charAt(0)=="/"){var a=document.createElement("a");a.href=__es5_base;return (a.protocol? a.protocol:"") + "//" + (a.host||"") + u;}return __es5_base_dir + u;}' +
'function origin(u){try{var a=document.createElement("a");a.href=u;return (a.protocol? a.protocol:"") + "//" + (a.host||"");}catch(e){return "";}}' +
'var allow=window.__es5ProxyAllow||{};window.__es5ProxyAllow=allow;' +
'allow[origin(__es5_base)]=1;' +
'function proxyJs(u){var a=abs(u);var o=origin(a);if(__es5_active)allow[o]=1;allow[o]=1;if(allow[o]&&__es5_server){try{var testUrl=new URL(a);if(testUrl.protocol==="http:"||testUrl.protocol==="https:")return __es5_server + "/p?url=" + encodeURIComponent(a).replace(/%3A/gi, ":").replace(/%2F/gi, "/") + "&es5=' + (useEs5?'1':'0') + '&vpn=' + (useVpn?'1':'0') + '";}catch(e){}}return a;}' +
'function proxyCss(u){var a=abs(u);var o=origin(a);if(__es5_active)allow[o]=1;if(allow[o]&&__es5_server)return __es5_server + "/s?url=" + encodeURIComponent(a).replace(/%3A/gi, ":").replace(/%2F/gi, "/");return a;}' +
'function proxyRaw(u){var a=abs(u);if(!/^https?:\\/\\//i.test(a))return u;if(__es5_server&&a.indexOf(__es5_server)===-1){return __es5_server+"/r?url="+encodeURIComponent(a).replace(/%3A/gi, ":").replace(/%2F/gi, "/");}return a;}' +
'function mapArr(items, fn){if(!items||!items.length)return items;for(var i=0;i<items.length;i++){items[i]=fn(items[i]);}return items;}' +
'try{' +
'if(Lampa&&Lampa.Utils){' +
'if(Lampa.Utils.putScriptAsync&&!Lampa.Utils.putScriptAsync.__es5_proxy){var o=Lampa.Utils.putScriptAsync;var w=function(items,complite,error,success,show_logs){try{items=mapArr(items,proxyJs);}catch(e){}return o.call(this,items,complite,error,success,show_logs);};w.__es5_proxy=1;Lampa.Utils.putScriptAsync=w;}' +
'if(Lampa.Utils.putScript&&!Lampa.Utils.putScript.__es5_proxy){var o2=Lampa.Utils.putScript;var w2=function(items,complite,error,success,show_logs){try{items=mapArr(items,proxyJs);}catch(e){}return o2.call(this,items,complite,error,success,show_logs);};w2.__es5_proxy=1;Lampa.Utils.putScript=w2;}' +
'if(Lampa.Utils.putStyle&&!Lampa.Utils.putStyle.__es5_proxy){var o3=Lampa.Utils.putStyle;var w3=function(items,complite,error){try{items=mapArr(items,proxyCss);}catch(e){}return o3.call(this,items,complite,error);};w3.__es5_proxy=1;Lampa.Utils.putStyle=w3;}' +
'}' +
'if(!document.__es5_proxy_patch){' +
'document.__es5_proxy_patch=1;' +
'var origAppend=Element.prototype.appendChild;' +
'var origInsertBefore=Element.prototype.insertBefore;' +
'function patchNode(node){' +
'if(node&&node.tagName==="SCRIPT"&&node.src){' +
'var a=abs(node.src);var o2=origin(a);if(__es5_active)allow[o2]=1;allow[o2]=1;if(allow[o2]&&__es5_server&&node.src.indexOf(__es5_server)===-1){try{var testUrl=new URL(a);if(testUrl.protocol==="http:"||testUrl.protocol==="https:"){node.src=__es5_server+"/p?url="+encodeURIComponent(a).replace(/%3A/gi, ":").replace(/%2F/gi, "/")+"&es5=' + (useEs5?'1':'0') + '&vpn=' + (useVpn?'1':'0') + '";}}catch(e){}}' +
'}' +
'}' +
'Element.prototype.appendChild=function(node){' +
'patchNode(node);' +
'return origAppend.call(this,node);' +
'};' +
'Element.prototype.insertBefore=function(node, child){' +
'patchNode(node);' +
'return origInsertBefore.call(this,node, child);' +
'};' +
'}' +
'}catch(e){}' +
(useEs5 ? 'try{if(window.__es5CssVarsApply)window.__es5CssVarsApply();}catch(e){}' : '') +
(useVpn ? (
'var _origFetch=window.fetch;' +
'var fetch=function(input,init){' +
'try{' +
'if(typeof input==="string"){input=proxyRaw(input);}' +
'else if(input&&input.url){input=new Request(proxyRaw(input.url),input);}' +
'}catch(e){}' +
'return _origFetch.call(window,input,init);' +
'};' +
'var _RealXHR=window.XMLHttpRequest;' +
'var XMLHttpRequest=function(){' +
'var xhr=new _RealXHR();' +
'var origOpen=xhr.open;' +
'xhr.open=function(method,u,async,user,pass){' +
'var args=Array.prototype.slice.call(arguments);' +
'try{args[1]=proxyRaw(args[1]);}catch(e){}' +
'return origOpen.apply(xhr,args);' +
'};' +
'return xhr;' +
'};' +
'try{' +
'if(Lampa&&Lampa.Reguest){' +
'var _RealReguest=Lampa.Reguest;' +
'var _reguestMethods=["silent","native","send","get","post","ajax"];' +
'var ReguestShim=function(){' +
'var inst=new _RealReguest();' +
'for(var i=0;i<_reguestMethods.length;i++){' +
'(function(mName){' +
'var orig=inst[mName];' +
'if(typeof orig==="function"){' +
'inst[mName]=function(url){' +
'var args=Array.prototype.slice.call(arguments);' +
'try{args[0]=proxyRaw(url);}catch(e){}' +
'return orig.apply(inst,args);' +
'};' +
'}' +
'})(_reguestMethods[i]);' +
'}' +
'return inst;' +
'};' +
'Lampa=Object.create(Lampa);' +
'Lampa.Reguest=ReguestShim;' +
'}' +
'}catch(e){}'
) : '') +
(useStream ? (
'try{' +
'if(Lampa&&Lampa.Player){' +
'if(!Lampa.hasOwnProperty("Player")) Lampa=Object.create(Lampa);' +
'var _RealPlayer=Lampa.Player;' +
'Lampa.Player=Object.create(_RealPlayer);' +
'Lampa.Player.play=function(data){' +
'try{' +
'if(data&&data.url&&typeof data.url==="string"&&data.url.indexOf(__es5_server)===-1&&data.url.indexOf("warp.cfhttp.top")===-1){' +
'data.url=__es5_server+"/r?url="+encodeURIComponent(data.url).replace(/%3A/gi, ":").replace(/%2F/gi, "/");' +
'}' +
'}catch(e){}' +
'return _RealPlayer.play.apply(this,arguments);' +
'};' +
'}' +
'}catch(e){}'
) : '') +
'try{' +
'var __mock_script = document.createElement("script");' +
'__mock_script.src = __es5_base;' +
'var __orig_cs_desc;' +
'try {' +
'__orig_cs_desc = Object.getOwnPropertyDescriptor(Document.prototype, "currentScript") || Object.getOwnPropertyDescriptor(HTMLDocument.prototype, "currentScript") || Object.getOwnPropertyDescriptor(document, "currentScript");' +
'if(!__orig_cs_desc) { document.currentScript = __mock_script; } else {' +
'Object.defineProperty(document, "currentScript", { get: function(){ return __mock_script; }, configurable: true });' +
'}' +
'} catch(e_cs) {}' +
transpiledCode +
'}catch(e){try{console.error("ES5 proxy plugin error",e&&e.message?e.message:e);}catch(e2){}}' +
'finally {' +
'try { if(__orig_cs_desc) Object.defineProperty(document, "currentScript", __orig_cs_desc); else delete document.currentScript; } catch(e_cs2) {}' +
'}' +
'__es5_active=false;' +
(useEs5 ? 'try{if(window.__es5CssVarsApply)window.__es5CssVarsApply();}catch(e){}' : '') +
'})();'
const parts = []
if (useEs5) {
parts.push(fetchPolyfill)
if (needRegenerator) parts.push(regeneratorRuntimeCode)
parts.push(cssVarsPonyfill)
}
parts.push(patchLoaders)
return parts.join('\n')
}
async function cssInline(url, depth, clientUA) {
if (depth > MAX_CSS_DEPTH) return ''
const { text } = await fetchText(url, 25_000, clientUA)
const base = url.replace(/[?#].*$/, '').replace(/[^/]*$/, '')
let css = text
css = css.replace(/@import\s+(?:url\()?\s*["']?([^"')\s]+)["']?\s*\)?\s*;/gi, (m, imp) => {
try {
const abs = new URL(String(imp), base).toString()
return '/*__es5_import__:' + abs + '*/'
} catch (e) {
return ''
}
})
const imports = []
css.replace(/\/\*__es5_import__:(.*?)\*\//g, (m, u) => {
imports.push(u)
return m
})
for (let i = 0; i < imports.length; i++) {
let inlined = ''
try {
inlined = await cssInline(imports[i], depth + 1, clientUA)
} catch (e) {
inlined = ''
}
css = css.replace('/*__es5_import__:' + imports[i] + '*/', inlined)
}
css = css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (m, q, u) => {
const raw = String(u || '').trim()
if (!raw) return m
if (/^(data:|blob:|https?:\/\/|\/\/)/i.test(raw)) return 'url(' + raw + ')'
try {
const abs = new URL(raw, base).toString()
return 'url(' + abs + ')'
} catch (e) {
return m
}
})
return css
}
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', '*');
next();
})
app.get('/health', (req, res) => {
res.status(200).json({ ok: true })
})
app.get('/client/es5_proxy_store.js', (req, res) => {
res.setHeader('content-type', 'application/javascript; charset=utf-8')
res.setHeader('cache-control', 'public, max-age=600')
res.sendFile(path.join(__dirname, '..', 'es5_proxy_store.js'))
})
app.get('/s', async (req, res) => {
const source = normalizeUrl(req.query.url)
if (!source) return res.status(400).type('text/plain').send('Bad url')
try {
const css = await cssInline(source, 0, req.headers['user-agent'])
res.setHeader('content-type', 'text/css; charset=utf-8')
res.setHeader('cache-control', 'public, max-age=600')
res.status(200).send(css)
} catch (e) {
const code = e && e.status ? e.status : 500
res.status(code).type('text/plain').send('CSS fetch error')
}
})
app.get('/p', async (req, res) => {
let rawUrl = req.query.url;
// Попытка раскодировать URL, если он пришел дважды закодированным
if(rawUrl && (rawUrl.indexOf('%3A') !== -1 || rawUrl.indexOf('%3a') !== -1)){
try { rawUrl = decodeURIComponent(rawUrl); } catch(e){}
}
const source = normalizeUrl(rawUrl)
if (!source) return res.status(400).type('text/plain').send('Bad url')
const useEs5 = req.query.es5 !== '0';
const useVpn = req.query.vpn !== '0';
const useStream = req.query.stream === '1';
try {
const { text } = await fetchText(source, 25_000, req.headers['user-agent'])
let code = text;
let needRegenerator = false;
if (useEs5) {
const result = await babel.transformAsync(text, {
babelrc: false,
configFile: false,
sourceType: 'script',
compact: true,
comments: false,
presets: [
[
require('@babel/preset-env'),
{
targets: { chrome: '38' },
bugfixes: true,
loose: true,
modules: false
}
]
]
})
if (result && result.code) {
code = result.code;
needRegenerator = code.indexOf('regeneratorRuntime') !== -1;
}
}
const serverBase = getSelfBase(req)
const out = jsWrapper(serverBase, source, code, needRegenerator, useVpn, useEs5, useStream)
res.setHeader('content-type', 'application/javascript; charset=utf-8')
res.setHeader('cache-control', 'public, max-age=600')
res.status(200).send(out)
} catch (e) {
const code = e && e.status ? e.status : 500
res.status(code).type('text/plain').send('JS transform error')
}
})
// Универсальный "сырой" прокси: любые данные как есть — JSON от API источника,
// картинки, m3u8/mp4-потоки и т.д. Без Babel и без лимита в MAX_BYTES, со стримингом
// и поддержкой Range (нужно для перемотки видео).
const RAW_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
// Заголовки, которые НЕ пробрасываем на апстрим — специфичные для соединения
// клиент<->наш сервер, пересылать их дальше некорректно/бессмысленно.
const REQ_HEADERS_SKIP = new Set([
'host', 'connection', 'content-length', 'accept-encoding',
'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host',
'cf-connecting-ip', 'cf-ray', 'cf-visitor', 'x-real-ip',
'origin', 'referer'
])
const RES_HEADERS_PASS = [
'content-type', 'content-length', 'content-range', 'accept-ranges',
'cache-control', 'expires', 'last-modified', 'etag', 'set-cookie'
]
app.all('/r', async (req, res) => {
let rawUrl = req.query.url
if (rawUrl && (rawUrl.indexOf('%3A') !== -1 || rawUrl.indexOf('%3a') !== -1)) {
try { rawUrl = decodeURIComponent(rawUrl) } catch (e) {}
}
const source = normalizeUrl(rawUrl)
if (!source) return res.status(400).type('text/plain').send('Bad url')
const method = (req.method || 'GET').toUpperCase()
if (RAW_METHODS.indexOf(method) === -1) {
return res.status(405).type('text/plain').send('Method not allowed')
}
if (method === 'OPTIONS') return res.sendStatus(204)
const controller = new AbortController()
req.on('close', () => controller.abort())
const headers = {}
for (const h in req.headers) {
if (REQ_HEADERS_SKIP.has(h)) continue
if (h.indexOf('sec-') === 0) continue // sec-fetch-*, sec-ch-ua* — метаданные браузера, апстриму не нужны и могут смутить его антибот-защиту
headers[h] = req.headers[h]
}
const targetUrl = new URL(source);
headers['origin'] = targetUrl.origin;
headers['referer'] = targetUrl.origin + '/';
if (!headers['user-agent']) {
headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
}
let body
if (method !== 'GET' && method !== 'HEAD') {
const chunks = []
for await (const chunk of req) chunks.push(chunk)
if (chunks.length) body = Buffer.concat(chunks)
}
try {
const upstream = await fetch(source, {
method,
headers,
body,
redirect: 'follow',
signal: controller.signal
})
const isJson = (upstream.headers.get('content-type') || '').toLowerCase().includes('application/json');
const isM3u8 = (upstream.headers.get('content-type') || '').toLowerCase().includes('mpegurl') || source.toLowerCase().includes('.m3u8');
if (isJson) {
const buffer = await upstream.arrayBuffer();
let text = Buffer.from(buffer).toString('utf8');
// Replace wss:// urls to go through our proxy
const serverBase = getSelfBase(req);
const wsBase = serverBase.replace(/^http/, 'ws');
text = text.replace(/wss:\/\/[^"'\s]+/g, (match) => {
return wsBase + '/ws?url=' + encodeURIComponent(match);
});
const outBuf = Buffer.from(text, 'utf8');
res.status(upstream.status);
for (const h of RES_HEADERS_PASS) {
if (h === 'content-length') {
res.setHeader(h, outBuf.length);
continue;
}
let v;
if (h === 'set-cookie') {
const cookies = upstream.headers.getSetCookie ? upstream.headers.getSetCookie() : [];
if (cookies.length) {
// Strip domain from cookies so they apply to our proxy's domain
v = cookies.map(c => c.replace(/domain=[^;]+;?\s*/gi, ''));
}
} else {
v = upstream.headers.get(h);
}
if (v) res.setHeader(h, v);
}
res.setHeader('access-control-allow-origin', '*');
return res.end(outBuf);
} else if (isM3u8) {
const buffer = await upstream.arrayBuffer();
let text = Buffer.from(buffer).toString('utf8');
const serverBase = getSelfBase(req);
const baseUrl = new URL(source);
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (line && !line.startsWith('#')) {
try {
const absoluteUrl = new URL(line, baseUrl).toString();
lines[i] = serverBase + '/r?url=' + encodeURIComponent(absoluteUrl);
} catch(e) {}
} else if (line.startsWith('#EXT-X-STREAM-INF:') || line.startsWith('#EXT-X-MEDIA:') || line.startsWith('#EXT-X-I-FRAME-STREAM-INF:')) {
lines[i] = line.replace(/URI=["']([^"']+)["']/g, (match, uri) => {
try {
const absoluteUrl = new URL(uri, baseUrl).toString();
return `URI="${serverBase}/r?url=${encodeURIComponent(absoluteUrl)}"`;
} catch(e) {
return match;
}
});
}
}
text = lines.join('\n');
const outBuf = Buffer.from(text, 'utf8');
res.status(upstream.status);
for (const h of RES_HEADERS_PASS) {
if (h === 'content-length') {
res.setHeader(h, outBuf.length);
continue;
}
let v = upstream.headers.get(h);
if (v) res.setHeader(h, v);
}
res.setHeader('access-control-allow-origin', '*');
return res.end(outBuf);
}
res.status(upstream.status)
for (const h of RES_HEADERS_PASS) {
if (h === 'content-length') {
const ct = upstream.headers.get('content-type') || '';
if (!ct.includes('video/') && !ct.includes('audio/') && !upstream.headers.has('content-range')) {
continue; // fetch decompresses gzip/br, so original content-length is invalid for text/json
}
}
let v;
if (h === 'set-cookie') {
const cookies = upstream.headers.getSetCookie ? upstream.headers.getSetCookie() : [];
if (cookies.length) {
v = cookies.map(c => c.replace(/domain=[^;]+;?\s*/gi, ''));
}
} else {
v = upstream.headers.get(h)
}
if (v) res.setHeader(h, v)
}
res.setHeader('access-control-allow-origin', '*')
if (!upstream.body) return res.end()
Readable.fromWeb(upstream.body).pipe(res)
} catch (e) {
if (!res.headersSent) {
const code = e && e.status ? e.status : 502
res.status(code).type('text/plain').send('Proxy error')
} else {
try { res.end() } catch (e2) {}
}
}
})
app.options('*', (req, res) => res.sendStatus(204))
const port = Number(process.env.PORT || 7860)
const server = app.listen(port, () => {
console.log('lampa-es5-proxy on :' + port)
})
server.on('upgrade', (req, clientSocket, head) => {
try {
const urlObj = new URL(req.url, 'http://localhost');
if (urlObj.pathname === '/ws') {
let targetUrlStr = urlObj.searchParams.get('url');
if (!targetUrlStr) {
clientSocket.destroy();
return;
}
const targetUrl = new URL(targetUrlStr);
// Pass along any extra query parameters Lampa might have appended (like &nws_id=...)
urlObj.searchParams.forEach((value, key) => {
if (key !== 'url') {
targetUrl.searchParams.append(key, value);
}
});
// Filter headers carefully
const proxyHeaders = {};
for (const h in req.headers) {
if (['host', 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'cf-connecting-ip', 'cf-ray', 'cf-visitor'].includes(h.toLowerCase())) continue;
proxyHeaders[h] = req.headers[h];
}
proxyHeaders['host'] = targetUrl.hostname;
proxyHeaders['origin'] = targetUrl.protocol.replace('ws', 'http') + '//' + targetUrl.hostname;
if (!proxyHeaders['user-agent']) {
proxyHeaders['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
}
const options = {
port: targetUrl.port || (targetUrl.protocol === 'wss:' ? 443 : 80),
host: targetUrl.hostname,
servername: targetUrl.hostname, // CRITICAL FOR SNI (Cloudflare drops without this)
headers: proxyHeaders,
path: targetUrl.pathname + targetUrl.search,
rejectUnauthorized: false
};
const proto = targetUrl.protocol === 'wss:' ? require('https') : require('http');
const proxyReq = proto.request(options);
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
let headers = 'HTTP/1.1 101 Switching Protocols\r\n';
for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {
headers += proxyRes.rawHeaders[i] + ': ' + proxyRes.rawHeaders[i+1] + '\r\n';
}
headers += '\r\n';
clientSocket.write(headers);
if (proxyHead && proxyHead.length) clientSocket.write(proxyHead);
proxySocket.pipe(clientSocket);
clientSocket.pipe(proxySocket);
});
// Handle server rejecting the WS connection (e.g. 403 or 400)
proxyReq.on('response', (proxyRes) => {
console.error('WS Proxy rejected with status:', proxyRes.statusCode);
clientSocket.write(`HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}\r\n\r\n`);
clientSocket.destroy();
});
proxyReq.on('error', (err) => {
console.error('WS Proxy error:', err.message);
clientSocket.destroy();
});
clientSocket.on('error', () => {
proxyReq.destroy();
});
// Send the initial upgrade request
proxyReq.end();
} else {
clientSocket.destroy();
}
} catch(e) {
console.error('WS Upgrade catch error:', e.message);
clientSocket.destroy();
}
})