Spaces:
Sleeping
Sleeping
| 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(); | |
| } | |
| }) | |