'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 = function(err) { if (finished) { return; } finished = true; try { socket.destroy(); } catch (E) { // ignore } callback(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); return callback(null, socket); } }; socket.on('data', onSocketData); } ); socket.once('error', tempSocketErr); } module.exports = httpProxyClient;