You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
8.0 KiB
277 lines
8.0 KiB
#!/usr/bin/env node
|
|
/*
|
|
* ഓം ബ്രഹ്മാർപ്പണം
|
|
* proxy.js
|
|
* Created: Sat Mar 21 2020 02:04:37 GMT+0530 (GMT+05:30)
|
|
* Copyright 2020 Harish Karumuthil<harish2704@gmail.com>
|
|
*/
|
|
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const url = require('url');
|
|
const fs = require('fs');
|
|
const util = require('util');
|
|
const fsPromise = fs.promises;
|
|
const { dirname } = require('path');
|
|
const forge = require('node-forge');
|
|
const pki = forge.pki;
|
|
const tls = require('tls');
|
|
const generateKeyPair = util.promisify(require('crypto').generateKeyPair);
|
|
const net = require('net');
|
|
const os = require('os');
|
|
|
|
const httpsPort = os.tmpdir() + `/proxy-kutti-${Date.now()}.sock`
|
|
const configHome = os.homedir() + '/.config/proxy-kutti';
|
|
const configFile = process.env.PROXY_KUTTI_CONFIG || configHome + '/config';
|
|
const config = {
|
|
port: 8080,
|
|
host: '127.0.0.1',
|
|
cache_dir: os.homedir() + '/.cache/proxy-kutti',
|
|
root_ca_key: configHome + '/rootCA.key',
|
|
root_ca_cert: configHome + '/rootCA.pem',
|
|
url_rewrites: '#http[s]://(.*)/7.7.1908/#http://mirrors.centos/7.7.1908/# #http[s]://(.*)epel/7/x86_64/#http://mirror.epel/7/x86_64/#',
|
|
};
|
|
|
|
try {
|
|
Object.assign(config, require(configFile));
|
|
} catch (e) {}
|
|
Object.keys(config).forEach(function(k) {
|
|
config[k] = process.env['PROXY_KUTTI_' + k] || config[k];
|
|
});
|
|
|
|
const urlMappings = config.url_rewrites.split(' ').map(function(pattern) {
|
|
debugger;
|
|
if (pattern[0] === pattern.slice(-1)) {
|
|
const [search, replace] = pattern.slice(1, -1).split(pattern[0]);
|
|
return { search: new RegExp(search), replace };
|
|
}
|
|
throw new Error(`Invalid url_rewrite "${pattern}"`);
|
|
});
|
|
|
|
|
|
function mapUrl(origUrl) {
|
|
let out = origUrl;
|
|
|
|
let i = 0,
|
|
l = urlMappings.length,
|
|
mapping;
|
|
while (i < l) {
|
|
urlMap = urlMappings[i];
|
|
out = out.replace(urlMap.search, urlMap.replace);
|
|
i++;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const runnninRequests = {};
|
|
function untillRequestFinished( cachedFile ){
|
|
return new Promise(res => runnninRequests[ cachedFile ].on('close', res ));
|
|
}
|
|
function startNewRequest( cachedFile ){
|
|
const stream = fs.createWriteStream( cachedFile );
|
|
runnninRequests[cachedFile] = stream;
|
|
stream.on('close', () => delete runnninRequests[cachedFile] );
|
|
return stream;
|
|
}
|
|
|
|
async function getContent(httpModule, origReq, origRes) {
|
|
const origUrl = url.parse(origReq.url);
|
|
const mappedUrlStr = mapUrl(origReq.url);
|
|
const mappedUrl = url.parse(mappedUrlStr);
|
|
const mappedPort = mappedUrl.port ? ':' + mappedUrl.port : '';
|
|
const method = origReq.method;
|
|
const proto = httpModule === http ? 'http':'https';
|
|
let cachedFile = `${config.cache_dir}/${proto}/${mappedUrl.host}${mappedPort}/${method}${mappedUrl.path}`;
|
|
if( mappedUrl.path.slice(-1) === '/' ){
|
|
cachedFile += '#index.data';
|
|
} else {
|
|
cachedFile += '.data';
|
|
}
|
|
const cachedFileMeta = `${cachedFile}.meta`;
|
|
let proxyRes;
|
|
let isHit = false;
|
|
|
|
|
|
if( cachedFile in runnninRequests ){
|
|
await untillRequestFinished( cachedFile );
|
|
}
|
|
if (false !== (await fsPromise.access(cachedFileMeta).catch(() => false))) {
|
|
proxyRes = fs.createReadStream(cachedFile);
|
|
Object.assign( proxyRes, JSON.parse(await fsPromise.readFile(cachedFileMeta)) );
|
|
isHit = true;
|
|
} else {
|
|
proxyRes = await new Promise(res => {
|
|
const proxyReq = httpModule.request(
|
|
{
|
|
host: origUrl.host,
|
|
port: origUrl.port,
|
|
path: origUrl.path,
|
|
method,
|
|
headers: origReq.headers,
|
|
},
|
|
res
|
|
);
|
|
origReq.pipe( proxyReq );
|
|
});
|
|
await fsPromise.mkdir(dirname(cachedFile), { recursive: true });
|
|
/**
|
|
* write metda data only if the request completed successfully
|
|
* Otherwise, partial & invalid cached content will be served next time
|
|
*/
|
|
origRes.on('finish', () => fsPromise.writeFile(cachedFileMeta, JSON.stringify({ headers: proxyRes.headers, statusCode: proxyRes.statusCode } )) )
|
|
proxyRes.pipe( startNewRequest(cachedFile));
|
|
}
|
|
|
|
console.log(`${new Date().toISOString()} ${isHit ? 'Hit!' : 'Miss'} ${method} ${origReq.url} => ${cachedFile}`);
|
|
|
|
origRes.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
proxyRes.pipe(origRes);
|
|
/**
|
|
* Don't let download to continue if client closes the connection before it is finished
|
|
*/
|
|
origRes.on('close', () => proxyRes.destroy() )
|
|
|
|
return proxyRes;
|
|
}
|
|
|
|
|
|
function createFakeCertificateByDomain(caKey, caCert, domain) {
|
|
const keys = pki.rsa.generateKeyPair(2048);
|
|
const cert = pki.createCertificate();
|
|
cert.publicKey = keys.publicKey;
|
|
|
|
cert.serialNumber = new Date().getTime() + '';
|
|
cert.validity.notBefore = new Date();
|
|
cert.validity.notBefore.setFullYear(
|
|
cert.validity.notBefore.getFullYear() - 1
|
|
);
|
|
cert.validity.notAfter = new Date();
|
|
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
|
|
var attrs = [
|
|
{
|
|
name: 'commonName',
|
|
value: domain,
|
|
},
|
|
{
|
|
name: 'organizationName',
|
|
value: 'Proxy-kutti',
|
|
},
|
|
];
|
|
|
|
cert.setSubject(attrs);
|
|
cert.setIssuer(caCert.subject.attributes);
|
|
|
|
cert.setExtensions([
|
|
{
|
|
name: 'subjectAltName',
|
|
altNames: [
|
|
{
|
|
type: 2,
|
|
value: domain,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'extKeyUsage',
|
|
serverAuth: true,
|
|
},
|
|
]);
|
|
cert.sign(caKey, forge.md.sha256.create());
|
|
|
|
return {
|
|
key: forge.pki.privateKeyToPem(keys.privateKey),
|
|
cert: forge.pki.certificateToPem(cert),
|
|
};
|
|
}
|
|
|
|
|
|
function initHttpsMitmProxy() {
|
|
const caCertPath = config.root_ca_cert;
|
|
const caKeyPath = config.root_ca_key;
|
|
const caCertPem = fs.readFileSync(caCertPath);
|
|
const caKeyPem = fs.readFileSync(caKeyPath);
|
|
const caCert = forge.pki.certificateFromPem(caCertPem);
|
|
const caKey = forge.pki.decryptRsaPrivateKey(caKeyPem, 'secret');
|
|
const fakeCertObj = createFakeCertificateByDomain(caKey, caCert, 'localhost');
|
|
|
|
debugger;
|
|
const https_opts = {
|
|
key: fakeCertObj.key,
|
|
cert: fakeCertObj.cert,
|
|
SNICallback: (hostname, done) => {
|
|
let certObj = createFakeCertificateByDomain(caKey, caCert, hostname);
|
|
done(
|
|
null,
|
|
tls.createSecureContext({
|
|
key: certObj.key,
|
|
cert: certObj.cert,
|
|
})
|
|
);
|
|
},
|
|
};
|
|
|
|
|
|
const httpsProxy = https.createServer(https_opts, (req, res) => {
|
|
req.url = `https://${req.headers.host}${req.url}`;
|
|
getContent(https, req, res);
|
|
});
|
|
|
|
httpsProxy.listen( httpsPort, '127.0.0.1');
|
|
}
|
|
|
|
|
|
function main() {
|
|
const httpProxy = http.createServer(getContent.bind(null, http));
|
|
|
|
const isHttpMitmEnabled =
|
|
fs.existsSync(config.root_ca_cert) && fs.existsSync(config.root_ca_key);
|
|
let httpsMsg = '';
|
|
if (isHttpMitmEnabled === false) {
|
|
httpsMsg = `https requests are not cached since it is not configured.
|
|
Make sure that the files
|
|
${config.root_ca_cert}
|
|
${config.root_ca_key}
|
|
exists and accessible to the process.
|
|
Refer documentation more details.\n`;
|
|
} else {
|
|
initHttpsMitmProxy();
|
|
}
|
|
|
|
const util = require('util');
|
|
httpProxy.on('connect', function(req, res) {
|
|
res.write(
|
|
'HTTP/1.0 200 Connection established\r\nProxy-agent: proxy-kutti\r\n\r\n'
|
|
);
|
|
const [host, port] = isHttpMitmEnabled
|
|
? ['127.0.0.1', httpsPort]
|
|
: req.url.split(':');
|
|
var httpsProxyConnection = net.createConnection(port, host);
|
|
res.on('close', () => httpsProxyConnection.destroy());
|
|
res.pipe(httpsProxyConnection);
|
|
httpsProxyConnection.pipe(res);
|
|
});
|
|
|
|
httpProxy.listen(config.port, config.host, function() {
|
|
console.log(`Proxy-kutti is running...
|
|
|
|
Using env variables
|
|
PROXY_KUTTI_CONFIG=${configFile}
|
|
|
|
Current Configuration ( edit ${configFile}.(json|js) or set env variable PROXY_KUTTI_<config-key>=<value> to change )
|
|
${JSON.stringify(config, null, 2).slice(2, -2)}
|
|
|
|
${httpsMsg}
|
|
Run the following command shell to start using this proxy
|
|
export http_proxy=http://${config.host}:${config.port}
|
|
${isHttpMitmEnabled? 'export https_proxy=http://'+config.host+':'+config.port: ''}
|
|
|
|
`);
|
|
});
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
process.on('uncaughtException', function (err) {
|
|
console.log(err);
|
|
})
|
|
}
|
|
|