├── .gitignore ├── benchmarks ├── README.md ├── proxy.js └── hello.js ├── .editorconfig ├── LICENSE ├── tests ├── compat.test.js └── proxy.test.js ├── package.json ├── index.d.ts ├── compat.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | ```sh 2 | $ wrk -c 20 -d30s -t 2 http://127.0.0.1:8000 3 | ``` 4 | -------------------------------------------------------------------------------- /benchmarks/proxy.js: -------------------------------------------------------------------------------- 1 | const proxy = require('../') 2 | require('http').createServer((req, res) => { 3 | proxy.web(req, res, { 4 | hostname: 'localhost', 5 | port: 9000 6 | }, err => err && console.error(err)) 7 | }).listen(8000) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /benchmarks/hello.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster') 2 | 3 | if (cluster.isMaster) { 4 | for (let n = 0; n < 8; ++n) { 5 | cluster.fork() 6 | } 7 | } else { 8 | require('http').createServer((req, res) => { 9 | res.end('Hello world!') 10 | }).listen(9000) 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 Robert Nagy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/compat.test.js: -------------------------------------------------------------------------------- 1 | const proxy = require('../index') 2 | const http = require('http') 3 | 4 | test('onReq and onRes are optional', async () => { 5 | let server 6 | let proxyServer 7 | let req 8 | try { 9 | await new Promise(resolve => { 10 | server = http.createServer((req, res) => { 11 | res.end() 12 | }).listen(0) 13 | proxyServer = http.createServer((req, res) => { 14 | proxy.web(req, res, { port: server.address().port }) 15 | }).listen(0, () => { 16 | req = http 17 | .get({ port: proxyServer.address().port }) 18 | .on('response', resolve) 19 | .end() 20 | }) 21 | }) 22 | } finally { 23 | server.close() 24 | proxyServer.close() 25 | req.abort() 26 | } 27 | }) 28 | 29 | test('onReq sets path', async () => { 30 | let server 31 | let proxyServer 32 | let req 33 | try { 34 | await new Promise(resolve => { 35 | server = http.createServer((req, res) => { 36 | expect(req.url).toEqual('/test') 37 | res.end() 38 | }).listen(0) 39 | proxyServer = http.createServer((req, res) => { 40 | proxy.web(req, res, { port: server.address().port, path: '/test' }) 41 | }).listen(0, () => { 42 | req = http 43 | .get({ port: proxyServer.address().port }) 44 | .on('response', resolve) 45 | .end() 46 | }) 47 | }) 48 | } finally { 49 | server.close() 50 | proxyServer.close() 51 | req.abort() 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http2-proxy", 3 | "version": "5.0.53", 4 | "types": "index.d.ts", 5 | "scripts": { 6 | "test": "jest", 7 | "lint": "eslint .", 8 | "format": "prettier --write *.js && eslint --fix *.js" 9 | }, 10 | "main": "index.js", 11 | "author": "Robert Nagy ", 12 | "license": "MIT", 13 | "repository": "nxtedition/node-http2-proxy", 14 | "keywords": [ 15 | "http2", 16 | "http", 17 | "proxy" 18 | ], 19 | "eslintConfig": { 20 | "extends": [ 21 | "standard" 22 | ], 23 | "overrides": [ 24 | { 25 | "files": [ 26 | "*.test.js" 27 | ], 28 | "env": { 29 | "jest": true 30 | }, 31 | "plugins": [ 32 | "jest" 33 | ] 34 | } 35 | ], 36 | "parser": "babel-eslint" 37 | }, 38 | "devDependencies": { 39 | "babel-eslint": "^10.1.0", 40 | "eslint": "^6.8.0", 41 | "eslint-config-standard": "^14.1.1", 42 | "eslint-plugin-import": "^2.20.2", 43 | "eslint-plugin-jest": "^23.8.2", 44 | "eslint-plugin-node": "^11.1.0", 45 | "eslint-plugin-promise": "^4.2.1", 46 | "eslint-plugin-standard": "^4.0.1", 47 | "husky": "^4.2.5", 48 | "jest": "^25.4.0", 49 | "prettier": "^2.0.5", 50 | "validate-commit-msg": "^2.14.0" 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "commit-msg": "validate-commit-msg", 55 | "pre-commit": "files=`git diff --cached --diff-filter=d --name-only | grep '\\.js$' || true` && ( [ -z \"$files\" ] || eslint --format=unix $files ) && yarn test" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/proxy.test.js: -------------------------------------------------------------------------------- 1 | const proxy = require('../index') 2 | const http = require('http') 3 | 4 | test('proxies when socket is sync', async () => { 5 | let server 6 | let proxyServer 7 | let req 8 | try { 9 | await new Promise((resolve, reject) => { 10 | server = http.createServer((req, res) => { 11 | res.end() 12 | }).listen(0) 13 | proxyServer = http.createServer((req, res) => { 14 | proxy({ req, res }, options => new Promise(resolve => { 15 | options.port = server.address().port 16 | const ureq = http.request(options) 17 | ureq.on('socket', () => resolve(ureq)) 18 | })) 19 | }).listen(0, () => { 20 | req = http 21 | .get({ port: proxyServer.address().port }) 22 | .on('response', resolve) 23 | .end() 24 | }) 25 | }) 26 | } finally { 27 | server.close() 28 | proxyServer.close() 29 | req.abort() 30 | } 31 | }) 32 | 33 | test('proxies when socket is async', async () => { 34 | let server 35 | let proxyServer 36 | let req 37 | try { 38 | await new Promise((resolve, reject) => { 39 | server = http.createServer((req, res) => { 40 | res.end() 41 | }).listen(0) 42 | proxyServer = http.createServer((req, res) => { 43 | proxy({ req, res }, options => { 44 | options.port = server.address().port 45 | return http.request(options) 46 | }) 47 | }).listen(0, () => { 48 | req = http 49 | .get({ port: proxyServer.address().port }) 50 | .on('response', resolve) 51 | .end() 52 | }) 53 | }) 54 | } finally { 55 | server.close() 56 | proxyServer.close() 57 | req.abort() 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'http2-proxy' { 2 | import * as Http from 'http'; 3 | import * as Http2 from 'http2'; 4 | import * as Net from 'net'; 5 | import * as Tls from 'tls'; 6 | 7 | // Http1-web 8 | export function web<_req extends Http.IncomingMessage, _res extends Http.ServerResponse>( 9 | req: _req, 10 | res: _res, 11 | options: http1WebOptions, 12 | callback?: ( 13 | err: Error, 14 | req: _req, 15 | res: _res 16 | ) => void 17 | ): Promise | void; 18 | 19 | // Http2-web 20 | export function web<_req extends Http2.Http2ServerRequest, _res extends Http2.Http2ServerResponse>( 21 | req: _req, 22 | res: _res, 23 | options: http2WebOptions, 24 | callback?: ( 25 | err: Error, 26 | req: _req, 27 | res: _res 28 | ) => void 29 | ): Promise | void; 30 | 31 | // Http1-ws 32 | export function ws<_req extends Http.IncomingMessage>( 33 | req: _req, 34 | socket: Net.Socket, 35 | head: Buffer, 36 | options: wsHttp1Options, 37 | callback?: ( 38 | err: Error, 39 | req: _req, 40 | socket: Net.Socket, 41 | head: Buffer 42 | ) => void 43 | ): Promise | void; 44 | 45 | // Http2-ws 46 | export function ws<_req extends Http2.Http2ServerRequest>( 47 | req: _req, 48 | socket: Tls.TLSSocket, 49 | head: Buffer, 50 | options: wsHttp2Options, 51 | callback?: ( 52 | err: Error, 53 | req: _req, 54 | socket: Tls.TLSSocket, 55 | head: Buffer 56 | ) => void 57 | ): Promise | void; 58 | 59 | 60 | interface http2Options extends Tls.ConnectionOptions { 61 | timeout?: number; 62 | hostname: string; 63 | port: number; 64 | protocol?: 'https'; 65 | path?: string; 66 | proxyTimeout?: number; 67 | proxyName?: string; 68 | socketPath?: string; 69 | 70 | onReq?( 71 | req: Http2.Http2ServerRequest, 72 | options: Http.RequestOptions, 73 | callback: (err?: Error) => void 74 | ): Promise; 75 | } 76 | 77 | interface http1Options extends Net.ConnectOpts { 78 | timeout?: number; 79 | hostname: string; 80 | port: number; 81 | protocol?: 'http' | 'https'; 82 | path?: string; 83 | proxyTimeout?: number; 84 | proxyName?: string; 85 | socketPath?: string; 86 | 87 | onReq?( 88 | req: Http.IncomingMessage, 89 | options: Http.RequestOptions, 90 | callback: (err?: Error) => void 91 | ): Promise; 92 | } 93 | 94 | interface http2WebOptions extends http2Options { 95 | onRes?( 96 | req: Http2.Http2ServerRequest, 97 | res: Http2.Http2ServerResponse, 98 | proxyRes: Http.IncomingMessage, 99 | callback: (err?: Error) => any 100 | ): Promise; 101 | } 102 | 103 | interface http1WebOptions extends http1Options { 104 | onRes?( 105 | req: Http.IncomingMessage, 106 | res: Http.ServerResponse, 107 | proxyRes: Http.IncomingMessage, 108 | callback: (err?: Error) => any 109 | ): Promise; 110 | } 111 | 112 | interface wsHttp2Options extends http2Options { 113 | onRes?( 114 | req: Http2.Http2ServerRequest, 115 | socket: Tls.TLSSocket, 116 | proxyRes: Http.IncomingMessage, 117 | callback: (err?: Error) => any 118 | ): Promise; 119 | } 120 | 121 | interface wsHttp1Options extends http1Options { 122 | onRes?( 123 | req: Http.IncomingMessage, 124 | socket: Net.Socket, 125 | proxyRes: Http.IncomingMessage, 126 | callback: (err?: Error) => any 127 | ): Promise; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /compat.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const https = require('https') 3 | 4 | const tlsOptions = [ 5 | 'ca', 6 | 'cert', 7 | 'ciphers', 8 | 'clientCertEngine', 9 | 'crl', 10 | 'dhparam', 11 | 'ecdhCurve', 12 | 'honorCipherOrder', 13 | 'key', 14 | 'passphrase', 15 | 'pfx', 16 | 'rejectUnauthorized', 17 | 'secureOptions', 18 | 'secureProtocol', 19 | 'servername', 20 | 'sessionIdContext', 21 | 'highWaterMark', 22 | 'checkServerIdentity', 23 | ]; 24 | 25 | module.exports = function (proxy) { 26 | proxy.ws = function ws (req, socket, head, options, callback) { 27 | const promise = compat({ req, socket, head }, options) 28 | if (!callback) { 29 | return promise 30 | } 31 | // Legacy compat... 32 | promise 33 | .then(() => callback(null, req, socket, head)) 34 | .catch(err => callback(err, req, socket, head)) 35 | } 36 | 37 | proxy.web = function web (req, res, options, callback) { 38 | const promise = compat({ req, res }, options) 39 | if (!callback) { 40 | return promise 41 | } 42 | // Legacy compat... 43 | promise 44 | .then(() => callback(null, req, res)) 45 | .catch(err => callback(err, req, res)) 46 | } 47 | 48 | async function compat (ctx, options) { 49 | const { req, res } = ctx 50 | 51 | const { 52 | hostname, 53 | port, 54 | path, 55 | socketPath, 56 | protocol, 57 | timeout, 58 | proxyTimeout, 59 | proxyName, 60 | onReq, 61 | onRes 62 | } = options 63 | 64 | // Legacy compat... 65 | if (timeout != null) { 66 | req.setTimeout(timeout) 67 | } 68 | 69 | await proxy( 70 | { ...ctx, proxyName }, 71 | async ureq => { 72 | for (const key of tlsOptions) { 73 | if (Reflect.has(options, key)) { 74 | const value = Reflect.get(options, key); 75 | Reflect.set(ureq, key, value); 76 | } 77 | } 78 | 79 | if (hostname !== undefined) { 80 | ureq.hostname = hostname 81 | } 82 | if (port !== undefined) { 83 | ureq.port = port 84 | } 85 | if (path !== undefined) { 86 | ureq.path = path 87 | } 88 | if (proxyTimeout !== undefined) { 89 | ureq.timeout = proxyTimeout 90 | } 91 | if (socketPath !== undefined) { 92 | ureq.socketPath = socketPath 93 | } 94 | 95 | let ret 96 | if (onReq) { 97 | if (onReq.length <= 2) { 98 | ret = await onReq(req, ureq) 99 | } else { 100 | // Legacy compat... 101 | ret = await new Promise((resolve, reject) => { 102 | const promiseOrReq = onReq(req, ureq, (err, val) => 103 | err ? reject(err) : resolve(val) 104 | ) 105 | if (promiseOrReq) { 106 | if (promiseOrReq.then) { 107 | promiseOrReq.then(resolve).catch(reject) 108 | } else if (promiseOrReq.abort) { 109 | resolve(promiseOrReq) 110 | } else { 111 | throw new Error( 112 | 'onReq must return a promise or a request object' 113 | ) 114 | } 115 | } else { 116 | resolve() 117 | } 118 | }) 119 | } 120 | } 121 | 122 | if (!ret) { 123 | let agent 124 | if (protocol == null || /^(http|ws):?$/.test(protocol)) { 125 | agent = http 126 | } else if (/^(http|ws)s:?$/.test(protocol)) { 127 | agent = https 128 | } else { 129 | throw new Error('invalid protocol') 130 | } 131 | ret = agent.request(ureq) 132 | } 133 | 134 | return ret 135 | }, 136 | onRes 137 | ? async (proxyRes, headers) => { 138 | proxyRes.headers = headers 139 | if (onRes.length <= 3) { 140 | return onRes(req, res, proxyRes) 141 | } else { 142 | // Legacy compat... 143 | return new Promise((resolve, reject) => { 144 | const promise = onRes(req, res, proxyRes, (err, val) => 145 | err ? reject(err) : resolve(val) 146 | ) 147 | if (promise && promise.then) { 148 | promise.then(resolve).catch(reject) 149 | } 150 | }) 151 | } 152 | } 153 | : null 154 | ) 155 | } 156 | 157 | return proxy 158 | } 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http2-proxy 2 | 3 | A simple http/2 & http/1.1 spec compliant proxy helper for Node. 4 | 5 | ## Features 6 | 7 | - Proxies HTTP 2, HTTP 1 and WebSocket. 8 | - Simple and high performance. 9 | - [Hop by hop header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers). 10 | - [Connection header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection). 11 | - [Via header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via). 12 | - [Forward header handling](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forward). 13 | 14 | ## Installation 15 | 16 | ```bash 17 | $ npm install http2-proxy 18 | ``` 19 | 20 | ## Notes 21 | 22 | `http2-proxy` requires at least node **v10.0.0**. 23 | 24 | Fully async/await compatible and all callback based usage is optional and discouraged. 25 | 26 | During 503 it is safe to assume that nothing was read or written. This makes it safe to retry request (including non idempotent methods). 27 | 28 | Use a final and/or error handler since errored responses won't be cleaned up automatically. This makes it possible to perform retries. 29 | 30 | ```js 31 | const finalhandler = require('finalhandler') 32 | 33 | const defaultWebHandler = (err, req, res) => { 34 | if (err) { 35 | console.error('proxy error', err) 36 | finalhandler(req, res)(err) 37 | } 38 | } 39 | 40 | const defaultWSHandler = (err, req, socket, head) => { 41 | if (err) { 42 | console.error('proxy error', err) 43 | socket.destroy() 44 | } 45 | } 46 | ``` 47 | 48 | ## HTTP/1 API 49 | 50 | You must pass `allowHTTP1: true` to the `http2.createServer` or `http2.createSecureServer` factory methods. 51 | 52 | ```js 53 | import http2 from 'http2' 54 | import proxy from 'http2-proxy' 55 | 56 | const server = http2.createServer({ allowHTTP1: true }) 57 | server.listen(8000) 58 | ``` 59 | 60 | You can also use `http-proxy2` with the old `http` && `https` API's. 61 | 62 | ```js 63 | import http from 'http' 64 | 65 | const server = http.createServer() 66 | server.listen(8000) 67 | ``` 68 | 69 | ## API 70 | 71 | ### Proxy HTTP/2, HTTP/1 and WebSocket 72 | 73 | ```js 74 | server.on('request', (req, res) => { 75 | proxy.web(req, res, { 76 | hostname: 'localhost' 77 | port: 9000 78 | }, defaultWebHandler) 79 | }) 80 | server.on('upgrade', (req, socket, head) => { 81 | proxy.ws(req, socket, head, { 82 | hostname: 'localhost' 83 | port: 9000 84 | }, defaultWsHandler) 85 | }) 86 | ``` 87 | 88 | ### Use [Connect](https://www.npmjs.com/package/connect) & [Helmet](https://www.npmjs.com/package/helmet) 89 | 90 | ```js 91 | const app = connect() 92 | app.use(helmet()) 93 | app.use((req, res, next) => proxy 94 | .web(req, res, { 95 | hostname: 'localhost' 96 | port: 9000 97 | }, err => { 98 | if (err) { 99 | next(err) 100 | } 101 | }) 102 | ) 103 | server.on('request', app) 104 | ``` 105 | 106 | ### Add x-forwarded Headers 107 | 108 | ```js 109 | server.on('request', (req, res) => { 110 | proxy.web(req, res, { 111 | hostname: 'localhost' 112 | port: 9000, 113 | onReq: (req, { headers }) => { 114 | headers['x-forwarded-for'] = req.socket.remoteAddress 115 | headers['x-forwarded-proto'] = req.socket.encrypted ? 'https' : 'http' 116 | headers['x-forwarded-host'] = req.headers['host'] 117 | } 118 | }, defaultWebHandler) 119 | }) 120 | ``` 121 | 122 | ### Follow Redirects 123 | 124 | ```js 125 | const http = require('follow-redirects').http 126 | 127 | server.on('request', (req, res) => { 128 | proxy.web(req, res, { 129 | hostname: 'localhost' 130 | port: 9000, 131 | onReq: (req, options) => http.request(options) 132 | }, defaultWebHandler) 133 | }) 134 | ``` 135 | 136 | ### Add Response Header 137 | 138 | ```js 139 | server.on('request', (req, res) => { 140 | proxy.web(req, res, { 141 | hostname: 'localhost' 142 | port: 9000, 143 | onReq: (req, options) => http.request(options), 144 | onRes: (req, res, proxyRes) => { 145 | res.setHeader('x-powered-by', 'http2-proxy') 146 | res.writeHead(proxyRes.statusCode, proxyRes.headers) 147 | proxyRes.pipe(res) 148 | } 149 | }, defaultWebHandler) 150 | }) 151 | ``` 152 | 153 | ### Proxy HTTP2 154 | 155 | HTTP proxying can be achieved using http2 client compat 156 | libraries such as: 157 | 158 | https://github.com/hisco/http2-client 159 | https://github.com/spdy-http2/node-spdy 160 | https://github.com/grantila/fetch-h2 161 | https://github.com/szmarczak/http2-wrapper 162 | 163 | ```js 164 | const http = require('http2-wrapper') 165 | 166 | server.on('request', (req, res) => { 167 | proxy.web(req, res, { 168 | hostname: 'localhost' 169 | port: 9000, 170 | onReq: (req, options) => http.request(options) 171 | }, defaultWebHandler) 172 | }) 173 | ``` 174 | 175 | ### Try Multiple Upstream Servers (Advanced) 176 | 177 | ```js 178 | const http = require('http') 179 | const proxy = require('http2-proxy') 180 | const createError = require('http-errors') 181 | 182 | server.on('request', async (req, res) => { 183 | try { 184 | res.statusCode = null 185 | for await (const { port, timeout, hostname } of upstream) { 186 | if (req.aborted || res.readableEnded) { 187 | return 188 | } 189 | 190 | let error = null 191 | let bytesWritten = 0 192 | try { 193 | return await proxy.web(req, res, { 194 | port, 195 | timeout, 196 | hostname, 197 | onRes: async (req, res, proxyRes) => { 198 | if (proxyRes.statusCode >= 500) { 199 | throw createError(proxyRes.statusCode, proxyRes.message) 200 | } 201 | 202 | function setHeaders () { 203 | if (!bytesWritten) { 204 | res.statusCode = proxyRes.statusCode 205 | for (const [ key, value ] of Object.entries(headers)) { 206 | res.setHeader(key, value) 207 | } 208 | } 209 | } 210 | 211 | // NOTE: At some point this will be possible 212 | // proxyRes.pipe(res) 213 | 214 | proxyRes 215 | .on('data', buf => { 216 | setHeaders() 217 | bytesWritten += buf.length 218 | if (!res.write(buf)) { 219 | proxyRes.pause() 220 | } 221 | }) 222 | .on('end', () => { 223 | setHeaders() 224 | res.addTrailers(proxyRes.trailers) 225 | res.end() 226 | }) 227 | .on('close', () => { 228 | res.off('drain', onDrain) 229 | })) 230 | 231 | res.on('drain', onDrain) 232 | 233 | function onDrain () { 234 | proxyRes.resume() 235 | } 236 | } 237 | }) 238 | } catch (err) { 239 | if (!err.statusCode) { 240 | throw err 241 | } 242 | 243 | error = err 244 | 245 | if (err.statusCode === 503) { 246 | continue 247 | } 248 | 249 | if (req.method === 'HEAD' || req.method === 'GET') { 250 | if (!bytesWritten) { 251 | continue 252 | } 253 | 254 | // TODO: Retry range request 255 | } 256 | 257 | throw err 258 | } 259 | } 260 | 261 | throw error || new createError.ServiceUnavailable() 262 | } catch (err) { 263 | defaultWebHandler(err) 264 | } 265 | } 266 | ``` 267 | 268 | ### `[async] web (req, res, options[, callback])` 269 | 270 | - `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). 271 | - `res`: [`http.ServerResponse`](https://nodejs.org/api/http.html#http_class_http_serverresponse) or [`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#http2_class_http2_http2serverresponse). 272 | - `options`: See [Options](#options) 273 | - `callback(err, req, res)`: Called on completion or error. 274 | 275 | See [`request`](https://nodejs.org/api/http.html#http_event_request) 276 | 277 | ### `[async] ws (req, socket, head, options[, callback])` 278 | 279 | - `req`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage). 280 | - `socket`: [`net.Socket`](https://nodejs.org/api/net.html#net_class_net_socket). 281 | - `head`: [`Buffer`](https://nodejs.org/api/buffer.html#buffer_class_buffer). 282 | - `options`: See [Options](#options). 283 | - `callback(err, req, socket, head)`: Called on completion or error. 284 | 285 | See [`upgrade`](https://nodejs.org/api/http.html#http_event_upgrade) 286 | 287 | ### `options` 288 | 289 | - `hostname`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target hostname. 290 | - `port`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) target port. 291 | - `protocol`: Agent protocol (`'http'` or `'https'`). Defaults to `'http'`. 292 | - `path`: Target pathname. Defaults to `req.originalUrl || req.url`. 293 | - `proxyTimeout`: Proxy [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback) timeout. 294 | - `proxyName`: Proxy name used for **Via** header. 295 | - `[async] onReq(req, options[, callback])`: Called before proxy request. If returning a truthy value it will be used as the request. 296 | - `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) 297 | - `options`: Options passed to [`http.request(options)`](https://nodejs.org/api/http.html#http_http_request_options_callback). 298 | - `callback(err)`: Called on completion or error. 299 | - `[async] onRes(req, resOrSocket, proxyRes[, callback])`: Called on proxy response. Writing of response must be done inside this method if provided. 300 | - `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). 301 | - `resOrSocket`: For `web` [`http.ServerResponse`](https://nodejs.org/api/http.html#http_class_http_serverresponse) 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). 302 | - `proxyRes`: [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage). 303 | - `callback(err)`: Called on completion or error. 304 | 305 | ## License 306 | 307 | [MIT](LICENSE) 308 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | const compat = require('./compat') 3 | 4 | const CONNECTION = 'connection' 5 | const HOST = 'host' 6 | const KEEP_ALIVE = 'keep-alive' 7 | const PROXY_AUTHORIZATION = 'proxy-authorization' 8 | const PROXY_AUTHENTICATE = 'proxy-authenticate' 9 | const PROXY_CONNECTION = 'proxy-connection' 10 | const TE = 'te' 11 | const FORWARDED = 'forwarded' 12 | const TRAILER = 'trailer' 13 | const TRANSFER_ENCODING = 'transfer-encoding' 14 | const UPGRADE = 'upgrade' 15 | const VIA = 'via' 16 | const AUTHORITY = ':authority' 17 | const HTTP2_SETTINGS = 'http2-settings' 18 | 19 | const kReq = Symbol('req') 20 | const kRes = Symbol('res') 21 | const kProxyCallback = Symbol('callback') 22 | const kProxyReq = Symbol('proxyReq') 23 | const kProxyRes = Symbol('proxyRes') 24 | const kProxySocket = Symbol('proxySocket') 25 | const kConnected = Symbol('connected') 26 | const kOnRes = Symbol('onRes') 27 | 28 | module.exports = compat(proxy) 29 | 30 | async function proxy ( 31 | { req, socket, res = socket, head, proxyName }, 32 | onReq, 33 | onRes 34 | ) { 35 | if (req.aborted) { 36 | return 37 | } 38 | 39 | const headers = getRequestHeaders(req, proxyName) 40 | 41 | if (head !== undefined) { 42 | if (req.method !== 'GET') { 43 | throw new HttpError('only GET request allowed', null, 405) 44 | } 45 | 46 | if (req.headers[UPGRADE] !== 'websocket') { 47 | throw new HttpError('missing upgrade header', null, 400) 48 | } 49 | 50 | if (head && head.length) { 51 | res.unshift(head) 52 | } 53 | 54 | setupSocket(res) 55 | 56 | headers[CONNECTION] = 'upgrade' 57 | headers[UPGRADE] = 'websocket' 58 | } 59 | 60 | const proxyReq = await onReq({ 61 | method: req.method, 62 | path: req.originalUrl || req.url, 63 | headers 64 | }) 65 | 66 | if (req.aborted) { 67 | if (proxyReq.abort) { 68 | proxyReq.abort() 69 | } else if (proxyReq.destroy) { 70 | proxyReq.destroy() 71 | } 72 | return 73 | } 74 | 75 | let callback 76 | const promise = new Promise((resolve, reject) => { 77 | callback = err => (err ? reject(err) : resolve()) 78 | }) 79 | 80 | req[kRes] = res 81 | req[kProxyReq] = proxyReq 82 | 83 | res[kReq] = req 84 | res[kRes] = res 85 | res[kProxySocket] = null 86 | res[kProxyRes] = null 87 | res[kProxyCallback] = callback 88 | 89 | proxyReq[kReq] = req 90 | proxyReq[kRes] = res 91 | proxyReq[kConnected] = false 92 | proxyReq[kOnRes] = onRes 93 | 94 | res 95 | .on('close', onComplete) 96 | .on('finish', onComplete) 97 | .on('error', onComplete) 98 | 99 | req 100 | .on('aborted', onComplete) 101 | .on('error', onComplete) 102 | 103 | proxyReq 104 | .on('error', onProxyReqError) 105 | .on('timeout', onProxyReqTimeout) 106 | .on('response', onProxyReqResponse) 107 | .on('upgrade', onProxyReqUpgrade) 108 | 109 | deferToConnect.call(proxyReq) 110 | 111 | return promise 112 | } 113 | 114 | function onSocket (socket) { 115 | if (!socket.connecting) { 116 | onProxyConnect.call(this) 117 | } else { 118 | socket.once('connect', onProxyConnect.bind(this)) 119 | } 120 | } 121 | 122 | function deferToConnect () { 123 | if (this.socket) { 124 | onSocket.call(this, this.socket) 125 | } else { 126 | this.once('socket', onSocket) 127 | } 128 | } 129 | 130 | function onComplete (err) { 131 | const res = this[kRes] 132 | const req = res[kReq] 133 | 134 | if (!res[kProxyCallback]) { 135 | return 136 | } 137 | 138 | const proxyReq = req[kProxyReq] 139 | 140 | const proxySocket = res[kProxySocket] 141 | const proxyRes = res[kProxyRes] 142 | const callback = res[kProxyCallback] 143 | 144 | req[kProxyReq] = null 145 | 146 | res[kProxySocket] = null 147 | res[kProxyRes] = null 148 | res[kProxyCallback] = null 149 | 150 | res 151 | .off('close', onComplete) 152 | .off('finish', onComplete) 153 | .off('error', onComplete) 154 | 155 | req 156 | .off('close', onComplete) 157 | .off('aborted', onComplete) 158 | .off('error', onComplete) 159 | .off('data', onReqData) 160 | .off('end', onReqEnd) 161 | 162 | if (err) { 163 | err.connectedSocket = Boolean(proxyReq && proxyReq[kConnected]) 164 | err.reusedSocket = Boolean(proxyReq && proxyReq.reusedSocket) 165 | } 166 | 167 | if (proxyReq) { 168 | proxyReq.off('drain', onProxyReqDrain) 169 | if (proxyReq.abort) { 170 | proxyReq.abort() 171 | } else if (proxyReq.destroy) { 172 | proxyReq.destroy() 173 | } 174 | } 175 | 176 | if (proxySocket) { 177 | proxySocket.destroy() 178 | } 179 | 180 | if (proxyRes) { 181 | proxyRes.destroy() 182 | } 183 | 184 | callback(err) 185 | } 186 | 187 | function onProxyConnect () { 188 | this[kConnected] = true 189 | 190 | if ( 191 | this.method === 'GET' || 192 | this.method === 'HEAD' || 193 | this.method === 'OPTIONS' 194 | ) { 195 | // Dump request. 196 | this[kReq].resume() 197 | this.end() 198 | } else { 199 | this[kReq] 200 | .on('data', onReqData) 201 | .on('end', onReqEnd) 202 | this 203 | .on('drain', onProxyReqDrain) 204 | } 205 | } 206 | 207 | function onReqEnd () { 208 | this[kProxyReq].end() 209 | } 210 | 211 | function onReqData (buf) { 212 | if (!this[kProxyReq].write(buf)) { 213 | this.pause() 214 | } 215 | } 216 | 217 | function onProxyReqDrain () { 218 | this[kReq].resume() 219 | } 220 | 221 | function onProxyReqError (err) { 222 | err.statusCode = this[kConnected] ? 502 : 503 223 | onComplete.call(this, err) 224 | } 225 | 226 | function onProxyReqTimeout () { 227 | onComplete.call(this, new HttpError('proxy timeout', 'ETIMEDOUT', 504)) 228 | } 229 | 230 | async function onProxyReqResponse (proxyRes) { 231 | const res = this[kRes] 232 | 233 | res[kProxyRes] = proxyRes 234 | proxyRes[kRes] = res 235 | 236 | const headers = setupHeaders(proxyRes.headers) 237 | 238 | proxyRes.on('aborted', onProxyResAborted).on('error', onProxyResError) 239 | 240 | if (this[kOnRes]) { 241 | try { 242 | await this[kOnRes](proxyRes, headers) 243 | } catch (err) { 244 | onComplete.call(this, err) 245 | } 246 | } else if (!res.writeHead) { 247 | if (!proxyRes.upgrade) { 248 | res.write( 249 | createHttpHeader( 250 | `HTTP/${proxyRes.httpVersion} ${proxyRes.statusCode} ${proxyRes.statusMessage}`, 251 | proxyRes.headers 252 | ) 253 | ) 254 | proxyRes.pipe(res) 255 | } 256 | } else { 257 | res.statusCode = proxyRes.statusCode 258 | for (const [key, value] of Object.entries(headers)) { 259 | res.setHeader(key, value) 260 | } 261 | proxyRes.on('end', onProxyResEnd).pipe(res) 262 | } 263 | } 264 | 265 | function onProxyReqUpgrade (proxyRes, proxySocket, proxyHead) { 266 | const res = this[kRes] 267 | 268 | res[kProxySocket] = proxySocket 269 | proxySocket[kRes] = res 270 | 271 | setupSocket(proxySocket) 272 | 273 | if (proxyHead && proxyHead.length) { 274 | proxySocket.unshift(proxyHead) 275 | } 276 | 277 | res.write( 278 | createHttpHeader('HTTP/1.1 101 Switching Protocols', proxyRes.headers) 279 | ) 280 | 281 | proxySocket 282 | .on('error', onProxyResError) 283 | .on('close', onProxyResAborted) 284 | .pipe(res) 285 | .pipe(proxySocket) 286 | } 287 | 288 | function onProxyResError (err) { 289 | err.statusCode = 502 290 | onComplete.call(this, err) 291 | } 292 | 293 | function onProxyResAborted () { 294 | onComplete.call(this, new HttpError('proxy aborted', 'ECONNRESET', 502)) 295 | } 296 | 297 | function onProxyResEnd () { 298 | if (this.trailers) { 299 | this[kRes].addTrailers(this.trailers) 300 | } 301 | } 302 | 303 | function createHttpHeader (line, headers) { 304 | let head = line 305 | for (const [key, value] of Object.entries(headers)) { 306 | if (!Array.isArray(value)) { 307 | head += `\r\n${key}: ${value}` 308 | } else { 309 | for (let i = 0; i < value.length; i++) { 310 | head += `\r\n${key}: ${value[i]}` 311 | } 312 | } 313 | } 314 | head += '\r\n\r\n' 315 | return Buffer.from(head, 'ascii') 316 | } 317 | 318 | function getRequestHeaders (req, proxyName) { 319 | const headers = {} 320 | for (const [key, value] of Object.entries(req.headers)) { 321 | if (key.charAt(0) !== ':' && key !== 'host') { 322 | headers[key] = value 323 | } 324 | } 325 | 326 | // TODO(fix): [ ":" ] vs 327 | // See, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via. 328 | if (proxyName) { 329 | if (headers[VIA]) { 330 | for (const name of headers[VIA].split(',')) { 331 | if (name.endsWith(proxyName)) { 332 | throw new HttpError('loop detected', null, 508) 333 | } 334 | } 335 | headers[VIA] += ',' 336 | } else { 337 | headers[VIA] = '' 338 | } 339 | 340 | headers[VIA] += `${req.httpVersion} ${proxyName}` 341 | } 342 | 343 | function printIp (address, port) { 344 | const isIPv6 = net.isIPv6(address) 345 | let str = `${address}` 346 | if (isIPv6) { 347 | str = `[${str}]` 348 | } 349 | if (port) { 350 | str = `${str}:${port}` 351 | } 352 | if (isIPv6 || port) { 353 | str = `"${str}"` 354 | } 355 | return str 356 | } 357 | 358 | const forwarded = [ 359 | `by=${printIp(req.socket.localAddress, req.socket.localPort)}`, 360 | `for=${printIp(req.socket.remoteAddress, req.socket.remotePort)}`, 361 | `proto=${req.socket.encrypted ? 'https' : 'http'}`, 362 | `host=${printIp(req.headers[AUTHORITY] || req.headers[HOST] || '')}` 363 | ].join(';') 364 | 365 | if (headers[FORWARDED]) { 366 | headers[FORWARDED] += `, ${forwarded}` 367 | } else { 368 | headers[FORWARDED] = `${forwarded}` 369 | } 370 | 371 | return setupHeaders(headers) 372 | } 373 | 374 | function setupSocket (socket) { 375 | socket.setTimeout(0) 376 | socket.setNoDelay(true) 377 | socket.setKeepAlive(true, 0) 378 | } 379 | 380 | function setupHeaders (headers) { 381 | const connection = headers[CONNECTION] 382 | 383 | if (connection && connection !== CONNECTION && connection !== KEEP_ALIVE) { 384 | for (const name of connection.toLowerCase().split(',')) { 385 | delete headers[name.trim()] 386 | } 387 | } 388 | 389 | // Remove hop by hop headers 390 | delete headers[CONNECTION] 391 | delete headers[PROXY_CONNECTION] 392 | delete headers[KEEP_ALIVE] 393 | delete headers[PROXY_AUTHENTICATE] 394 | delete headers[PROXY_AUTHORIZATION] 395 | delete headers[TE] 396 | delete headers[TRAILER] 397 | delete headers[TRANSFER_ENCODING] 398 | delete headers[UPGRADE] 399 | 400 | delete headers[HTTP2_SETTINGS] 401 | 402 | return headers 403 | } 404 | 405 | class HttpError extends Error { 406 | constructor (msg, code, statusCode) { 407 | super(msg) 408 | this.code = code 409 | this.statusCode = statusCode || 500 410 | } 411 | } 412 | --------------------------------------------------------------------------------