http2-proxy
Advanced tools
Comparing version 4.2.15 to 5.0.0
336
index.js
@@ -0,4 +1,4 @@ | ||
const net = require('net') | ||
const http = require('http') | ||
const https = require('https') | ||
const url = require('url') | ||
@@ -9,2 +9,3 @@ const CONNECTION = 'connection' | ||
const PROXY_AUTHORIZATION = 'proxy-authorization' | ||
const PROXY_AUTHENTICATE = 'proxy-authenticate' | ||
const PROXY_CONNECTION = 'proxy-connection' | ||
@@ -20,14 +21,4 @@ const TE = 'te' | ||
module.exports = { | ||
ws (req, socket, head, options, callback) { | ||
return proxy.call(this, req, socket, head || null, options, callback) | ||
}, | ||
web (req, res, options, callback) { | ||
return proxy.call(this, req, res, undefined, options, callback) | ||
} | ||
} | ||
const kReq = Symbol('req') | ||
const kRes = Symbol('res') | ||
const kSelf = Symbol('self') | ||
const kProxyCallback = Symbol('callback') | ||
@@ -37,10 +28,27 @@ const kProxyReq = Symbol('proxyReq') | ||
const kProxySocket = Symbol('proxySocket') | ||
const kOnProxyRes = Symbol('onProxyRes') | ||
const kHead = Symbol('head') | ||
const kConnected = Symbol('connected') | ||
const kOnRes = Symbol('onRes') | ||
function proxy (req, res, head, options, callback) { | ||
if (typeof options === 'string') { | ||
options = new url.URL(options) | ||
module.exports = proxy | ||
proxy.ws = function ws (req, socket, head, options, callback) { | ||
const promise = compat(ctx, options) | ||
if (!callback) { | ||
return promise | ||
} | ||
promise | ||
.then(() => callback(null, req, socket, head)) | ||
.then(err => callback(err, req, socket, head)) | ||
} | ||
proxy.web = function web (req, res, options, callback) { | ||
const promise = compat(ctx, options) | ||
if (!callback) { | ||
return promise | ||
} | ||
promise | ||
.then(() => callback(null, req, res)) | ||
.then(err => callback(err, req, res)) | ||
} | ||
async function compat (ctx, options) { | ||
const { | ||
@@ -58,31 +66,64 @@ hostname, | ||
let promise | ||
if (!callback) { | ||
promise = new Promise((resolve, reject) => { | ||
callback = (err, ...args) => err ? reject(err) : resolve(args) | ||
}) | ||
if (timeout != null) { | ||
req.setTimeout(timeout) | ||
} | ||
req[kRes] = res | ||
await proxy( | ||
{ ...ctx, proxyName }, | ||
async headers => { | ||
const options = { | ||
method: req.method, | ||
hostname, | ||
port, | ||
path, | ||
headers, | ||
timeout: proxyTimeout | ||
} | ||
res[kReq] = req | ||
res[kRes] = res | ||
res[kSelf] = this | ||
res[kProxyCallback] = callback | ||
res[kProxyReq] = null | ||
res[kProxySocket] = null | ||
res[kHead] = head | ||
res[kOnProxyRes] = onRes | ||
if (onReq) { | ||
return await new Promise((resolve, reject) => { | ||
const promise = onReq(req, options, (err, val) => err ? reject(err) : resolve(val)) | ||
if (promise.then) { | ||
promise.then(resolve).catch(reject) | ||
} | ||
}) | ||
} else { | ||
let agent | ||
if (protocol == null || /^(http|ws):?$/.test(protocol)) { | ||
agent = http | ||
} else if (/^(http|ws)s:?$/.test(protocol)) { | ||
agent = https | ||
} else { | ||
throw new HttpError(`invalid protocol`, null, 500) | ||
} | ||
return agent.request(options) | ||
} | ||
}, | ||
async (proxyRes, headers) => { | ||
proxyRes.headers = headers | ||
return new Promise((resolve, reject) => { | ||
const promise = onRes(req, res, proxyRes, (err, val) => err ? reject(err) : callback(val)) | ||
if (promise.then) { | ||
promise.then(resolve).catch(reject) | ||
} | ||
}) | ||
} | ||
) | ||
} | ||
const headers = getRequestHeaders(req, { proxyName }) | ||
async function proxy ({ req, socket, res = socket, head, proxyName }, onReq, onRes) { | ||
let callback | ||
let promise = new Promise((resolve, reject) => { | ||
callback = err => err ? reject(err) : resolve() | ||
}) | ||
const headers = getRequestHeaders(req, proxyName) | ||
if (head !== undefined) { | ||
if (req.method !== 'GET') { | ||
process.nextTick(onComplete.bind(res), new HttpError('method not allowed', null, 405)) | ||
return promise | ||
throw new HttpError('only GET request allowed', null, 405) | ||
} | ||
if (sanitize(req.headers[UPGRADE]) !== 'websocket') { | ||
process.nextTick(onComplete.bind(res), new HttpError('bad request', null, 400)) | ||
return promise | ||
if (req.headers[UPGRADE] !== 'websocket') { | ||
throw new HttpError('missing upgrade header', null, 400) | ||
} | ||
@@ -100,49 +141,17 @@ | ||
if (proxyName) { | ||
if (headers[VIA]) { | ||
headers[VIA] += `,${req.httpVersion} ${proxyName}` | ||
} else { | ||
headers[VIA] = `${req.httpVersion} ${proxyName}` | ||
} | ||
} | ||
const proxyReq = await onReq(headers) | ||
if (timeout != null) { | ||
req.setTimeout(timeout) | ||
} | ||
req[kRes] = res | ||
const reqOptions = { | ||
method: req.method, | ||
hostname, | ||
port, | ||
path, | ||
headers, | ||
timeout: proxyTimeout | ||
} | ||
res[kReq] = req | ||
res[kRes] = res | ||
res[kProxySocket] = null | ||
res[kProxyReq] = proxyReq | ||
res[kProxyRes] = null | ||
res[kProxyCallback] = callback | ||
let proxyReq | ||
try { | ||
if (onReq) { | ||
proxyReq = onReq.call(res[kSelf], req, reqOptions) | ||
} | ||
if (!proxyReq) { | ||
let agent | ||
if (protocol == null || /^(http|ws):?$/.test(protocol)) { | ||
agent = http | ||
} else if (/^(http|ws)s:?$/.test(protocol)) { | ||
agent = https | ||
} else { | ||
throw new HttpError(`invalid protocol`, null, 500) | ||
} | ||
proxyReq = agent.request(reqOptions) | ||
} | ||
} catch (err) { | ||
process.nextTick(onComplete.bind(res), err) | ||
return promise | ||
} | ||
proxyReq[kReq] = req | ||
proxyReq[kRes] = res | ||
res[kProxyReq] = proxyReq | ||
proxyReq[kConnected] = false | ||
proxyReq[kOnRes] = onRes | ||
@@ -158,12 +167,24 @@ res | ||
.on('error', onComplete) | ||
.on('timeout', onRequestTimeout) | ||
.pipe(proxyReq) | ||
.on('error', onProxyError) | ||
.on('timeout', onProxyTimeout) | ||
.on('response', onProxyResponse) | ||
.on('upgrade', onProxyUpgrade) | ||
proxyReq | ||
.on('error', onProxyReqError) | ||
.on('timeout', onProxyReqTimeout) | ||
.on('response', onProxyReqResponse) | ||
.on('upgrade', onProxyReqUpgrade) | ||
deferToConnect.call(proxyReq, onProxyConnect) | ||
return promise | ||
} | ||
function deferToConnect (cb) { | ||
this.once('socket', function (socket) { | ||
if (!socket.connecting) { | ||
cb.call(this) | ||
} else { | ||
socket.once('connect', cb.bind(this)) | ||
} | ||
}) | ||
} | ||
function onComplete (err) { | ||
@@ -173,5 +194,3 @@ const res = this[kRes] | ||
const callback = res[kProxyCallback] | ||
if (!callback) { | ||
if (!res[kProxyCallback]) { | ||
return | ||
@@ -181,34 +200,27 @@ } | ||
const proxySocket = res[kProxySocket] | ||
const proxyReq = res[kProxyReq] | ||
const proxyRes = res[kProxyRes] | ||
const proxyReq = res[kProxyReq] | ||
const callback = res[kProxyCallback] | ||
res[kProxySocket] = undefined | ||
res[kProxyRes] = undefined | ||
res[kProxyReq] = undefined | ||
res[kSelf] = undefined | ||
res[kHead] = undefined | ||
res[kOnProxyRes] = undefined | ||
res[kProxyCallback] = undefined | ||
res[kProxySocket] = null | ||
res[kProxyReq] = null | ||
res[kProxyRes] = null | ||
res[kProxyCallback] = null | ||
res | ||
.removeListener('close', onComplete) | ||
.removeListener('finish', onComplete) | ||
.removeListener('error', onComplete) | ||
.off('close', onComplete) | ||
.off('finish', onComplete) | ||
.off('error', onComplete) | ||
req | ||
.removeListener('close', onComplete) | ||
.removeListener('aborted', onComplete) | ||
.removeListener('error', onComplete) | ||
.removeListener('timeout', onRequestTimeout) | ||
.off('close', onComplete) | ||
.off('aborted', onComplete) | ||
.off('error', onComplete) | ||
if (proxySocket) { | ||
if (proxySocket.destroy) { | ||
proxySocket.destroy() | ||
} | ||
proxySocket.destroy() | ||
} | ||
if (proxyRes) { | ||
if (proxyRes.destroy) { | ||
proxyRes.destroy() | ||
} | ||
proxyRes.destroy() | ||
} | ||
@@ -224,40 +236,21 @@ | ||
if (err) { | ||
err.statusCode = err.statusCode || 500 | ||
err.code = err.code || res.code | ||
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') { | ||
err.statusCode = 503 | ||
} else if (/HPE_INVALID/.test(err.code)) { | ||
err.statusCode = 502 | ||
} | ||
} | ||
if (res[kHead] === undefined) { | ||
callback.call(res[kSelf], err, req, res, { proxyReq, proxyRes }) | ||
} else { | ||
callback.call(res[kSelf], err, req, res, res[kHead], { proxyReq, proxyRes, proxySocket }) | ||
} | ||
callback(err) | ||
} | ||
function onRequestTimeout () { | ||
onComplete.call(this, new HttpError('request timeout', null, 408)) | ||
function onProxyConnect () { | ||
this[kConnected] = true | ||
this[kReq].pipe(this) | ||
} | ||
function onProxyError (err) { | ||
err.statusCode = 502 | ||
function onProxyReqError (err) { | ||
err.statusCode = this[kConnected] ? 502 : 503 | ||
onComplete.call(this, err) | ||
} | ||
function onProxyTimeout () { | ||
onComplete.call(this, new HttpError('gateway timeout', null, 504)) | ||
function onProxyReqTimeout () { | ||
onComplete.call(this, new HttpError('proxy timeout', 'ETIMEDOUT', 504)) | ||
} | ||
function onProxyAborted () { | ||
onComplete.call(this, new HttpError('response aborted', 'ECONNRESET', 502)) | ||
} | ||
function onProxyResponse (proxyRes) { | ||
async function onProxyReqResponse (proxyRes) { | ||
const res = this[kRes] | ||
const req = res[kReq] | ||
@@ -267,11 +260,11 @@ res[kProxyRes] = proxyRes | ||
const headers = setupHeaders(proxyRes.headers) | ||
proxyRes | ||
.on('aborted', onProxyAborted) | ||
.on('error', onComplete) | ||
.on('aborted', onProxyResAborted) | ||
.on('error', onProxyResError) | ||
const headers = setupHeaders(proxyRes.headers) | ||
if (res[kOnProxyRes]) { | ||
if (this[kOnRes]) { | ||
try { | ||
res[kOnProxyRes].call(res[kSelf], req, res, proxyRes, err => onComplete.call(this, err)) | ||
await this[kOnRes](proxyRes, headers) | ||
} catch (err) { | ||
@@ -294,3 +287,3 @@ onComplete.call(this, err) | ||
function onProxyUpgrade (proxyRes, proxySocket, proxyHead) { | ||
function onProxyReqUpgrade (proxyRes, proxySocket, proxyHead) { | ||
const res = this[kRes] | ||
@@ -310,4 +303,4 @@ | ||
proxySocket | ||
.on('error', onComplete) | ||
.on('close', onProxyAborted) | ||
.on('error', onProxyResError) | ||
.on('close', onProxyResAborted) | ||
.pipe(res) | ||
@@ -317,2 +310,11 @@ .pipe(proxySocket) | ||
function onProxyResError (err) { | ||
err.statusCode = 502 | ||
onComplete.call(this, err) | ||
} | ||
function onProxyResAborted () { | ||
onComplete.call(this, new HttpError('proxy aborted', 'ECONNRESET', 502)) | ||
} | ||
function createHttpHeader (line, headers) { | ||
@@ -333,3 +335,3 @@ let head = line | ||
function getRequestHeaders (req, { proxyName }) { | ||
function getRequestHeaders (req, proxyName) { | ||
const headers = {} | ||
@@ -342,7 +344,8 @@ for (const [ key, value ] of Object.entries(req.headers)) { | ||
// TODO(fix): <host> [ ":" <port> ] vs <pseudonym> | ||
// See, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via. | ||
if (proxyName) { | ||
proxyName = sanitize(proxyName) | ||
if (headers[VIA]) { | ||
for (const name of headers[VIA].split(',')) { | ||
if (sanitize(name).endsWith(proxyName)) { | ||
if (name.endsWith(proxyName)) { | ||
throw new HttpError('loop detected', null, 508) | ||
@@ -356,12 +359,23 @@ } | ||
headers[VIA] += `${req.httpVersion} ${proxyName}` | ||
headers[VIA] += `${req.httpVersion} ${req.proxyName}` | ||
} | ||
function printIp (address) { | ||
return /:.*:/.test(address) ? `"[${address}]"` : address | ||
function printIp (address, port) { | ||
const isIPv6 = net.isIPv6(address) | ||
let str = `${address}` | ||
if (isIPv6) { | ||
str = `[${str}]` | ||
} | ||
if (port) { | ||
str = `${str}:${port}` | ||
} | ||
if (isIPv6) { | ||
str = `"${str}"` | ||
} | ||
return str | ||
} | ||
const forwarded = [ | ||
`by=${printIp(req.socket.localAddress)}`, | ||
`for=${printIp(req.socket.remoteAddress)}`, | ||
`by=${printIp(req.socket.localAddress, req.socket.localPort)}`, | ||
`for=${printIp(req.socket.remoteAddress, req.socket.remotePort)}`, | ||
`proto=${req.socket.encrypted ? 'https' : 'http'}`, | ||
@@ -387,3 +401,3 @@ `host=${printIp(req.headers[AUTHORITY] || req.headers[HOST] || '')}` | ||
function setupHeaders (headers) { | ||
const connection = sanitize(headers[CONNECTION]) | ||
const connection = headers[CONNECTION] | ||
@@ -398,9 +412,11 @@ if (connection && connection !== CONNECTION && connection !== KEEP_ALIVE) { | ||
delete headers[CONNECTION] | ||
delete headers[PROXY_CONNECTION] | ||
delete headers[KEEP_ALIVE] | ||
delete headers[PROXY_AUTHENTICATE] | ||
delete headers[PROXY_AUTHORIZATION] | ||
delete headers[TE] | ||
delete headers[TRAILER] | ||
delete headers[TRANSFER_ENCODING] | ||
delete headers[TE] | ||
delete headers[UPGRADE] | ||
delete headers[PROXY_AUTHORIZATION] | ||
delete headers[PROXY_CONNECTION] | ||
delete headers[TRAILER] | ||
delete headers[HTTP2_SETTINGS] | ||
@@ -411,6 +427,2 @@ | ||
function sanitize (name) { | ||
return name ? name.trim().toLowerCase() : '' | ||
} | ||
class HttpError extends Error { | ||
@@ -417,0 +429,0 @@ constructor (msg, code, statusCode) { |
{ | ||
"name": "http2-proxy", | ||
"version": "4.2.15", | ||
"version": "5.0.0", | ||
"scripts": { | ||
@@ -5,0 +5,0 @@ "dev": "nodemon --inspect=9308 src", |
@@ -5,3 +5,3 @@ # node-http2-proxy | ||
### Features | ||
## Features | ||
@@ -15,3 +15,3 @@ - Proxies HTTP 2, HTTP 1 and WebSocket. | ||
### Installation | ||
## Installation | ||
@@ -22,5 +22,5 @@ ```sh | ||
### Notes | ||
## Notes | ||
`http2-proxy` requires at least node **v9.5.0**. | ||
`http2-proxy` requires at least node **v10.0.0**. | ||
@@ -49,3 +49,3 @@ Request & Response errors are emitted to the server object either as `clientError` for http/1 or `streamError` for http/2. See the NodeJS documentation for further details. | ||
### HTTP/1 API | ||
## HTTP/1 API | ||
@@ -71,4 +71,6 @@ You must pass `allowHTTP1: true` to the `http2.createServer` or `http2.createSecureServer` factory methods. | ||
#### Proxy HTTP/2, HTTP/1 and WebSocket | ||
## API | ||
### Proxy HTTP/2, HTTP/1 and WebSocket | ||
```js | ||
@@ -89,3 +91,3 @@ server.on('request', (req, res) => { | ||
#### Use [Connect](https://www.npmjs.com/package/connect) & [Helmet](https://www.npmjs.com/package/helmet) | ||
### Use [Connect](https://www.npmjs.com/package/connect) & [Helmet](https://www.npmjs.com/package/helmet) | ||
@@ -108,3 +110,3 @@ ```js | ||
#### Add x-forwarded headers | ||
### Add x-forwarded headers | ||
@@ -125,3 +127,3 @@ ```js | ||
#### Follow Redirects | ||
### Follow Redirects | ||
@@ -140,3 +142,3 @@ ```js | ||
#### web (req, res, options, [callback]) | ||
### web (req, res, options, [callback]) | ||
@@ -150,3 +152,3 @@ - `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). | ||
#### ws (req, socket, head, options, [callback]) | ||
### ws (req, socket, head, options, [callback]) | ||
@@ -170,5 +172,6 @@ - `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage). | ||
- `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. If returning a truthy value it will be used as the request. | ||
- `onReq(req, options, callback)`: Called before proxy request. If returning a truthy value it will be used as the 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). | ||
- `callback(err)`: Called on completion or error. Optionally a promise can be returned. | ||
- `onRes(req, resOrSocket, proxyRes, callback)`: Called before proxy response. | ||
@@ -178,3 +181,3 @@ - `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). | ||
- `proxyRes`: [`http.ServerResponse`](https://nodejs.org/api/http.html#http_class_http_serverresponse). | ||
- `callback(err)`: Called on completion or error.. | ||
- `callback(err)`: Called on completion or error. Optionally a promise can be returned. | ||
@@ -181,0 +184,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
Network access
Supply chain riskThis module accesses the network.
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
366
175
18631
9
5