@httptoolkit/httpolyglot
Advanced tools
Comparing version 0.2.0 to 1.0.0
153
lib/index.js
@@ -0,51 +1,71 @@ | ||
const net = require('net'); | ||
const tls = require('tls'); | ||
const http = require('http'); | ||
const https = require('https'); | ||
const http2 = require('http2'); | ||
const SocketWrapper = require('_stream_wrap'); | ||
const EventEmitter = require('events').EventEmitter; | ||
const inherits = require('util').inherits; | ||
const httpSocketHandler = http._connectionListener; | ||
function Server(tlsconfig, requestListener) { | ||
if (!(this instanceof Server)) return new Server(tlsconfig, requestListener); | ||
function Server(tlsConfig, requestListener) { | ||
if (!(this instanceof Server)) return new Server(tlsConfig, requestListener); | ||
if (typeof tlsconfig === 'function') { | ||
requestListener = tlsconfig; | ||
tlsconfig = undefined; | ||
if (typeof tlsConfig === 'function') { | ||
requestListener = tlsConfig; | ||
tlsConfig = undefined; | ||
} | ||
if (typeof tlsconfig === 'object') { | ||
this.removeAllListeners('connection'); | ||
// We bind the listener, so 'this' always refers to us, not each subserver. | ||
// This means 'this' is consistent (and this.close() works). If you need to | ||
// access a specific subserver, you'll need to use ._{http,http2,tls}Server | ||
const boundListener = requestListener.bind(this); | ||
https.Server.call(this, tlsconfig, requestListener); | ||
// Create subservers for each supported protocol: | ||
this._httpServer = new http.Server(boundListener); | ||
this._http2Server = http2.createServer({}, boundListener); | ||
// capture https socket handler, it's not exported like http's socket | ||
// handler | ||
const connev = this._events.connection; | ||
if (typeof connev === 'function') { | ||
this._tlsHandler = connev; | ||
} else { | ||
this._tlsHandler = connev[connev.length - 1]; | ||
} | ||
this.removeListener('connection', this._tlsHandler); | ||
this._connListener = connectionListener; | ||
this.on('connection', connectionListener); | ||
// copy from http.Server | ||
this.timeout = 2 * 60 * 1000; | ||
this.allowHalfOpen = true; | ||
this.httpAllowHalfOpen = false; | ||
if (typeof tlsConfig === 'object') { | ||
// 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 = tls.Server(tlsConfig, (tlsSocket) => { | ||
if (tlsSocket.alpnProtocol === false || tlsSocket.alpnProtocol === 'http/1.1') { | ||
this._httpServer.emit('connection', tlsSocket); | ||
} else { | ||
this._http2Server.emit('connection', tlsSocket); | ||
} | ||
}); | ||
} else { | ||
http.Server.call(this, requestListener); | ||
// Fake server that rejects all connections: | ||
this._tlsServer = new EventEmitter(); | ||
this._tlsServer.on('connection', (socket) => socket.destroy()); | ||
} | ||
} | ||
inherits(Server, https.Server); | ||
Server.prototype.setTimeout = function (msecs, callback) { | ||
this.timeout = msecs; | ||
if (callback) this.on('timeout', callback); | ||
}; | ||
const subServers = [this._httpServer, this._http2Server, this._tlsServer]; | ||
Server.prototype.__httpSocketHandler = httpSocketHandler; | ||
// We ourselves just act as a plain TCP server, accepting and examing | ||
// each connection, then passing it to the right subserver. | ||
net.Server.call(this, connectionListener); | ||
// 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); | ||
}) | ||
}); | ||
} | ||
inherits(Server, net.Server); | ||
function onError(err) {} | ||
const TLS_HANDSHAKE_BYTE = 0x16; // SSLv3+ or TLS handshake | ||
const HTTP2_PREFACE = 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'; | ||
const HTTP2_PREFACE_BUFFER = Buffer.from(HTTP2_PREFACE); | ||
const connectionListener = function (socket) { | ||
@@ -59,3 +79,3 @@ const data = socket.read(1); | ||
socket.once('readable', () => { | ||
this._connListener(socket); | ||
connectionListener.call(this, socket); | ||
}); | ||
@@ -65,19 +85,58 @@ } else { | ||
// Put the peeked data back into the socket | ||
const firstByte = data[0]; | ||
socket.unshift(data); | ||
if (firstByte < 32 || firstByte >= 127) { | ||
// tls/ssl | ||
// Pass the socket to the correct subserver: | ||
if (firstByte === TLS_HANDSHAKE_BYTE) { | ||
// TLS sockets don't allow half open | ||
socket.allowHalfOpen = false; | ||
this._tlsHandler(socket); | ||
this._tlsServer.emit('connection', socket); | ||
} else { | ||
// The above unshift isn't always sufficient to invisibly replace the | ||
// read data. The rawPacket property on errors in the clientError event | ||
// specifically is missing this data - this prop makes it available. | ||
// Bit of a hacky fix, but sufficient to allow for manual workarounds. | ||
socket.__httpPeekedData = data; | ||
if (firstByte === HTTP2_PREFACE_BUFFER[0]) { | ||
// The connection _might_ be HTTP/2. To confirm, we need to keep | ||
// reading until we get the whole stream: | ||
http2Listener.call(this, socket); | ||
} else { | ||
// The above unshift isn't always sufficient to invisibly replace the | ||
// read data. The rawPacket property on errors in the clientError event | ||
// for plain HTTP servers loses this data - this prop makes it available. | ||
// Bit of a hacky fix, but sufficient to allow for manual workarounds. | ||
socket.__httpPeekedData = data; | ||
this._httpServer.emit('connection', socket); | ||
} | ||
} | ||
} | ||
}; | ||
this.__httpSocketHandler(socket); | ||
const http2Listener = function (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_BUFFER.length) { | ||
socket.unshift(data); | ||
if (data.slice(0, HTTP2_PREFACE_BUFFER.length).equals(HTTP2_PREFACE_BUFFER)) { | ||
// We have a full match for the preface - it's definitely HTTP/2. | ||
h2Server.emit('connection', new SocketWrapper(socket)); | ||
return; | ||
} else { | ||
h1Server.emit('connection', socket); | ||
return; | ||
} | ||
} else if (!data.equals(HTTP2_PREFACE_BUFFER.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', () => { | ||
http2Listener.call(this, socket, data); | ||
}); | ||
}; | ||
@@ -87,4 +146,4 @@ | ||
exports.createServer = function (tlsconfig, requestListener) { | ||
return new Server(tlsconfig, requestListener); | ||
exports.createServer = function (tlsConfig, requestListener) { | ||
return new Server(tlsConfig, requestListener); | ||
}; |
{ | ||
"name": "@httptoolkit/httpolyglot", | ||
"version": "0.2.0", | ||
"version": "1.0.0", | ||
"author": "Tim Perry <pimterry@gmail.com>", | ||
@@ -16,2 +16,3 @@ "description": "Serve http and https connections over the same port with node.js", | ||
"https", | ||
"http2", | ||
"multiplex", | ||
@@ -29,3 +30,4 @@ "polyglot" | ||
"url": "http://github.com/httptoolkit/httpolyglot.git" | ||
} | ||
}, | ||
"dependencies": {} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
15380
222
1
10
7