@luminati-io/luminati-proxy
Version:
A configurable local proxy for brightdata.com
239 lines (218 loc) • 7.16 kB
JavaScript
// LICENSE_CODE ZON ISC
; /*jslint node:true, esnext: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 {SSL_OP_NO_TLSv1_1} = 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 cert;
}
function ssl(keys, extra_ssl_ips){
const hosts = new LRUCache({
max: 5000,
ttl: 30*date.ms.MIN,
ttlAutopurge: true,
updateAgeOnGet: true,
ttlResolution: 10*date.ms.MIN,
noDisposeOnSet: true,
});
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})));
}
return {
key: keys.privateKeyPem,
cert: pki.certificateToPem(ip_cert),
ca: E.ca.cert,
SNICallback: (name, cb)=>{
if (hosts.has(name))
return cb(null, hosts.get(name));
const cert = gen_cert(keys, name, [{type: 2, value: name}]);
hosts.set(name, tls.createSecureContext({
key: keys.privateKeyPem,
cert: pki.certificateToPem(cert),
ca: E.ca.cert,
secureOptions: SSL_OP_NO_TLSv1_1,
}));
cb(null, hosts.get(name, true));
}
};
}
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)
};
};