Skip to content

Commit 546b81d

Browse files
committed
feat: added native HTTP2 stream API support
1 parent 54f00e8 commit 546b81d

File tree

2 files changed

+164
-56
lines changed

2 files changed

+164
-56
lines changed

README.md

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ $ npm install http2-proxy
2121

2222
`http2-proxy` requires node **v8.5.0** or newer with `http2` enabled. Pass the `--expose-http2` option when starting node **v8.x.x**.
2323

24-
### Usage
24+
### HTTP/1 API
2525

2626
You must pass `allowHTTP1: true` to the `http2.createServer` or `http2.createSecureServer` factory methods.
2727

@@ -33,7 +33,7 @@ const server = http2.createServer({ allowHTTP1: true })
3333
server.listen(8000)
3434
```
3535

36-
#### Proxy HTTP 1.1/2 and WebSocket
36+
#### Proxy HTTP/2, HTTP/1.1 and WebSocket
3737

3838
```js
3939
server.on('request', (req, res) => {
@@ -74,7 +74,7 @@ server.on('request', (req, res) => {
7474
})
7575
```
7676

77-
#### Add x-forwarded headers
77+
#### Add x-forwarded headers
7878

7979
```javascript
8080
server.on('request', (req, res) => {
@@ -94,8 +94,6 @@ server.on('request', (req, res) => {
9494
})
9595
```
9696

97-
### API
98-
9997
#### web (req, res, options, [callback])
10098

10199
- `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)
@@ -133,6 +131,79 @@ See [`upgrade`](https://nodejs.org/api/http.html#http_event_upgrade)
133131
- `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)
134132
- `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)
135133

134+
### HTTP/2 API
135+
136+
If HTTP/1 support is not required use the HTTP/2 `stream` API which has a lower overhead.
137+
138+
```js
139+
import http2 from 'http2'
140+
import proxy from 'http2-proxy'
141+
142+
const server = http2.createServer()
143+
server.listen(8000)
144+
```
145+
146+
#### Proxy HTTP/2
147+
148+
```js
149+
server.on('stream', (stream, headers) => {
150+
proxy.web(stream, headers, {
151+
hostname: 'localhost'
152+
port: 9000
153+
}, err => {
154+
if (err) {
155+
console.error('proxy error', err)
156+
}
157+
})
158+
})
159+
```
160+
161+
#### Add x-forwarded headers
162+
163+
```javascript
164+
server.on('stream', (stream, headers) => {
165+
proxy.web(stream, { headers }, {
166+
hostname: 'localhost'
167+
port: 9000,
168+
onReq: (stream, headers) => {
169+
headers['x-forwarded-for'] = stream.socket.remoteAddress
170+
headers['x-forwarded-proto'] = stream.socket.encrypted ? 'https' : 'http'
171+
headers['x-forwarded-host'] = stream.headers['host']
172+
}
173+
}, err => {
174+
if (err) {
175+
console.error('proxy error', err)
176+
}
177+
})
178+
})
179+
```
180+
181+
#### web (stream, headers, options, [callback])
182+
183+
- `stream`: [`http2.Http2Stream`](https://nodejs.org/api/http2.html#http2_class_http2_http2stream)
184+
- `headers`: Request headers object
185+
- `options`: See [Options](#options)
186+
- `callback(err)`: Called on completion or error. Optional
187+
188+
Returns a promise if no callback is provided.
189+
190+
See [`request`](https://nodejs.org/api/http.html#http_event_request)
191+
192+
### Options
193+
194+
- `hostname`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target hostname
195+
- `port`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target port
196+
- `proxyTimeout`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) timeout
197+
- `proxyName`: Proxy name used for **Via** header
198+
- `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
199+
- `onReq(req, options)`: Called before proxy request
200+
- `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)
201+
- `options`: Options passed to [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback)
202+
- `onRes(req, headers)`: Called before proxy response
203+
- `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)
204+
- `headers`: Response headers object
205+
206+
136207
### License
137208

138209
[MIT](LICENSE)

index.js

Lines changed: 88 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
HTTP2_HEADER_PROXY_AUTHORIZATION,
1919
HTTP2_HEADER_HTTP2_SETTINGS,
2020
HTTP2_HEADER_VIA,
21+
HTTP2_HEADER_STATUS,
2122
// XXX https://github.com/nodejs/node/issues/15337
2223
HTTP2_HEADER_FORWARDED = 'forwarded'
2324
} = http2.constants
@@ -26,8 +27,8 @@ module.exports = {
2627
ws (req, socket, head, options, callback) {
2728
proxy(req, socket, head, options, callback)
2829
},
29-
web (req, res, options, callback) {
30-
proxy(req, res, null, options, callback)
30+
web (reqOrStream, resOrHeaders, options, callback) {
31+
proxy(reqOrStream, resOrHeaders, null, options, callback)
3132
}
3233
}
3334

@@ -39,6 +40,12 @@ const kProxyRes = Symbol('proxyRes')
3940
const kProxySocket = Symbol('proxySocket')
4041
const kOnProxyRes = Symbol('onProxyRes')
4142

43+
const RESPOND_OPTIONS = {
44+
getTrailers: function () {
45+
return this[kProxyRes].trailers
46+
}
47+
}
48+
4249
function proxy (req, res, head, {
4350
hostname,
4451
port,
@@ -48,7 +55,19 @@ function proxy (req, res, head, {
4855
onReq,
4956
onRes
5057
}, callback) {
51-
req[kRes] = res
58+
let reqHeaders = req.respond
59+
let reqMethod = req.method
60+
let reqUrl = req.url
61+
62+
if (req instanceof http2.Http2Stream) {
63+
reqHeaders = res
64+
reqMethod = reqHeaders[HTTP2_HEADER_METHOD]
65+
reqUrl = reqHeaders[HTTP2_HEADER_PATH]
66+
67+
res = req
68+
} else {
69+
req[kRes] = res
70+
}
5271

5372
res[kReq] = req
5473
res[kRes] = res
@@ -67,41 +86,39 @@ function proxy (req, res, head, {
6786
})
6887
}
6988

70-
if (res instanceof net.Socket) {
71-
if (req.method !== 'GET') {
72-
return onFinish.call(res, createError('method not allowed', null, 405))
73-
}
74-
75-
if (sanitize(req.headers[HTTP2_HEADER_UPGRADE]) !== 'websocket') {
76-
return onFinish.call(res, createError('bad request', null, 400))
77-
}
78-
}
79-
8089
if (req.httpVersion !== '1.1' && req.httpVersion !== '2.0') {
8190
return onFinish.call(res, createError('http version not supported', null, 505))
8291
}
8392

84-
if (proxyName && req.headers[HTTP2_HEADER_VIA]) {
85-
for (const name of req.headers[HTTP2_HEADER_VIA].split(',')) {
93+
if (proxyName && reqHeaders[HTTP2_HEADER_VIA]) {
94+
for (const name of reqHeaders[HTTP2_HEADER_VIA].split(',')) {
8695
if (sanitize(name).endsWith(proxyName.toLowerCase())) {
8796
return onFinish.call(res, createError('loop detected', null, 508))
8897
}
8998
}
9099
}
91100

92-
if (timeout != null) {
93-
req.setTimeout(timeout, onRequestTimeout)
94-
}
95-
96101
if (res instanceof net.Socket) {
102+
if (reqMethod !== 'GET') {
103+
return onFinish.call(res, createError('method not allowed', null, 405))
104+
}
105+
106+
if (sanitize(reqHeaders[HTTP2_HEADER_UPGRADE]) !== 'websocket') {
107+
return onFinish.call(res, createError('bad request', null, 400))
108+
}
109+
97110
if (head && head.length) {
98111
res.unshift(head)
99112
}
100113

101114
setupSocket(res)
102115
}
103116

104-
const headers = getRequestHeaders(req)
117+
if (timeout != null) {
118+
req.setTimeout(timeout, onRequestTimeout)
119+
}
120+
121+
const headers = getRequestHeaders(reqHeaders, req.socket)
105122

106123
if (proxyName) {
107124
if (headers[HTTP2_HEADER_VIA]) {
@@ -112,10 +129,10 @@ function proxy (req, res, head, {
112129
}
113130

114131
const options = {
115-
method: req.method,
132+
method: reqMethod,
116133
hostname,
117134
port,
118-
path: req.url,
135+
path: reqUrl,
119136
headers,
120137
timeout: proxyTimeout
121138
}
@@ -132,14 +149,21 @@ function proxy (req, res, head, {
132149

133150
res[kProxyReq] = proxyReq
134151

135-
res
136-
.on('finish', onFinish)
137-
.on('close', onFinish)
138-
.on('error', onFinish)
152+
if (req instanceof http2.Http2Stream) {
153+
req
154+
.on('streamClosed', onFinish)
155+
.on('finish', onFinish)
156+
} else {
157+
req
158+
.on('aborted', onFinish)
159+
.on('close', onFinish)
160+
res
161+
.on('finish', onFinish)
162+
.on('close', onFinish)
163+
.on('error', onFinish)
164+
}
139165

140166
req
141-
.on('aborted', onFinish)
142-
.on('close', onFinish)
143167
.on('error', onFinish)
144168
.pipe(proxyReq)
145169
.on('error', onFinish)
@@ -191,7 +215,11 @@ function onFinish (err, statusCode) {
191215
if (res.headersSent !== false) {
192216
res.destroy()
193217
} else {
194-
res.writeHead(statusCode)
218+
if (res instanceof http2.Http2Stream) {
219+
res.respond({ [HTTP2_HEADER_STATUS]: statusCode })
220+
} else {
221+
res.writeHead(statusCode)
222+
}
195223
res.end()
196224
}
197225

@@ -227,16 +255,27 @@ function onProxyResponse (proxyRes) {
227255
} else {
228256
setupHeaders(proxyRes.headers)
229257

230-
res.statusCode = proxyRes.statusCode
231-
for (const key of Object.keys(proxyRes.headers)) {
232-
res.setHeader(key, proxyRes.headers[key])
233-
}
258+
if (res instanceof http2.Http2Stream) {
259+
proxyRes.headers[HTTP2_HEADER_STATUS] = proxyRes.status
234260

235-
if (this[kOnProxyRes]) {
236-
this[kOnProxyRes](this[kReq], res)
261+
if (this[kOnProxyRes]) {
262+
this[kOnProxyRes](this[kReq], proxyRes.headers)
263+
}
264+
265+
res.respond(proxyRes.headers, RESPOND_OPTIONS)
266+
} else {
267+
res.statusCode = proxyRes.statusCode
268+
for (const key of Object.keys(proxyRes.headers)) {
269+
res.setHeader(key, proxyRes.headers[key])
270+
}
271+
272+
if (this[kOnProxyRes]) {
273+
this[kOnProxyRes](this[kReq], res)
274+
}
275+
276+
res.writeHead(res.statusCode)
237277
}
238278

239-
res.writeHead(res.statusCode)
240279
proxyRes
241280
.on('end', onProxyTrailers)
242281
.on('error', onFinish)
@@ -298,28 +337,26 @@ function onProxyUpgrade (proxyRes, proxySocket, proxyHead) {
298337
.pipe(proxySocket)
299338
}
300339

301-
function getRequestHeaders (req) {
302-
const host = req.headers[HTTP2_HEADER_AUTHORITY] || req.headers[HTTP2_HEADER_HOST]
303-
const upgrade = req.headers[HTTP2_HEADER_UPGRADE]
304-
const forwarded = req.headers[HTTP2_HEADER_FORWARDED]
340+
function getRequestHeaders (reqHeaders, reqSocket) {
341+
const host = reqHeaders[HTTP2_HEADER_AUTHORITY] || reqHeaders[HTTP2_HEADER_HOST]
342+
const upgrade = reqHeaders[HTTP2_HEADER_UPGRADE]
343+
const forwarded = reqHeaders[HTTP2_HEADER_FORWARDED]
305344

306-
const headers = setupHeaders(Object.assign({}, req.headers))
345+
const headers = setupHeaders(Object.assign({}, reqHeaders))
307346

308-
if (req.httpVersionMajor === 2) {
309-
// Remove pseudo headers
310-
delete headers[HTTP2_HEADER_AUTHORITY]
311-
delete headers[HTTP2_HEADER_METHOD]
312-
delete headers[HTTP2_HEADER_PATH]
313-
delete headers[HTTP2_HEADER_SCHEME]
314-
}
347+
// Remove pseudo headers
348+
delete headers[HTTP2_HEADER_AUTHORITY]
349+
delete headers[HTTP2_HEADER_METHOD]
350+
delete headers[HTTP2_HEADER_PATH]
351+
delete headers[HTTP2_HEADER_SCHEME]
315352

316353
if (upgrade) {
317354
headers[HTTP2_HEADER_CONNECTION] = 'upgrade'
318355
headers[HTTP2_HEADER_UPGRADE] = 'websocket'
319356
}
320357

321-
headers[HTTP2_HEADER_FORWARDED] = `by=${req.socket.localAddress}`
322-
headers[HTTP2_HEADER_FORWARDED] += `; for=${req.socket.remoteAddress}`
358+
headers[HTTP2_HEADER_FORWARDED] = `by=${reqSocket.localAddress}`
359+
headers[HTTP2_HEADER_FORWARDED] += `; for=${reqSocket.remoteAddress}`
323360

324361
if (forwarded) {
325362
const expr = /for=\s*([^\s]+)/ig
@@ -336,7 +373,7 @@ function getRequestHeaders (req) {
336373
headers[HTTP2_HEADER_FORWARDED] += `; host=${host}`
337374
}
338375

339-
headers[HTTP2_HEADER_FORWARDED] += `; proto=${req.socket.encrypted ? 'https' : 'http'}`
376+
headers[HTTP2_HEADER_FORWARDED] += `; proto=${reqSocket.encrypted ? 'https' : 'http'}`
340377

341378
return headers
342379
}

0 commit comments

Comments
 (0)