"use strict";
/**
* Creates new instance of `GlobalProxy`.
*
* @class
* @name GlobalProxy
* @classdesc - Contains methods to run and manage global transparent proxy.
* @arg {object} [opts] - global proxy options
* @arg {number} [opts.port=0] - global proxy port
* @arg {number} [opts.timeout=0] - global proxy timeout
* @arg {boolean} [opts.installCertificate=false] - flag to install global
* proxy certificate as trusted in order to manage `https` connection or no
* @arg {?string} [opts.rootPath] - Folder where proxy starts in order to
* generate self-signed certificate. By default is `current work directory`.
* @arg {boolean} [opts.useCache=false] - flag to cache and take from cache responses
*/
var fs = require("fs");
var path = require("path");
var util = require("util");
var expect = require("chai").expect;
const getPort = require("get-port");
var MitmProxy = require("http-mitm-proxy").Proxy;
var spawn = require("cross-spawn");
var U = require("glace-utils");
var BaseProxy = require("./baseProxy");
var cache = require("./middleware/cache");
var middleware = require("./middleware");
var LOG = U.logger;
var _onError = MitmProxy.prototype._onError;
/**
* Patch mitm proxy error processing, in order to avoid default response
* finalizing on reconnect.
*
* @ignore
*/
MitmProxy.prototype._onError = function (kind, ctx, err) {
if (!ctx) return _onError.apply(this, arguments);
var req = ctx.clientToProxyRequest;
if (req._reconnect > 0 && !req.socket.destroyed) {
this.onErrorHandlers.forEach(function(handler) {
return handler(ctx, err, kind);
});
} else {
_onError.apply(this, arguments);
};
};
var GlobalProxy = function (opts) {
opts = U.defVal(opts, {});
BaseProxy.call(this, opts);
this._installCertificate = U.defVal(opts.installCertificate, false);
this._sslCaDir = U.defVal(opts.sslCaDir, path.resolve(U.cwd, ".http-mitm-proxy"));
this._certificatePath = path.resolve(this._sslCaDir, "certs", "ca.pem");
this._proxy = new MitmProxy();
this._proxy.onError((ctx, err) => {
if (!ctx) {
LOG.error(err);
return;
}
var req = ctx.clientToProxyRequest;
if (req._reconnect > 0 && !req.socket.destroyed) {
LOG.warn(util.format("Request reconnected", U.getReqKey(req)));
req._reconnect--;
this._proxy._onHttpServerRequest(ctx.isSSL,
ctx.clientToProxyRequest,
ctx.proxyToClientResponse);
} else {
LOG.error(util.format(U.getReqKey(req), err));
};
});
this._proxy.onRequest(async (ctx, callback) => {
this.req = ctx.clientToProxyRequest;
if (this.req._reconnect === undefined) {
this.req._reconnect = this._reconnect;
};
this.res = ctx.proxyToClientResponse;
for (var mw of middleware) if (await mw.call(this)) return;
delete this.req;
delete this.res;
return callback();
});
// TODO not sure that it works reliable
this._proxy.onRequestData((ctx, chunk, callback) => {
if (ctx.clientToProxyRequest.body) {
chunk = new Buffer("");
};
return callback(null, chunk);
});
this._proxy.onResponse((ctx, callback) => {
if (ctx.clientToProxyRequest.body) {
ctx.proxyToServerRequest.end(ctx.clientToProxyRequest.body);
};
return callback(null);
});
};
util.inherits(GlobalProxy, BaseProxy);
module.exports = GlobalProxy;
/**
* Starts global proxy if it's not started yet.
*
* @async
* @method
*/
GlobalProxy.prototype.start = async function () {
if (this.isRunning) return;
if (!this._port) this._port = await getPort();
return new Promise((resolve, reject) => {
this._proxy.listen({ port: this._port,
silent: true,
sslCaDir: this._sslCaDir,
timeout: this._timeout },
err => {
if (err) reject(err);
resolve();
});
}).then(() => cache.init()).then(() => {
this.isRunning = true;
if (this._installCertificate) {
if (process.platform !== "win32") {
throw new Error("For your platform certificate" +
"installation isn't implemented");
};
expect(fs.existsSync(this._certificatePath),
`Proxy certificate ${this._certificatePath} is absent`)
.to.be.true;
var proc = spawn.sync("certutil", [ "-addstore",
"-enterprise",
"-f", "Root",
this._certificatePath ]);
if (proc.status !== 0) {
throw new Error(
"Can't install proxy certificate as trusted:\n" +
proc.stdout.toString());
};
};
});
};
/**
* Stops global proxy if it's not stopped yet.
*
* @method
*/
GlobalProxy.prototype.stop = function () {
if (!this.isRunning) return;
this._proxy.close();
this.isRunning = false;
};