@luminati-io/luminati-proxy
Version:
A configurable local proxy for luminati.io
309 lines (306 loc) • 11.4 kB
JavaScript
// LICENSE_CODE ZON ISC
'use strict'; /*jslint node:true, esnext:true*/
const fs = require('fs');
const file = require('../util/file.js');
const date = require('../util/date.js');
const etask = require('../util/etask.js');
const zerr = require('../util/zerr.js');
const {qw} = require('../util/string.js');
const lpm_config = require('../util/lpm_config.js');
const zutil = require('../util/util.js');
const stringify = require('json-stable-stringify');
const Server = require('./server.js');
const migrate = require('./migration.js');
const logger = require('./logger.js').child({category: 'Conf'});
const {Netmask} = require('netmask');
const pkg = require('../package.json');
const util_lib = require('./util.js');
const consts = require('./consts.js');
let prompt;
try {
prompt = require('prompt-sync')();
} catch(e){
console.log('prompt-sync does not exist');
}
class Config {
constructor(mgr, defaults, opt){
this.mgr = mgr;
this.defaults = defaults;
this.opt = opt;
}
// XXX krzysztof: to implement
prepare_proxy(proxy){
return proxy;
}
save({skip_cloud_update}={}){
if (this.is_read_only())
return;
if (!skip_cloud_update)
this.mgr.config_ts = date();
const config = this._serialize(this.mgr.proxies, this.mgr._defaults,
this.mgr.config_ts);
try {
if (fs.existsSync(this.opt.filename))
fs.renameSync(this.opt.filename, this.opt.filename+'.backup');
fs.writeFileSync(this.opt.filename, config);
} catch(e){
logger.error('Could not save config %s: %s', this.opt.filename,
e.message);
}
if (!skip_cloud_update)
return this.upload(config);
}
upload(config){
if (this.mgr.skip_config_sync())
return;
let _this = this;
return etask(function*(){
this.on('uncaught', e=>
logger.error('Could not upload config: %s', zerr.e2s(e)));
const parsed_config = JSON.parse(config);
parsed_config._defaults = zutil.omit(parsed_config._defaults||{},
qw`customer account_id lpm_token version sync_config
ask_sync_config cluster`);
try { yield _this.mgr.lpm_f.update_conf(parsed_config); }
catch(e){
logger.warn('upload: %s', e.message);
if (e.message!='not_authorized')
throw e;
}
});
}
get_string(){
if (file.exists(this.opt.filename))
{
const buffer = fs.readFileSync(this.opt.filename);
let raw_config = buffer.toString();
try {
let parsed_config = JSON.parse(raw_config);
delete parsed_config.ts;
raw_config = stringify(parsed_config, {space: ' '});
} catch(e){}
return raw_config;
}
return '';
}
set_string(content, {skip_cloud_update}={}){
if (this.is_read_only())
return;
let conf = content;
if (!skip_cloud_update)
{
this.mgr.config_ts = date();
try {
let parsed_conf = JSON.parse(content);
parsed_conf.ts = this.mgr.config_ts;
conf = stringify(parsed_conf, {space: ' '});
} catch(e){ conf = content; }
}
file.write_e(this.opt.filename, conf);
if (!skip_cloud_update)
return this.upload(conf);
}
get_proxy_configs(){
const {explicit_proxy_opt={}, port, zagent} = this.mgr.argv;
const {dropin, dropin_port} = this.mgr.mgr_opts;
const _defaults = Object.assign({}, zutil.pick(this.defaults,
...lpm_config.default_fields));
let proxies = [];
let ts;
lpm_config.numeric_fields.forEach(f=>{
if (explicit_proxy_opt[f])
explicit_proxy_opt[f] = +explicit_proxy_opt[f];
});
if (this.opt.filename || this.opt.cloud_config)
{
logger.notice(`Loaded config ${this.opt.cloud_config ?
'from cloud' : this.opt.filename}`);
const conf = this._load_config();
Object.assign(_defaults, zutil.pick(conf._defaults,
...lpm_config.default_fields));
proxies = proxies.concat(conf.proxies);
ts = conf.ts;
}
if (explicit_proxy_opt.port)
{
proxies.filter(p=>p.port==explicit_proxy_opt.port)
.forEach((p, i)=>{
logger.warn('Conflict between config Proxy #%s and '
+'explicit parameter "--%s %s". Proxy settings will '
+'be overridden by explicit parameters.', i, 'port',
explicit_proxy_opt.port);
});
let proxy = proxies.find(p=>qw`port`.some(k=>
explicit_proxy_opt[k] && p[k]==explicit_proxy_opt[k]));
if (!proxy)
proxy = proxies.find(p=>!p.port || p.port==port);
if (!proxy)
{
proxies.push(Object.assign({proxy_type: 'persist'},
explicit_proxy_opt));
}
else
{
Object.assign(proxy, explicit_proxy_opt);
if (!proxy.port)
proxy.port = port;
}
}
if (dropin && _defaults.dropin!==false)
{
Server.dropin.listen_port = dropin_port;
Server.dropin.port = dropin_port;
proxies.push(Object.assign({}, Server.dropin, zutil.pick(_defaults,
...qw`zone test_url`)));
}
Object.assign(_defaults, explicit_proxy_opt);
const max_port = port || Server.default.port;
const next = (max, key)=>{
while (proxies.some(p=>p[key]==max))
max++;
return max;
};
proxies.filter(c=>!c.port)
.forEach(c=>c.port = c.port || next(max_port, 'port'));
proxies = proxies.map(this._prepare_proxy);
if (zagent)
{
const {logs, har_limit} = _defaults;
_defaults.logs = Math.min(logs, this.defaults.logs);
_defaults.har_limit = [-1, 1024].includes(har_limit) ?
har_limit : this.defaults.har_limit;
}
return {_defaults, proxies, ts};
}
_prepare_proxy(proxy){
if (Array.isArray(proxy.whitelist_ips))
{
proxy.whitelist_ips = proxy.whitelist_ips.filter(ip=>{
try {
new Netmask(ip);
return true;
} catch(e){
logger.warn(`Invalid netmask ${ip} removed`);
return false;
}
});
}
if (proxy.rules && !Array.isArray(proxy.rules))
{
logger.warn('[%s] Removed invalid rules, should be an array',
proxy.port);
delete proxy.rules;
}
if (proxy.reverse_lookup_values &&
!Array.isArray(proxy.reverse_lookup_values))
{
logger.warn('[%s] Removed invalid reverse lookup values, should '
+'be an array', proxy.port);
delete proxy.reverse_lookup_values;
}
if (proxy.ext_proxies && (!Array.isArray(proxy.ext_proxies) ||
proxy.ext_proxies.length>consts.MAX_EXT_PROXIES))
{
logger.warn('[%s] Removed invalid external proxies value, should '
+'be an array with a max size of %s', proxy.port,
consts.MAX_EXT_PROXIES);
delete proxy.ext_proxies;
}
if (typeof proxy.session=='string' && !proxy.session)
delete proxy.session;
return proxy;
}
_serialize(proxies, _defaults, ts){
proxies = proxies.filter(p=>p.proxy_type=='persist' && !p.conflict);
proxies = proxies.map(p=>zutil.omit(p, qw`stats proxy_type zones
www_whitelist_ips logs conflict version customer gb_cost banlist
error`));
const default_wl = this.mgr.get_default_whitelist();
if (default_wl.length)
{
proxies.forEach(p=>{
p.whitelist_ips = (p.whitelist_ips||[]).filter(
ip=>!default_wl.includes(ip));
});
}
const all_defaults = Object.assign({}, this.defaults, _defaults);
delete all_defaults.zone;
proxies = proxies.map(p=>util_lib.omit_defaults(p, all_defaults));
const omitted = qw`config no_usage_stats force`;
const filtered_defaults = zutil.pick(_defaults,
...lpm_config.default_fields.filter(f=>!omitted.includes(f)));
_defaults = util_lib.omit_defaults(filtered_defaults, this.defaults);
return stringify({proxies, _defaults, ts}, {space: ' '});
}
_load_json(){
const pure_config = {_defaults: {version: pkg.version}};
let s;
try {
s = file.read_e(this.opt.filename);
s = s.replace(/^\uFEFF/, '');
if (!s)
return pure_config;
} catch(e){
logger.error(`${this.opt.filename} has not been found`);
return pure_config;
}
try {
const res = JSON.parse(s);
return res;
} catch(e){
const msg = `Failed parsing json file ${this.opt.filename}: `
+`${e.message}`;
console.warn(msg);
let close = 'y';
try {
const question = `Do you want to reset the config file and`
+` continue?`;
close = prompt(`${question} [y/N]`);
} catch(e){
console.warn('propmpt failed');
return pure_config;
}
if (close=='y')
return pure_config;
throw msg;
}
}
_load_config(){
const conf = this.opt.cloud_config &&
zutil.clone_deep(this.opt.cloud_config) || this._load_json();
conf.proxies = conf.proxies||[];
conf._defaults = conf._defaults||{};
if (this.opt.cloud_config)
{
const {lpm_token, customer, account_id, sync_config} =
this.mgr._defaults||{};
conf._defaults = Object.assign({
lpm_token,
customer,
account_id,
sync_config,
version: pkg.version,
}, conf._defaults);
}
conf.proxies.forEach(p=>Object.assign(p, {proxy_type: 'persist'}));
return migrate(zutil.pick(conf, ...qw`_defaults proxies ts`));
}
save_local_backup(){
if (this.is_read_only())
return;
try {
if (file.exists(this.opt.filename+'.local_backup'))
return;
file.copy_e(this.opt.filename, this.opt.filename+'.local_backup');
} catch(e){
logger.error('Could not save config backup %s: %s',
this.opt.filename, e.message);
}
}
is_read_only(){
return !this.opt.filename || typeof this.opt.filename != 'string' ||
this.mgr.opts.read_only;
}
}
module.exports = Config;