@journeyapps/https-proxy-socket
Version:
Node library to enable opening Socket connections via an HTTPS proxy.
197 lines • 7.71 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpsProxySocket = void 0;
const tls = __importStar(require("tls"));
const createProxyAgent_1 = require("./createProxyAgent");
const setServername_1 = require("./utils/setServername");
const parseOptions_1 = require("./utils/parseOptions");
const util_1 = require("util");
const debug = (0, util_1.debug)('https-proxy');
/**
* The HttpsProxySocket class allows creating Socket connections via an HTTPS proxy.
* HTTP proxies are not supported.
* For http(s) requests, use HttpsProxyAgent as a wrapper around this.
*/
class HttpsProxySocket {
proxy;
proxyConfig;
/**
*
* @param options - The connection options to the proxy. At least host and port are required.
* Use {rejectUnauthorized: true} to ignore certificates for the proxy (not the endpoint).
* @param proxyConfig - { auth: 'username:password' } for basic auth.
* { headers: {key: 'value'} } for custom headers.
*/
constructor(options, proxyConfig) {
const sanitizedOptions = (0, parseOptions_1.parseOptions)(options);
if (!options) {
throw new Error('an HTTP(S) proxy server `host` and `port` must be specified!');
}
debug('creating new HttpsProxyAgent instance: %o', sanitizedOptions);
this.proxyConfig = proxyConfig || {};
this.proxy = sanitizedOptions;
}
/**
* Create a new Socket connection.
*
* @param options - host and port
*/
connect(options) {
return new Promise(async (resolve, reject) => {
this._connect(options, (error, socket) => {
if (error) {
reject(error);
}
else {
if (!socket) {
return reject(new Error('No socket returned from proxy'));
}
resolve(socket);
}
});
});
}
/**
* Construct an agent for http(s) requests.
*
* @param options - to set additional TLS options for https requests, e.g. rejectUnauthorized
*/
agent(options) {
return (0, createProxyAgent_1.createProxyAgent)(this, options);
}
_connect(opts, cb) {
const proxy = this.proxy;
// create a socket connection to the proxy server
const socket = tls.connect((0, setServername_1.setServername)(proxy));
// we need to buffer any HTTP traffic that happens with the proxy before we get
// the CONNECT response, so that if the response is anything other than an "200"
// response code, then we can re-play the "data" events on the socket once the
// HTTP parser is hooked up...
let buffers = [];
let buffersLength = 0;
function read() {
const b = socket.read();
if (b) {
ondata(b);
}
else {
socket.once('readable', read);
}
}
function cleanup() {
socket.removeListener('data', ondata);
socket.removeListener('end', onend);
socket.removeListener('error', onerror);
socket.removeListener('close', onclose);
socket.removeListener('readable', read);
}
function onclose(err) {
debug('onclose had error %o', err);
}
function onend() {
debug('onend');
}
function onerror(err) {
cleanup();
cb(err, null);
}
const END_OF_HEADERS = '\r\n\r\n';
function ondata(b) {
buffers.push(b);
buffersLength += b.length;
// Headers (including URLs) are generally ISO-8859-1 or ASCII.
// The subset used by an HTTPS proxy should always be safe as ASCII.
const buffered = Buffer.concat(buffers, buffersLength);
const str = buffered.toString('ascii');
if (str.indexOf(END_OF_HEADERS) < 0) {
// keep buffering
debug('have not received end of HTTP headers yet...');
if (socket.readable) {
read();
}
else {
socket.once('data', ondata);
}
return;
}
const firstLine = str.substring(0, str.indexOf('\r\n'));
const statusCode = parseInt(firstLine.split(' ')[1], 10);
debug('got proxy server response: %o', firstLine);
if (200 == statusCode) {
// 200 Connected status code!
const sock = socket;
// nullify the buffered data since we won't be needing it
buffers = [];
cleanup();
cb(null, sock);
}
else {
// some other status code that's not 200... need to re-play the HTTP header
// "data" events onto the socket once the HTTP machinery is attached so that
// the user can parse and handle the error status code
cleanup();
// nullify the buffered data since we won't be needing it
buffers = [];
cleanup();
socket.end();
cb(new Error('Proxy connection failed: ' + firstLine), null);
}
}
socket.on('error', onerror);
socket.on('close', onclose);
socket.on('end', onend);
if (socket.readable) {
read();
}
else {
socket.once('data', ondata);
}
const host = `${opts.host}:${opts.port}`;
let msg = 'CONNECT ' + host + ' HTTP/1.1\r\n';
const headers = Object.assign({}, this.proxyConfig.headers);
if (this.proxyConfig.auth) {
headers['Proxy-Authorization'] = 'Basic ' + Buffer.from(this.proxyConfig.auth).toString('base64');
}
headers['Host'] = host;
headers['Connection'] = 'close';
Object.keys(headers).forEach(function (name) {
msg += name + ': ' + headers[name] + '\r\n';
});
socket.write(msg + '\r\n');
}
}
exports.HttpsProxySocket = HttpsProxySocket;
//# sourceMappingURL=HttpsProxySocket.js.map