UNPKG

luminati-proxy-stripped

Version:

A configurable local proxy for luminati.io

1,480 lines (1,425 loc) 88.6 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint node:true, esnext:true*/ const _ = require('lodash'); const events = require('events'); const http = require('http'); const https = require('https'); const dns = require('dns'); const url = require('url'); const tls = require('tls'); const net = require('net'); const fs = require('fs'); const zlib = require('zlib'); const stringify = require('json-stable-stringify'); const find_iface = require('./find_iface.js'); const stream = require('stream'); const request = require('request'); const request_stats = require('request-stats'); const util = require('util'); const log = require('./log.js'); const username = require('./username.js'); const http_shutdown = require('http-shutdown'); const Socks = require('./socks.js'); const ssl = require('./ssl.js'); const version = require('../package.json').version; const etask = require('../util/etask.js'); const zurl = require('../util/url.js'); const zutil = require('../util/util.js'); const zerr = require('../util/zerr.js'); const date = require('../util/date.js'); const zfile = require('../util/file.js'); const lpm_config = require('../util/lpm_config.js'); const restore_case = require('../util/http_hdr.js').restore_case; const qw = require('../util/string.js').qw; const assign = Object.assign; const E = module.exports = Luminati; E.pool_types = { sequential: 0, 'round-robin': 1, }; E.user_agent = 'luminati-proxy-manager/'+version; E.hola_agent = 'proxy='+version+' node='+process.version +' platform='+process.platform; // should be similar to lpm certificate E.https_servername = 'zproxy.luminati.io'; E.superproxy_domains = ['zproxy.lum-superproxy.io', 'zproxy.luminati.io', 'zproxy.luminati-china.io']; E.default = assign({}, lpm_config.luminati_default); E.dropin = assign({}, E.default, { port: E.default.proxy_port, listen_port: E.default.proxy_port, multiply: false, sticky_ip: true, allow_proxy_auth: true, pool_size: 0, max_requests: 0, keep_alive: false, session_duration: 0, session: false, seed: false, }); const lpm_headers = qw`x-hola-context proxy-authorization x-hola-agent x-lpm-src-addr x-lpm-session x-hola-timeline-debug x-lpm-reserved`; const loopback_ip = '127.0.0.1'; const ip_re = /^(https?:\/\/)?(\d+\.\d+\.\d+\.\d+)([$/:?])/i; let write_http_reply = (_stream, res, headers)=>{ headers = assign(headers||{}, res.headers||{}); if (_stream.x_hola_context) headers['x-hola-context'] = _stream.x_hola_context; if (_stream.cred) headers['x-lpm-authorization'] = _stream.cred; _stream.resp_written = true; if (_stream instanceof http.ServerResponse) return _stream.writeHead(res.statusCode, res.statusMessage, headers); let head = `HTTP/1.1 ${res.statusCode} ${res.statusMessage}\r\n`; for (let field in headers) head += `${field}: ${headers[field]}\r\n`; _stream.write(head+'\r\n'); }; let is_superproxy_domain = d=>E.superproxy_domains.includes(d); let reverse_lookup_dns = ip=>etask(function*resolve(){ try { let domains = yield etask.nfn_apply(dns, '.reverse', [ip]); return domains&&domains.length ? domains[0] : ip; } catch(e){ return ip; } }); let reverse_lookup_values = values=>{ const domains = {}; for (let line of values) { const m = line.match(/^\s*(\d+\.\d+\.\d+\.\d+)\s+([^\s]+)/); if (m) domains[m[1]] = m[2]; } return ip=>domains[ip]||ip; }; let req_remote_ip = req=>{ if (req.original_ip) return req.original_ip; if (req.headers['x-lpm-src-addr']) return req.headers['x-lpm-src-addr']; if (req.socket) { if (req.socket.remoteAddress) return req.socket.remoteAddress; if (req.socket.socket && req.socket.socket.remoteAddress) return req.socket.socket.remoteAddress; } return null; }; const rand_range = (start=0, end=1)=>Math.round( start+Math.random()*(end-start)); const parse_ip_url = url=>{ let match = url.match(ip_re); if (!match) return null; return {url: match[0]||'', protocol: match[1]||'', ip: match[2]||'', suffix: match[3]||''}; }; 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)); let 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; }; const req_util = { is_ssl: req=>req.socket instanceof tls.TLSSocket, is_connect: req=>req.method == 'CONNECT', full_url: req=>{ if (!req_util.is_ssl(req)) return req.url; let url = req.url.replace(/^(https?:\/\/[^\/]+)?\//, req.headers.host+'/'); return `https://${url}`; }, gen_id: (id, retry)=>{ if (!id) id = 'r-0-'+rand_range(1, 1000000); if (retry) id = id.replace(/-[0-9]*-/, `-${retry}-`); return id; }, }; const create_count_stream = resp=>new stream.Transform({ transform(data, encoding, cb){ if (resp.body_size<1024) resp.body.push(data.slice(0, 1024-resp.body_size)); resp.body_size += data.length; cb(null, data); }, }); class Router extends events.EventEmitter { constructor(opt){ super(); this.log = log(opt.listen_port, opt.log); this.opt = opt; if (opt.null_response) this.null_response = new RegExp(opt.null_response, 'i'); if (opt.bypass_proxy) this.bypass_proxy = new RegExp(opt.bypass_proxy, 'i'); if (opt.direct_include) { this.direct = {include: opt.direct_include && new RegExp(opt.direct_include, 'i')}; } this.proxy_internal_bypass = opt.proxy_internal_bypass; } is_null_response(url, req){ if (req && !this.is_bypass_proxy(req) && req.ctx.only_bypass) return true; return this.null_response && this.null_response.test(url); } is_fw_check(req){ return !!req.ctx.h_firewall_check; } is_proxy_req(req){ return !(this.is_fw_check(req)||this.is_null_response(req.ctx.url, req) ||this.is_bypass_proxy(req)); } send_fw_check_resp(req, res){ return res.end(JSON.stringify({pass: true})); } send_null_response(req, res){ const ctx = req.ctx; ctx.log.debug('Returning null response: %s %s', req.method, ctx.url); let status = req.method=='CONNECT' ? 501 : 200; write_http_reply(res, {statusCode: status, statusMessage: 'NULL'}); res.end(); ctx.timeline.track('end'); ctx.response.status_code = status; ctx.response.status_message = 'NULL'; return ctx.response; } is_bypass_proxy(req){ let ctx = req.ctx; let _url = ctx.url; let is_ssl = ctx.is_connect; let only_bypass = ctx.only_bypass; let intern = this.proxy_internal_bypass; let match_domain = (mask, hostname)=>{ let mp = mask.split('.'); let hp = hostname.split('.').slice(-mp.length); return mp.every((p, i)=>p=='*' || hp[i]==p); }; if (this.bypass_proxy && this.bypass_proxy.test(_url)) return true; if (!intern && !only_bypass) return false; let hostname = is_ssl ? _url.split(':')[0] : url.parse(_url).hostname; return intern && intern.some(x=>match_domain(x, hostname)) || only_bypass && only_bypass.some(x=>match_domain(x, hostname)); } is_direct(_url){ return this.direct && this.direct.include && this.direct.include.test(_url); } } class Context { constructor(req, res, lum, opt){ this.sp = etask(function*context(){ yield this.wait(); }); lum.sp.spawn(this.sp); this.opt = opt||{}; this.lum = lum; this.rules = lum.rules; this.banlist = lum.banlist; this.agent = lum.protocol; this.port = opt.listen_port; this.log = log(this.port, opt.log); this.req = req; this.res = res; this.retry = -1; this.id = null; this.timeline = new Timeline(this.port); this.only_bypass = this.opt.only_bypass; this.proxy_retry = opt.proxy_retry || 1; this.proxy_port = this.lum.opt.proxy_port; } static init_req_ctx(req, res, lum, opt={}){ // XXX maximk: should handle retry in a new context req.ctx = req.ctx || new Context(req, res, lum, opt); req.ctx.request_start(); return req.ctx; } request_start(){ this.responded = false; this.race_reqs = this.opt.race_reqs||0; this.proxies = []; this.retry = this.retry+1; this.req_timeout = this.req.timeout||0; this.sys_timeout = this.opt.timeout*1000 || 0; this.timeout = Math.min(this.sys_timeout, this.req_timeout); this.id = req_util.gen_id(this.id, this.retry); this.log = log(`${this.port} ${this.id}`, this.opt.log); // XXX maximk: should handle retry with a new timeline this.ref = (this.ref||0) + 1; if (this.retry) this.timeline.retry(this.port); this.req_url = this.req.url; this.url = req_util.full_url(this.req); this.domain = url.parse(this.url).hostname; this.process_headers(); this.src_addr = this.req.connection.remoteAddress; if (this.src_addr==loopback_ip&&this.h_src_addr) this.src_addr = this.h_src_addr; this.response = {}; this.is_connect = req_util.is_connect(this.req); this.is_ssl = req_util.is_ssl(this.req); } init_stats(){ if (!this.response) this.init_response(); // can be create several times due to retry let sp = etask(function*req_stats(){ yield this.wait(); }); this.sp.spawn(sp); this.wait_bw = sp; request_stats(this.req, this.res, rstats=>{ this.response.in_bw += rstats.res.bytes; this.response.out_bw += rstats.req.bytes; sp.return(); }); } track_success(is_success){ if (!this.response) this.init_response(); this.response.success = is_success; } set_reverse_lookup_res(rev){ if (!rev) return; this.url = rev.url; this.domain = rev.hostname; } process_headers(){ this.headers = this.req.headers; this.raw_headers = this.req.rawHeaders; if (!this.saved_hdrs) this.saved_hdrs = Object.assign({}, this.headers); else { this.headers = this.req.headers = Object.assign({}, this.saved_hdrs); } lpm_headers.forEach(h=>{ let v_name = 'h_'+h.replace(/^(x-hola-|x-lpm-)/, '') .replace('-', '_'); this[v_name] = this.headers[h]||null; delete this.headers[h]; }); } init_response(){ this.res.x_hola_context = this.h_context; this.headers = this.rules ? this.headers : restore_case(this.headers, this.raw_headers); this.response = { request: { method: this.req.method, url: this.req_url, url_full: this.url, headers: this.headers, raw_headers: this.raw_headers, body: '', }, timeline: this.timeline, body_size: 0, context: this.h_context||'RESPONSE', body: [], in_bw: 0, out_bw: 0, }; [this.req, this.res].forEach(r=>qw`data end`.forEach(e=>{ let ev; if (r[ev = `_on${e}`]) { r.removeListener(e, r[ev]); delete r[ev]; } })); this.req.on('data', this.req._ondata = chunk=>{ this.response.request.body += chunk; }); // XXX ovidiu: remove this when eventemitter possible leak fixed this.res.setMaxListeners(45); } complete_req(){ this.ref--; if (this.ref > 0) return; this.sp.return(); delete this.req; } // capitalize the outgoing headers the same way the incoming request did format_headers(headers){ let req_header_format = {}; for (let i = 0; i<this.raw_headers.length; i+=2) { let header = this.raw_headers[i]; req_header_format[header.toLowerCase()] = header; } let formatted = {}; _.forEach(headers, (v, k)=>{ formatted[req_header_format[k]||k] = v; }); return formatted; } } function Sess_mgr(lum, opt){ events.EventEmitter.call(this); this.opt = opt; this.log = lum.log; this.lum = lum; this.reserved_sessions = []; this.reserved_keep_alive = (this.opt.reserved_keep_alive||50)*date.ms.SEC; this.setMaxListeners(Number.MAX_SAFE_INTEGER); if (opt.session_duration) { this.session_duration = (''+opt.session_duration).split(':') .map(i=>+i*1000); if (this.session_duration.length==1) this.session_duration = this.session_duration[0]; } if (opt.max_requests) { if (opt.max_requests==0) this.max_requests = 0; else { this.max_requests = (''+opt.max_requests).split(':').map(i=>+i) .filter(i=>i); } } this.password = encodeURIComponent(opt.password); this.pool_type = E.pool_types[opt.pool_type]||E.pool_types.sequential; this.session_id = 1; this.sticky_sessions = {}; this.keep_alive = opt.keep_alive && opt.keep_alive*1000; this.reserved_keep_alive = (opt.keep_alive||opt.reserved_keep_alive||50)*1000; this.seed = opt.seed|| Math.ceil(Math.random()*Number.MAX_SAFE_INTEGER).toString(16); if ((opt.max_requests||opt.session_duration||this.keep_alive) && !opt.pool_size&&!opt.sticky_ip&&opt.session!==true) { // XXX lee, gilad - can this warning be removed this.log.warn('empty pool_size, session flags are ignored'); } } util.inherits(Sess_mgr, events.EventEmitter); Sess_mgr.prototype.start = function(){ this.sp = etask(function*sesssion_manager(){ yield this.wait(); }); }; Sess_mgr.prototype.calculate_username = function(opt){ opt = assign.apply({}, [this.opt, this, opt].map(o=>_.pick(o||{}, qw`customer zone country state city session asn dns request_timeout cid ip raw direct debug password mobile vip carrier ext_proxy`))); if (opt.ext_proxy) { return Object.assign({password: opt.password}, _.pick(opt.ext_proxy, 'username', 'password')); } let opt_usr = _.omit(opt, qw`request_timeout password`); if (opt_usr.ip) opt_usr = _.omit(opt_usr, qw`session`); if (opt.request_timeout) opt_usr.timeout = opt.request_timeout; return {username: username.calc(opt_usr, this.opt.short_username), password: opt.password}; }; Sess_mgr.prototype.refresh_sessions = function(){ this.log.notice('Refreshing all sessions'); this.emit('refresh_sessions'); if (this.opt.pool_size) { if (this.pool_type==E.pool_types.sequential && this.sessions && this.sessions.sessions.length) { this.stop_keep_alive(this.sessions.sessions.shift()); this.pool_fetch(); } else { // XXX marka/lee: set flag for other instncies of current session // that can be in use (in _pool_fetch) if (this.sessions) this.sessions.canceled = true; this.sessions = {sessions: []}; if (this.pool_type==E.pool_types['round-robin']) this.pool(this.opt.pool_size); this.pool_ready = true; } } 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){ let session_id, ips, ip, vips, vip, cred, session, host; let ext_proxy, ext_proxies, proxy_port; if (pool.canceled||this.stopped) return; ips = this.opt.ips||[]; vips = this.opt.vips||[]; ext_proxies = this.opt.ext_proxies||[]; 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.lum.hosts.shift(); this.lum.hosts.push(host); vip = vips[this.session_id%vips.length]; ip = ips[this.session_id%ips.length]; } cred = this.calculate_username({ip: ip, session: session_id, vip: vip, ext_proxy: ext_proxy}); session = { host: host, session: session_id, ip: ip, vip: vip, ext_proxy: ext_proxy, count: 0, bandwidth_max_downloaded: 0, created: Date.now(), username: cred.username, pool: pool, proxy_port: proxy_port, }; this.log.info('new session added %s:%s', host, session_id); const max_requests = this.max_requests; if (max_requests && !session.max_requests) { session.max_requests = max_requests[0]; if (max_requests.length>1) { session.max_requests += Math.floor(Math.random()* (max_requests[1]-max_requests[0]+1)); } } return session; }; Sess_mgr.prototype.pool_fetch = function(){ let pool = this.sessions; let session = this.establish_session( `${this.lum.port}_${this.seed}`, pool); if (session) pool.sessions.push(session); }; Sess_mgr.prototype.pool = function(count){ if (!count) return; for (let i=0; i<count; i++) this.pool_fetch(); this.log.debug('initialized pool - %s', this.opt.pool_size); this.pool_ready = true; }; Sess_mgr.prototype.set_keep_alive = etask._fn( function*set_keep_alive(_this, session){ let keep_alive = session.reserved ? _this.reserved_keep_alive : _this.keep_alive; if (!keep_alive) return; _this.stop_keep_alive(session); session.keep_alive = this; while (session.keep_alive) { _this.log.debug('Schedule keep alive %s:%s', session.host, session.session); yield etask.sleep(keep_alive); yield _this._keep_alive_handler(session); } _this.log.warn('session %s: keep alive ended', session.session); }); Sess_mgr.prototype._keep_alive_handler = etask._fn( function*_keep_alive_handler(_this, session){ if (!session.reserved && _this.is_session_expired(session) || _this.lum.stopped) { return false; } _this.log.info('Keep alive %s:%s', session.host, session.session); let res = yield _this.info_request(session, _this.opt.request_timeout*1000, 'SESSION KEEP ALIVE'); let info = res.info; let sess_info = session.info; if (!res) res = {res: {status_code: 502}, err: 'Unknown Error'}; let proxy_err = _this.lum._check_proxy_response(session.host, res, {from: 'keep alive', error: res.err}); if (proxy_err||res.err||!info) { _this.log.warn('session %s: keep alive failed, removing session', session.session); _this.remove_session(session); return proxy_err||res; } if (!sess_info || !sess_info.ip) sess_info = info; if (info.ip != sess_info.ip) { _this.log.warn('session %s: ip change %s -> %s', session.session, sess_info.ip, info.ip); } session.info = sess_info; return res; }); Sess_mgr.prototype.remove_session = function(session){ session.canceled = true; this.stop_keep_alive(session); if (session.reserved) this.remove_reserved(session); if (!session.pool) return; let sessions = _.isArray(session.pool) ? session.pool : session.pool.sessions; _.remove(sessions, s=>s===session); }; Sess_mgr.prototype.stop_keep_alive = function(session){ if (!session.keep_alive) return; session.keep_alive.return(); session.keep_alive = null; }; Sess_mgr.prototype.is_session_expired = function(session, check_only = false) { if (!session||session.canceled||session.pool&&session.pool.canceled) return true; const now = Date.now(); const last_res = session && session.last_res; if (last_res && this.lum.banlist.has(last_res.ip)) { this.log.info('session %s:%s expired, ip %s banned', session.host, session.session, last_res.ip); session.last_res = null; return true; } if (check_only&&!session.pool&&session!=this.session&&!session.reserved) return this.stop_keep_alive(session); if (check_only) this.sp.spawn(this.set_keep_alive(session)); const duration = this.session_duration; if (!check_only) session.count++; if (!check_only && duration && !session.expire) { session.expire = now+duration[0]; if (duration.length>1) { session.expire += Math.floor(Math.random()* (duration[1]-duration[0]+1)); } } let expired = session.max_requests && session.count>=session.max_requests || session.expire && now>session.expire; if (expired&&!session.reserved) { this.stop_keep_alive(session); this.log.debug('session %s:%s expired', session.host, session.session); } return expired; }; Sess_mgr.prototype.info_request = etask._fn( function*info_request(_this, session, timeout, context){ let host = session.host || _this.lum.hosts[0]; let cred = _this.calculate_username(session); let protocol = _this.opt.secure_proxy ? 'https' : 'http'; let proxy_url = `${protocol}://${cred.username}:${cred.password}@${host}:${_this.opt.proxy_port}`; _this.log.debug('info_request via %s', proxy_url); let opt = { url: _this.opt.test_url, proxy: proxy_url, timeout: timeout, headers: { 'x-hola-agent': E.hola_agent, host: 'zproxy.hola.org', }, proxyHeaderWhiteList: E.hola_headers, proxyHeaderExclusiveList: E.hola_headers, followRedirect: false, }; const timeline = new Timeline(); const res = { request: { method: 'GET', url: opt.url, headers: opt.headers, body: '', }, timeline: timeline, context: context, body: '', proxy: { host: host, username: cred.username, } }; let err, info; try { request(opt).on('response', _res=>{ timeline.track('response'); assign(res, { status_code: _res.statusCode, status_message: _res.statusMessage, headers: _res.headers, raw_headers: _res.rawHeaders, }); }).on('data', data=>res.body+=data) .on('error', _err=>{ err = _err; this.continue(); }).on('end', ()=>{ timeline.track('end'); this.continue(); }).on('finish', ()=>{ timeline.track('end'); this.continue(); }).on('close', ()=>{ timeline.track('end'); this.continue(); }); const __this = this; let finished; _this.sp.spawn(etask(function*timeout_info_request(){ yield etask.sleep(5000); if (!finished) __this.continue(); })); yield this.wait(); finished = true; if (err) throw err; res.body_size = res.body.length; let ct; if (res.status_code==200&&(ct = res.headers&& res.headers['content-type'])&& ct.match(/\/json/)) { info = JSON.parse(res.body); } _this.emit('response', res); _this.log.debug('info_request %O %O', res, info); } catch(e){ err = e; res.status_code = 502; _this.log.warn('info_request '+zerr.e2s(err)); } return {res, err, info}; }); Sess_mgr.prototype.request_session = function(req){ let session = this._request_session(req); let ctx = req.ctx; let rules = ctx.rules; if (session && (!session.session || ctx.retry || ctx.h_session)) { if (session.authorization && this.session && !(this.session.authorization && _.isEqual(session.authorization, this.session.authorization))) { this.session = null; } if (ctx.h_session && this.session && this.session.session!= ctx.h_session) { this.session = null; } if (req.session) session.session = req.session; else if (!this.session && rules && !ctx.h_session) { this.session = session; session.session = rules.gen_session(); } else if (this.session) session = this.session; } return session; }; Sess_mgr.prototype._request_session = function(req){ let ctx = req.ctx; if (ctx.only_bypass) return; let authorization = this.opt.allow_proxy_auth && username.parse(ctx.h_proxy_authorization); if (authorization) { if (authorization.timeout) authorization.request_timeout = authorization.timeout; ctx.log.debug(`Using request authorization %O`, authorization); if (ctx.h_session) authorization.session = ctx.h_session; return {authorization}; } if (ctx.h_session) return {session: ctx.h_session}; if (ctx.h_reserved) { ctx.log.info('selecting reserved session'); return this.get_reserved_session(); } if (this.opt.pool_size) { let sessions; if (!this.sessions) { this.sessions = {sessions: sessions = []}; if (this.pool_type==E.pool_types['round-robin']) this.pool(this.opt.pool_size); else this.pool_fetch(); this.pool_ready = true; } else { sessions = this.sessions.sessions; if (!sessions.length) this.pool_fetch(); } let session = sessions[0]; if (this.is_session_expired(session)) { sessions.shift(); if (sessions.length<this.opt.pool_size) this.pool_fetch(); } else if (this.pool_type==E.pool_types['round-robin']) sessions.push(sessions.shift()); ctx.log.debug(`Selecting pool session %s`, session&&session.session); return session; } if (this.opt.sticky_ip) { const ip = ctx.src_addr.replace(/\./g, '_'); if (!this.sticky_sessions[ip]) { this.sticky_sessions[ip] = this.establish_session( `${ctx.port}_${ip}_${this.seed}`, this.sticky_sessions); } let session = this.sticky_sessions[ip]; if (this.is_session_expired(session)) this.sticky_sessions[ip] = null; ctx.log.debug(`Selecting sticky session %s`, session&&session.session); return session; } if (this.opt.session===true) { if (!this.session) this.session = this.establish_session(this.seed, this); let session = this.session; if (this.is_session_expired(session)) this.session = null; ctx.log.debug(`Selecting seed session %s`, session&&session.session); return session; } if (this.opt.session) { if (!this.session) { this.session = { session: this.opt.session, count: 0, bandwidth_max_downloaded: 0, created: Date.now(), pool: this, }; } let session = this.session; this.sp.spawn(this.set_keep_alive(session)); ctx.log.debug(`Selecting explicit session %s`, session.session); return session; } ctx.log.debug(`Not using session`); return {session: false}; }; Sess_mgr.prototype.get_req_cred = function(req){ const ctx = req.ctx; const auth = ctx.session&&ctx.session.authorization||{}; const opt = { ext_proxy: ctx.session&&ctx.session.ext_proxy, ip: ctx.session&&ctx.session.ip||this.opt.ip, session: ctx.session&&ctx.session.session, direct: this.lum.router.is_direct(ctx.url), vip: ctx.session&&ctx.session.vip||this.opt.vip, }; return this.calculate_username(assign({}, opt, auth)); }; Sess_mgr.prototype.reserve_session = function(sess){ if (sess.reserved||_.find(this.reserve_sessions, sess)) return; this.log.info('Adding session '+sess.session+' to reserved pool'); this.reserved_sessions.push(sess); sess.reserved = true; this.sp.spawn(this.set_keep_alive(sess)); }; Sess_mgr.prototype.get_reserved_sessions = function(){ return this.reserved_sessions; }; Sess_mgr.prototype.get_reserved_session = function(){ let sess = this.reserved_sessions[0]||null; if (this.reserved_sessions.length>1) { this.reserved_sessions.shift(); this.reserved_sessions.push(sess); } return sess; }; Sess_mgr.prototype.remove_reserved = function(session){ this.log.info('delete reserved session %O', session); _.delete(this.reserved_sessions, session); this.stop_keep_alive(session); session.reserved = false; }; Sess_mgr.prototype.stop = function(){ if (this.sp) this.sp.return(); }; function Luminati(opt, mgr){ events.EventEmitter.call(this); this.mgr = mgr||null; this.overload = false; this.rejected = 0; opt.listen_port = opt.listen_port||opt.port||E.default.port; this.log = log(opt.listen_port, opt.log); this.http = opt.secure_proxy ? https : http; let agents = mgr&&mgr.agents; if (!agents) { agents = { http: new http.Agent({keepAlive: true, keepAliveMsecs: 5000}), https: new https.Agent({keepAlive: true, keepAliveMsecs: 5000, servername: E.https_servername}), }; } this.protocol = agents[opt.secure_proxy ? 'https' : 'http']; this.req_status_cnt = {total: 0, success: 0}; opt = this.opt = assign({}, E.default, opt); if (opt.rules) this.rules = new Rules(this, opt.rules||{}); this.banlist = new Ip_cache(); if (opt.timeout) this.timeout = opt.timeout*1000; this.router = new Router(opt); this.router.on('response', res=>this.emit('response', res)); this.active = 0; this.failure = {}; this.requests_queue = []; this.throttle_queue = []; this.http_server = http.createServer((req, res, head)=>{ if (this.is_overload()) return this.send_overload(res, 'http_req'); if (req.headers.host=='trigger.domain' || /^\/hola_trigger/.test(req.url)) { return res.end(); } if (!req.url.startsWith('http:')) req.url = 'http://'+req.headers.host+req.url; this.sp.spawn(this._handler(req, res, head)); }).on('connection', socket=>socket.setNoDelay()); http_shutdown(this.http_server); this.socks_server = new Socks({port: opt.port||E.default.port, log: opt.log}); if (opt.ssl) { this.authorization = {}; this.req_remote_ip = {}; this.https_server = https.createServer( assign({requestCert: false}, ssl()), (req, res, head)=>{ if (this.is_overload()) return this.send_overload(res, 'https_req'); let remote_ip = this.req_remote_ip[req.socket.remotePort]; if (remote_ip && req.socket.remoteAddress=='127.0.0.1') { this.log.info('Request ip fixed %s %s', remote_ip, req.url); req.original_ip = remote_ip; } let authorization = this.authorization[req.socket.remotePort]; if (authorization) req.headers['proxy-authorization'] = authorization; this.sp.spawn(this._handler(req, res, head)); } ).on('connection', socket=>socket.setNoDelay()); http_shutdown(this.https_server); this.http_server.on('connect', (req, res, head)=>{ if (this.is_overload()) return this.send_overload(res, 'http_connect'); if (parse_ip_url(req.url)||zurl.parse(req.url).port==43) return this.sp.spawn(this._handler(req, res, head)); write_http_reply(res, {statusCode: 200, statusMessage: 'OK'}); const socket = net.connect({host: '127.0.0.1', port: this.https_server.address().port}); socket.setNoDelay(); let port, authorization = req.headers['proxy-authorization']; let remote_ip = req_remote_ip(req); socket.on('connect', ()=>{ port = socket.localPort; if (remote_ip) this.req_remote_ip[port] = remote_ip; if (authorization) this.authorization[port] = authorization; }).on('close', ()=>{ delete this.authorization[port]; delete this.req_remote_ip[port]; }); socket.on('error', err=>{ if (this.is_overload(err)) return; this.log.error(`Socket error: %O`, { authorization: authorization, error: zerr.e2s(err), port: (this.https_server.address()||{}).port }); }); res.pipe(socket).pipe(res); req.on('end', ()=>socket.end()); }); } else { this.http_server.on('connect', (req, res, head)=>this.sp.spawn( this._handler(req, res, head))); } this.tcp_server = new net.createServer(socket=>{ // XXX krzysztof: errors are handled by http/https/socks socket.on('error', err=>null); socket.once('data', data=>{ socket.pause(); const protocol_byte = data[0]; if (protocol_byte==22) this.https_server.emit('connection', socket); else if (32<protocol_byte&&protocol_byte<127) this.http_server.emit('connection', socket); else if (protocol_byte==5) this.socks_server.connect(socket); else socket.end(); socket.unshift(data); socket.resume(); }); }); http_shutdown(this.tcp_server); if (opt.history_aggregator) this.on('response', resp=>this._aggregate_history(resp)); if (opt.reverse_lookup_dns===true) this.reverse_lookup = reverse_lookup_dns; else if (opt.reverse_lookup_file && fs.existsSync(opt.reverse_lookup_file)) { this.reverse_lookup = reverse_lookup_values( zfile.read_lines_e(opt.reverse_lookup_file)); } else if (opt.reverse_lookup_values) this.reverse_lookup = reverse_lookup_values(opt.reverse_lookup_values); this.session_mgr = new Sess_mgr(this, opt); this.session_mgr.on('response', r=>this.emit('response', r)); } util.inherits(E, events.EventEmitter); E.prototype._aggregate_history = function(response){ if (!response||!response.timeline) { this.perr(new Error('invalid_response')); this.log.error('Invalid response %O', response); return; } let headers = response.headers||{}; let proxy_info = qw`x-hola-timeline-debug x-hola-unblocker-debug` .map(h=>headers[h]||'') .map(h=>h.match(/(\d+\.\d+\.\d+\.\d+) ([^ ]+)/)) .find(i=>i)||['', '', '']; let node_latency = +((headers['x-hola-timeline-debug']||'') .match(/(\d+) ?ms/)||[0, response.timeline.get('response')])[1]; let data = { port: this.port, url: response.request.url, method: response.request.method, request_headers: stringify(response.request.headers), request_body: response.request.body, response_headers: stringify(headers), response_body: _.isArray(response.body) ? Buffer.concat(response.body) : response.body, status_code: response.status_code, status_message: response.status_message, timestamp: response.timeline.get('create'), elapsed: response.timeline.get_delta('end'), response_time: response.timeline.get_delta('response'), node_latency: node_latency, proxy_peer: proxy_info[1]||headers['x-hola-ip'], country: proxy_info[2], timeline: stringify(response.timeline.req_chain), content_size: response.body_size, context: response.context, }; if (response.proxy) { data.super_proxy = response.proxy.host; data.username = response.proxy.username; } if (response.success) data.success = +response.success; data.in_bw = response.in_bw; data.out_bw = response.out_bw; this.opt.history_aggregator(data); }; E.prototype.refresh_ip = etask._fn(function*lum_refresh_ip(_this, ctx, ip){ if (!_this.opt.ips.includes(ip)) return; _this.log.notice('Refreshing IP %s', ip); ctx.timeline.refresh_ip(ip); const allocated_ips = yield _this.mgr.request_allocated_ips( _this.opt.zone); const new_ips = yield _this.mgr.refresh_ips(_this.opt.zone, zurl.ip2num(ip)); if (new_ips.error) return _this.log.warn('Refresgin IP failed: %s', new_ips.error); const ips_map = map_ips(allocated_ips.ips, new_ips.ips.map(i=>i.ip)); _this.opt.ips = _this.opt.ips.map(_ip=>ips_map[_ip]); _this.config.ips = _this.opt.ips; _this.mgr.proxies.find(p=>p.port==_this.port).ips = _this.opt.ips; _this.mgr.save_config(); _this.session_mgr.refresh_sessions(); ctx.timeline.finish_refresh_ip(); _this.log.notice('IP has been refreshed %s->%s', ip, ips_map[ip]); }); function map_ips(old_ips, new_ips){ if (old_ips.length!=new_ips.length) { throw new Error('Refreshing IPs Error. List length mismatch %s!=%s', old_ips.length, new_ips.length); } const map = {}; for (let i in old_ips) map[old_ips[i]] = new_ips[i]; return map; } E.prototype.get_other_port = function(port){ if (!this.mgr) return null; return this.mgr.get_server(port); }; E.prototype._timeout = function(func, time, param){ return setTimeout(()=>{ try { func.call(this, param); } catch(e){ this.log.error('Async error '+zerr.e2s(e)); } }, time); }; E.prototype._handler = etask._fn( function*_handler(_this, req, res, head){ if (_this.is_overload()) return _this.send_overload(res, 'handler'); this.finally(()=>_this.complete_req(this.error, req, res)); req.on('close', ()=>this.return()); req.on('finish', ()=>this.return()); try { req._queued = Date.now(); _this.active++; _this.emit('idle', false); req.on('error', this.throw_fn()); res.on('error', this.throw_fn()); req.on('timeout', ()=>this.throw(new Error('request timeout'))); let wips = _this.opt.whitelist_ips; if (wips.length) { let ip = req_remote_ip(req), i; for (i=0; i<wips.length && wips[i]!=ip; i++); if (i==wips.length) { _this.log.debug('Request ip not in whitelist %s %s', req.url, ip); write_http_reply(res, { statusCode: 403, statusMessage: 'Forbidden', headers: {Connection: 'close'}, }); res.end(); return; } } this.info.url = req.url; if (_this.opt.throttle && _this.active>_this.opt.throttle) { _this.log.debug('Placing request on throttle queue'); _this.throttle_queue.push(this); yield this.wait(); } if (_this.opt.only_bypass || _this.hosts && _this.hosts.length) return yield _this._request(req, res, head); _this.requests_queue.push([req, res, head]); } catch(e){ _this.log.debug('_handler error %s', zerr.e2s(e)); throw e; } }); E.prototype.complete_req = function(err, req, res){ if (err) this.log.debug('complete err %s', zerr.e2s(err)); if (err&&this.is_overload(err)) this.send_overload(res, 'complete'); else if (err && err.proxy_error) this.send_proxy_error(req, res, err); else if (err) this.send_lpm_error(req, res, err); this.log.debug('complete req %s, active: %s', req.ctx.id, this.active); this.active--; if (!this.active) { this.emit('idle', true); return; } if (this.throttle_queue.length) { this.log.debug('Taking request from throttle queue'); this.throttle_queue.shift().continue(); } }; E.prototype.error_handler = function error_handler(source, err){ if (this.is_overload(err)) return; this.log.error(source+' error '+zerr.e2s(err)); let {code} = err; if (code=='EADDRINUSE') { err = new Error(`There's already an application which runs on `+ `${err.address}:${err.port}`); err.raw = true; err.code = code; } err.lum_source = source; if (this.listenerCount('error')) this.emit('error', err); else throw err; }; E.prototype.listen = etask._fn(function*listen(_this, listen_port, hostname){ _this.proxy = [].concat(_this.opt.proxy); _this.session_mgr.start(); _this.resolve_proxy(); if (!_this.sp) { _this.sp = etask(function*luminati_listen(){ return yield this.wait(); }); } _this.sp.spawn(_this.session_mgr.sp); listen_port = listen_port||_this.opt.listen_port||0; hostname = hostname||find_iface(_this.opt.iface); if (!hostname) { hostname = '0.0.0.0'; _this.opt.iface = '0.0.0.0'; } _this.port = listen_port; yield _this.socks_server.set_tcp_server(_this.tcp_server); yield etask.nfn_apply(_this.tcp_server, '.listen', [listen_port, hostname]); const _http = _this.http_server; const _https = _this.https_server; _http.on('error', err=>_this.error_handler('HTTP', err)); if (_https) { _https.on('error', err=>_this.error_handler('HTTPS', err)); yield etask.nfn_apply(_https, '.listen', [0, '127.0.0.1']); _this.log.debug('HTTPS port: '+_https.address().port); } _this.emit('ready'); return _this; }); E.prototype.stop = etask._fn(function*stop(_this, force){ if (_this.stopped) return; if (_this.sp) { _this.sp.return(); _this.sp = null; } _this.session_mgr.stop(); _this.stopped = true; _this.socks_server.stop(force); let tasks = {}; if (_this.protocol) _this.protocol.destroy(); const stop_method = force ? '.forceShutdown' : '.shutdown'; tasks.tcp = etask.nfn_apply(_this.tcp_server, stop_method, []); if (_this.https_server) tasks.https = etask.nfn_apply(_this.https_server, stop_method, []); yield etask.all(tasks); return _this; }); E.prototype._check_proxy_response = function _check_proxy_response(proxy, res, context) { let err = new Error(); let status_code = res.status_code || res.statusCode || 0; if (res.headers&&res.headers['x-luminati-error']) { err.message = res.headers['x-luminati-error']; err.code = status_code; err.retry = false; err.proxy_error = true; if (err.code == 502 && err.message.match(/^Proxy Error/)) err.retry = true; } if (!err.message) { delete this.failure[proxy]; return false; } if (!this.opt.proxy_switch) return err; let failures = this.failure[proxy] = (this.failure[proxy]||0)+1; this.log.warn('invalid proxy response %O', {host: proxy, code: status_code, context: context}); if (failures>=this.opt.proxy_switch) { this.log.warn('removing failed proxy server %O', {host: proxy}); this.sp.spawn(this.remove_proxy(proxy)); } return err; }; E.prototype.remove_proxy = etask._fn(function*remove_proxy(_this, proxy){ if (_this.hosts) { _this.hosts = _this.hosts.filter(h=>h!=proxy); if (_this.opt.proxy_cache) _this.opt.proxy_cache.remove(proxy); } if (_this.session_mgr.sessions) { _this.session_mgr.sessions.sessions = _this.session_mgr .sessions.sessions.filter( s=>s.proxy!=proxy||_this.session_mgr.stop_keep_alive(s)); } delete _this.failure[proxy]; yield _this.resolve_proxy(); }); E.prototype.resolve_proxy = etask._fn(function*resolve_proxy(_this){ if (_this.opt.only_bypass) return; let hosts = {}, proxies = _this.proxy.slice(0); if (!_this.hosts && _this.opt.proxy_cache) _this.hosts = yield _this.opt.proxy_cache.get(proxies); _this.resolving_proxies = true; [].concat(_this.hosts||[]).forEach(h=>hosts[h] = false); const timestamp = Date.now(); while (proxies.length && Object.keys(hosts).length<_this.opt.proxy_count && Date.now()-timestamp<30000) { let proxy = proxies.shift(), ips; if (/^\d+\.\d+\.\d+\.\d+$/.test(proxy)) { _this.log.debug('using super proxy %s', proxy); hosts[proxy] = false; continue; } proxies.push(proxy); let domain = proxy; if (proxy.length==2) domain = lpm_config.default_superproxy_domain; if (is_superproxy_domain(domain)) { domain = `customer-${_this.opt.customer}-` +`session-${Date.now()}.${domain}`; } try { _this.log.debug('resolving proxies from %s', domain); ips = ips || (yield etask.nfn_apply(dns, '.resolve', [domain])); _this.log.debug('resolved %s (%s) %O', proxy, domain, ips); ips.forEach(ip=>hosts[ip] = proxy); } catch(e){ _this.log.debug('Failed to resolve %s (%s): %s', proxy, domain, zerr.e2s(e)); } } _this.hosts = _.shuffle(Object.keys(hosts)); if (_this.opt.proxy_cache) yield _this.opt.proxy_cache.add(_.toPairs(hosts).filter(p=>p[1])); if (!_this.hosts.length) _this.log.error('Failed to resolve any proxies'); _this.resolving_proxies = false; let queue = _this.requests_queue; _this.requests_queue = []; queue.forEach(args=>_this._request.apply(_this, args)); }); E.prototype.get_req_host = etask._fn(function*get_req_host(_this, req){ const {ctx} = req; // XXX maximk: watch for resolving hosts here if (!ctx.session || !ctx.session.host) yield true; return ctx.session&&ctx.session.host || (ctx.only_bypass ? loopback_ip : _this.hosts[0]); }); E.prototype.perr = function(id, info, opt){ let rc = this.mgr.rmt_cfg.get(); if (info instanceof Error) info = {error: info}; info = Object.assign( {opt: this.opt, overloaded: this.is_overload()}, info||{} ); if (rc.send_proxy_perr) { if (this.mgr) return this.mgr.perr(id, info, opt); return zerr.perr(id, info, opt); } return false; }; E.prototype.reverse_lookup_url = etask._fn( function*reverse_lookup_url(_this, url){ let ip_url, rev_domain; if (!_this.reverse_lookup || !(ip_url = parse_ip_url(url))) return false; rev_domain = yield _this.reverse_lookup(ip_url.ip); if (ip_url.ip==rev_domain) return false; return { url: url.replace(ip_url.url, `${ip_url.protocol}${rev_domain}${ip_url.suffix}`), hostname: rev_domain, }; }); E.prototype.init_proxy_req = etask._fn( function*init_proxy_req(_this, req, res){ const {ctx} = req; ctx.init_stats(); ctx.session = _this.session_mgr.request_session(req); ctx.host = yield _this.get_req_host(req); if (_this.router.is_bypass_proxy(req)) return; ctx.proxy_port = ctx.session&&ctx.session.proxy_port || ctx.proxy_port; ctx.cred = _this.session_mgr.get_req_cred(req); res.cred = ctx.cred.username; ctx.log.info('requesting using %s', ctx.cred.username); ctx.response.proxy = { host: ctx.host, username: ctx.cred.username, }; ctx.connect_headers = { 'proxy-authorization': 'Basic '+ new Buffer(ctx.cred.username+':'+ctx.cred.password)