UNPKG

@httptoolkit/httpolyglot

Version:

Serve http and https connections over the same port with node.js

212 lines 9.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createServer = void 0; const net = require("net"); const tls = require("tls"); const http = require("http"); const http2 = require("http2"); const events_1 = require("events"); // 0x16 first byte is very unlikely to be a false positive as TLS is so widely used & intermediated const isTls = (data) => data[0] === 0x16; // SSLv3+ or TLS handshake // H2 hello is effectively guaranteed not to be a false positive, as it's very so specific, but we // need to wait for the full message to confirm this. const HTTP2_PREFACE = Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'); const couldBeHttp2 = (data) => data[0] === HTTP2_PREFACE[0]; const isHttp2 = (data) => data.subarray(0, HTTP2_PREFACE.length).equals(HTTP2_PREFACE); const H1_METHOD_PREFIXES = http.METHODS.map(m => m + ' '); const H1_LONGEST_PREFIX = Math.max(...H1_METHOD_PREFIXES.map(m => m.length)); const couldBeHttp1 = (initialData) => { const initialString = initialData.subarray(0, H1_LONGEST_PREFIX).toString('utf8'); for (let method of H1_METHOD_PREFIXES) { const comparisonLength = Math.min(method.length, initialString.length); if (initialString.slice(0, comparisonLength) === method.slice(0, comparisonLength)) { return true; } } return false; }; // [0x4, 0x1|0x2] for SOCKS v4 seems fairly reliable. Some other protocols (Cassandra's CQL) use // the first byte for versions similarly, but shouldn't conflict in practice AFAICT, and no // other popular protocols actively match 0x4 that I can see. const isSocksV4 = (data) => { return data[0] === 0x04 && // Start of SOCKS v4 client hello (data.byteLength > 1 && (data[1] === 0x1 || data[1] === 0x2)); // Any command sent is valid }; // [0x5, len, len-bytes-of-auth-methods]. 0x5 doesn't have any visible conflicts either, and // using len as a max-expected-length check should keep that fairly tight too. const isSocksV5 = (data) => { return data[0] === 0x05 && // Start of SOCKS v5 client hello (data.byteLength > 1 && // If auth method length is present, it's non-zero and could match data[1] > 0 && // (i.e. message isn't longer than it should be) data.byteLength <= 2 + data[1]); }; const isSocks = (data) => isSocksV4(data) || isSocksV5(data); function onError(err) { } class Server extends net.Server { _httpServer; _http2Server; _tlsServer; _socksHandler; _unknownProtocolHandler; constructor(configOrListener, listener) { // We just act as a plain TCP server, accepting and examing // each connection, then passing it to the right subserver. super((socket) => this.connectionListener(socket)); let config = {}; let requestListener; if (typeof configOrListener === 'function') { requestListener = configOrListener; } else { config = configOrListener; requestListener = listener; } // We bind the request listener, so 'this' always refers to us, not each subserver. // This means 'this' is consistent (and this.close() works). // Use `Function.prototype.bind` directly as frameworks like Express generate // methods from `http.METHODS`, and `BIND` is an included HTTP method. const boundListener = Function.prototype.bind.call(requestListener, this); // Create subservers for each supported protocol: this._httpServer = new http.Server(boundListener); this._http2Server = http2.createServer({}, boundListener); if (config.tls) { if (config.tls instanceof tls.Server) { // If we've been given a preconfigured TLS server, we use that directly, and // subscribe to connections there this._tlsServer = config.tls; this._tlsServer.on('secureConnection', this.tlsListener.bind(this)); } else { // If we have TLS config, create a TLS server, which will pass sockets to // the relevant subserver once the TLS connection is set up. this._tlsServer = new tls.Server(config.tls, this.tlsListener.bind(this)); } } else { // Fake TLS server that rejects all connections: this._tlsServer = new events_1.EventEmitter(); this._tlsServer.on('connection', (socket) => socket.destroy()); } this._socksHandler = config.socks; this._unknownProtocolHandler = config.unknownProtocol; const subServers = [this._httpServer, this._http2Server, this._tlsServer]; if (this._socksHandler) subServers.push(this._socksHandler); if (this._unknownProtocolHandler) subServers.push(this._unknownProtocolHandler); // Proxy all event listeners setup onto the subservers, so any // subscriptions on this server are fed from all the subservers this.on('newListener', function (eventName, listener) { subServers.forEach(function (subServer) { subServer.addListener(eventName, listener); }); }); this.on('removeListener', function (eventName, listener) { subServers.forEach(function (subServer) { subServer.removeListener(eventName, listener); }); }); } connectionListener(socket) { const data = socket.read(); if (data === null) { socket.removeListener('error', onError); socket.on('error', onError); socket.once('readable', () => { this.connectionListener(socket); }); } else { socket.removeListener('error', onError); // Put the peeked data back into the socket socket.unshift(data); // Pass the socket to the correct subserver: if (isTls(data)) { // TLS sockets don't allow half open socket.allowHalfOpen = false; this._tlsServer.emit('connection', socket); } else if (this._socksHandler && isSocks(data)) { this._socksHandler.emit('connection', socket); } else { if (couldBeHttp2(data)) { // The connection _might_ be HTTP/2. To confirm, we need to keep // reading until we get the whole stream: this.http2Listener(socket); } else if (this._unknownProtocolHandler && !couldBeHttp1(data)) { this._unknownProtocolHandler.emit('connection', socket); } else { // We pass everything else to the HTTP server, which can handle requests // and/or reject unparseable client-error connections as it sees fit. this._httpServer.emit('connection', socket); } } } } tlsListener(tlsSocket) { if (tlsSocket.alpnProtocol === false || // Old non-ALPN client tlsSocket.alpnProtocol === 'http/1.1' || // Modern HTTP/1.1 ALPN client tlsSocket.alpnProtocol === 'http 1.1' // Broken ALPN client (e.g. https-proxy-agent) ) { this._httpServer.emit('connection', tlsSocket); } else { this._http2Server.emit('connection', tlsSocket); } } http2Listener(socket, pastData) { const h1Server = this._httpServer; const h2Server = this._http2Server; const newData = socket.read() || Buffer.from([]); const data = pastData ? Buffer.concat([pastData, newData]) : newData; if (data.length >= HTTP2_PREFACE.length) { socket.unshift(data); if (isHttp2(data)) { // We have a full match for the preface - it's definitely HTTP/2. // For HTTP/2 we hit issues when passing non-socket streams (like H2 streams for proxying H2-over-H2). // Setting isStreamBase to false is an effective workaround for this in Node 14+ const socketWithInternals = socket; if (socketWithInternals._handle) { socketWithInternals._handle.isStreamBase = false; } h2Server.emit('connection', socket); return; } else { h1Server.emit('connection', socket); return; } } else if (!data.equals(HTTP2_PREFACE.slice(0, data.length))) { socket.unshift(data); // Haven't finished the preface length, but something doesn't match already h1Server.emit('connection', socket); return; } // Not enough data to know either way - try again, waiting for more: socket.removeListener('error', onError); socket.on('error', onError); socket.once('readable', () => { this.http2Listener.call(this, socket, data); }); } } function createServer(configOrListener, listener) { let config; let requestListener; if (typeof configOrListener === 'function') { config = {}; requestListener = configOrListener; } else { config = configOrListener; requestListener = listener; } return new Server(config, requestListener); } exports.createServer = createServer; ; //# sourceMappingURL=index.js.map