UNPKG

pacproxy-share

Version:

a website module used to share other websites, it can be deployed inside pacproxy-server, Nginx, LiteSpeed.

277 lines (220 loc) 11.2 kB
const http = require('http'); const https = require('https'); const zlib = require('zlib'); const replace = require('./replace.js'); const rejectAccess = (req, res) => response(res,403); const webAgent = new https.Agent({ keepAlive: true, maxCachedSessions : 1000 }); const shareModule = { "domain" : 'localhost', "root" : '/sharepath/', "ip" : '0.0.0.0', "logging" : false, "https" : true, "onNotFound" : rejectAccess } function share(options){ if(!options || typeof options != 'object') return share({'https': false}); if("domain" in options) { shareModule.domain=options.domain.toLowerCase().trim(); if(!checkDomain(shareModule.domain)) return console.error('Invalid domain name: %s', shareModule.domain); } if("root" in options) shareModule.root=options.root; if(!shareModule.root.startsWith('/')) shareModule.root='/'+shareModule.root; if(!shareModule.root.endsWith('/')) shareModule.root=shareModule.root + '/'; if("ip" in options) shareModule.ip=options.ip; if("port" in options) shareModule.port=options.port; if("logging" in options) shareModule.logging=options.logging; if("https" in options) shareModule.https=options.https; if("onNotFound" in options) shareModule.onNotFound=options.onNotFound; console.info('\nShare Internet with prefix: %s://%s%s%s', shareModule.https ? 'https' : 'http', shareModule.domain, (shareModule.https ? '' : ':'+ shareModule.port), shareModule.root); return shareHandler; } /** * handle website requests */ function shareHandler(req, res) { const visitorIP = req.socket.remoteAddress; log('%s %s %s ', visitorIP, req.headers.host, req.url); const host = req.headers.host; if(!host || host.split(':')[0].toLowerCase() != shareModule.domain) return shareModule.onNotFound(req, res); let url = req.url; if(url.includes(host + shareModule.root)) { if(req.method=='GET') return response(res, 301, {'location': url.split(host)[1]}); else url = url.split(host)[1]; } if(!url.startsWith(shareModule.root)){ if(req.headers.referer && req.headers.referer.includes(host + shareModule.root)) { const domainIndex = req.headers.referer.indexOf(shareModule.root) + shareModule.root.length; const domainRefer = req.headers.referer.slice(domainIndex).split('/')[0].toLowerCase(); if(!checkDomain(domainRefer)) return shareModule.onNotFound(req, res); else if(req.method == 'GET') return response(res, 301, {'location': shareModule.root + domainRefer + req.url}); else url = shareModule.root + domainRefer + req.url } else return shareModule.onNotFound(req, res); } url = url.slice(shareModule.root.length); try{ var parsed = new URL('https://' + url); } catch (e) { return response(res, 403); } if(!checkDomain(parsed.host)) return response(res, 403); const headers = {...req.headers}; const filterHeaders = {'User-Agent': headers['user-agent'], 'Host': parsed.host}; if(headers['accept-encoding']) filterHeaders['Accept-Encoding'] = headers['accept-encoding']; if(headers['accept-language']) filterHeaders['Accept-Language'] = headers['accept-language']; if(headers['cache-control']) filterHeaders['Cache-Control'] = headers['cache-control']; headers.host = parsed.host; parsed.method = req.method; parsed.headers = filterHeaders if(req.method=='POST') parsed.headers= headers; if(shareModule.ip !== '0.0.0.0'){ parsed.localAddress = shareModule.ip; } parsed.agent = webAgent; try { requestRemote(parsed, req, res); } catch (e) { log('%s Error %s ', visitorIP, e.message); } } function requestRemote(parsed, req, res) { const visitorIP = req.socket.remoteAddress; log('%s Fetch %s ', visitorIP, parsed.toString()); const host = req.headers.host; const rhost = parsed.host; let agent = http; if(parsed.protocol == 'https:') agent = https; var gotResponse = false; const onResponse = (proxyRes) => { if(isLocalIP(proxyRes.socket.remoteAddress)) return response(res,403); let statusCode = proxyRes.statusCode ? proxyRes.statusCode : 200; const headers = {...proxyRes.headers}; delete headers['cookie']; delete headers['set-cookie']; gotResponse = true; if ([301, 302, 307].includes(statusCode) && proxyRes.headers['location']) { let location = proxyRes.headers['location'].trim(); if(location.includes(host + shareModule.root)) { if(shareModule.https) location = location.replace('http', 'https'); else location = location.replace('https', 'http'); } else if(location.startsWith('/')) location = shareModule.root + rhost + location; else if(shareModule.https) location = location.replace('https://', 'https://' + host + shareModule.root).replace('http://', 'https://' + host + shareModule.root) else location = location.replace('http://', 'http://' + host + shareModule.root).replace('https://', 'http://' + host + shareModule.root) headers['location'] = location; res.writeHead(statusCode, headers); return proxyRes.pipe(res); } if(parsed.method != 'GET' ){ res.writeHead(statusCode, headers); return proxyRes.pipe(res); } const resHtml = headers['content-type'] && ( headers['content-type'].includes('text/html') || headers['content-type'].includes('text/css')); let resJs = rhost.endsWith('.ganjingworld.com') && parsed.pathname.endsWith('.js') && ( parsed.pathname.includes('/pages/_app-') || parsed.pathname.includes('/_next/static/chunks/main-') ); if(!resJs) resJs = rhost.endsWith('.shenyuncreations.com') && parsed.pathname.endsWith('.js') && ( parsed.pathname.includes('/pages/_app-') || parsed.pathname.includes('/_next/static/chunks/main-') ); if(!resJs) resJs = rhost.endsWith('.falundafa.org') && parsed.pathname.includes('functions.js'); if(resJs|| resHtml) delete headers['content-length']; const decoding = (headers['content-encoding'] || '').toLowerCase() ; const encoding = (headers['content-encoding'] || req.headers['content-encoding'] || '').toLowerCase() ; res.writeHead(statusCode, headers); let pipend = proxyRes; if (resHtml || resJs) { if(!decoding) pipend = proxyRes; else if(decoding.includes('gzip')) pipend = pipend.pipe( zlib.createGunzip()); else if(decoding.includes('zstd')) pipend = pipend.pipe(zlib.createZstdDecompress()) else if(decoding.includes('br')) pipend = pipend.pipe(zlib.createBrotliDecompress()) else if(decoding.includes('deflate')) pipend = pipend.pipe(zlib.createInflateRaw()) } if (resHtml) { if(shareModule.https){ pipend = pipend.pipe(replace('src="//', 'src="https://')).pipe(replace("src='//", "src='https://")); pipend = pipend.pipe(replace('href="//', 'href="https://')).pipe(replace("href='//", "href='https://")); } else{ pipend = pipend.pipe(replace('src="//', 'src="http://')).pipe(replace("src='//", "src='http://")); pipend = pipend.pipe(replace('href="//', 'href="http://')).pipe(replace("href='//", "href='http://")); } pipend = pipend.pipe(replace('src="/', 'src="' + shareModule.root + rhost + '/')).pipe(replace("src='/", "src='"+shareModule.root + rhost + '/')); pipend = pipend.pipe(replace('href="/', 'href="' + shareModule.root + rhost + '/')).pipe(replace("href='/", "href='" + shareModule.root + rhost + '/')); pipend = pipend.pipe(replace('url="/', 'url="' + shareModule.root + rhost + '/')).pipe(replace("url='/", "url='"+shareModule.root + rhost + '/')); pipend = pipend.pipe(replace('url("/', 'url("' + shareModule.root + rhost + '/')).pipe(replace("url('/", "url('"+shareModule.root + rhost + '/')); pipend = pipend.pipe(replace('url: "/', 'url: "' + shareModule.root + rhost + '/')).pipe(replace("url: '/", "url: '"+shareModule.root + rhost + '/')); pipend = pipend.pipe(replace('href=/', 'href=' + shareModule.root + rhost + '/')).pipe(replace('src=/', 'src=' + shareModule.root + rhost + '/')).pipe(replace('url(/', 'url(' + shareModule.root + rhost + '/')) } if (resHtml || resJs) { if( (rhost.endsWith('.ganjingworld.com') || rhost.endsWith('.shenyuncreations.com')) && parsed.pathname.includes('/_next/static/chunks/main-') ) pipend = pipend.pipe(replace('"/_next/', '"' + shareModule.root + rhost + '/_next/')) else if(rhost.endsWith('.ganjingworld.com') && (parsed.pathname.includes('/embed/') || parsed.pathname.includes('/live/')) || parsed.pathname.includes('/video/') ) pipend = pipend.pipe(replace('https://www.ganjingworld.', (shareModule.https? 'https://' : 'http://') + host + shareModule.root + "www.ganjingworld.")); else if(shareModule.https) pipend = pipend.pipe(replace('https://', 'https://' + host + shareModule.root)).pipe(replace('http://', 'https://' + host + shareModule.root)); else pipend = pipend.pipe(replace('http://', 'http://' + host + shareModule.root)).pipe(replace('https://', 'http://' + host + shareModule.root)); if(rhost.endsWith('.soundofhope.org') && parsed.pathname.startsWith('/post/')) pipend = pipend.pipe(replace('//media.soundofhope.org', '//' + host + shareModule.root + "media.soundofhope.org")); if(encoding.includes('gzip')) pipend = pipend.pipe(zlib.createGzip()); else if(encoding.includes('zstd')) pipend = pipend.pipe(zlib.createZstdCompress()) else if(encoding.includes('br')) pipend = pipend.pipe(zlib.createBrotliCompress()) else if(encoding.includes('deflate')) pipend = pipend.pipe(zlib.createDeflateRaw()) pipend.pipe(res); } else { proxyRes.pipe(res); } } var proxyReq = agent.request(parsed, onResponse); proxyReq.on('error', (err) => { log('%s REQUEST %s ', visitorIP, err); if (gotResponse) {} else if ('ENOTFOUND' == err.code) response(res,400); else response(res,500); gotResponse = true; req.socket.end(); }); const endRequest = ()=>{ proxyReq.end(); res.removeListener('finish', endRequest); req.removeListener('end', endRequest); } res.on('finish', ()=>endRequest()); req.on('end', ()=>endRequest()); if(!req.writableEnded) req.pipe(proxyReq); else endRequest(); } function gErrorHandler(e) { log('General Error %s ', e.message); } function checkDomain(address) { if (!address) return false; else if(address.length > 255) return false var domainParts = address.split('.'); if(domainParts.length<2) return false; if (domainParts.some(function (part) { return part.length > 63; })) return false; return true; } function response(res, httpCode, headers, content) { res.on('error', gErrorHandler); if(headers) res.writeHead(httpCode, headers); else res.writeHead(httpCode); if(content) res.write(content); res.end(); } function log(...args) { if (shareModule.logging) console.log(...args); } function isLocalIP(address) { if(!address) return true; address = address.toLowerCase(); if(address.startsWith('::ffff:')) address = address.slice(7); if(address.startsWith('::') || address.startsWith('0')) return true; if(address.startsWith('fc') || address.startsWith('fd') || address.startsWith('fe')) return true; if(address.startsWith('192.168.') || address.startsWith('10.') || address.startsWith('127.') || address.startsWith('169.254.') || address.startsWith('172.16')) return true; return false; } process.on('uncaughtException', gErrorHandler); module.exports = share;