@luminati-io/luminati-proxy
Version:
A configurable local proxy for brightdata.com
1,250 lines (1,184 loc) • 44.2 kB
JavaScript
// LICENSE_CODE ZON ISC
'use strict'; /*jslint node:true, esnext:true, evil: true*/
const events = require('events');
const dns = require('dns');
const url = require('url');
const net = require('net');
const fs = require('fs');
const http_shutdown = require('http-shutdown');
const winston = require('winston');
const stringify = require('json-stable-stringify');
const {Netmask} = require('netmask');
const request = require('../util/lpm_request.js');
const qw = require('../util/string.js').qw;
const lpm_config = require('../util/lpm_config.js');
const zfile = require('../util/file.js');
const zerr = require('../util/zerr.js');
const date = require('../util/date.js');
const etask = require('../util/etask.js');
const zutil = require('../util/util.js');
const zurl = require('../util/url.js');
const Srv_send_mixin = require('./mixins/server_send.js');
const Srv_handle_mixin = require('./mixins/server_handle.js');
const mixin_core = require('./mixins/core.js');
const Smtp = require('./smtp.js');
const Ws = require('./ws.js');
const lutil = require('./util.js');
const {find_iface, ensure_socket_close, req_util, Timeouts, REDIRECT_PARTS,
ensure_socket_emit_close, is_redirect_status} = lutil;
const requester = require('./requester.js');
const username = require('./username.js');
const sessions = require('./session.js');
const Context = require('./context.js');
const Router = require('./router.js');
const Rules = require('./rules.js');
const {Ip_cache} = require('./ip_cache.js');
const Throttle_mgr = require('./throttle_mgr.js');
const consts = require('./consts.js');
const Https_agent = require('./https_agent.js');
let hosts_cursor = 0, super_proxy_ports_cursor = 0, req_list = new Set();
const MAX_REDIRECTS = 10;
const ip_re = /^(https?:\/\/)?(\d+\.\d+\.\d+\.\d+)([$/:?])/i;
const logs_remote_ignore_ctx = ['PROXY TESTER TOOL', 'STATUS CHECK'];
const 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; }
});
const 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;
};
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 get_content_type = data=>{
if (data.response_body=='unknown')
return 'unknown';
let content_type;
let res = 'other';
try {
const headers = JSON.parse(data.response_headers);
content_type = headers['content-type']||'';
} catch(e){ content_type = ''; }
if (content_type.match(/json/))
res = 'xhr';
else if (content_type.match(/html/))
res = 'html';
else if (content_type.match(/javascript/))
res = 'js';
else if (content_type.match(/css/))
res = 'css';
else if (content_type.match(/image/))
res = 'img';
else if (content_type.match(/audio|video/))
res = 'media';
else if (content_type.match(/font/))
res = 'font';
return res;
};
class Server extends events.EventEmitter {
constructor(opt, worker){
super();
events.EventEmitter.call(this);
this.active = 0;
this.sp = etask(function*server_listen_constructor(){
return yield this.wait();
});
opt.listen_port = opt.listen_port || opt.port || E.default.port;
opt = this.opt = Object.assign({}, E.default, opt);
this.timeouts = new Timeouts();
this.worker = worker;
this.cache = worker.cache;
this.ensure_socket_close = ensure_socket_close;
this.ensure_socket_emit_close = ensure_socket_emit_close;
this.ws_handler = new Ws();
this.socket2headers = new Map();
this.bw_limit_exp = false;
this.init_tcp_server();
this.on('response', resp=>this.usage(resp));
this.https_agent = new Https_agent({
keepAlive: true,
keepAliveMsecs: 5000,
maxFreeSockets: 50,
});
this.setMaxListeners(30);
this.update_config(opt);
}
}
const E = module.exports = Server;
mixin_core.assign(E, Srv_send_mixin, Srv_handle_mixin);
E.default = Object.assign({}, lpm_config.server_default);
E.dropin = {
port: E.default.proxy_port,
listen_port: E.default.proxy_port,
};
E.prototype.is_custom_error = e=>e.custom||e.message=='Authentication failed';
E.prototype.update_hosts = function(hosts, cn_hosts){
this.hosts = (hosts||[this.opt.proxy]).slice();
this.cn_hosts = (cn_hosts||[]).slice();
};
E.prototype.set_opt = function(opt){
Object.assign(this.opt, opt);
};
E.prototype.update_config = function(opt){
if (this.session_mgr)
this.session_mgr.stop();
opt = this.opt = Object.assign({}, this.opt, opt);
this.logger = require('./logger.js').child({category: `[${opt.port}]`});
this.logger.set_level(opt.log);
this.req_logger = winston.loggers.get('reqs');
if (opt.zagent && opt.logs_settings)
this.remote_logger = this.logger.create_remote(opt.logs_settings);
this.reverse_lookup = null;
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);
opt.whitelist_ips = opt.whitelist_ips || [];
if (opt.ext_proxies)
opt.session = true;
opt.use_flex_tls = opt.zagent && opt.tls_lib=='flex_tls';
this.update_hosts(this.opt.hosts, this.opt.cn_hosts);
this.requester = requester.create_requester(this.opt);
this.router = new Router(opt);
if (this.rules && this.rules.av_client)
this.rules.close_av_client();
this.rules = new Rules(this, opt.rules, opt.av_check && opt.av_server_url,
opt.ssl);
this.session_mgr = new sessions.Sess_mgr(this, opt);
this.banlist = new Ip_cache(opt.banlist);
this.throttle_mgr = Throttle_mgr.init(this, opt.throttle);
this.session_mgr.on('response', r=>this.emit('response', r));
this.smtp_server = new Smtp(this, {
port: opt.port,
log: opt.log,
ips: opt.smtp,
});
this.update_lb_ips(opt);
this.update_bw_limit(opt);
};
E.prototype.update_bw_limit = function(opt){
opt = this.opt = Object.assign({}, this.opt, opt);
let bw_limit_exp = zutil.get(opt, `bw_limit.expires.${opt.port}`);
bw_limit_exp = bw_limit_exp && date(bw_limit_exp);
this.bw_limit_exp = opt.zagent && bw_limit_exp instanceof Date &&
!isNaN(bw_limit_exp.getTime()) && bw_limit_exp;
if (this.bw_limit_exp)
this.close_all_reqs();
};
E.prototype.update_lb_ips = function(opt){
this.opt = Object.assign(this.opt, opt);
this.lb_ips = new Set(opt.lb_ips||[]);
};
E.prototype.get_req_remote_ip = function(req){
if (req.original_ip)
return req.original_ip;
if (req.socket)
{
let ip;
if (ip = this.worker.socks_server.get_remote_ip(req.socket.remotePort))
return ip;
if (req.socket._parent && req.socket._parent.lpm_forwarded_for)
return req.socket._parent.lpm_forwarded_for;
if (req.socket.lpm_forwarded_for)
return req.socket.lpm_forwarded_for;
if (req.socket.socket && req.socket.socket.lpm_forwarded_for)
return req.socket.socket.lpm_forwarded_for;
if (req.socket.remoteAddress)
return req.socket.remoteAddress;
if (req.socket.socket && req.socket.socket.remoteAddress)
return req.socket.socket.remoteAddress;
}
return null;
};
E.prototype.bypass_intercepting = function(req_url){
if (this.opt.smtp && this.opt.smtp.length)
return true;
const _url = zurl.parse(req_url);
if (_url.port==443)
return false;
return parse_ip_url(req_url) || _url.port==43 || _url.port==80 ||
_url.hostname=='app.multiloginapp.com';
};
E.prototype.init_tcp_server = function(){
this.tcp_server = new net.createServer(socket=>{
this.tcp_server.running = true;
socket.setTimeout(this.opt.socket_inactivity_timeout);
socket.once('error', err=>null);
socket.once('timeout', ()=>this.ensure_socket_close(socket));
const is_lb_req = this.lb_ips.has(socket.remoteAddress);
const is_smtp_req = this.opt.smtp && this.opt.smtp.length;
if (!is_lb_req && is_smtp_req)
return this.smtp_server.connect(socket);
let lb_transform_stream;
if (is_lb_req)
{
lb_transform_stream = new lutil.Lb_transform();
lb_transform_stream.on('parsed', ({remote_ip})=>{
socket.lpm_forwarded_for = remote_ip;
if (is_smtp_req)
{
socket.unpipe(lb_transform_stream);
this.smtp_server.connect(socket);
socket.resume();
}
});
socket.pipe(lb_transform_stream);
if (is_smtp_req)
return;
}
else if (is_smtp_req)
return this.smtp_server.connect(socket);
(lb_transform_stream||socket).once('data', data=>{
if (lb_transform_stream)
socket.unpipe(lb_transform_stream);
if (!this.tcp_server.running)
return socket.end();
socket.pause();
const protocol_byte = data[0];
socket.lpm_server = this;
// first byte of TLS handshake is 0x16 = 22 byte
if (protocol_byte==22)
this.worker.tls_server.emit('connection', socket);
// any non-control ASCII character
else if (32<protocol_byte && protocol_byte<127)
this.worker.http_server.emit('connection', socket);
// initial greeting from SOCKS5 client is 0x05 = 5 byte
else if (protocol_byte==5)
{
this.worker.socks_server.connect(socket, {
port: this.opt.port,
remote_ip: socket.lpm_forwarded_for||socket.remoteAddress,
is_whitelisted_ip: this.is_whitelisted_ip.bind(this),
});
}
else
socket.end();
socket.unshift(data);
socket.resume();
});
});
http_shutdown(this.tcp_server);
};
E.prototype.process_x_ports_header = req=>{
let header = req.headers && req.headers['x-lpm-ports'];
if (!header)
return;
delete req.headers['x-lpm-ports'];
let meta = null;
try {
meta = JSON.parse(header);
} catch(e){
return `Parse failed: ${header}; ${zerr.e2s(e)}`;
}
let every_key_is_num = Object.keys(meta).every(k=>!isNaN(parseInt(k)));
let every_val_is_array = Object.values(meta).every(v=>Array.isArray(v));
if (!every_key_is_num || !every_val_is_array)
return 'Failed parse x-lpm-ports - wrong format';
req.headers_orig = Object.assign({}, req.headers);
req.ports_meta = meta;
};
E.prototype.usage_start = function(req){
if (!Number(this.opt.logs))
return;
const data = {
uuid: req.ctx.uuid,
port: this.port,
url: req.url,
method: req.method,
headers: req.headers,
timestamp: Date.now(),
context: req.ctx.h_context,
};
this.emit('usage_start', data);
};
E.prototype.refresh_sessions = function(){
this.emit('refresh_sessions');
this.session_mgr.refresh_sessions();
};
const get_hostname = _url=>{
let url_parts;
if (url_parts = _url.match(/^([^/]+?):(\d+)$/))
return url_parts[1];
return url.parse(_url).hostname;
};
E.prototype.send_stats = function(lum_traffic, hostname, in_bw, out_bw, scs){
let stats = {hostname: zurl.get_root_domain(hostname||''),
in_bw, out_bw, port: this.port, lum_traffic, success: !!scs};
this.emit('usage_stats', stats);
};
E.prototype.log_req = function(_url, method, remote_address, hostname, headers,
lb_ip, in_bw, out_bw, sp, user, password, auth_type, status_code,
status_message, start_time, req_chain)
{
const opts = username.parse_opt(user);
const bw = in_bw+out_bw;
const auth = user && user+(this.opt.zagent ? '' : ':'+password)||'no_auth';
const time = Date.now()-start_time;
const timing = (req_chain||[]).map(t=>({
blocked: t.create-start_time,
wait: t.connect-t.create||0,
ttfb: t.first_byte-(t.connect||start_time)||0,
receive: t.end-(t.first_byte||t.connect||start_time)
}));
let str = `${remote_address}${lb_ip ? '('+lb_ip+')' : ''} ${method} `
+`${_url} ${status_code} ${status_message} ${time}ms ${bw} `
+`${sp||'no_proxy'} ${auth} ${auth_type||'no_auth_type'} `
+`${timing.length ? JSON.stringify(timing) : ''}`;
if (this.logger.level=='debug')
str+=` ${JSON.stringify(headers)}`;
this.logger.info(str);
let message = {
ts: date(),
customer: this.opt.customer_id||this.opt.account_id||this.opt.customer,
zone: opts.zone,
method,
host: hostname,
status_code,
sp,
bw,
port: this.port,
time,
auth_type,
ip: remote_address,
};
if (lb_ip)
message.lb_ip = lb_ip;
if (message.status_code!=200)
message.status_message = status_message;
this.req_logger.log({level: 'info', message});
};
const extract_log_data = (res, req)=>{
const headers = res.headers||{};
lutil.parce_brd_debug(res);
const in_bw = Math.max(res.brd_debug.bytes_down||0, res.in_bw||0);
const out_bw = Math.max(res.brd_debug.bytes_up||0, res.out_bw||0);
req = req||res.request;
let _url = req.url_full||req.url||'';
const hostname = get_hostname(_url);
const auth_type = res.lpm_auth_type;
if (_url.length>consts.MAX_URL_LENGTH)
_url = _url.slice(0, consts.MAX_URL_LENGTH);
return {headers, in_bw, out_bw, _url, hostname, auth_type};
};
E.prototype.usage = function(response){
if (!response)
return;
// XXX mikhailpo: ugly hack for prevent double logging of a single request
response.usage_logged = true;
const {headers, in_bw, out_bw, _url, hostname,
auth_type} = extract_log_data(response);
let stat_success = +(response.status_code &&
(response.status_code=='unknown' || consts.SUCCESS_STATUS_CODE_RE
.test(response.status_code)));
this.send_stats(response.lum_traffic, hostname, in_bw, out_bw,
stat_success);
let user, super_proxy, password, url_parts;
if (response.proxy)
{
super_proxy = response.proxy.host+':'+response.proxy.proxy_port;
user = response.proxy.username;
password = response.proxy.password;
}
this.log_req(_url, response.request.method, response.remote_address,
hostname, headers, response.lp_ip, in_bw, out_bw, super_proxy, user,
password, auth_type, response.status_code, response.status_message,
response.request.start_time, response.timeline.req_chain);
if (!Number(this.opt.logs) && !this.remote_logger
&& response.context!='PROXY TESTER TOOL')
{
return;
}
const is_ssl = response.request.url.endsWith(':443') &&
response.status_code=='200';
const status_code = is_ssl ? 'unknown' : response.status_code || 'unknown';
const encoding = response.headers && response.headers['content-encoding'];
const response_body = is_ssl ? 'unknown' :
lutil.decode_body(response.body, encoding, this.opt.har_limit,
response.body_size);
const data = {
uuid: response.uuid,
port: this.port,
url: _url,
method: response.request.method,
request_headers: JSON.stringify(response.request.headers),
request_body: response.request.body,
response_headers: stringify(headers),
response_body,
status_code,
status_message: response.status_message,
timestamp: response.timeline.get('create'),
elapsed: response.timeline.get_delta('end'),
proxy_peer: headers['x-luminati-ip'],
timeline: stringify(response.timeline.req_chain),
content_size: response.body_size,
context: response.context,
remote_address: response.remote_address,
rules: response.rules,
lum_traffic: response.lum_traffic,
in_bw,
out_bw,
super_proxy,
username: user,
};
if (!this.opt.zagent)
data.password = password;
if (response.success)
data.success = +response.success;
if (url_parts = data.url.match(/^([^/]+?):(\d+)$/))
{
data.protocol = url_parts[2]==443 ? 'https' : 'http';
data.hostname = url_parts[1];
}
else
{
const {protocol, hostname: hn} = url.parse(data.url);
data.protocol = (protocol||'https:').slice(0, -1);
data.hostname = hn;
}
if (!data.hostname)
this.perr('empty_hostname', data);
data.hostname = zurl.get_root_domain(data.hostname||'');
data.content_type = get_content_type(data);
data.success = +(data.status_code && (data.status_code=='unknown' ||
consts.SUCCESS_STATUS_CODE_RE.test(data.status_code)));
if (this.remote_logger && !logs_remote_ignore_ctx.includes(data.context))
this.remote_logger.info(data);
else
this.emit('usage', data);
};
E.prototype.refresh_ip = function(ctx, ip, vip){
this.emit('refresh_ip', {ip, vip, port: this.opt.port});
};
E.prototype.is_whitelisted = function(req){
const auth_header = req.headers['proxy-authorization'];
if (auth_header)
{
const auth = Buffer.from(auth_header.replace('Basic ', ''), 'base64')
.toString();
const [user, pass] = auth.split(':');
const lpm_token = (this.opt.lpm_token||'').split('|')[0];
if (user=='lpm'||user=='token'||user.includes(','))
delete req.headers['proxy-authorization'];
if (user=='token' && this.opt.token_auth && pass==this.opt.token_auth)
{
req.lpm_auth_type = 'token';
return true;
}
if (user=='lpm' && lpm_token && pass==lpm_token)
{
req.lpm_auth_type = 'lpm_token';
return true;
}
let lpm_user_opt;
if (pass==this.opt.user_password &&
(user.replace(/,/, '@')==this.opt.user ||
(lpm_user_opt = username.parse_opt(`lpm_user-${user}`)) &&
lpm_user_opt.lpm_user==this.opt.user ||
(lpm_user_opt = username.parse_opt(
`lpm_user-${Buffer.from(user, 'hex').toString('utf8')}`))&&
lpm_user_opt.lpm_user==this.opt.user))
{
delete req.headers['proxy-authorization'];
if (lpm_user_opt)
{
const h_fields = ['session', 'country', 'state', 'city', 'asn',
'zip'];
for (let p of h_fields)
{
if (!req.headers['x-lpm-'+p] && lpm_user_opt[p])
req.headers['x-lpm-'+p] = lpm_user_opt[p];
}
}
req.lpm_auth_type = 'lpm_user';
return true;
}
if (consts.USERNAME_PREFS.some(p=>user.startsWith(p+'-')))
{
// proxy_type is undefined for dropin port
const ignore = !this.opt.zagent && !this.opt.proxy_type;
const parsed_auth = username.parse(auth_header);
const right_customer = this.opt.account_id==parsed_auth.customer
|| this.opt.customer_id==parsed_auth.customer
|| this.opt.customer==parsed_auth.customer;
const right_zone_password = this.opt.zone==parsed_auth.zone &&
parsed_auth.password;
if (parsed_auth.customer && parsed_auth.zone &&
parsed_auth.password)
{
let whitelist = this.opt.zone_auth_type_whitelist || [];
const success = right_customer && right_zone_password &&
whitelist.includes(parsed_auth.customer)
|| ignore;
req.lpm_auth_type = success ? 'zone' : undefined;
return success;
}
if (parsed_auth.auth=='token' && parsed_auth.password)
{
const success = parsed_auth.password==this.opt.token_auth
|| parsed_auth.password==lpm_token;
req.lpm_auth_type = success ? 'lum_token' : undefined;
return success;
}
}
}
const ip_whitelisted = this.is_whitelisted_ip(this.get_req_remote_ip(req));
if (ip_whitelisted)
req.lpm_auth_type = 'ip';
return ip_whitelisted;
};
E.prototype.is_whitelisted_ip = function(ip){
if (ip=='127.0.0.1')
return true;
return this.opt.whitelist_ips.map(_ip=>new Netmask(_ip)).some(_ip=>{
try { return _ip.contains(ip); }
catch(e){ return false; }
});
};
E.prototype.log_req_without_res = function(req, res, status_code,
status_message)
{
const {headers, in_bw, out_bw, _url, hostname,
auth_type} = extract_log_data(res, req);
const remote_address = res.remote_address||this.get_req_remote_ip(req);
const req_chain = res.timeline ? res.timeline.req_chain : [];
let super_proxy, user, password;
this.log_req(_url, req.method, remote_address, hostname, headers,
res.lp_ip, in_bw, out_bw, super_proxy, user, password, auth_type,
status_code, status_message, req.start_time, req_chain);
};
E.prototype.close_req_socket = function(req){
if (!req_list.has(req))
return;
req_list.delete(req);
this.ensure_socket_close(req.socket);
};
E.prototype.close_all_reqs = function(){
req_list.forEach(this.close_req_socket.bind(this));
};
E.prototype.store_request = function(req){
const socket = req.socket, p_socket = socket._parent;
if (req_list.has(req) || req.destroyed || socket.destroyed
|| p_socket&&p_socket.destroyed)
{
return;
}
this.ensure_socket_emit_close(socket);
req_list.add(req);
socket.setMaxListeners(socket.getMaxListeners()+1);
socket.once('close', ()=>this.close_req_socket(req));
if (!p_socket)
return;
this.ensure_socket_emit_close(p_socket);
p_socket.setMaxListeners(p_socket.getMaxListeners()+1);
p_socket.once('close', ()=>this.close_req_socket(req));
};
E.prototype.listen = etask._fn(function*listen(_this){
try {
if (!_this.sp)
{
_this.sp = etask(function*server_listen(){
return yield this.wait();
});
}
_this.sp.spawn(_this.session_mgr.sp);
let hostname = find_iface(_this.opt.iface);
if (!hostname)
{
hostname = '0.0.0.0';
_this.opt.iface = '0.0.0.0';
}
_this.port = _this.opt.listen_port;
_this.tcp_server.once('error', e=>{
this.throw(e);
});
_this.tcp_server.listen(_this.opt.listen_port, hostname,
this.continue_fn());
yield this.wait();
_this.emit('ready');
return _this;
} catch(e){
_this.emit('error', e);
}
});
E.prototype.stop = etask._fn(function*stop(_this){
try {
if (_this.stopped)
return;
_this.stopped = true;
if (_this.sp)
{
_this.sp.return();
_this.sp = null;
}
_this.timeouts.clear();
_this.banlist.clear_timeouts();
_this.session_mgr.stop();
_this.ws_handler.stop();
_this.requester.stop();
_this.https_agent.destroy();
_this.tcp_server.running = false;
yield etask.nfn_apply(_this.tcp_server, '.forceShutdown', []);
_this.emit('stopped');
return _this;
} catch(e){
if (e.code=='ERR_SERVER_NOT_RUNNING')
_this.emit('stopped');
else
_this.emit('error', e);
}
});
E.prototype.check_proxy_response = function(res){
const message = res.headers && res.headers['x-luminati-error'];
if (!message)
return false;
const err = new Error();
err.message = message;
err.code = res.status_code || res.statusCode || 0;
err.custom = true;
err.proxy_error = true;
err.retry = false;
if (err.code==502 && err.message.match(/^Proxy Error/))
err.retry = true;
return err;
};
E.prototype.get_next_host = function(is_cn){
let _hosts = this.hosts;
if (is_cn && (this.cn_hosts||[]).length)
_hosts = this.cn_hosts;
if (!_hosts.length)
throw new Error('No hosts available');
if (!_hosts[hosts_cursor])
hosts_cursor = 0;
return _hosts[hosts_cursor++];
};
E.prototype.get_req_cred = function(req){
const ctx = req.ctx;
const auth = username.parse(ctx.h_proxy_authorization) || {};
if (!auth.password || auth.auth)
delete auth.password;
delete auth.auth;
if (ctx.h_session)
auth.session = ctx.h_session;
if (ctx.h_country)
auth.country = ctx.h_country;
if (ctx.h_state)
auth.state = ctx.h_state;
if (ctx.h_city)
auth.city = ctx.h_city;
if (ctx.h_zip)
auth.zip = ctx.h_zip;
if (ctx.h_asn)
auth.asn = ctx.h_asn;
if (auth.tool)
{
delete auth.tool;
delete auth.password;
}
if (ctx.retry)
{
delete auth.zone;
delete auth.password;
delete auth.customer;
}
const opt = {
ext_proxy: ctx.session && ctx.session.ext_proxy,
ip: ctx.h_ip || ctx.session && ctx.session.ip || this.opt.ip,
vip: ctx.session && ctx.session.vip || this.opt.vip,
session: ctx.session && ctx.session.session,
direct: ctx.is_direct,
unblocker: this.opt.unblock,
debug: ctx.opt.debug,
const: ctx.opt.const,
customer: this.opt.customer_id||this.opt.account_id||this.opt.customer,
};
if (ctx.session && ctx.session.asn)
opt.asn = ctx.session.asn;
return username.calculate_username(Object.assign({}, this.opt, opt, auth));
};
E.prototype.get_proxy_port = function(){
const use_pp = this.opt.new_proxy_port&&
this.opt.proxy_port===E.default.proxy_port;
const def_pp = use_pp ? E.default.new_proxy_port : this.opt.proxy_port;
const {super_proxy_ports} = this.opt;
if (!super_proxy_ports || super_proxy_ports.length<2)
return def_pp;
if (!super_proxy_ports[super_proxy_ports_cursor])
super_proxy_ports_cursor = 0;
return super_proxy_ports[super_proxy_ports_cursor++];
};
E.prototype.init_proxy_req = function(req, res){
const {ctx} = req;
ctx.init_stats();
ctx.host = this.session_mgr.get_session_host(ctx.session);
if (this.router.is_bypass_proxy(req))
return;
ctx.proxy_port = ctx.session && ctx.session.proxy_port ||
this.get_proxy_port();
ctx.cred = this.get_req_cred(req);
res.cred = ctx.cred.username;
res.port = ctx.port;
res.lpm_auth_type = req.lpm_auth_type;
ctx.response.proxy = {
host: ctx.host,
proxy_port: ctx.proxy_port,
username: ctx.cred.username,
password: ctx.cred.password,
};
ctx.connect_headers = {
'proxy-authorization': 'Basic '+
Buffer.from(ctx.cred.username+':'+ctx.cred.password)
.toString('base64'),
};
if (ctx.session && ctx.session.ext_proxy)
return;
let agent = lpm_config.hola_agent;
const auth = username.parse(ctx.h_proxy_authorization);
if (auth && auth.tool)
agent = agent+' tool='+auth.tool;
if (this.opt.zagent)
{
agent = agent+' lpm_port='+this.opt.port;
if (this.opt.user)
ctx.connect_headers['x-lpm-user'] = this.opt.user;
}
ctx.connect_headers['x-hola-agent'] = agent;
};
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.route_bropin_port = function(req, res, head){
let retry_port;
if (this.opt.proxy_type || !(retry_port=req.headers['x-lpm-route']))
return;
delete req.headers['x-lpm-route'];
this.rules.retry(req, res, head, {retry_port, silent: true});
return 'switched';
};
E.prototype.lpm_request = etask._fn(
function*lpm_request(_this, req, res, head, post, opt){
req.setMaxListeners(req.getMaxListeners() + 1);
req.once('aborted', ()=>{
_this.usage_abort(req);
req.setMaxListeners(Math.max(req.getMaxListeners() - 1, 0));
});
res.once('error', e=>{
_this.logger.error(zerr.e2s(e));
_this.usage_abort(req);
});
_this.restore_ports_meta(req);
const ctx = Context.init_req_ctx(req, res, _this,
Object.assign(_this.opt, opt));
this.finally(()=>{
ctx.complete_req();
});
try {
if (ctx.req_sp)
ctx.req_sp.spawn(this);
if (!ctx.req_sp)
ctx.req_sp = this;
_this.add_headers(req);
_this.apply_ports_meta(req);
ctx.init_response();
if (_this.refresh_task)
{
yield _this.refresh_task;
_this.refresh_task = null;
ctx.timeline.track('create');
}
if (_this.reverse_lookup)
{
ctx.set_reverse_lookup_res(
yield _this.reverse_lookup_url(ctx.url));
}
if (ctx.is_connect && parse_ip_url(ctx.url))
{
_this.logger.warn('HTTPS to IP: %s is sent from super proxy',
ctx.url);
}
if (!req.ctx.retry)
_this.usage_start(req);
let resp = yield _this.route_bropin_port(req, res, head);
if (!resp)
resp = yield _this.rules.pre(req, res, head);
if (!resp)
{
_this.init_proxy_req(req, res);
resp = yield _this.route_req(req, res, head);
}
else if (resp!='switched' && !resp.body_size && _this.rules)
yield _this.rules.post(req, res, head, resp);
if (resp=='switched')
{
_this.emit('switched');
yield this.wait();
}
if (resp instanceof Error)
throw resp;
if (!resp)
throw new Error('invalid_response');
if (ctx.wait_bw)
yield this.wait_ext(ctx.wait_bw);
_this.prepare_resp(req, resp);
_this.emit('response', resp);
if (post)
yield post(resp);
return ctx.req_sp.return(resp);
} catch(e){
const resp = ctx.response;
resp.status_code = 502;
resp.statusCode = 502;
resp.hola_headers = Object.assign({}, e.payload && e.payload.headers,
resp.hola_headers);
if (yield _this.rules.post(req, res, head, resp))
return yield ctx.req_sp.wait();
_this.prepare_resp(req, resp);
resp.headers = {Connection: 'close', 'x-lpm-error': e.message};
_this.emit('response', resp);
if (post)
yield post(resp);
if (_this.handle_custom_error(e, req, res, ctx))
return ctx.req_sp.return();
return ctx.req_sp.throw(e);
}
});
E.prototype.over_max_redirects = function(req){
if (!req.ctx)
return false;
req.ctx.n_redirects = (req.ctx.n_redirects||0)+1;
return req.ctx.n_redirects>MAX_REDIRECTS;
};
E.prototype.update_req_redirect = function(req, location){
let prev_url = req_util.full_url(req);
req.parts = req.parts || new URL(prev_url);
if (!location)
throw new Error('Redirect without location');
let safe_location = /[^\u0021-\u00ff]/.test(location)
? encodeURI(location) : location;
let new_url = url.resolve(prev_url, safe_location);
new_url = new URL(new_url);
for (let k of REDIRECT_PARTS)
new_url[k] = new_url[k]||req.parts[k];
this.logger.info('Redirect %s->%s', prev_url, new_url);
req.headers = Object.assign({}, req.headers, {referer: prev_url});
req.url = url.format(new_url);
if (req.method!='HEAD')
req.method = 'GET';
};
E.prototype.should_redirect = function(req, proxy_res){
let status = proxy_res.status_code || proxy_res.statusCode;
return this.opt.follow_redirect && is_redirect_status(status) &&
!this.over_max_redirects(req) && !!proxy_res.headers.location;
};
E.prototype.redirect_req =
etask._fn(function*redirect_req(_this, req, res, head, proxy, proxy_res){
let location = proxy_res.headers.location;
_this.update_req_redirect(req, location);
let {rule, opt} = _this.rules.get_fake_retry(req, proxy_res);
if (yield _this.rules.action(req, res, head, rule, opt))
return _this.abort_proxy_req(req, proxy);
});
E.prototype.prepare_resp = function(req, resp){
req.ctx.timeline.track('end');
resp.remote_address = this.get_req_remote_ip(req);
if (req.socket && this.lb_ips.has(req.socket.remoteAddress))
resp.lb_ip = req.socket.remoteAddress;
const auth = username.parse(req.ctx.h_proxy_authorization);
if (auth && auth.tool=='proxy_tester')
resp.context = 'PROXY TESTER TOOL';
resp.rules = req.ctx.get_rules_executed();
resp.lum_traffic = !req.ctx.is_bypass_proxy && !this.opt.ext_proxies &&
!req.ctx.is_from_cache && !req.ctx.is_null_response &&
!req.ctx.is_malware;
resp.lpm_auth_type = req.lpm_auth_type;
};
E.prototype.get_user_agent = function(){
const ua = (this.opt.headers||[]).find(f=>
f.name.toLowerCase()=='user-agent');
if (!ua || !ua.value)
return;
if (!ua.value.startsWith('random'))
return ua.value;
const ua_version = Math.floor(Math.random()*2240)+1800;
if (ua.value=='random_mobile')
{
return `Mozilla/5.0 (iPhone; CPU iPhone OS 13_2 like Mac OS X)`
+` AppleWebKit/605.1.15 (KHTML, like Gecko)`
+` CriOS/80.0.${ua_version}.95 Mobile/15E148 Safari/604.1`;
}
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36`
+` (KHTML, like Gecko) Chrome/80.0.${ua_version}.122 Safari/537.36`;
};
E.prototype.add_headers = function(req){
const added_headers = {};
(this.opt.headers||[]).forEach(header=>{
added_headers[header.name] = header.value;
});
const ua = this.get_user_agent();
if (ua)
added_headers['user-agent'] = ua;
Object.assign(req.headers, added_headers);
};
E.prototype.restore_ports_meta = function(req){
Object.assign(req.headers, req.headers_orig||{});
};
E.prototype.apply_ports_meta = function(req){
if (!req.ports_meta || !req.ports_meta[this.opt.port])
return;
req.ports_meta[this.opt.port].forEach(h=>
delete req.headers[h.toLowerCase()]);
};
E.prototype.route_req = etask._fn(function*route_req(_this, req, res, head){
try {
_this.logger.debug('%s:%s - %s %s', req.socket.remoteAddress,
req.socket.remotePort, req.method, req.ctx.url);
req.setMaxListeners(30);
if (_this.opt.session_termination && (req.ctx.session||{}).terminated)
return _this.router.send_internal_redirection(req, res);
else if (_this.router.is_fake_request(req))
return yield _this.send_fake_request(this, req, res);
else if (!_this.hosts.length)
throw new Error('No hosts when processing request');
else if (_this.router.is_bypass_proxy(req))
return yield _this.send_bypass_req(this, req, res, head);
else
return yield _this.send_proxy_req(this, req, res, head);
} catch(e){
return e;
}
});
E.prototype.perr = function(id, info){
Object.assign(info, {
customer: this.opt.customer,
account_id: this.opt.account_id,
customer_id: this.opt.customer_id,
});
this.logger.info('sending perr: %s', JSON.stringify(info));
lutil.perr(id, info);
};
E.prototype.log_fn = function(e, ctx, source){
if (!this.is_custom_error(e))
this.logger.error('fn: %s %s', e.message, ctx.url);
if (!this.opt.zagent)
return;
const msg = e && e.message || '';
let perr_id = 'conn_unknown';
if (msg=='socket hang up')
perr_id = 'conn_socket_hang_up';
else if (msg=='read ECONNRESET')
perr_id = 'conn_read_connreset';
else if (msg.startsWith('connect ETIMEDOUT'))
perr_id = 'conn_connect_etimedout';
else if (msg.startsWith('connect ECONNREFUSED'))
perr_id= 'conn_connect_econnrefused';
else if (msg=='BAD_DECRYPT')
perr_id = 'conn_flex_tls_bad_decrypt';
else if (msg=='Cannot call write after a stream was destroyed')
perr_id = 'conn_flex_tls_err_stream_destroyed';
else if (msg=='SSLV3_ALERT_CLOSE_NOTIFY')
perr_id = 'conn_flex_tls_sslv3_alert_close_notify';
else if (msg.startsWith('flex_tls_reuse_destroyed_socket'))
perr_id = 'conn_flex_tls_reuse_destroyed_socket';
else if (msg.includes('flex_tls') || (source||'').includes('flex_tls'))
perr_id = 'conn_flex_tls_unknown';
this.perr(perr_id, {
error: zerr.e2s(e),
ctx: source,
url: ctx.url,
cred: ctx.cred,
headers: ctx.headers,
host: ctx.host,
port: ctx.port,
});
};
E.prototype.log_throw_fn = function(task, ctx, source){
return e=>{
this.log_fn(e, ctx, source);
task.throw(e);
};
};
E.prototype.is_ip_banned = function(ip, domain){
if (!ip)
return false;
return this.banlist.has(ip, domain);
};
E.prototype.get_reused_conn = function(ctx){
const socket_name = ctx.get_socket_name();
if (this.https_agent.freeSockets[socket_name])
{
this.logger.debug('reusing socket: %s %s', ctx.domain,
ctx.cred.username);
const headers = this.socket2headers.get(socket_name);
const socket = this.https_agent.freeSockets[socket_name][0];
return {socket, res: {headers: Object.assign({}, headers)}};
}
};
E.prototype.request_new_socket = etask._fn(
function*_request_new_socket(_this, task, req, res, head){
const ctx = req.ctx;
task.once('cancel', ()=>this.return());
const conn = yield _this.requester.request_socket(task, ctx, {
on_error: _this.log_throw_fn(this, ctx, 'request_new_socket'),
use_flex_tls: _this.opt.use_flex_tls,
on_flex_tls_err: _this.log_throw_fn(this, ctx,
'flex_tls, conn.socket'),
});
const socket_name = ctx.get_socket_name();
_this.socket2headers.set(socket_name, Object.assign({}, conn.res.headers));
conn.socket.once('close', ()=>{
_this.socket2headers.delete(socket_name);
});
if (etask.is_final(task))
conn.socket.end();
if (_this.opt.session_termination && conn.res.statusCode==502 &&
conn.res.statusMessage==consts.NO_PEERS_ERROR_SSL)
{
return _this.handle_session_termination(req, res);
}
if (conn.res.statusCode!=200)
{
const proxy_err = _this.check_proxy_response(conn.res);
const can_retry = _this.rules.can_retry(req,
{retry: ctx.proxy_retry});
if (can_retry && proxy_err && proxy_err.retry)
{
_this.rules.retry(req, res, head);
return yield this.wait();
}
if (proxy_err)
throw proxy_err;
}
const domain = req_util.get_domain(req);
const ip = conn.res.headers['x-luminati-ip'];
if (_this.is_ip_banned(ip, domain) &&
(req.retry||0)<_this.opt.max_ban_retries)
{
_this.refresh_sessions();
_this.rules.retry(req, res, head);
return yield this.wait();
}
else if (_this.is_ip_banned(ip, domain))
throw new Error('Too many banned IPs');
conn.res.once('error', _this.log_throw_fn(this, ctx,
'request_new_socket, conn.res'));
conn.socket.once('error', _this.log_throw_fn(this, ctx,
'request_new_socket, conn.socket'));
return conn;
});
E.prototype.usage_abort = etask._fn(function*(_this, req){
const response = req.ctx.response;
if (response.usage_logged)
return;
if (req.ctx.wait_bw)
yield this.wait_ext(req.ctx.wait_bw);
if (!response.timeline.get('end'))
_this.prepare_resp(req, response);
const in_bw = response.in_bw||0;
const out_bw = response.out_bw||0;
let _url = response.request.url_full||response.request.url||'';
const hostname = get_hostname(_url);
const auth_type = req.lpm_auth_type;
_this.send_stats(response.lum_traffic, hostname, in_bw, out_bw, false);
if (_url.length>consts.MAX_URL_LENGTH)
_url = _url.slice(0, consts.MAX_URL_LENGTH);
let user, super_proxy, password;
if (response.proxy)
{
super_proxy = response.proxy.host+':'+response.proxy.proxy_port;
user = response.proxy.username;
password = response.proxy.password;
}
_this.log_req(response, _url, hostname, in_bw, out_bw, super_proxy, user,
password, auth_type, 499, 'aborted');
if (!Number(_this.opt.logs) && !_this.remote_logger
&& response.context!='PROXY TESTER TOOL')
{
return;
}
const data = {
uuid: response.uuid,
port: _this.port,
url: response.request.url,
method: response.request.method,
request_headers: JSON.stringify(response.request.headers),
request_body: response.request.body,
status_code: 'canceled',
timestamp: response.timeline.get('create'),
elapsed: response.timeline.get_delta('end'),
timeline: stringify(response.timeline.req_chain),
context: response.context,
remote_address: _this.get_req_remote_ip(req),
rules: req.ctx.get_rules_executed(),
};
if (response.proxy)
{
data.super_proxy = response.proxy.host+':'+response.proxy.proxy_port;
data.username = response.proxy.username;
data.password = response.proxy.password;
}
if (_this.opt.zagent)
delete data.password;
if (_this.remote_logger && !logs_remote_ignore_ctx.includes(data.context))
_this.remote_logger.info(data);
else
_this.emit('usage_abort', data);
});
E.prototype.request = function(){
const args = [].slice.call(arguments);
if (typeof args[0]=='string')
args[0] = {url: args[0]};
args[0].proxy = args[0].proxy||`http://127.0.0.1:${this.port}`;
return request.apply(null, args);
};
E.prototype.banip = function(ip, ms, session, domain){
this.banlist.add(ip, ms, domain);
this.emit('banip', {ip, ms, domain});
if (session)
this.session_mgr.replace_session(session);
return true;
};
E.prototype.unbanip = function(ip, domain){
if (!this.banlist.has(ip, domain))
return false;
this.banlist.delete(ip, domain);
this.emit('unbanip', {ip, domain});
return true;
};
E.prototype.unbanips = function(){
if (!this.banlist.cache.size)
return false;
this.banlist.clear();
return true;
};
E.hola_headers = qw`proxy-connection proxy-authentication x-hola-agent
x-hola-context x-luminati-timeline x-luminati-peer-timeline
x-luminati-error x-lpm-error x-lpm-authorization x-luminati-ip
x-lpm-user`;