wssagent
Version:
transform websocket encrypt proxy into normal local proxy, support CDN. 将远端的加密websocket代理转成本地普通代理,支持CDN中转
277 lines (230 loc) • 8.33 kB
JavaScript
const net = require('net');
const tls = require('tls');
const WebSocket = require('ws');
const dnsPromises = require('dns').promises;
const https = require('https');
const dnsPacket = require('dns-packet');
const AliveAgent = require('agentkeepalive').HttpsAgent;
const ipAgents = [];
//websocket connection agent
var httpsagent = new AliveAgent();
//wss url like wss://site.domain/url
var wssurl = '';
//wssagent listening port,also proxy port in firefox settings
var proxyport = 0 ;
//proxy servr ip address
var wssip = '';
//share proxy in local network
var shareproxy = false;
//DOH(DNS Over Https) server domain
var dohServer = '';
//connect Domain will replace domain in wssurl when connecting to proxy
var connectDomain = '';
var dohips = false;
var wssips = [];
var server = false;
var tlsserver = false;
var random = 0;
var wssDomain = '';
var dohurl = '';
function getRandom(arr) {
if(!arr) return false;
if(!Array.isArray(arr)) return arr;
if(arr.length==0) return false;
if(arr.length==1) return arr[0];
random++;
if(random>=513) random=0;
const choice = random % arr.length ;
return arr[choice];
}
function localhostLookup(hostname, opts, cb) {
if(opts && opts.all) cb(null, [{"address":'127.0.0.1', "family":4}]);
else cb(null, '127.0.0.1', 4);
}
function dohLookup(hostname, opts, cb) {
if(opts && opts.all) cb(null, [{"address":getRandom(dohips), "family":4}]);
else cb(null, getRandom(dohips), 4);
}
function wssLookup(hostname, opts, cb) {
if(opts && opts.all) cb(null, [{"address":getRandom(wssips), "family":4}]);
else cb(null, getRandom(wssips), 4);
}
function toRFC8484 (buffer) {
return buffer.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_')
}
async function dohResolve(host) {
const packet = dnsPacket.encode({
type: 'query',
flags: dnsPacket.RECURSION_DESIRED,
questions: [{
type: 'A',
name: host
}]
})
let vserver = dohServer;
let vpath = '/dns-query';
if(dohServer.toLocaleLowerCase().startsWith('https')){
const parsed = new URL(dohServer);
vserver = parsed.host;
vpath = parsed.pathname;
console.log(vpath);
}
const pth = vpath + '?dns=' + toRFC8484(packet);
const options = {
hostname: vserver,
port: 443,
path: pth,
method: 'GET',
headers: {
'Content-Type': 'application/dns-message',
},
timeout: 1000,
lookup: dohLookup
}
await dnsPromises.resolve4(vserver)
.then((ips)=>{
dohips = ips;
})
.catch((error)=>{
console.log('DOH: ' + error);
});
return new Promise(function(resolve, reject) {
if(!dohips.length) reject('DOH: DOH Server DNS error');
let request = https.get(options, (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(new Error('DOH statusCode=' + res.statusCode));
}
res.on('data', (d) => {
let data = dnsPacket.decode(d);
if(!data.answers) reject('DOH: wrong dns data');
if(data.answers.length==0) reject('DOH: no dns record for domain: ' + host);
resolve(data.answers.filter(answer => {return answer.type=='A'} ));
});
})
request.on('error', (e) => {
reject('DOH: ' + e);
});
});
}
dnsPromises.setServers(['1.1.1.1', '8.8.8.8', '208.67.222.222', '8.8.4.4', '208.67.220.220']);
async function run(configs){
if(configs) {
wssurl = configs.wssurl;
if(!wssurl.toLowerCase().startsWith('wss://')) return console.log('invalid wssurl');
if('proxyport' in configs) proxyport = configs.proxyport;
if('shareproxy' in configs) shareproxy = configs.shareproxy;
if(configs.wssip) wssip = configs.wssip;
if(configs.dohServer) dohServer = configs.dohServer;
if(configs.connectDomain) connectDomain = configs.connectDomain;
if(!wssip && !dohServer) dohServer = 'dns.cloudflare.com';
} else {
return console.log('invalid arguments');
}
if(wssip) wssips = wssip.split(',');
console.log('\r\nShare Proxy: ' + shareproxy);
console.log('\r\nDOH Server: ' + dohServer);
console.log('\r\nWSSIP array: ' + JSON.stringify(wssips));
console.log('\r\nConnect Domain: ' + connectDomain);
let url = new URL(wssurl);
wssDomain = url.host.split(":")[0];
if(connectDomain){
url.host = connectDomain;
wssurl = url.toString();
}
if(dohServer) {
await dohResolve(wssDomain)
.then( answers => {
let ips = answers.map(answer => answer.data );
ips.forEach(ip => console.log('\r\nDOH Got Proxy Server IP (WSSIP): ' + ip));
if(ips.length && !wssips.length) wssips = ips;
if(dohServer.toLowerCase().startsWith('https')) dohurl = dohServer;
else dohurl = 'https://' + dohServer + '/dns-query';
console.log('\r\nDOH url (for Firefox): ' + dohurl); })
.catch(err => {
console.log('\r\n' + err);
});
}
if(wssips.length) return start();
await dnsPromises.resolve4(wssDomain)
.then( ips =>{
wssips = ips;
ips.forEach(ip => console.log('\r\nDNS Got Proxy Server IP (WSSIP): ' + ip));
})
.catch( error =>{
console.log(error);
console.log('\r\nPlease specify WSSIP, or use a different WSSURL');
cb(error);
});
if(wssips.length) start();
}
function start() {
if(wssurl.toLowerCase().endsWith('/pac')) return connect();
else if(!wssurl.toLowerCase().endsWith('/tls')) return connect();
let vshare = shareproxy;
let vport = proxyport;
shareproxy = false;
proxyport = 0;
connect();
tlsserver = net.createServer(function(socket) {
let connOptions = {lookup : localhostLookup, rejectUnauthorized: false, keepAlive: true};
if(connectDomain) connOptions = {lookup : localhostLookup, keepAlive: true};
if(vshare) setAgent(socket.remoteAddress);
const upstream = tls.connect(server.address().port, wssDomain, connOptions);
socket.on('end', () => upstream.destroy());
socket.on('error', () => upstream.destroy());
upstream.on('end', () => socket.destroy());
upstream.on('error', () => socket.destroy());
socket.setNoDelay(true);
socket.pipe(upstream).pipe(socket);
});
tlsserver.on('error', (err) => {
console.log('\r\n tls server error '+ err);
})
if(vshare){
tlsserver.listen(vport, ()=>console.log('\r\nwssagent tls serve in port : ' + tlsserver.address().port));
} else {
tlsserver.listen(vport, '127.0.0.1', ()=>console.log('\r\nwssagent tls serve in port : ' + tlsserver.address().port));
}
}
function connect() {
server = net.createServer(c => {
if(shareproxy) setAgent(c.remoteAddress);
let connOptions = {lookup : wssLookup, agent: httpsagent};
if(connectDomain) connOptions = {lookup : wssLookup, agent: httpsagent, rejectUnauthorized: false} ;
const ws = new WebSocket(wssurl, connOptions);
const duplex = WebSocket.createWebSocketStream(ws);
duplex.on('close', () => c.destroy());
duplex.on('error', () => c.destroy());
c.on('end', () => {duplex.destroy(); ws.close(1000);});
c.on('error', () => {duplex.destroy(); ws.close(1000);});
duplex.pipe(c);
c.pipe(duplex);
})
server.on('error', (err) => {
console.log('\r\n server error '+ err);
})
let cb = {};
if(wssurl.toLowerCase().endsWith('/pac')) cb = ()=>console.log('\r\nwssagent pac serve in port : ' + server.address().port);
else if(!wssurl.toLowerCase().endsWith('/tls')) cb = ()=>console.log('\r\nwssagent serve in port : ' + server.address().port);
if(shareproxy){
server.listen(proxyport, cb);
} else {
server.listen(proxyport, '127.0.0.1', cb);
}
}
function setAgent(remoteIp){
if(!remoteIp) return;
let ipAgent = ipAgents.find(item => item.ip==remoteIp);
if(!ipAgent){
ipAgent = {ip: remoteIp, agent: new AliveAgent() };
ipAgents.push(ipAgent);
if(ipAgents.length>256) ipAgents.shift();
}
httpsagent = ipAgent.agent;
}
process.on('uncaughtException', (error)=>console.log(error))
exports.run = run ;