UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

466 lines (439 loc) 15.5 kB
// 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 {NO_PEERS_ERROR} = require('./consts.js'); const zerr = require('../util/zerr.js'); const request = require('request'); const lpm_config = require('../util/lpm_config.js'); const {SEC, HOUR} = date.ms; const E = module.exports; const logger = require('./logger'); 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; if (Number(opt.keep_alive)===opt.keep_alive) this.keep_alive = opt.keep_alive; else { this.keep_alive = !!((opt.pool_size || opt.sticky_ip) && !this.opt.static && opt.preset=='long_availability'); } 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.reset_idle_pool(); this.init(); } util.inherits(Sess_mgr, events.EventEmitter); Sess_mgr.prototype.init = function(){ this.sessions = {sessions: []}; // XXX krzysztof: is this needed? hosts should always exist if (!this.serv.hosts) return; if (this.opt.ips && this.opt.static) 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.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; if (this.opt.static) session.ip = ip; this.sessions.sessions.push(session); if (this.opt.static) 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.stop_keep_alive(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.stop_keep_alive(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.seed}`, this.sessions, opt); if (session) { this.sessions.sessions.push(session); this.sp.spawn(this.set_keep_alive(session, opt)); } } 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.reset_idle_pool = function(){ const offset = Number.isInteger(this.opt.idle_pool) ? this.opt.idle_pool : HOUR; this.idle_date = +date()+offset; }; Sess_mgr.prototype.set_keep_alive = etask._fn( function*set_keep_alive(_this, session, opt={}){ try { if (!_this.keep_alive) return; _this.stop_keep_alive(session); session.keep_alive = this; const keep_alive_sec = _this.keep_alive===true ? 45 : _this.keep_alive; if (!opt.immediate) yield etask.sleep(keep_alive_sec*SEC); let idle = false; while (!idle && session.keep_alive && session.keep_alive==this) { yield _this.keep_alive_handler(session); yield etask.sleep(keep_alive_sec*SEC); idle = +date()>_this.idle_date; } logger.debug('session %s: keep alive ended', session.session); } catch(e){ logger.error(zerr.e2s(e)); } }); Sess_mgr.prototype.keep_alive_handler = etask._fn( function*keep_alive_handler(_this, session){ // XXX krzysztof: double check if this is needed. why not removing session if (_this.is_session_expired(session) || _this.serv.stopped) return; logger.debug('Keep alive %s:%s', session.host, session.session); const res = yield _this.send_info_request(session); const proxy_err = _this.serv.check_proxy_response(session.host, res.res, 'SESSION KEEP ALIVE'); let err; const curr_ips = _this.sessions && _this.sessions.sessions.map(s=>s.info && s.info.ip) || []; const this_ip = session.info && session.info.ip; if (proxy_err || res.err || !res.info) err = 'keep alive failed'; else if (res.info.ip!=this_ip && curr_ips.includes(res.info.ip)) err = `IP ${res.info.ip} already in the pool`; else if (_this.serv.is_ip_banned(res.info.ip)) err = `IP ${res.info.ip} is banned`; if (err) { if (_this.opt.session_termination && res.res && res.res.statusCode==502 && res.res.statusMessage==NO_PEERS_ERROR) { _this.serv.handle_session_termination(); } return _this.replace_session(session, err); } let sess_info = session.info; if (!sess_info || !sess_info.ip) sess_info = res.info; if (res.info.ip!=sess_info.ip) { logger.debug('session %s: ip change %s -> %s', session.session, sess_info.ip, res.info.ip); if (_this.opt.session_termination) _this.serv.handle_session_termination(); } session.info = res.info; session.last_res = {ts: Date.now(), ip: res.info.ip, session: session.session}; }); 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.serialize_session = function(session){ return { session: session.session, ip: session.ip || session.last_res && session.last_res.ip, host: session.host, username: session.username, created: session.created, }; }; Sess_mgr.prototype.get_sessions = function(){ return (this.sessions && this.sessions.sessions || []).reduce((acc, s)=> Object.assign({}, acc, {[s.session]: this.serialize_session(s)}), {}); }; Sess_mgr.prototype.remove_session = function(session){ session.canceled = true; this.stop_keep_alive(session); 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 (this.opt.static && session.ip && this.opt.ips && this.opt.ips.includes(session.ip)) { this.serv.emit('remove_static_ip', session.ip); } }; Sess_mgr.prototype.stop_keep_alive = function(session){ if (!session || !session.keep_alive) return; session.keep_alive.return(); session.keep_alive = null; }; Sess_mgr.prototype.request_completed = function(session){ if (!session || session.canceled || session.pool && session.pool.canceled) return true; if (!session.pool && session!=this.session) return this.stop_keep_alive(session); this.sp.spawn(this.set_keep_alive(session)); }; 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; if (expired) this.stop_keep_alive(session); return expired; }; Sess_mgr.prototype.send_info_request = etask._fn( function*send_info_request(_this, session){ const opt = { url: _this.opt.test_url, proxy: 'http://127.0.0.1:'+_this.opt.port, headers: { 'x-hola-agent': lpm_config.hola_agent, 'x-lpm-keep-alive': session.session, }, }; let res, err, info; try { request(opt, (error, _res)=>{ if (error) err = error; else this.continue(_res); }); res = yield this.wait(); const ct = res.headers && res.headers['content-type']; if (res.statusCode==200 && ct && ct.match(/\/json/)) info = JSON.parse(res.body); } catch(e){ err = e; logger.warn(zerr.e2s(e)); } return {res, info, err}; }); Sess_mgr.prototype.request_session = function(req){ const ctx = req.ctx; if (ctx.h_session) this.serv.emit('feature_used', 'h_session'); const sessions = this.sessions && this.sessions.sessions || []; if (ctx.h_keep_alive) return sessions.find(s=>s.session==ctx.h_keep_alive); 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._get_oldest_session = function(){ return this.sessions.sessions.sort((a, b)=>a.created-b.created)[0]; }; 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); } if (this.opt.preset=='long_availability') return this._get_oldest_session(); 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.seed}`, 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; };