http2-proxy
Advanced tools
Comparing version 0.2.20 to 1.0.0
321
index.js
const http2 = require('http2') | ||
const http = require('http') | ||
const net = require('net') | ||
const assert = require('assert') | ||
@@ -26,10 +27,18 @@ const { | ||
ws (req, socket, head, options, callback) { | ||
impl(req, socket, head, options, callback) | ||
proxy(req, socket, head, options, callback) | ||
}, | ||
web (req, res, options, callback) { | ||
impl(req, res, null, options, callback) | ||
proxy(req, res, null, options, callback) | ||
} | ||
} | ||
function impl (req, resOrSocket, headOrNil, { | ||
const kReq = Symbol('req') | ||
const kRes = Symbol('res') | ||
const kProxyCallback = Symbol('callback') | ||
const kProxyReq = Symbol('proxyReq') | ||
const kProxyRes = Symbol('proxyRes') | ||
const kProxySocket = Symbol('proxySocket') | ||
const kOnProxyRes = Symbol('onProxyRes') | ||
function proxy (req, res, head, { | ||
hostname, | ||
@@ -43,48 +52,28 @@ port, | ||
}, callback) { | ||
let hasError = false | ||
let proxyReq | ||
req[kRes] = res | ||
function onError (err, statusCode = err.statusCode || 500) { | ||
if (hasError) { | ||
return | ||
} | ||
res[kReq] = req | ||
res[kRes] = res | ||
res[kProxyCallback] = callback | ||
res[kProxyReq] = null | ||
res[kProxyRes] = null | ||
res[kProxySocket] = null | ||
if (proxyReq && !proxyReq.aborted) { | ||
proxyReq.abort() | ||
} | ||
assert(typeof callback === 'function' || callback == null) | ||
hasError = true | ||
let promise | ||
if (!err.code) { | ||
err.code = resOrSocket.code | ||
} | ||
if (resOrSocket.closed === true || | ||
resOrSocket.headersSent !== false || | ||
!resOrSocket.writeHead | ||
) { | ||
resOrSocket.destroy() | ||
} else { | ||
resOrSocket.writeHead(statusCode) | ||
resOrSocket.end() | ||
} | ||
if (callback) { | ||
callback(err, req, resOrSocket) | ||
} else { | ||
throw err | ||
} | ||
if (!callback) { | ||
promise = new Promise((resolve, reject) => { | ||
res[kProxyCallback] = err => err ? reject(err) : resolve() | ||
}) | ||
} | ||
(req.stream || req).on('error', onError) | ||
resOrSocket.on('error', onError) | ||
if (resOrSocket instanceof net.Socket) { | ||
if (res instanceof net.Socket) { | ||
if (req.method !== 'GET') { | ||
return onError(createError('method not allowed', null, 405)) | ||
return onFinish.call(res, createError('method not allowed', null, 405)) | ||
} | ||
if (!req.headers[HTTP2_HEADER_UPGRADE] || | ||
req.headers[HTTP2_HEADER_UPGRADE].toLowerCase() !== 'websocket') { | ||
return onError(createError('bad request', null, 400)) | ||
if (sanitize(req.headers[HTTP2_HEADER_UPGRADE]) !== 'websocket') { | ||
return onFinish.call(res, createError('bad request', null, 400)) | ||
} | ||
@@ -94,3 +83,3 @@ } | ||
if (req.httpVersion !== '1.1' && req.httpVersion !== '2.0') { | ||
return onError(createError('http version not supported', null, 505)) | ||
return onFinish.call(res, createError('http version not supported', null, 505)) | ||
} | ||
@@ -101,3 +90,3 @@ | ||
if (sanitize(name).endsWith(proxyName.toLowerCase())) { | ||
return onError(createError('loop detected', null, 508)) | ||
return onFinish.call(res, createError('loop detected', null, 508)) | ||
} | ||
@@ -108,11 +97,11 @@ } | ||
if (timeout) { | ||
req.setTimeout(timeout, () => onError(createError('request timeout', null, 408))) | ||
req.setTimeout(timeout, onRequestTimeout) | ||
} | ||
if (resOrSocket instanceof net.Socket) { | ||
if (headOrNil && headOrNil.length) { | ||
resOrSocket.unshift(headOrNil) | ||
if (res instanceof net.Socket) { | ||
if (head && head.length) { | ||
res.unshift(head) | ||
} | ||
setupSocket(resOrSocket) | ||
setupSocket(res) | ||
} | ||
@@ -143,111 +132,176 @@ | ||
proxyReq = http.request(options) | ||
const proxyReq = http.request(options) | ||
const onProxyError = err => { | ||
if (proxyReq.aborted) { | ||
return | ||
} | ||
proxyReq.abort() | ||
proxyReq[kReq] = req | ||
proxyReq[kRes] = res | ||
proxyReq[kOnProxyRes] = onRes | ||
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') { | ||
err.statusCode = 503 | ||
} else if (/HPE_INVALID/.test(err.code)) { | ||
err.statusCode = 502 | ||
} else if (err.code === 'ECONNRESET') { | ||
err.statusCode = 502 | ||
} else { | ||
err.statusCode = 500 | ||
} | ||
res[kProxyReq] = proxyReq | ||
onError(err) | ||
} | ||
function onFinish () { | ||
if (!proxyReq.aborted) { | ||
proxyReq.abort() | ||
} | ||
} | ||
resOrSocket | ||
res | ||
.on('finish', onFinish) | ||
.on('close', onFinish) | ||
.on('error', onFinish) | ||
.on('close', onFinish) | ||
req | ||
.on('aborted', onFinish) | ||
.on('close', onFinish) | ||
.on('error', onFinish) | ||
.on('close', onFinish) | ||
.pipe(proxyReq) | ||
.on('error', onProxyError) | ||
.on('error', onFinish) | ||
// NOTE http.ClientRequest emits "socket hang up" error when aborted | ||
// before having received a response, i.e. there is no need to listen for | ||
// proxyReq.on('aborted', ...). | ||
.on('timeout', () => onProxyError(createError('gateway timeout', null, 504))) | ||
.on('response', proxyRes => { | ||
proxyRes.on('aborted', () => onProxyError(createError('socket hang up', 'ECONNRESET', 502))) | ||
.on('timeout', onProxyTimeout) | ||
.on('response', onProxyResponse) | ||
.on('upgrade', onProxyUpgrade) | ||
if (resOrSocket instanceof net.Socket) { | ||
if (onRes) { | ||
onRes(req, resOrSocket) | ||
} | ||
return promise | ||
} | ||
if (!proxyRes.upgrade) { | ||
resOrSocket.end() | ||
} | ||
} else { | ||
setupHeaders(proxyRes.headers) | ||
function onFinish (err, statusCode) { | ||
const res = this[kRes] | ||
resOrSocket.statusCode = proxyRes.statusCode | ||
for (const key of Object.keys(proxyRes.headers)) { | ||
resOrSocket.setHeader(key, proxyRes.headers[key]) | ||
} | ||
assert(res) | ||
if (onRes) { | ||
onRes(req, resOrSocket) | ||
} | ||
if (!res[kProxyCallback]) { | ||
return | ||
} | ||
resOrSocket.writeHead(resOrSocket.statusCode) | ||
proxyRes | ||
.on('end', () => resOrSocket.addTrailers(proxyRes.trailers)) | ||
.on('error', onProxyError) | ||
.pipe(resOrSocket) | ||
} | ||
}) | ||
if (res[kProxyReq]) { | ||
res[kProxyReq].abort() | ||
res[kProxyReq] = null | ||
} | ||
if (resOrSocket instanceof net.Socket) { | ||
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => { | ||
setupSocket(proxySocket) | ||
if (res[kProxySocket]) { | ||
res[kProxySocket].end() | ||
res[kProxySocket] = null | ||
} | ||
if (proxyHead && proxyHead.length) { | ||
proxySocket.unshift(proxyHead) | ||
} | ||
if (res[kProxyRes]) { | ||
res[kProxyRes].destroy() | ||
res[kProxyRes] = null | ||
} | ||
let head = 'HTTP/1.1 101 Switching Protocols' | ||
if (err) { | ||
err.statusCode = statusCode || err.statusCode || 500 | ||
err.code = err.code || res.code | ||
for (const key of Object.keys(proxyRes.headers)) { | ||
const value = proxyRes.headers[key] | ||
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') { | ||
err.statusCode = 503 | ||
} else if (/HPE_INVALID/.test(err.code)) { | ||
err.statusCode = 502 | ||
} | ||
} | ||
if (!Array.isArray(value)) { | ||
head += '\r\n' + key + ': ' + value | ||
} else { | ||
for (let i = 0; i < value.length; i++) { | ||
head += '\r\n' + key + ': ' + value[i] | ||
} | ||
} | ||
} | ||
if (res.headersSent !== false) { | ||
res.destroy() | ||
} else { | ||
res.writeHead(statusCode) | ||
res.end() | ||
} | ||
head += '\r\n\r\n' | ||
res[kProxyCallback].call(null, err, res[kReq], res) | ||
res[kProxyCallback] = null | ||
} | ||
resOrSocket.write(head) | ||
function onRequestTimeout () { | ||
onFinish.call(this, createError('request timeout', null, 408)) | ||
} | ||
proxyRes.on('error', onProxyError) | ||
function onProxyTimeout () { | ||
onFinish.call(this, createError('gateway timeout', null, 504)) | ||
} | ||
proxySocket | ||
.on('error', onProxyError) | ||
.pipe(resOrSocket) | ||
.pipe(proxySocket) | ||
}) | ||
function onProxyResponse (proxyRes) { | ||
if (this.aborted) { | ||
return | ||
} | ||
const res = this[kRes] | ||
res[kProxyRes] = proxyRes | ||
proxyRes[kRes] = res | ||
proxyRes.on('aborted', onProxyAborted) | ||
if (res instanceof net.Socket) { | ||
if (!proxyRes.upgrade) { | ||
res.end() | ||
} | ||
} else { | ||
setupHeaders(proxyRes.headers) | ||
res.statusCode = proxyRes.statusCode | ||
for (const key of Object.keys(proxyRes.headers)) { | ||
res.setHeader(key, proxyRes.headers[key]) | ||
} | ||
if (this[kOnProxyRes]) { | ||
this[kOnProxyRes](this[kReq], res) | ||
} | ||
res.writeHead(res.statusCode) | ||
proxyRes | ||
.on('end', onProxyTrailers) | ||
.on('error', onFinish) | ||
.pipe(res) | ||
} | ||
} | ||
function onProxyTrailers () { | ||
this[kRes].addTrailers(this.trailers) | ||
} | ||
function onProxyAborted () { | ||
onFinish.call(this, createError('socket hang up', 'ECONNRESET', 502)) | ||
} | ||
function onProxyUpgrade (proxyRes, proxySocket, proxyHead) { | ||
if (this.aborted) { | ||
return | ||
} | ||
const res = this[kRes] | ||
res[kProxySocket] = proxySocket | ||
res[kProxyRes] = proxyRes | ||
proxyRes[kRes] = res | ||
proxySocket[kRes] = res | ||
setupSocket(proxySocket) | ||
if (proxyHead && proxyHead.length) { | ||
proxySocket.unshift(proxyHead) | ||
} | ||
let head = 'HTTP/1.1 101 Switching Protocols' | ||
for (const key of Object.keys(proxyRes.headers)) { | ||
const value = proxyRes.headers[key] | ||
if (!Array.isArray(value)) { | ||
head += '\r\n' + key + ': ' + value | ||
} else { | ||
for (let i = 0; i < value.length; i++) { | ||
head += '\r\n' + key + ': ' + value[i] | ||
} | ||
} | ||
} | ||
head += '\r\n\r\n' | ||
this[kRes].write(head) | ||
proxyRes | ||
.on('error', onFinish) | ||
proxySocket | ||
.on('error', onFinish) | ||
.pipe(this[kRes]) | ||
.pipe(proxySocket) | ||
} | ||
function getRequestHeaders (req) { | ||
@@ -260,7 +314,9 @@ const host = req.headers[HTTP2_HEADER_AUTHORITY] || req.headers[HTTP2_HEADER_HOST] | ||
// Remove pseudo headers | ||
delete headers[HTTP2_HEADER_AUTHORITY] | ||
delete headers[HTTP2_HEADER_METHOD] | ||
delete headers[HTTP2_HEADER_PATH] | ||
delete headers[HTTP2_HEADER_SCHEME] | ||
if (req.httpVersionMajor === 2) { | ||
// Remove pseudo headers | ||
delete headers[HTTP2_HEADER_AUTHORITY] | ||
delete headers[HTTP2_HEADER_METHOD] | ||
delete headers[HTTP2_HEADER_PATH] | ||
delete headers[HTTP2_HEADER_SCHEME] | ||
} | ||
@@ -306,6 +362,7 @@ if (upgrade) { | ||
for (const name of connection.split(',')) { | ||
delete headers[sanitize(name)] | ||
delete headers[name.trim()] | ||
} | ||
} | ||
// Remove hop by hop headers | ||
delete headers[HTTP2_HEADER_CONNECTION] | ||
@@ -312,0 +369,0 @@ delete headers[HTTP2_HEADER_KEEP_ALIVE] |
{ | ||
"name": "http2-proxy", | ||
"version": "0.2.20", | ||
"version": "1.0.0", | ||
"scripts": { | ||
@@ -5,0 +5,0 @@ "dev": "nodemon --inspect=9308 --expose-http2 src", |
@@ -8,4 +8,4 @@ # node-http2-proxy | ||
- Proxies HTTP 2, HTTP 1.1 and WebSocket | ||
- Simple and easy to follow implementation | ||
- [Hop by hop header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) | ||
- Simple and high performance | ||
- [Hop by hop header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers). | ||
- [Connection header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection) | ||
@@ -44,3 +44,7 @@ - [Via header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via) | ||
port: 9000 | ||
}, err => console.error(err, 'proxy error')) | ||
}, err => { | ||
if (err) { | ||
console.error('proxy error', err) | ||
} | ||
}) | ||
}) | ||
@@ -51,3 +55,7 @@ server.on('upgrade', (req, socket, head) => { | ||
port: 9000 | ||
}, err => console.error('proxy error', err)) | ||
}, err => { | ||
if (err) { | ||
console.error('proxy error', err) | ||
} | ||
}) | ||
}) | ||
@@ -64,3 +72,7 @@ ``` | ||
onRes: (req, res) => helmet(req, res, () => {}) | ||
}, err => console.error('proxy error', err)) | ||
}, err => { | ||
if (err) { | ||
console.error('proxy error', err) | ||
} | ||
}) | ||
}) | ||
@@ -81,3 +93,7 @@ ``` | ||
} | ||
}, err => console.error(err, 'proxy error')) | ||
}, err => { | ||
if (err) { | ||
console.error('proxy error', err) | ||
} | ||
}) | ||
}) | ||
@@ -88,19 +104,23 @@ ``` | ||
#### web (req, res, options, onProxyError) | ||
#### web (req, res, options, [callback]) | ||
- `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) or [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverrequest). | ||
- `res`: [`http.ServerResponse`](https://nodejs.org/api/http.html#http_http_request_options_callback) or [`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverresponse). | ||
- `options`: See [Options](#options). | ||
- `onProxyError(err)`: Called on error. | ||
- `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) or [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverrequest) | ||
- `res`: [`http.ServerResponse`](https://nodejs.org/api/http.html#http_http_request_options_callback) or [`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverresponse) | ||
- `options`: See [Options](#options) | ||
- `callback(err)`: Called on completion or error. Optional | ||
Returns a promise if no callback is provided. | ||
See [`request`](https://nodejs.org/api/http.html#http_event_request) | ||
#### ws (req, socket, head, options, onProxyError) | ||
#### ws (req, socket, head, options, [callback]) | ||
- `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage). | ||
- `socket`: [`net.Socket`](https://nodejs.org/api/net.html#net_class_net_socket). | ||
- `head`: [`Buffer`](https://nodejs.org/api/buffer.html#buffer_class_buffer). | ||
- `options`: See [Options](#options). | ||
- `onProxyError(err)`: Called on error. | ||
- `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) | ||
- `socket`: [`net.Socket`](https://nodejs.org/api/net.html#net_class_net_socket) | ||
- `head`: [`Buffer`](https://nodejs.org/api/buffer.html#buffer_class_buffer) | ||
- `options`: See [Options](#options) | ||
- `callback(err)`: Called on completion or error. Optional | ||
Returns a promise if no callback is provided. | ||
See [`upgrade`](https://nodejs.org/api/http.html#http_event_upgrade) | ||
@@ -110,13 +130,13 @@ | ||
- `hostname`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target hostname. | ||
- `port`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target port. | ||
- `proxyTimeout`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) timeout. | ||
- `proxyName`: Proxy name used for **Via** header. | ||
- `hostname`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target hostname | ||
- `port`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target port | ||
- `proxyTimeout`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) timeout | ||
- `proxyName`: Proxy name used for **Via** header | ||
- `timeout`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) or [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverrequest) timeout | ||
- `onReq(req, options)`: Called before proxy request. | ||
- `onReq(req, options)`: Called before proxy request | ||
- `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) or [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverrequest) | ||
- `options`: Options passed to [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback). | ||
- `onRes(req, resOrSocket)`: Called before proxy response. | ||
- `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) or [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverrequest). | ||
- `resOrSocket`: For `web` [`http.ServerResponse`](https://nodejs.org/api/http.html#http_http_request_options_callback) or [`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverresponse) and for `ws` [`net.Socket`](https://nodejs.org/api/net.html#net_class_net_socket). | ||
- `options`: Options passed to [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) | ||
- `onRes(req, resOrSocket)`: Called before proxy response | ||
- `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) or [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverrequest) | ||
- `resOrSocket`: For `web` [`http.ServerResponse`](https://nodejs.org/api/http.html#http_http_request_options_callback) or [`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverresponse) and for `ws` [`net.Socket`](https://nodejs.org/api/net.html#net_class_net_socket) | ||
@@ -123,0 +143,0 @@ ### License |
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
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
16088
321
1
139