143 lines
4.2 KiB
JavaScript
143 lines
4.2 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Minimal HTTP/S proxy client
|
|
*/
|
|
|
|
const net = require('net');
|
|
const tls = require('tls');
|
|
const urllib = require('url');
|
|
|
|
/**
|
|
* Establishes proxied connection to destinationPort
|
|
*
|
|
* httpProxyClient("http://localhost:3128/", 80, "google.com", function(err, socket){
|
|
* socket.write("GET / HTTP/1.0\r\n\r\n");
|
|
* });
|
|
*
|
|
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
|
|
* @param {Number} destinationPort Port to open in destination host
|
|
* @param {String} destinationHost Destination hostname
|
|
* @param {Function} callback Callback to run with the rocket object once connection is established
|
|
*/
|
|
function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
|
let proxy = urllib.parse(proxyUrl);
|
|
|
|
// create a socket connection to the proxy server
|
|
let options;
|
|
let connect;
|
|
let socket;
|
|
|
|
options = {
|
|
host: proxy.hostname,
|
|
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
|
};
|
|
|
|
if (proxy.protocol === 'https:') {
|
|
// we can use untrusted proxies as long as we verify actual SMTP certificates
|
|
options.rejectUnauthorized = false;
|
|
connect = tls.connect.bind(tls);
|
|
} else {
|
|
connect = net.connect.bind(net);
|
|
}
|
|
|
|
// Error harness for initial connection. Once connection is established, the responsibility
|
|
// to handle errors is passed to whoever uses this socket
|
|
let finished = false;
|
|
let tempSocketErr = err => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
finished = true;
|
|
try {
|
|
socket.destroy();
|
|
} catch (E) {
|
|
// ignore
|
|
}
|
|
callback(err);
|
|
};
|
|
|
|
let timeoutErr = () => {
|
|
let err = new Error('Proxy socket timed out');
|
|
err.code = 'ETIMEDOUT';
|
|
tempSocketErr(err);
|
|
};
|
|
|
|
socket = connect(options, () => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
|
|
let reqHeaders = {
|
|
Host: destinationHost + ':' + destinationPort,
|
|
Connection: 'close'
|
|
};
|
|
if (proxy.auth) {
|
|
reqHeaders['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64');
|
|
}
|
|
|
|
socket.write(
|
|
// HTTP method
|
|
'CONNECT ' +
|
|
destinationHost +
|
|
':' +
|
|
destinationPort +
|
|
' HTTP/1.1\r\n' +
|
|
// HTTP request headers
|
|
Object.keys(reqHeaders)
|
|
.map(key => key + ': ' + reqHeaders[key])
|
|
.join('\r\n') +
|
|
// End request
|
|
'\r\n\r\n'
|
|
);
|
|
|
|
let headers = '';
|
|
let onSocketData = chunk => {
|
|
let match;
|
|
let remainder;
|
|
|
|
if (finished) {
|
|
return;
|
|
}
|
|
|
|
headers += chunk.toString('binary');
|
|
if ((match = headers.match(/\r\n\r\n/))) {
|
|
socket.removeListener('data', onSocketData);
|
|
|
|
remainder = headers.substr(match.index + match[0].length);
|
|
headers = headers.substr(0, match.index);
|
|
if (remainder) {
|
|
socket.unshift(Buffer.from(remainder, 'binary'));
|
|
}
|
|
|
|
// proxy connection is now established
|
|
finished = true;
|
|
|
|
// check response code
|
|
match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
|
if (!match || (match[1] || '').charAt(0) !== '2') {
|
|
try {
|
|
socket.destroy();
|
|
} catch (E) {
|
|
// ignore
|
|
}
|
|
return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || '')));
|
|
}
|
|
|
|
socket.removeListener('error', tempSocketErr);
|
|
socket.removeListener('timeout', timeoutErr);
|
|
socket.setTimeout(0);
|
|
|
|
return callback(null, socket);
|
|
}
|
|
};
|
|
socket.on('data', onSocketData);
|
|
});
|
|
|
|
socket.setTimeout(httpProxyClient.timeout || 30 * 1000);
|
|
socket.on('timeout', timeoutErr);
|
|
|
|
socket.once('error', tempSocketErr);
|
|
}
|
|
|
|
module.exports = httpProxyClient;
|