UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for brightdata.com

1,304 lines (1,228 loc) 55.1 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint node:true, esnext:true, evil: true, es9: true*/ const os = require('os'); const fs = require('fs'); const net = require('net'); const https = require('https'); const http = require('http'); const _ = require('lodash4'); const semver = require('semver'); const express = require('express'); const {Netmask} = require('netmask'); const cookie = require('cookie'); const pkg = require('../../package.json'); const request = require('../../util/lpm_request.js').defaults({gzip: true}); const zerr = require('../../util/zerr.js'); const etask = require('../../util/etask.js'); const date = require('../../util/date.js'); const lpm_config = require('../../util/lpm_config.js'); const zutil = require('../../util/util.js'); const file = require('../../util/file.js'); const user_agent = require('../../util/user_agent.js'); const zurl = require('../../util/url.js'); const logger = require('../logger.js').child({category: 'WEBAPI'}); const consts = require('../consts.js'); const ssl = require('../ssl.js'); const cities = require('../cities.js'); const util_lib = require('../util.js'); const mw = require('../middleware.js'); const puppeteer = require('../puppeteer.js'); const mixin_core = require('./core.js'); const {get_source, get_username, format_json, convert_bytes} = util_lib; const {assign, keys, values, entries} = Object; const MIXIN_LABEL = module.exports = 'mgr_web_api'; const E = mixin_core.new_mixin(MIXIN_LABEL); E.default = assign({}, lpm_config.manager_default); E.prototype.limit_zagent = function(req, res, next){ if (this.argv.zagent) return res.status(403).send('This action is not allowed in Cloud'); next(); }; E.prototype.lpm_token_auth_mw = function(req, res, next){ const tokens = [(this._defaults.lpm_token||'').split('|')[0], this._defaults.token_auth].filter(Boolean); const auth_lpm_token = [cookie.parse(req.headers.cookie||'').lpm_token, req.headers.authorization, req.query.lpm_token].filter(Boolean)[0]; const bypass = req.is_localhost || !this.logged_in; if (bypass || !tokens.length || tokens.includes(auth_lpm_token)) return void next(); logger.warn('[Token Auth] access denied for %s %s', util_lib.req_util.get_remote_ip(req), req.url); this.err2res({ status: 403, headers: {'x-lpm-block-token': req.headers.authorization ||req.query.lpm_token||'no_token'}, msg: 'Valid lpm_token required to use this API', }, res); }; E.prototype.api_error_handler = function(err, req, res, next){ this.perr('crash_api', {error: zerr.e2s(err)}); logger.error('API error: %s %s %s', req.method, req.originalUrl, zerr.e2s(err)); res.status(500).send('Server error: '+err.message); }; E.prototype.init_common_api = function(app){ app.use(mw.security.headers); app.use(mw.rate_limit.default); app.use(this.authenticate_mw.bind(this)); const limit_zagent = this.limit_zagent.bind(this); app.use(mw.logger.api(this.argv.www)); app.get('/consts', this.get_consts_api.bind(this)); app.get('/defaults', (req, res)=>res.json(this.opts)); app.get('/version', this.version_api.bind(this)); app.get('/last_version', this.last_version_api.bind(this)); app.get('/node_version', this.node_version_api.bind(this)); app.get('/mode', (req, res)=>res.json({logged_in: this.logged_in})); app.get('/conn', (req, res)=>res.json(this.conn)); app.put('/api_url', this.api_url_update_api.bind(this)); app.put('/pmgr_domain', this.pmgr_domain_update_api.bind(this)); app.get('/proxies_running', this.proxies_running_get_api.bind(this)); app.get('/proxies/:port?', this.proxies_get_api.bind(this)); app.post('/proxies', this.proxy_create_api.bind(this)); app.post('/proxies/delete', this.proxies_delete_api.bind(this)); app.put('/proxies/:port', mw.validator.port_conf, this.proxy_update_api.bind(this)); app.delete('/proxies/:port', this.proxy_delete_api.bind(this)); app.post('/proxy_dup', this.proxy_dup_api.bind(this)); app.post('/proxies/:port/banip', this.proxy_banip_api.bind(this)); app.post('/proxies/:port/banips', this.proxy_banips_api.bind(this)); app.post('/proxies/:port/unbanip', this.proxy_unbanip_api.bind(this)); app.post('/proxies/:port/unbanips', this.proxy_unbanips_api.bind(this)); app.get('/generate_proxies/:port', this.generate_proxies_api.bind(this)); app.get('/banlist/:port', this.get_banlist_api.bind(this)); app.post('/banip', this.global_banip_api.bind(this)); app.get('/sessions/:port', this.get_sessions_api.bind(this)); app.post('/refresh_sessions/:port', this.refresh_sessions_api.bind(this)); app.get('/proxy_status/:port', this.proxy_status_get_api.bind(this)); app.get('/browser/:port', this.open_browser_api.bind(this)); app.get('/logs', this.logs_get_api.bind(this)); app.get('/logs_cloud', this.logs_cloud_get_api.bind(this)); app.get('/logs_har', this.logs_har_get_api.bind(this)); app.post('/logs_resend', this.logs_resend_api.bind(this)); app.get('/logs_suggestions', this.logs_suggestions_api.bind(this)); app.put('/logs_reset', this.logs_reset_api.bind(this)); app.post('/test_logs_remote', this.test_logs_remote.bind(this)); app.get('/settings', this.get_settings_api.bind(this)); app.put('/settings', mw.validator.settings, this.update_settings_api.bind(this)); app.get('/tls_warning', (req, res)=>res.json(this.tls_warning)); app.post('/creds_user', limit_zagent, this.creds_user_api.bind(this)); app.post('/verify_two_step', limit_zagent, this.verify_two_token_api.bind(this)); app.get('/config', this.config_get_api.bind(this)); app.post('/config', limit_zagent, this.config_set_api.bind(this)); app.get('/allocated_ips', this.allocated_ips_get_api.bind(this)); app.get('/allocated_vips', this.allocated_vips_get_api.bind(this)); app.get('/lpm_users', this.lpm_users_get_api.bind(this)); app.post('/lpm_user', this.lpm_user_add_api.bind(this)); app.post('/refresh_ip/:port', this.refresh_ip_api.bind(this)); app.post('/refresh_ips', this.refresh_ips_api.bind(this)); app.post('/shutdown', limit_zagent, this.shutdown_api.bind(this)); app.post('/logout', limit_zagent, this.logout_api.bind(this)); app.post('/upgrade', limit_zagent, this.upgrade_api.bind(this)); app.post('/downgrade', limit_zagent, this.downgrade_api.bind(this)); app.post('/restart', limit_zagent, this.restart_api.bind(this)); app.get('/all_locations', this.get_all_locations_api.bind(this)); app.get('/all_carriers', this.get_all_carriers_api.bind(this)); app.post('/test/:port', this.proxy_tester_api.bind(this)); app.get('/recent_stats', this.stats_get_api.bind(this)); app.post('/report_bug', this.report_bug_api.bind(this)); app.post('/enable_ssl', this.enable_ssl_api.bind(this)); app.post('/update_ips', this.update_ips_api.bind(this)); app.get('/zones', this.get_zones_api.bind(this)); app.put('/whitelist_ip', this.add_www_whitelist_ip_api.bind(this)); app.put('/wip', this.add_wip_api.bind(this)); app.delete('/whitelist_ip', this.remove_www_whitelist_ip_api.bind(this)); app.delete('/wip', this.remove_wip_api.bind(this)); app.post('/perr', this.perr_api.bind(this)); app.post('/emit_ws', this.emit_ws_api.bind(this)); app.get('/general_logs', this.get_general_logs_api.bind(this)); app.post('/log_level', this.set_log_level_api.bind(this)); app.post('/cloud_auth', this.cloud_auth_api.bind(this)); app.post('/cloud_unauth', this.cloud_unauth_api.bind(this)); app.get('/lpm_stats', this.lpm_stats_api.bind(this)); app.get('/server_conf', (req, res)=>res.json(this.server_conf)); app.get('/bw_limit/:port', this.get_bw_limit_api.bind(this)); app.put('/bw_limit/:port', this.set_bw_limit_api.bind(this)); app.get('/bw_limit_stats/:port?', this.get_bw_limit_stats_api.bind(this)); app.get('/i18n', this.get_lang_resources.bind(this)); app.get('/collect_data', this.get_collect_data_api.bind(this)); }; E.prototype.create_api_v2 = function(){ const app = express(); app.use(this.lpm_token_auth_mw.bind(this)); this.init_common_api(app); app.put('/kill_workers', this.kill_workers_api.bind(this)); app.put('/run_workers', this.run_workers_api.bind(this)); app.post('/gen_token', mw.rate_limit.gen, this.gen_token_api.bind(this)); app.post('/gen_cert', mw.rate_limit.gen, this.gen_cert_api.bind(this)); app.use(this.api_error_handler.bind(this)); return app; }; E.prototype.create_api = function(){ const app = express(); app.use(mw.deprecation.default()); app.use(mw.deprecation.sunset('11-01-2023')); this.init_common_api(app); app.get('/logs_reset', this.logs_reset_api.bind(this)); app.get('/kill_workers', this.kill_workers_api.bind(this)); app.get('/run_workers', this.run_workers_api.bind(this)); app.post('/add_whitelist_ip', this.add_www_whitelist_ip_api.bind(this)); app.post('/add_wip', this.add_wip_api.bind(this)); app.get('/refresh_sessions/:port', this.refresh_sessions_api.bind(this)); app.get('/gen_token', mw.rate_limit.gen, this.gen_token_api.bind(this)); app.get('/gen_cert', mw.rate_limit.gen, this.gen_cert_api.bind(this)); app.use(this.api_error_handler.bind(this)); return app; }; E.prototype.create_api_server = function(app){ let http_server; let https_server; if (process.env.SSL_CERT && process.env.SSL_KEY) { try { const https_opts = { cert: file.read_e(process.env.SSL_CERT), key: file.read_e(process.env.SSL_KEY), secureOptions: consts.SSL_OP_NO_TLSv1_1, }; if (process.env.SNI_CERTS) { const certs = cookie.parse(process.env.SNI_CERTS); https_opts.SNICallback = util_lib.sni_callback_fn(certs); } logger.notice('Using SSL to the web interface'); https_server = https.createServer(https_opts, app); this.start_web_socket(https_server); } catch(e){ logger.warn('Could not find SSL certificates: %s', e.message); } } if (!https_server) { http_server = http.createServer(app); this.start_web_socket(http_server); return http_server; } http_server = http.createServer((req, res)=>{ let location = 'https://'+req.headers.host+req.url; res.writeHead(301, {location}); res.end(); }); const tcp_server = net.createServer(socket=>{ tcp_server.running = true; socket.setTimeout(this.argv.socket_inactivity_timeout); socket.once('error', err=>null); socket.once('timeout', ()=>this.ensure_socket_close(socket)); let lb_transform_stream; if (this.lb_ips?.includes(socket.remoteAddress)) { lb_transform_stream = new util_lib.Lb_transform(); lb_transform_stream.on('parsed', ({remote_ip})=>{ socket.lpm_forwarded_for = remote_ip; }); socket.pipe(lb_transform_stream); } (lb_transform_stream||socket).once('data', data=>{ if (lb_transform_stream) socket.unpipe(lb_transform_stream); if (!tcp_server.running) return socket.end(); socket.pause(); let protocol_byte = data[0]; if (protocol_byte==22) https_server.emit('connection', socket); else http_server.emit('connection', socket); socket.unshift(data); socket.resume(); }); }); return tcp_server; }; E.prototype.get_zones_api = function(req, res){ res.json(this.zones_mgr.get_formatted()); }; E.prototype.get_consts_api = function(req, res){ const proxy = entries(lpm_config.proxy_fields).reduce( (acc, [k, v])=>assign(acc, {[k]: {desc: v}}), {}); Object.getOwnPropertyNames(E.default) .filter(E.default.propertyIsEnumerable.bind(E.default)) .forEach(k=>proxy[k] && assign(proxy[k], {def: E.default[k]})); if (proxy.zone) proxy.zone.def = this._defaults.zone; proxy.dns.values = ['', 'local', 'remote']; const ifaces = keys(os.networkInterfaces()) .map(iface=>({key: iface, value: iface})); ifaces.unshift({key: 'All', value: '0.0.0.0'}); ifaces.unshift({key: 'dynamic (default)', value: ''}); proxy.iface.values = ifaces; res.json({proxy, consts}); }; E.prototype.enable_ssl_api = etask._fn( function*mgr_enable_ssl(_this, req, res){ const port = req.body.port; let proxies = _this.proxies.slice(); if (port) proxies = proxies.filter(p=>p.port==port); for (let i in proxies) { const p = proxies[i]; if (p.port!=_this._defaults.dropin_port && !p.ssl) { yield _this.proxy_update(p, {ssl: true}, {source: get_source(req), username: get_username(req)}); } } res.send('ok'); }); E.prototype.update_ips_api = etask._fn( function*mgr_update_ips(_this, req, res){ const ips = req.body.ips||[]; const vips = req.body.vips||[]; const proxy = _this.proxies.find(p=>p.port==req.body.port); yield _this.proxy_update(proxy, {ips, vips}, {source: get_source(req), username: get_username(req)}); res.send('ok'); }); E.prototype.report_bug_api = etask._fn( function*mgr_report_bug(_this, req, res){ let log_file = ''; const config_file = Buffer.from(_this.config.get_string()) .toString('base64'); if (file.exists(logger.lpm_filename)) { let buffer = fs.readFileSync(logger.lpm_filename); buffer = buffer.slice(buffer.length-50000); log_file = buffer.toString('base64'); } const reqs = _this.filtered_get({query: {limit: 100}}).items.map(x=>({ url: x.url, status_code: x.status_code, })); const har = JSON.stringify(reqs); const browser = user_agent.guess_browser(req.get('user-agent')).browser; const response = yield _this.api_request({ method: 'POST', endpoint: '/lpm/report_bug', form: {report: {config: config_file, log: log_file, har, desc: req.body.desc, lpm_v: pkg.version, email: req.body.email, browser, os: util_lib.UOS}}, }); res.status(response.statusCode).json(response.body); }); E.prototype.proxy_dup_api = etask._fn( function*mgr_proxy_dup_api(_this, req, res, next){ this.on('uncaught', next); const port = req.body.port; const proxy = zutil.clone_deep(_this.proxies.filter(p=>p.port==port)[0]); try { proxy.port = get_free_port(_this.proxy_ports, _this.argv.zagent); } catch(e){ return res.status(400).json({errors: [{msg: e.message, field: 'port'}]}); } yield _this.create_new_proxy(proxy); res.json({proxy}); }); const get_free_port = (proxies, zagent)=>{ const proxy_ports = util_lib.get_ports(proxies); let port = Math.max(...proxy_ports, 23999)+1; if (zagent) { if (port<=32000) return port; return find_free_port(proxy_ports); } return port; }; const find_free_port = ports=>{ let port = 24000; while (ports.includes(String(port)) && port<=32000) port++; if (port<=32000) return port; throw new Error('No free ports'); }; E.prototype.proxy_create_api = etask._fn( function*mgr_proxy_create_api(_this, req, res, next){ this.on('uncaught', next); if (!req.body.proxy.port) { try { req.body.proxy.port = get_free_port(_this.proxy_ports, _this.argv.zagent); } catch(e){ return res.status(400).json({errors: [{msg: e.message, field: 'port'}]}); } } const port = +req.body.proxy.port; if (req.body.proxy?.users?.length) req.body.proxy.users = req.body.proxy.users.map(x=>x.toLowerCase()); if (req.body.proxy.multiply_users && req.body.create_users) { try { yield _this.add_lpm_users(req.body.proxy.users); } catch(e){ return res.status(400).json({errors: [{msg: e.message, field: 'users'}]}); } } const {ext_proxies, multiply} = req.body.proxy; const errors = yield _this.proxy_check({port, ext_proxies, multiply}); if (errors.length) return res.status(400).json({errors}); const proxy = assign({}, req.body.proxy, {port}); if (proxy.bw_limit) _this.update_bw_limits(_this, proxy); _this.add_config_change('create_proxy_port', port, req.body.proxy, get_source(req), get_username(req)); const {proxy_port, proxy_err} = yield _this.create_new_proxy(proxy); if (proxy_err) return res.status(400).json({errors: [{msg: proxy_err}]}); res.json({data: proxy_port.opt}); }); E.prototype.proxy_update_api = etask._fn( function*mgr_proxy_update_api(_this, req, res, next){ this.on('uncaught', next); logger.info('proxy_update_api'); const old_port = req.params.port; const old_proxy = _this.proxies.find(p=>p.port==old_port); if (!old_proxy) { return res.status(400).json( {errors: [{msg: `No proxy at port ${old_port}`}]}); } if (old_proxy.proxy_type!='persist') return res.status(400).json({errors: [{msg: 'Proxy is read-only'}]}); // XXX krzysztof: get rid of proxy check, move this logic inside // validate_proxy const errors = yield _this.proxy_check(assign({}, old_proxy, req.body.proxy), old_port); if (errors.length) return res.status(400).json({errors}); if (!req.body.proxy) { return res.status(400).json({errors: [{ msg: `Request body should contain 'proxy' field`, body: JSON.stringify(req.body), }]}); } const {proxy_port, proxy_err} = yield _this.proxy_update(old_proxy, req.body.proxy, {source: get_source(req), username: get_username(req)}); if (proxy_err) return res.status(400).json({errors: [{msg: proxy_err}]}); res.json({data: proxy_port}); }); E.prototype.api_url_update_api = etask._fn( function*mgr_api_url_update_api(_this, req, res){ const old_domain = _this._defaults.api_domain; const api_domain = _this._defaults.api_domain = req.body.url.replace(/https?:\/\/(www\.)?/, ''); _this.conn.domain = yield _this.check_domain(); if (!_this.conn.domain) return void res.json({res: false}); yield _this.logged_update(); _this.add_config_change('update_api_domain', 'defaults', api_domain, get_source(req), get_username(req), old_domain); yield _this.config.save(); res.json({res: true}); }); E.prototype.proxy_banips_api = function(req, res){ const port = req.params.port; const proxy = this.proxy_ports[port]; if (!proxy) return res.status(400).send(`No proxy at port ${port}`); let {ips, domain, ms=0} = req.body||{}; ips = (ips||[]).filter(ip=>util_lib.is_ip(ip) || util_lib.is_eip(ip)); if (!ips.length) return res.status(400).send('No ips provided'); ips.forEach(ip=>proxy.banip(ip, +ms, domain)); return res.status(204).end(); }; E.prototype.global_banip_api = function(req, res){ const {ips, ip, domain, ms=0, ports} = req.body||{}; if (ips) { ips.forEach(_ip=>this.banip(_ip, domain, ms, ports)); return res.status(204).end(); } if (!ip || !(util_lib.is_ip(ip) || util_lib.is_eip(ip))) return res.status(400).send('No IP provided'); this.banip(ip, domain, +ms, ports); return res.status(204).end(); }; E.prototype.proxy_banip_api = function(req, res){ const port = req.params.port; const proxy = this.proxy_ports[port]; if (!proxy) return res.status(400).send(`No proxy at port ${port}`); const {ip, domain, ms=0} = req.body||{}; if (!ip || !(util_lib.is_ip(ip) || util_lib.is_eip(ip))) return res.status(400).send('No IP provided'); proxy.banip(ip, +ms, domain); return res.status(204).end(); }; E.prototype.proxy_unbanip_api = function(req, res){ const port = req.params.port; const server = this.proxy_ports[port]; if (!server) throw new Error(`No proxy at port ${port}`); const {ip, domain} = req.body; if (!ip || !(util_lib.is_ip(ip) || util_lib.is_eip(ip))) return res.status(400).send('No IP provided'); const {ips: banned_ips} = this.get_banlist(server, true); if (!banned_ips.some(({ip: banned_ip})=>banned_ip==ip)) return res.status(400).send('IP is not banned'); server.unbanip(ip, domain); return res.json(this.get_banlist(server, true)); }; E.prototype.proxy_unbanips_api = function(req, res){ const port = req.params.port; const server = this.proxy_ports[port]; if (!server) throw new Error(`No proxy at port ${port}`); server.unbanips(); return res.status(200).send('OK'); }; E.prototype.get_banlist_api = function(req, res){ const port = req.params.port; if (!port) return res.status(400).send('port number is missing'); const server = this.proxy_ports[port]; if (!server) return res.status(400).send('server does not exist'); res.json(this.get_banlist(server, req.query.full)); }; E.prototype.get_sessions_api = function(req, res){ const {port} = req.params; const server = this.proxy_ports[port]; if (!server) return res.status(400).send('server does not exist'); res.json({}); }; E.prototype.proxy_delete_api = etask._fn( function*mgr_proxy_delete_api(_this, req, res, next){ this.on('uncaught', next); logger.info('proxy_delete_api'); const port = +req.params.port; _this.add_config_change('remove_proxy_port', port, undefined, get_source(req), get_username(req)); yield _this.proxy_delete_wrapper([port]); res.sendStatus(204); }); E.prototype.proxies_delete_api = etask._fn( function*mgr_proxies_delete_api(_this, req, res, next){ this.on('uncaught', next); logger.info('proxies_delete_api'); const ports = req.body.ports||[]; ports.forEach(port=>_this.add_config_change('remove_proxy_port', port, undefined, get_source(req), get_username(req))); yield _this.proxy_delete_wrapper(ports, {skip_cloud_update: 1}); yield _this.config.save(); res.sendStatus(204); }); E.prototype.refresh_sessions_api = function(req, res){ const port = req.params.port; const proxy_port = this.proxy_ports[port]; if (!proxy_port || req.query.user && proxy_port.opt.user!=req.query.user) return res.status(400, 'Invalid proxy port').end(); const session_id = this.refresh_server_sessions(port); if (proxy_port.opt.rotate_session) return res.status(204).end(); res.json({session_id: `${port}_${session_id}`}); }; E.prototype.proxy_status_get_api = etask._fn( function*mgr_proxy_status_get_api(_this, req, res, next){ this.on('uncaught', next); const port = req.params.port; const proxy = _this.proxy_ports[port]; if (!proxy) return res.json({status: 'Unknown proxy'}); if (proxy?.opt?.zone) { const db_zone = _this.zones_mgr.get_obj(proxy.opt.zone)||{}; if ((db_zone.plan||{}).disable) return res.json({status: 'Disabled zone'}); } if (proxy?.opt?.smtp?.length) return res.json({status: 'ok', status_details: [{msg: 'SMTP proxy'}]}); const force = req.query.force!==undefined && req.query.force!=='false' && req.query.force!=='0'; const fields = ['status']; if (proxy?.opt?.proxy_type=='persist') { fields.push('status_details'); if (!proxy.status_details) { proxy.status_details = yield _this.proxy_check(proxy.opt, proxy.opt.port); } } if (force && proxy.status) proxy.status = undefined; for (let cnt=0; proxy.status===null && cnt<=22; cnt++) yield etask.sleep(date.ms.SEC); if (proxy.status===null) return res.json({status: 'Unexpected lock on status check.'}); if (proxy.status) return res.json(zutil.pick(proxy, ...fields)); yield _this.test_port(proxy, req.headers); res.json(zutil.pick(proxy, ...fields)); }); E.prototype.open_browser_api = etask._fn( function*mgr_open_browser_api(_this, req, res, next){ this.on('uncaught', next); if (!puppeteer) return res.status(400).send('Puppeteer not installed'); let responded = false; if (!puppeteer.ready) { res.status(206).send('Fetching chromium'); responded = true; } const {port} = req.params; try { const browser_opt = _this.get_browser_opt(port); yield puppeteer.open_page(_this._defaults.test_url, port, browser_opt); } catch(e){ logger.error('open_browser_api: %s', e.message); } if (!responded) res.status(200).send('OK'); }); E.prototype.proxy_tester_api = function(req, res){ const port = req.params.port; const proxy = this.proxy_ports[port]; if (!proxy) return res.status(500).send(`proxy port ${port} not found`); let response_sent = false; const handle_log = req_log=>{ if (req_log.details.context!='PROXY TESTER TOOL') return; this.removeListener('request_log', handle_log); response_sent = true; res.json(req_log); }; this.on('request_log', handle_log); const opt = assign(zutil.pick(req.body, ...['url', 'headers', 'body']), {followRedirect: false}); if (opt.body && typeof opt.body!='string') opt.body = JSON.stringify(opt.body); const password = proxy.opt.password; const user = 'tool-proxy_tester'; const basic = Buffer.from(user+':'+password).toString('base64'); opt.headers = opt.headers||{}; opt.headers['proxy-authorization'] = 'Basic '+basic; opt.headers['user-agent'] = req.get('user-agent'); if (+port) { opt.proxy = 'http://127.0.0.1:'+port; if (proxy.opt && proxy.opt.ssl) opt.ca = ssl.ca.cert; if (proxy.opt && proxy.opt.unblock) opt.rejectUnauthorized = false; } request(opt, err=>{ if (!err) return; this.removeListener('request_log', handle_log); logger.error('proxy_tester_api: %s', err.message); if (!response_sent) res.status(500).send(err.message); }); }; E.prototype.get_all_locations_api = function(req, res){ const data = cities.all_locations(); res.json(data); }; E.prototype.get_all_carriers_api = etask._fn( function*mgr_get_all_carriers(_this, req, res, next){ this.on('uncaught', next); const c_res = yield _this.api_request({ endpoint: '/lpm/carriers', no_throw: 1, force: 1, }); if (c_res.statusCode==200) return res.json(c_res.body); logger.warn('Unable to get carriers: %s %s %s', c_res.statusCode, c_res.statusMessage, c_res.body); res.json([]); }); E.prototype.logs_suggestions_api = function(req, res){ if (this.argv.high_perf) return res.json({ports: [], status_codes: [], protocols: []}); const ports = this.loki.colls.port.chain().data().map(r=>r.key); const protocols = this.loki.colls.protocol.chain().data().map(r=>r.key); const status_codes = this.loki.colls.status_code.chain().data() .map(r=>r.key); const suggestions = {ports, status_codes, protocols}; res.json(suggestions); }; E.prototype.logs_reset_api = function(req, res){ const ports = req.query.port && [+req.query.port] || undefined; this.loki.stats_clear(); this.loki.requests_clear(ports); this.lpm_f.event('Clear HAR', get_source(req), get_username(req)); res.send('ok'); }; E.prototype.logs_cloud_get_api = etask._fn( function*_logs_cloud_get_api(_this, req, res, next){ this.on('uncaught', next); if (_this.argv.high_perf) return {}; let result = _this.filtered_get(req); let orig = assign({}, _this.har(result.items), {total: result.total, skip: result.skip, sum_out: result.sum_out, sum_in: result.sum_in}); if (!_this.argv.zagent) return res.json(orig); let clogs = yield _this.cloud_mgr.get_logs(req.query); return res.json(_this.concat_logs(orig, ...clogs)||orig); }); E.prototype.logs_get_api = function(req, res){ if (this.argv.high_perf) return {}; const result = this.filtered_get(req); res.json(assign({}, this.har(result.items), {total: result.total, skip: result.skip, sum_out: result.sum_out, sum_in: result.sum_in})); }; E.prototype.logs_har_get_api = function(req, res){ this.lpm_f.event('Download HAR', get_source(req), get_username(req)); res.setHeader('content-disposition', 'attachment; filename=data.har'); const result = this.filtered_get(req); res.send(JSON.stringify(this.har(result.items), null, 4)); }; E.prototype.logs_resend_api = function(req, res){ const ids = req.body.uuids; for (let i in ids) { const r = this.loki.request_get_by_id(ids[i]); let proxy; if (!(proxy = this.proxy_ports[r.port])) continue; const opt = { proxy: 'http://127.0.0.1:'+r.port, url: r.url, method: 'GET', headers: JSON.parse(r.request_headers), followRedirect: false, }; if (proxy.opt.ssl) opt.ca = ssl.ca.cert; request(opt); } res.send('ok'); }; E.prototype.node_version_api = function(req, res){ if (process.versions && !!process.versions.electron) return res.json({is_electron: true}); const current_node = process?.versions?.node||'undefined'; res.json({ current: current_node, satisfied: semver.satisfies(current_node, pkg.recommendedNode), recommended: pkg.recommendedNode, }); }; E.prototype.last_version_api = etask._fn( function*mgr_last_version(_this, req, res, next){ this.on('uncaught', next); try { const r = yield util_lib.get_last_version(_this._defaults.api_domain); res.json({version: r.ver, newer: r.newer, versions: r.versions}); } catch(e){ logger.warn('could not fetch the latest version number %s', e.message); res.status(500).send(e.message); } }); // XXX krzysztof: improve mechanism for defaults values E.prototype.update_settings_api = etask._fn(function*mgr_update_settings_api(_this, req, res, next){ this.on('uncaught', next); if (_this.argv.zagent && ( (req.body.www_whitelist_ips||[]).some(util_lib.is_any_ip) || (req.body.whitelist_ips||[]).some(util_lib.is_any_ip))) { return res.status(400).send('Not allowed to set \'any\' or 0.0.0.0/0 ' +'as a whitelisted IP in Cloud Proxy Manager'); } if (_this.argv.zagent && req.body.logs) { if (_this.is_reseller()) { return res.status(400).send(`Request logs limit can't be set ` +'for resellers'); } if (req.body.logs>1000) { return res.status(400).send('Request logs limit can only have a ' +'maximum value of 1000 in Cloud Proxy Manager'); } if (_this._defaults.logs_settings && req.body.logs>0 && _this._defaults.logs_settings.type && !req.body.logs_settings) { return res.status(400).send('Request logs limit can not be' +' changed while remote logs delivery enabled'); } } if (_this.argv.zagent && req.body.har_limit!==undefined && ![-1, 1024].includes(req.body.har_limit)) { return res.status(400).send('Response size limit can only be 1KB or ' +'Disabled in Cloud Proxy Manager'); } if (req.body.logs_settings && !_this.argv.zagent) return res.status(400).send('Available only in Cloud Proxy Manager'); yield _this.update_settings(req.body, {origin: 1, source: get_source(req), username: get_username(req)}); if (req.query.pretty!==undefined) return res.send(format_json(_this.get_settings())); res.json(_this.get_settings()); }); E.prototype.get_settings_api = function(req, res){ if (req.query.pretty!==undefined) return res.send(format_json(this.get_settings())); res.json(this.get_settings()); }; E.prototype.config_get_api = function(req, res){ res.json({config: this.config.get_string()}); }; E.prototype.config_set_api = etask._fn( function*mgr_set_config(_this, req, res, next){ this.on('uncaught', next); _this.add_config_change(undefined, undefined, undefined, get_source(req), get_username(req)); yield _this.config.set_string(req.body.config); res.json({result: 'ok'}); _this.emit('config_changed'); }); E.prototype.creds_user_api = etask._fn( function*mgr_creds(_this, req, res, next){ this.on('uncaught', next); _this._defaults.customer = req.body.customer || _this._defaults.customer; _this._defaults.google_token = req.body.token; const login_result = yield _this.login_user(assign({}, req.body)); if (login_result.error || login_result.body) return res.json(login_result.body || login_result); if (login_result.customers) return res.json({customer_ids: login_result.account_ids}); _this._defaults.lpm_token = login_result; const cloud_conf = yield _this.lpm_f.login(); yield _this.logged_update(); if (cloud_conf) yield _this.apply_cloud_config(cloud_conf); _this.update_lpm_users(yield _this.lpm_users_get()); _this.add_first_whitelist(req.remote_ip); if (_this._defaults.password) res.cookie('local-login', _this._defaults.password); _this.lpm_f.get_server_conf(); res.json({result: 'ok'}); }); E.prototype.gen_token_api = etask._fn( function*gen_token_api(_this, req, res, next){ this.on('uncaught', next); const token = _this.gen_token(); _this._defaults.token_auth = token; _this.update_ports({token_auth: token}); _this.add_config_change('generate_token', 'defaults', token, get_source(req), get_username(req)); yield _this.config.save(); res.json({token}); }); E.prototype.proxies_running_get_api = function(req, res){ const proxies_running = []; const proxies_idx = new Map(this.proxies.map(p=>[p.port, p])); for (const p of values(this.proxy_ports)) { if (p.opt.port==this._defaults.dropin_port || req.query.user && p.opt.user!=req.query.user) { continue; } const config = assign({}, proxies_idx.get(p.opt.port) || proxies_idx.get(p.opt.master_port)); config.master_port = p.opt.master_port; if (config.master_port) { ['ips', 'vips', 'users', 'whitelist_ips'].forEach(k=> delete config[k]); } const p_opt_fields = ['proxy_type', 'port', 'ip', 'vip', 'user', 'dns', 'zone', 'tls_lib', 'route_err', 'proxy_connection_type']; p_opt_fields.forEach(prop=>config[prop] = p.opt[prop]); const p_fields = ['status', 'status_details']; p_fields.forEach(prop=>config[prop] = p[prop]); proxies_running.push(config); } const proxies_running_sorted = proxies_running.sort((a, b)=>a.port-b.port); if (req.query.pretty!==undefined) return res.send(format_json(proxies_running_sorted)); res.json(proxies_running_sorted); }; E.prototype.proxies_get_api = function(req, res){ const port = req.params.port; if (!port) return res.json(this.proxies); const proxies = this.proxies.reduce((acc, p)=> assign({}, acc, {[p.port]: p}), {}); const port_conf = proxies[port]; if (!port_conf) return res.status(400).send('invalid port number'); return res.json(port_conf); }; E.prototype.allocated_ips_get_api = etask._fn( function*mgr_allocated_ips_get(_this, req, res, next){ this.on('uncaught', next); try { res.send(yield _this.request_allocated_ips(req.query.zone)); } catch(e){ logger.warn('Could not get allocated IPs: %s', e.message); res.status(500).send(e.message); } }); E.prototype.allocated_vips_get_api = etask._fn( function*mgr_allocated_vips_get(_this, req, res, next){ this.on('uncaught', next); try { res.send(yield _this.request_allocated_vips(req.query.zone)); } catch(e){ logger.warn('Could not get allocated gIPs: %s', e.message); res.status(500).send(e.message); } }); E.prototype.lpm_user_add_api = etask._fn( function*mgr_user(_this, req, res, next){ this.on('uncaught', next); const _res = yield _this.api_request({ endpoint: '/lpm/lpm_users_add', method: 'POST', form: {worker: {email: req.body.email}}, }); if (_res.statusCode!=200) return res.status(_res.statusCode).send(_res.body); res.send('ok'); }); E.prototype.lpm_users_get_api = etask._fn( function*mgr_user(_this, req, res, next){ this.on('uncaught', next); const users = yield _this.lpm_users_get(); _this.update_lpm_users(users); if (req.query.pretty!==undefined) return res.send(format_json(users)); res.json(users); }); E.prototype.refresh_ip_api = etask._fn( function*mgr_refresh_ip_api(_this, req, res, next){ this.on('uncaught', next); const {port} = req.params; if (!port) return res.status(400).send('Port number is missing'); const proxy = _this.proxies.find(p=>p.port==port); if (!proxy) return res.status(400).send('Invalid port number'); let {ip, vip} = req.body; if (ip && vip || !ip && !vip) return res.status(400).send('Provide either IP or gIP'); if (ip && !util_lib.is_ip(ip)) return res.status(400).send('Invalid IP provided'); const serv_res = yield _this.refresh_ip(ip, vip, port); return res.json(serv_res); }); E.prototype.refresh_ips_api = etask._fn( function*mgr_refresh_ips(_this, req, res, next){ this.on('uncaught', next); const zone = req.body.zone; const vips = req.body.vips; let ips; if (req.body.ips && !Array.isArray(req.body.ips)) return res.status(400).send('ips should be an array of IPs'); else if (req.body.ips) ips = req.body.ips.map(ip=>zurl.ip2num(ip)).join(' '); const serv_res = yield _this.refresh_ips(zone, {vips, ips}); if (Array.isArray(serv_res.vips)) { serv_res.vips = serv_res.vips .map(v=>({...v, vip: _.isString(v.vip) ? v.vip : v.id})); } return res.json(serv_res); }); E.prototype.shutdown_api = function(req, res){ res.json({result: 'ok'}); this.stop(); }; E.prototype.logout_api = etask._fn( function*mgr_logout_api(_this, req, res, next){ this.on('uncaught', next); yield _this.logout(); res.cookie('local-login', ''); res.json({result: 'ok'}); }); E.prototype.restart_api = etask._fn(function*(_this, req, res, next){ this.on('uncaught', next); yield _this.restart(); res.json({result: 'ok'}); }); E.prototype.upgrade_api = etask._fn( function*mgr_upgrade(_this, req, res, next){ this.on('uncaught', next); yield _this.upgrade(e=>{ if (e) res.status(403).send(e); else res.json({result: 'ok'}); }); }); E.prototype.downgrade_api = etask._fn( function*mgr_downgrade(_this, req, res, next){ this.on('uncaught', next); yield _this._downgrade(e=>e ? res.status(403).send(e) : res.json({result: 'ok'})); }); E.prototype.stats_get_api = function(req, res){ this.loki.requests_trunc(); const stats = this.loki.stats_get(); const enable = !!values(this.proxy_ports) .filter(p=>!p.opt.ssl && p.opt.port!=this._defaults.dropin_port) .length; let _https; if ((_https = stats.protocol.find(p=>p.key=='https')) && _https.reqs>0) stats.ssl_warning = enable; stats.ssl_enable = enable; const stats_ports = this.loki.stats_group_by('port', 0); const ports = stats_ports.reduce((acc, el)=> assign({}, acc, {[el.key]: el}), {}); res.json(assign({ports}, stats)); }; E.prototype.lpm_stats_api = function(req, res){ const days = req.query.days||5; const query = {hostname: {$nin: ['lumtest.com', 'brdtest.com', 'geo.brdtest.com']}}; this.loki.requests_trunc(); const reqs = this.loki.requests_count(query); const recent_reqs = this.loki.requests_count(assign({}, query, {timestamp: {$gt: +date.add(date(), {day: -days})}})); res.json({total_requests: reqs, recent_requests: recent_reqs}); }; E.prototype.add_www_whitelist_ip_api = etask._fn( function*add_www_whitelist_ip_api(_this, req, res, next){ this.on('uncaught', next); let ip; if (!(ip = req.body.ip)) return res.status(400).send('You need to pass an IP to add\n'); try { ip = new Netmask(ip).base; } catch(e){ return res.status(422).send('Wrong format\n'); } const new_ips = [...new Set(_this._defaults.www_whitelist_ips).add(ip)]; _this.set_www_whitelist_ips(new_ips); _this.add_config_change('add_www_whitelist_ip', 'defaults', ip, get_source(req), get_username(req)); yield _this.config.save(); _this.wss.broadcast_json({msg: 'whitelisted', ip}); res.send('OK'); }); E.prototype.remove_www_whitelist_ip_api = etask._fn( function*remove_www_whitelist_ip_api(_this, req, res, next){ this.on('uncaught', next); let ip; if (!(ip = req.body.ip)) return res.status(400).send('You need to pass an IP to add\n'); try { ip = new Netmask(ip).base; } catch(e){ return res.status(422).send('Wrong format\n'); } let old_ips = new Set(_this._defaults.www_whitelist_ips); if (!old_ips.has(ip)) return res.status(404).send('IP is not whitelisted\n'); old_ips.delete(ip); const new_ips = Array.from(old_ips); _this.set_www_whitelist_ips(new_ips); _this.add_config_change('remove_www_whitelist_ip', 'defaults', ip, get_source(req), get_username(req)); yield _this.config.save(); res.send('OK'); }); E.prototype.cloud_unauth_api = function(req, res){ const expires = date(date.ms.DAY+Date.now()).toUTCString(); const set_cookie = `lpm_token=deleted; Max-Age=43200; Path=/; ` +`Expires=${expires}; Secure; SameSite=None`; res.header('Set-Cookie', set_cookie); res.send('OK'); }; E.prototype.cloud_auth_api = function(req, res){ const lpm_token = (this._defaults.lpm_token||'').split('|')[0]; if (!lpm_token || lpm_token!=req.body.lpm_token) return res.status(403).send('Forbidden'); // generating the cookie manually as express 4.16 does not support // sameSite in cookies generation and util/node_modules uses 4.16 const expires = date(date.ms.DAY+Date.now()).toUTCString(); const set_cookie = `lpm_token=${lpm_token}; Max-Age=43200; Path=/; ` +`Expires=${expires}; Secure; SameSite=None`; const username_cookie = `username=${req.body.username}; Max-Age=43200; ` +`Path=/; Expires=${expires}; Secure; SameSite=None`; res.header('Set-Cookie', [set_cookie, username_cookie]); if (!(this._defaults.whitelist_ips||[]).length) { const new_ips = [...new Set(this._defaults.whitelist_ips) .add(req.remote_ip)]; this.set_whitelist_ips(new_ips); } res.send('OK'); }; E.prototype.add_wip_api = etask._fn( function*add_wip_api(_this, req, res, next){ this.on('uncaught', next); const token_auth = _this._defaults.token_auth; if (!token_auth || token_auth!=req.headers.authorization) return res.status(403).send('Forbidden'); let ip; if (!(ip = req.body.ip)) return res.status(400).send('You need to pass an IP to add\n'); try { const _ip = new Netmask(ip); const mask = _ip.bitmask==32 ? '' : '/'+_ip.bitmask; ip = _ip.base+mask; } catch(e){ return res.status(422).send('Wrong format\n'); } if (_this.argv.zagent && util_lib.is_any_ip(ip)) { return res.status(400).send('Not allowed to set any whitelisted IP in ' +'Cloud Proxy Manager'); } const new_ips = [...new Set(_this._defaults.whitelist_ips).add(ip)]; _this.set_whitelist_ips(new_ips); _this.add_config_change('add_whitelist_ip', 'defaults', ip, get_source(req), get_username(req)); yield _this.config.save(); res.send('OK'); }); E.prototype.remove_wip_api = etask._fn( function*remove_wip_api(_this, req, res, next){ this.on('uncaught', next); const token_auth = _this._defaults.token_auth; if (!token_auth || token_auth!=req.headers.authorization) return res.status(403).send('Forbidden'); let ip; if (!(ip = req.body.ip)) return res.status(400).send('You need to pass an IP to remove\n'); try { const _ip = new Netmask(ip); const mask = _ip.bitmask==32 ? '' : '/'+_ip.bitmask; ip = _ip.base+mask; } catch(e){ return res.status(422).send('Wrong format\n'); } let old_ips = new Set(_this._defaults.whitelist_ips); if (!old_ips.has(ip)) return res.status(404).send('IP is not whitelisted\n'); old_ips.delete(ip); const new_ips = Array.from(old_ips); _this.set_whitelist_ips(new_ips); _this.add_config_change('remove_whitelist_ip', 'defaults', ip, get_source(req), get_username(req)); yield _this.config.save(); res.send('OK'); }); E.prototype.version_api = function(req, res){ return res.json({ version: pkg.version, argv: this.get_params().join(' '), is_upgraded: this.is_upgraded, backup_exist: this.backup_exist, }); }; E.prototype.get_bw_limit_api = function(req, res){ logger.info('get_bw_limit_api'); if (!this.argv.zagent) { return res.status(403).send('Not allowed to use BW limit in ' +'Proxy Manager on premise'); } const proxy_port = this.proxy_ports[+req.params.port]; const port = zutil.get(proxy_port, 'opt.master_port', +req.params.port); const proxy = this.proxies.find(p=>p.port==port); if (!proxy) return res.status(400).send('Invalid port number'); if (req.query.pretty!==undefined && proxy.bw_limit) { const {bytes, days, start, renewable, use_limit_webhook, th_webhook_value} = proxy.bw_limit; const limit = convert_bytes(bytes); return res.send(format_json({limit, days, start, renewable, use_limit_webhook, th_webhook_value})); } res.json(zutil.pick(proxy.bw_limit, 'bytes', 'days', 'start', 'renewable', 'use_limit_webhook', 'th_webhook_value')); }; E.prototype.set_bw_limit_api = etask._fn( function*set_bw_limit_api(_this, req, res, next){ this.on('uncaught', next); logger.info('set_bw_limit_api'); if (!_this.argv.zagent) { return res.status(403).send('Not allowed to use BW limit in ' +'Proxy Manager on premise'); } const old_proxy_port = _this.proxy_ports[+req.params.port]; const port = zutil.get(old_proxy_port, 'opt.master_port', +req.params.port); const proxy = _this.proxies.find(p=>p.port==port); if (!proxy) return res.status(400).send('Invalid port number'); if (proxy.proxy_type!='persist') return res.status(400).send('Proxy is read-only'); const bw_limit = keys(req.body).length && req.body || false; if (bw_limit) { bw_limit.renewable = bw_limit.renewable===undefined ? true : !!bw_limit.renewable; } const err = yield _this.proxy_check(assign({}, proxy, {bw_limit}), port); if (err.length) return res.status(400).send(err[0].msg); let {proxy_port, proxy_err} = yield _this.proxy_update(proxy, {bw_limit}, {source: get_source(req), username: get_username(req)}); if (proxy_err) return res.status(400).send(proxy_err); if (req.query.pretty!==undefined && proxy_port.bw_limit) { const {days, bytes, renewable, start} = proxy_port.bw_limit; const limit = convert_bytes(bytes); return res.send(format_json({limit, days, renewable, start})); } res.json(proxy_port.bw_limit||{}); }); E.prototype.get_bw_limit_stats_api = etask._fn( function*get_bw_limit_stats_api(_this, req, res, next){ this.on('uncaught', next); logger.info('get_bw_limit_stats_api'); if (!_this.argv.zagent) { return res.status(403).send('Not allowed to use BW limit in ' +'Proxy Manager on premise'); } const port = req.params.port; const proxy_port = port && _this.proxy_ports[+port]; if (port && !proxy_port || req.query.user && (!proxy_port || proxy_port.opt.user!=req.query.user)) { return res.status(400).send('Invalid port number'); } const response = yield _this.api_request({endpoint: '/lpm/bw_limit_stats', qs: {port: +port||undefined}}); if (response.statusCode!=200) return res.status(response.statusCode).send(response.body); if (req.query.pretty!==undefined) { for (const key_port of keys(response.body)) { if (response.body[key_port].usage) { response.body[key_port].usage.limit = convert_bytes( response.body[key_port].usage.limit); response.body[key_port].usage.used = convert_bytes( response.body[key_port].usage.used); } } return res.send(format_json(response.body)); } res.json(response.body); }); let proxies_counter = Math.floor(Math.random()*1e6); E.prototype.generate_proxies_api = function(req, res){ const p