@luminati-io/luminati-proxy
Version:
A configurable local proxy for luminati.io
303 lines (285 loc) • 9.78 kB
JavaScript
// LICENSE_CODE ZON ISC
'use strict'; /*jslint node:true, esnext:true, evil: true*/
const _ = require('lodash');
const events = require('events');
const date = require('../util/date.js');
const etask = require('../util/etask.js');
const url = require('url');
const util = require('util');
const username = require('./username.js');
const zerr = require('../util/zerr.js');
const {SEC} = date.ms;
const E = module.exports;
const logger = require('./logger').child({category: 'SESS'});
function Sess_mgr(serv, opt){
events.EventEmitter.call(this);
this.sp = etask(function*sesssion_manager(){ yield this.wait(); });
this.opt = opt;
this.serv = serv;
this.session_pools = new Map();
this.setMaxListeners(Number.MAX_SAFE_INTEGER);
if (opt.session_duration)
this.session_duration = opt.session_duration*SEC;
this.session_id = 1;
this.sticky_sessions = {};
if (opt.session!==true && opt.session)
opt.pool_size = 1;
this.seed = Math.ceil(Math.random()*Number.MAX_SAFE_INTEGER).toString(16);
this.pool_prefill = !(this.opt.rules||[]).some(r=>
r.action && r.action.reserve_session);
this.init();
}
util.inherits(Sess_mgr, events.EventEmitter);
Sess_mgr.prototype.init = function(){
this.sessions = {sessions: []};
if (this.opt.ips)
this.pool(this.opt.ips.length, {init: true});
};
Sess_mgr.prototype.add_to_pool = function(session={}){
const ip = session.ip || session.last_res && session.last_res.ip;
if (!ip || !this.opt.pool_size)
return;
if (this.sessions.sessions.length>=this.opt.pool_size)
return;
const curr_ips = this.sessions.sessions
.map(s=>s.ip || s.last_res && s.last_res.ip);
if (curr_ips.includes(ip))
return;
if (this.sessions.sessions.map(s=>s.session).includes(session.session))
return;
session.ip = ip;
this.sessions.sessions.push(session);
this.serv.emit('add_static_ip', ip);
};
Sess_mgr.prototype.refresh_sessions = function(){
this.emit('refresh_sessions');
if (this.opt.pool_size && this.sessions)
{
this.sessions.sessions.shift();
this.pool_fetch();
}
if (this.opt.sticky_ip)
{
this.sticky_sessions.canceled = true;
this.sticky_sessions = {};
}
if (this.opt.session==true && this.session)
this.session = null;
};
Sess_mgr.prototype.establish_session = function(prefix, pool, opt={}){
let session_id, asn, ip, vip, host, ext_proxy, proxy_port;
if (pool && pool.canceled || this.stopped)
return;
const init_ips_pool = opt.init && this.opt.ips && this.opt.ips.length;
const ips = (init_ips_pool || this.is_using_pool()) && this.opt.ips || [];
const vips = this.opt.vips || [];
const ext_proxies = this.opt.ext_proxies||[];
if (this.opt.session!==true && this.opt.session)
session_id = this.opt.session;
else
session_id = `${prefix}_${this.session_id++}`;
ext_proxy = ext_proxies[this.session_id%ext_proxies.length];
if (ext_proxy)
{
ext_proxy = parse_proxy_string(ext_proxy, {
username: this.opt.ext_proxy_username,
password: this.opt.ext_proxy_password,
port: this.opt.ext_proxy_port,
});
host = ext_proxy.host;
proxy_port = ext_proxy.port;
}
else
{
host = this.serv.hosts.shift();
this.serv.hosts.push(host);
vip = vips[this.session_id%vips.length];
ip = ips[this.session_id%ips.length];
if (Array.isArray(this.opt.asn))
asn = this.opt.asn[this.session_id%this.opt.asn.length];
}
const cred = username.calculate_username(Object.assign({}, this.opt,
{ip, session: session_id, vip, ext_proxy}));
const now = Date.now();
const session = {
host,
session: session_id,
ip,
vip,
ext_proxy,
count: 0,
created: now,
username: cred.username,
pool,
proxy_port,
};
if (asn)
session.asn = asn;
if (this.session_duration)
session.expire = now+this.session_duration;
if (this.opt.max_requests)
session.max_requests = this.opt.max_requests;
logger.debug('new session added %s:%s', host, session_id);
return session;
};
Sess_mgr.prototype.pool_fetch = function(opt={}){
try {
if (opt.immediate===undefined)
opt.immediate = true;
const session = this.establish_session(this.serv.port,
this.sessions, opt);
if (session)
this.sessions.sessions.push(session);
} catch(e){
logger.error(zerr.e2s(e));
}
};
Sess_mgr.prototype.pool = function(count, opt){
if (!count)
return;
for (let i=0; i<count; i++)
this.pool_fetch(opt);
};
Sess_mgr.prototype.replace_session = function(session, err, opt={}){
logger.debug('removing session %s: %s', session.session, err);
this.remove_session(session);
if (this.opt.pool_size && this.pool_prefill)
this.pool_fetch(Object.assign({immediate: false}, opt));
};
Sess_mgr.prototype.remove_session = function(session){
session.canceled = true;
for (let [, s_pool] of this.session_pools)
s_pool.remove(session, this);
if (!session.pool)
return;
const sessions = _.isArray(session.pool) ? session.pool :
session.pool.sessions;
_.remove(sessions, s=>s===session);
if (session.ip && this.opt.ips &&
this.opt.ips.includes(session.ip))
{
this.serv.emit('remove_static_ip', session.ip);
}
};
Sess_mgr.prototype.is_session_banned = function(session){
return session.last_res && this.serv.is_ip_banned(session.last_res.ip);
};
Sess_mgr.prototype.is_session_expired = function(session){
if (!session || session.canceled || session.pool && session.pool.canceled)
return true;
const expired = session.max_requests && session.count>session.max_requests
|| session.expire && Date.now()>session.expire;
return expired;
};
Sess_mgr.prototype.request_session = function(req){
const ctx = req.ctx;
if (ctx.h_session)
this.serv.emit('feature_used', 'h_session');
let session = this._request_session(ctx);
if (session && (!session.session || ctx.h_session))
{
if (ctx.h_session && this.session)
this.session = null;
if (this.session)
session = this.session;
}
return session;
};
Sess_mgr.prototype.is_using_pool = function(){
const sessions = this.sessions && this.sessions.sessions || [];
return this.pool_prefill || this.opt.pool_size==sessions.length;
};
Sess_mgr.prototype._request_session = function(ctx, opt={}){
if (ctx.h_session)
return {session: ctx.h_session};
// using sessions from the pool
if (this.opt.pool_size && this.is_using_pool())
{
this.session = null;
let sessions;
if (!this.sessions)
{
sessions = this.sessions.sessions;
let size = this.opt.pool_size;
this.pool(size, opt);
}
else
{
sessions = this.sessions.sessions;
if (sessions.length!=this.opt.pool_size)
this.pool(this.opt.pool_size-(sessions.length||0), opt);
}
let session = sessions.shift();
if (!opt.init)
session.count++;
if (!opt.init && (this.is_session_expired(session) ||
this.is_session_banned(session)))
{
if (this.opt.pool_size>1 && !this.is_session_banned(session))
{
session.count = 0;
sessions.push(session);
}
if (sessions.length<this.opt.pool_size)
this.pool_fetch();
session = this.sessions.sessions[0];
session.count++;
if (this.session_duration)
session.expire = Date.now()+this.session_duration;
}
else
sessions.unshift(session);
return session;
}
// sticky, session based on IP
if (!opt.init && this.opt.sticky_ip)
{
const ip = ctx.src_addr && ctx.src_addr.replace(/\./g, '_');
let session = this.sticky_sessions[ip];
if (!session||this.is_session_expired(session)||
this.is_session_banned(session))
{
session = this.sticky_sessions[ip] = this.establish_session(
`${this.opt.port}_${ip}`, this.sticky_sessions);
}
return session;
}
// use default random session
if (this.opt.session===true && !this.opt.sticky_ip)
{
if (this.session && !opt.init)
this.session.count++;
if (!this.session || this.is_session_expired(this.session) ||
this.is_session_banned(this.session))
{
this.session = this.establish_session(this.seed, this.sessions);
if (!opt.init)
this.session.count++;
}
return this.session;
}
return {session: false};
};
Sess_mgr.prototype.stop = function(){
if (this.sp)
this.sp.return();
};
E.Sess_mgr = Sess_mgr;
const parse_proxy_string = (_url, defaults)=>{
if (!_url.match(/^(http|https|socks|socks5):\/\//))
_url = `http://${_url}`;
_url = Object.assign({}, defaults, _.omitBy(url.parse(_url), v=>!v));
const proxy = {
protocol: _url.protocol,
host: _url.hostname,
port: _url.port,
username: _url.username,
password: _url.password
};
let auth = [];
if (_url.auth)
auth = _url.auth.split(':');
proxy.username = auth[0]||proxy.username||'';
proxy.password = auth[1]||proxy.password||'';
return proxy;
};