UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

283 lines (278 loc) 10.3 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint node:true, esnext:true*/ const zutil = require('../util/util.js'); const {migrate_trigger} = require('../util/rules_util.js'); const etask = require('../util/etask.js'); const zurl = require('../util/url.js'); const request = require('request'); const logger = require('./logger.js').child({category: 'Rules'}); const lutil = require('./util.js'); class Rules { constructor(server, rules=[]){ this.server = server; this.rules = zutil.clone_deep(rules); this.rules = this.rules.map(rule=>{ if (!rule.type || !rule.trigger_code) rule = migrate_trigger(rule); rule.trigger = new Trigger(rule); return rule; }); const rule_to_priority = rule=>{ const action = rule.action||{}; if (action.retry) return 1; if (action.retry_port) return 2; return 0; }; this.rules.sort(function(a, b){ return rule_to_priority(a)-rule_to_priority(b); }); } has_reserve_session_rules(){ return this.rules.some(r=>r.action && r.action.reserve_session); } pre(req, res, head){ if ('STATUS CHECK'==req.ctx.h_context) return; const _url = req.ctx.url||req.url_full||req.url; const opt = {url: _url, pre: 1}; for (let i=0; i<this.rules.length; i++) { const rule = this.rules[i]; if (rule.active===false) continue; if (rule.type!='before_send' && rule.type!='timeout' && !rule.action.cache) { continue; } if (!rule.trigger.test(opt)) continue; if (rule.type=='timeout' && opt.timeout) { const _this = this; req.min_req_task = etask(function*min_req_time(){ yield etask.sleep(opt.timeout); _this.action(req, res, head, rule, opt); }); return false; } return this.action(req, res, head, rule, opt); } } post(req, res, head, _res){ if ('STATUS CHECK'==req.ctx.h_context) return; for (let i=0; i<this.rules.length; i++) { const rule = this.rules[i]; if (rule.active===false) continue; const status = _res.statusCode || _res.status_code; const time_created = zutil.get(req, 'ctx.timeline.req.create')||0; const time_passed = Date.now()-time_created; const opt = {url: req.ctx.url, status, time_passed, _res, post: 1}; if (rule.type!='after_hdr') continue; if (!rule.trigger.test(opt) || req.ctx.skip_rule(rule)) continue; this.check_req_time_range(req, rule); if (this.action(req, res, head, rule, opt)) return true; } } post_body(req, res, head, _res, body){ if ('STATUS CHECK'==req.ctx.h_context) return; let _body; try { _body = lutil.decode_body(body, _res.headers['content-encoding']); } catch(e){ logger.error('error decoding body: %s when requesting url %s', e.message, req.ctx.url); return; } for (let i=0; i<this.rules.length; i++) { const rule = this.rules[i]; if (rule.active===false) continue; const status = _res.statusCode||_res.status_code; const time_passed = Date.now()-req.ctx.timeline.req.create; const opt = {url: req.ctx.url, status, body: _body, time_passed, _res, post_body: 1, res_data: body}; if (rule.type!='after_body' && !rule.action.cache) continue; if (!rule.trigger.test(opt) || req.ctx.skip_rule(rule)) continue; this.check_req_time_range(req, rule); let res_action; if (res_action = this.action(req, res, head, rule, opt)) return res_action; } } will_cache(req){ return this.rules.some(rule=>{ return rule.active!==false && rule.action && rule.action.cache && rule.trigger.test({url: req.ctx.url}); }); } post_need_body(req){ return this.will_cache(req) || this.rules.some(rule=> rule.type=='after_body' && rule.active!==false); } check_req_time_range(req, rule){ if (!rule.max_req_time && !rule.min_req_time) return false; const time_passed = Date.now()-req.ctx.timeline.req.create; const max_time = rule.max_req_time||+Infinity; const min_time = rule.min_req_time||0; const res = time_passed<=max_time && time_passed>=min_time; return res; } retry(req, res, head, opt={}){ const {port} = opt; if (!port || port==this.server.port) this.server.refresh_sessions(); if (!req.retry) req.retry = 0; req.retry++; logger.info('req retry %s %s', req.retry, req.ctx.url); (req.ctx.proxies||[]).forEach(p=>this.server.abort_proxy_req(req, p)); // XXX krzysztof: this is probably a request leak, one request is sent // extra when retrying on already retried request this.server.emit('retry', {port: port||this.server.port, req, res, head, post: _res=>this.post(req, res, head, _res)}); return true; } can_retry(req, action={}){ const retried = req.retry||0; let retry = Number(action.retry)||0; if (action.retry_port) retry = 20; return retried<retry; } send_request(req, opt, ip){ if (!zurl.is_valid_url(opt.url)) return; opt.method = opt.method || 'GET'; const has_payload = opt.method!='GET' && !!opt.payload; const url = /^https?:\/\//.test(opt.url) ? opt.url : 'http://'+opt.url; const req_opt = {url, method: opt.method}; if (has_payload) { const payload = Object.assign({}, opt.payload); const k = Object.keys(payload).find(_k=>payload[_k]=='$IP'); if (k) payload[k] = ip; req_opt.body = JSON.stringify(payload); req_opt.headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(req_opt.body), }; } request(req_opt, e=>{ if (e) { logger.error('error when requesting url %s : %s', req_opt.url, e.message); } }); } action(req, res, head, rule, opt){ // XXX krzysztof: temp work-around to get IP. Should be moved to // server.js and check why SMTP requests don't get to handler if (req.ctx && req.ctx.session) { req.ctx.session.last_res = req.ctx.session.last_res||{}; req.ctx.session.last_res.ip = opt._res && opt._res.headers && opt._res.headers['x-luminati-ip']; } if (req.ctx && req.ctx.rule_executed) req.ctx.rule_executed(rule); if (rule.action.null_response) return this.server.router.send_null_response(req, res); if (rule.action.bypass_proxy) { req.ctx.is_bypass_proxy = true; return; } if (rule.action.direct) { req.ctx.is_direct = true; return; } if (opt.pre && rule.action.cache && this.server.cache.has(req.ctx.url)) { req.ctx.is_from_cache = true; return this.server.router.send_cached(req, res, this.server.cache.get(req.ctx.url)); } if (opt.post_body && rule.action.cache) { this.server.cache.set(req.ctx.url, opt.res_data, opt._res.headers, rule.action.cache_duration); } if (rule.action.reserve_session) this.server.session_mgr.add_to_pool(req.ctx.session); const _res = opt._res; const headers = _res && _res.hola_headers || _res && _res.headers; const ip = headers && headers['x-luminati-ip']; const vip = headers && headers['x-luminati-gip']; if (rule.action.ban_ip!=undefined && _res) { const t = rule.action.ban_ip || 0; if (ip) this.server.banip(ip, t, req.ctx.session); } if (rule.action.ban_ip_global!=undefined && _res) { const t = rule.action.ban_ip_global || 0; if (ip) this.server.emit('banip_global', {ip, t}); if (req.ctx.session) this.server.session_mgr.replace_session(req.ctx.session); } if (rule.action.ban_ip_domain!=undefined && _res) { const t = rule.action.ban_ip_domain || 0; const domain = lutil.url2domain(opt.url); if (ip) this.server.banip(ip, t, req.ctx.session, domain); } if (rule.action.request_url) { this.send_request(req, rule.action.request_url, ip); return false; } if (!this.can_retry(req, rule.action)) return false; if (rule.action.refresh_ip && opt._res && ip) { const refresh_task = this.server.refresh_ip(req.ctx, ip, vip); this.server.refresh_task = refresh_task; } this.retry(req, res, head, {port: rule.action.retry_port}); return 'switched'; } } const E = module.exports = Rules; class Trigger { constructor(r){ const code = r.trigger_code+'\nreturn trigger;'; try { this.func = new Function(code)(); } catch(e){ logger.warn('trigger: invalid function'); } } test(opt){ if (!this.func) { logger.warn('trigger: invalid function'); return false; } try { return this.func(opt||{}); } catch(e){ logger.warn('trigger: function failed'); } return false; } } E.t = {Trigger};