UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for brightdata.com

269 lines (246 loc) 8.15 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint node:true, esnext:true, es9: true*/ const path = require('path'); const fs = require('fs'); const os = require('os'); const {spawn} = require('child_process'); const tls = require('tls'); const forge = require('node-forge'); const {LRUCache} = require('lru-cache'); const lpm_file = require('../util/lpm_file.js'); const date = require('../util/date.js'); const Cert_gen = require('../util/cert_util.js'); const etask = require('../util/etask.js'); const logger = require('./logger.js').child({category: 'SSL'}); const {MIN_TLS} = require('./consts.js'); const E = module.exports = ssl; const {compare} = Buffer, {assign} = Object, {pki} = forge; const cust_paths = { key: lpm_file.get_file_path('lpm.key'), cert: lpm_file.get_file_path('lpm.crt'), }; const sys_paths = { key: path.join(__dirname, '../bin/ca.key'), cert: path.join(__dirname, '../bin/ca.crt'), }; E.paths = {cust: cust_paths, sys: sys_paths}; E.ca = {}; E.use_custom_cert = false; let iface_ips = [].concat(...Object.values(os.networkInterfaces())); iface_ips = iface_ips.filter(i=>i.family=='IPv4').map(i=>i.address); let ip_cert; function fs_ca_exists(paths){ return fs.existsSync(paths.cert) && fs.existsSync(paths.key); } function fs_ca_remove(paths){ try { fs.unlinkSync(paths.cert); fs.unlinkSync(paths.key); return true; } catch(e){ logger.error('fs_ca_remove: ' + e.message); return false; } } function load_ca_files(paths){ if (!fs_ca_exists(paths)) return false; return E.ca = { cert: fs.readFileSync(paths.cert), key: fs.readFileSync(paths.key), }; } const load_cloud_ca = etask._fn(function*_load_ca(_this, mgr){ this.on('uncaught', e=>{ logger.error('load_cloud_ca: '+e.message); this.return(false); }); let {ca} = yield mgr.lpm_f.get_ca(); return E.set_ca(ca); }); function save_ca_files(ca, paths){ if (!verify_ca(ca)) return false; E.ca = ca; fs.writeFileSync(paths.key, E.ca.key); fs.writeFileSync(paths.cert, E.ca.cert); return true; } function verify_ca(ca){ ca = ca || E.ca; if (!ca) ca = E.ca; if (!ca || !ca.cert || !ca.key) return false; try { let crt = pki.certificateFromPem(ca.cert); if (crt.publicKey.n.toString(2).length < 2048) return false; pki.privateKeyFromPem(ca.key); } catch(e){ logger.error('verify_ca: ' + e.message); return false; } return true; } function equal_ca(ca, orig){ const to_buffer = val=>Buffer.isBuffer(val) ? val : Buffer.from(val); let _orig = assign({}, orig || E.ca), _ca = assign({}, ca); Object.keys(_orig).forEach(k=>_orig[k] = to_buffer(_orig[k])); Object.keys(_ca).forEach(k=>_ca[k] = to_buffer(_ca[k])); return compare(_orig.key, _ca.key)==0 && compare(_orig.cert, _ca.cert)==0; } function gen_cert(keys, name, alt_names){ const cert = pki.createCertificate(); cert.publicKey = pki.publicKeyFromPem(keys.publicKeyPem); cert.serialNumber = ''+Date.now(); cert.validity.notBefore = new Date(Date.now()-1*date.ms.DAY); cert.validity.notAfter = new Date(Date.now()+1*365*date.ms.DAY); cert.setSubject([{name: 'commonName', value: name}]); cert.setIssuer(pki.certificateFromPem(E.ca.cert).issuer.attributes); cert.setExtensions([{name: 'subjectAltName', altNames: alt_names}]); cert.sign(pki.privateKeyFromPem(E.ca.key), forge.md.sha256.create()); return { key: keys.privateKeyPem, cert: pki.certificateToPem(cert), ca: E.ca.cert.toString(), }; } function get_parent_hostname(hostname){ const i = hostname.indexOf('.'); return i > -1 && i != hostname.lastIndexOf('.') ? hostname.slice(i+1) : undefined; } const make_cache_gen_cert = ()=>{ if (!E.cluster_cache) return; return (keys, name, alt_names)=>E.cluster_cache.read({ name: 'generate_cert', cache_key: `cloud:lpm:certs:${name}`, load_fn: ()=>gen_cert(keys, name, alt_names), refresh_ttl: date.ms.DAY, expire_ttl: date.ms.WEEK, disable_cache: false, }); }; function ssl(keys, extra_ssl_ips){ const hosts = new LRUCache({max: 20000}); if (Array.isArray(extra_ssl_ips)) iface_ips = [...new Set(iface_ips.concat(extra_ssl_ips))]; if (!ip_cert) { ip_cert = gen_cert(keys, 'localhost', iface_ips.map(ip=>({type: 7, ip}))); } let gen_cert_fn = make_cache_gen_cert() || gen_cert; const sni_callback = (hostname, cb, opt={})=>{ const parent = get_parent_hostname(hostname); const _hostname = parent||hostname; let ctx; if (ctx = hosts.get(_hostname)) return void cb(null, ctx); etask(function*(){ this.on('uncaught', e=>{ logger.warn('sni_error: '+e.message); if (opt.force_local) return void this.throw(e); sni_callback(hostname, cb, {...opt, force_local: true}); }); const gen = opt.force_local ? gen_cert : gen_cert_fn; const cert = yield gen(keys, _hostname, [ {type: 2, value: _hostname}, {type: 2, value: '*.'+_hostname}, ]); ctx = tls.createSecureContext({ ...cert, minVersion: MIN_TLS, }); hosts.set(_hostname, ctx); cb(null, ctx); }); }; return { ...ip_cert, SNICallback: sni_callback, }; } E.gen_cert = function(){ const child = spawn('bash', [path.join(__dirname, '../bin/cert_gen.sh'), cust_paths.key, cust_paths.cert], { stdio: ['inherit', 'inherit', 'inherit'], }); child.on('exit', (code, signal)=>{ if (code==0) logger.notice('CA generated successfully'); else logger.warn('Could not generate CA'); }); }; E.load_ca = etask._fn(function*_load_ca(_this, mgr){ const err = new Error('Failed to load certificate'); this.on('uncaught', e=>{ logger.error('load_ca: '+e.message); this.return(); }); let info = mgr ? mgr.get_current_info() : {}; if (mgr && mgr.argv.zagent && (yield load_cloud_ca(mgr))) { E.use_custom_cert = true; return logger.notice('Loaded certificate from cloud'); } if (mgr && !mgr.argv.zagent && load_ca_files(cust_paths) && verify_ca()) { E.use_custom_cert = true; return logger.notice('Loaded local certificate'); } if (load_ca_files(sys_paths) && verify_ca()) { E.use_custom_cert = false; return logger.notice('Certificate loaded from sys folder'); } logger.warn('Unable to find local certificate'); if (!mgr) throw err; if (!mgr.argv.zagent && (yield load_cloud_ca(mgr))) return logger.notice('Loaded certificate from cloud'); logger.warn('Unable to find cloud certificate'); logger.notice('Generating certificate'); let {key, cert} = Cert_gen.create_root_ca(info); if (!mgr.argv.zagent && info.customer && save_ca_files({key, cert}, cust_paths) && verify_ca()) { return logger.notice('Saved new local certificate'); } if (E.set_ca({cert, key})) return logger.notice('Started with new certificate'); }); E.apply_cloud_ca = function(ca){ try { if (equal_ca(ca)) return false; return E.set_ca(ca); } catch(e){ logger.error('apply_cloud_ca: ' + e.message); return false; } }; E.remove_ca = function(paths){ if (!fs_ca_exists(paths)) return false; logger.notice('Removing certificate: ' + paths.cert); return fs_ca_remove(paths); }; E.set_ca = function(ca){ if (!verify_ca(ca)) return false; E.ca = ca; return true; }; E.buff_to_ca = function(buf){ if (!buf.cert || !buf.key) return false; return { cert: Buffer.from(buf.cert), key: Buffer.from(buf.key), }; };