├── .eslintignore ├── .npmignore ├── .gitignore ├── index.js ├── .prettierrc ├── test ├── fixtures │ ├── agent2-csr.pem │ ├── agent2.cnf │ ├── agent2-key.pem │ └── agent2-cert.pem ├── lib-http-proxy-passes-ws-incoming-test.js ├── lib-https-proxy-test.js ├── lib-http-proxy-passes-web-incoming-test.js ├── lib-http-proxy-common-test.js ├── lib-http-proxy-passes-web-outgoing-test.js └── lib-http-proxy-test.js ├── lib ├── http-proxy.js └── http-proxy │ ├── index.js │ ├── passes │ ├── web-outgoing.js │ ├── ws-incoming.js │ └── web-incoming.js │ └── common.js ├── package.json ├── LICENSE └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | CHANGELOG.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/http-proxy"); 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "arrowParens": "always", 5 | "singleQuote": false, 6 | "quoteProps": "as-needed", 7 | "bracketSpacing": true, 8 | "printWidth": 120, 9 | "tabWidth": 2, 10 | "useTabs": false, 11 | "plugins": [] 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/agent2-csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBXTCCAQcCAQAwfTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMQswCQYDVQQH 3 | EwJTRjEPMA0GA1UEChMGSm95ZW50MRAwDgYDVQQLEwdOb2RlLmpzMQ8wDQYDVQQD 4 | EwZhZ2VudDIxIDAeBgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMFwwDQYJ 5 | KoZIhvcNAQEBBQADSwAwSAJBAMl2/Ba0XSm4ayi4C0rJ+tYtQu8O31VVXezkLJlf 6 | +6fVgdpVhYg5QlihlPUoiM/wOsDWQ1ALnNhPlcLaQk+etQECAwEAAaAlMCMGCSqG 7 | SIb3DQEJBzEWExRBIGNoYWxsZW5nZSBwYXNzd29yZDANBgkqhkiG9w0BAQUFAANB 8 | AJnll2pt5l0pzskQSpjjLVTlFDFmJr/AZ3UK8v0WxBjYjCe5Jx4YehkChpxIyDUm 9 | U3J9q9MDUf0+Y2+EGkssFfk= 10 | -----END CERTIFICATE REQUEST----- 11 | -------------------------------------------------------------------------------- /test/fixtures/agent2.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 1024 3 | days = 999 4 | distinguished_name = req_distinguished_name 5 | attributes = req_attributes 6 | prompt = no 7 | 8 | [ req_distinguished_name ] 9 | C = US 10 | ST = CA 11 | L = SF 12 | O = Joyent 13 | OU = Node.js 14 | CN = agent2 15 | emailAddress = ry@tinyclouds.org 16 | 17 | [ req_attributes ] 18 | challengePassword = A challenge password 19 | 20 | -------------------------------------------------------------------------------- /lib/http-proxy.js: -------------------------------------------------------------------------------- 1 | // Use explicit /index.js to help browserify negociation in require "/lib/http-proxy" (!) 2 | const { Server: ProxyServer } = require("./http-proxy/index.js"); 3 | /** 4 | * Creates the proxy server. 5 | * 6 | * Examples: 7 | * 8 | * httpProxy.createProxyServer({ .. }, 8000) 9 | * // => "{ web: [Function], ws: [Function] ... }" 10 | * 11 | * @param {Object} Options Config object passed to the proxy 12 | * @return {Object} Proxy Proxy object with handlers for `ws` and `web` requests 13 | * @public 14 | */ 15 | 16 | function createProxyServer(options) { 17 | return new ProxyServer(options); 18 | } 19 | 20 | ProxyServer.createProxyServer = createProxyServer; 21 | ProxyServer.createServer = createProxyServer; 22 | ProxyServer.createProxy = createProxyServer; 23 | 24 | /** 25 | * Export the proxy "Server" as the main export. 26 | */ 27 | module.exports = ProxyServer; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@squarecloud/http-proxy", 3 | "version": "1.2.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/squarecloudofc/http-proxy.git" 7 | }, 8 | "description": "A Zero-Dependency Full-Featured HTTP and WebSocket Proxy for Node.js.", 9 | "author": "Charlie Robbins ", 10 | "maintainers": [ 11 | "João Otávio " 12 | ], 13 | "main": "index.js", 14 | "devDependencies": { 15 | "expect.js": "~0.3.1", 16 | "mocha": "^10.5.0", 17 | "prettier": "^3.3.2", 18 | "socket.io": "^4.7.5", 19 | "socket.io-client": "^4.7.5", 20 | "sse": "^0.0.8", 21 | "ws": "^8.17.1" 22 | }, 23 | "scripts": { 24 | "test": "mocha --exit test/*-test.js", 25 | "prettier": "prettier --write ." 26 | }, 27 | "engines": { 28 | "node": ">=18.0.0" 29 | }, 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /test/fixtures/agent2-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKV5jJ6LzpyAnlFA 3 | aPHCWdufA1mNr0OMZn/1zt9nKg46QgRTPVD6WtTzf4a4xnSqj8vAt3sut19UfGRy 4 | RXjwpV6Amj+CTrqcT6anPi3xEERhX83vCr36jtWu+praxifhniDzQvclq8xmb5Fy 5 | aXrd+Mql+W/tQInL4K7lfSkHOVINAgMBAAECgYAgN8JBPEdGAWTRvRCxwX1tXVBx 6 | pzxwkm2CjIFyzctKad1gqjROB/CXdbFmsaMyI+NLcIJI4ZeiX/i4NeeA3JnVtEUu 7 | 2X78XVX1K3QdjfCIZ2tZLY8n4c64Vx/jhIhh/aEjTnj1hrv/HN3L9XbZ95z9w2XT 8 | 3/Ex+LjJAyvJi+QOAQJBANhZOu2UuSo+/ilxtfHEZJBGA3M4xf7q/jI4J440tXEd 9 | f7m63LxBwrxyIFsvFxVhKMRvuOAjo7MGTF4Mif4zATECQQDDzWTojUTE/28p/bj1 10 | i04+KqGytHzb44SNIEpxZ78J4RKL8aIKsA9Dpuo0OPToZHwnw/3gjV88wAcf0fN3 11 | LUedAkA5d3ovoVGN5y2b1Jh/n6sWp4JDGNF+5OygakoXe+i2Ix/KNdhsKQuRAr+V 12 | eOyowSYTB5oT72OMKiAe2zXMT8ABAkEAshLTqhgIqi0xGXxyQYZfM3SsOI2jXIwQ 13 | GhV6/qZ+/OogZvvsBlJWn41nRPVj7xKsQ7Ig659HNUXjdNIJ3aJllQJBALmRoCgY 14 | WIGyOHX2JEIEZZymuhSPTliCDyK9MPU754VXDEAlpdB6gsgRErOWWJvXvN+qJUjc 15 | ILXwkZ4ClmIzNPs= 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /test/fixtures/agent2-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICfDCCAeUCFFM7/7kdip5m1oSjTPp/GFkB+7yeMA0GCSqGSIb3DQEBCwUAMH0x 3 | CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoM 4 | BkpveWVudDEQMA4GA1UECwwHTm9kZS5qczEPMA0GA1UEAwwGYWdlbnQyMSAwHgYJ 5 | KoZIhvcNAQkBFhFyeUB0aW55Y2xvdWRzLm9yZzAeFw0yMzA5MTkxMDI1NDVaFw0z 6 | MzA5MTYxMDI1NDVaMH0xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UE 7 | BwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwHTm9kZS5qczEPMA0GA1UE 8 | AwwGYWdlbnQyMSAwHgYJKoZIhvcNAQkBFhFyeUB0aW55Y2xvdWRzLm9yZzCBnzAN 9 | BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApXmMnovOnICeUUBo8cJZ258DWY2vQ4xm 10 | f/XO32cqDjpCBFM9UPpa1PN/hrjGdKqPy8C3ey63X1R8ZHJFePClXoCaP4JOupxP 11 | pqc+LfEQRGFfze8KvfqO1a76mtrGJ+GeIPNC9yWrzGZvkXJpet34yqX5b+1Aicvg 12 | ruV9KQc5Ug0CAwEAATANBgkqhkiG9w0BAQsFAAOBgQBWLRmk3Xko/LojOOin1H+I 13 | PeIryeFlN7Pl7x9xtui+5Dm7y7Wf1b33nUqo/PedJUzKvDFyMuWLaPODWOH6yBmv 14 | rOuP6AwLk4IH0IEfqljj3i3puMJ5ZTpJgh0rcOJJIp7N/vEk3UyZcQdt2CFMC9yf 15 | sUKsqr1SlQ4LM7Srn0hk6g== 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Square Cloud LTDA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ---- 24 | 25 | Based on http-party/node-http-proxy 26 | 27 | Copyright (c) 2010-present Charlie Robbins, Jarrett Cruger & the Contributors. 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining 30 | a copy of this software and associated documentation files (the 31 | "Software"), to deal in the Software without restriction, including 32 | without limitation the rights to use, copy, modify, merge, publish, 33 | distribute, sublicense, and/or sell copies of the Software, and to 34 | permit persons to whom the Software is furnished to do so, subject to 35 | the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be 38 | included in all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 42 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 43 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 44 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 45 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 46 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/lib-http-proxy-passes-ws-incoming-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const httpProxy = require("../lib/http-proxy/passes/ws-incoming"); 3 | const expect = require("expect.js"); 4 | 5 | describe("lib/http-proxy/passes/ws-incoming.js", () => { 6 | describe("#checkMethodAndHeader", () => { 7 | it("should drop non-GET connections", () => { 8 | let destroyCalled = false; 9 | const stubRequest = { 10 | method: "DELETE", 11 | headers: {}, 12 | }; 13 | const stubSocket = { 14 | destroy: () => { 15 | // Simulate Socket.destroy() method when call 16 | destroyCalled = true; 17 | }, 18 | }; 19 | returnValue = httpProxy.checkMethodAndHeader(stubRequest, stubSocket); 20 | expect(returnValue).to.be(true); 21 | expect(destroyCalled).to.be(true); 22 | }); 23 | 24 | it("should drop connections when no upgrade header", () => { 25 | let destroyCalled = false; 26 | const stubRequest = { 27 | method: "GET", 28 | headers: {}, 29 | }; 30 | const stubSocket = { 31 | destroy: () => { 32 | // Simulate Socket.destroy() method when call 33 | destroyCalled = true; 34 | }, 35 | }; 36 | returnValue = httpProxy.checkMethodAndHeader(stubRequest, stubSocket); 37 | expect(returnValue).to.be(true); 38 | expect(destroyCalled).to.be(true); 39 | }); 40 | 41 | it("should drop connections when upgrade header is different of `websocket`", () => { 42 | let destroyCalled = false; 43 | const stubRequest = { 44 | method: "GET", 45 | headers: { 46 | upgrade: "anotherprotocol", 47 | }, 48 | }; 49 | const stubSocket = { 50 | destroy: () => { 51 | // Simulate Socket.destroy() method when call 52 | destroyCalled = true; 53 | }, 54 | }; 55 | returnValue = httpProxy.checkMethodAndHeader(stubRequest, stubSocket); 56 | expect(returnValue).to.be(true); 57 | expect(destroyCalled).to.be(true); 58 | }); 59 | 60 | it("should return nothing when all is ok", () => { 61 | let destroyCalled = false; 62 | const stubRequest = { 63 | method: "GET", 64 | headers: { 65 | upgrade: "websocket", 66 | }, 67 | }; 68 | const stubSocket = { 69 | destroy: () => { 70 | // Simulate Socket.destroy() method when call 71 | destroyCalled = true; 72 | }, 73 | }; 74 | returnValue = httpProxy.checkMethodAndHeader(stubRequest, stubSocket); 75 | expect(returnValue).to.be(undefined); 76 | expect(destroyCalled).to.be(false); 77 | }); 78 | }); 79 | 80 | describe("#XHeaders", () => { 81 | it("return if no forward request", () => { 82 | const returnValue = httpProxy.XHeaders({}, {}, {}); 83 | expect(returnValue).to.be(undefined); 84 | }); 85 | 86 | it("set the correct X-Forwarded-* headers from req.connection", () => { 87 | const stubRequest = { 88 | connection: { 89 | remoteAddress: "192.168.1.2", 90 | remotePort: "8080", 91 | }, 92 | headers: { 93 | host: "192.168.1.2:8080", 94 | }, 95 | }; 96 | httpProxy.XHeaders(stubRequest, {}, { xfwd: true }); 97 | expect(stubRequest.headers["X-Forwarded-For"]).to.be("192.168.1.2"); 98 | expect(stubRequest.headers["X-Forwarded-Port"]).to.be(8080); 99 | expect(stubRequest.headers["X-Forwarded-Proto"]).to.be("ws"); 100 | }); 101 | 102 | it("set the correct X-Forwarded-* headers from req.socket", () => { 103 | const stubRequest = { 104 | socket: { 105 | remoteAddress: "192.168.1.3", 106 | remotePort: "8181", 107 | }, 108 | connection: { 109 | pair: true, 110 | }, 111 | headers: { 112 | host: "192.168.1.3:8181", 113 | }, 114 | }; 115 | httpProxy.XHeaders(stubRequest, {}, { xfwd: true }); 116 | expect(stubRequest.headers["X-Forwarded-For"]).to.be("192.168.1.3"); 117 | expect(stubRequest.headers["X-Forwarded-Port"]).to.be(8181); 118 | expect(stubRequest.headers["X-Forwarded-Proto"]).to.be("wss"); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /lib/http-proxy/index.js: -------------------------------------------------------------------------------- 1 | const EE3 = require("node:events"); 2 | const http = require("node:http"); 3 | const https = require("node:https"); 4 | const web = require("./passes/web-incoming"); 5 | const ws = require("./passes/ws-incoming"); 6 | 7 | /** 8 | * Returns a function that creates the loader for 9 | * either `ws` or `web`"s passes. 10 | * 11 | * 12 | * @param {String} Type Either "ws" or "web" 13 | * 14 | * @return {Function} Loader Function that when called returns an iterator for the right passes 15 | * @private 16 | */ 17 | 18 | function createRightProxy(type) { 19 | return function (options) { 20 | return function (req, res /*, [head], [opts] */) { 21 | const passes = type === "ws" ? this.wsPasses : this.webPasses; 22 | const args = [].slice.call(arguments); 23 | let cntr = args.length - 1; 24 | let head; 25 | let cbl; 26 | 27 | /* optional args parse begin */ 28 | if (typeof args[cntr] === "function") { 29 | cbl = args[cntr]; 30 | cntr--; 31 | } 32 | 33 | let requestOptions = options; 34 | if (!(args[cntr] instanceof Buffer) && args[cntr] !== res) { 35 | requestOptions = Object.assign({}, options); 36 | Object.assign(requestOptions, args[cntr]); 37 | 38 | cntr--; 39 | } 40 | 41 | if (args[cntr] instanceof Buffer) { 42 | head = args[cntr]; 43 | } 44 | 45 | /* optional args parse end */ 46 | ["target", "forward"].forEach(function (e) { 47 | if (typeof requestOptions[e] === "string") { 48 | requestOptions[e] = new URL(requestOptions[e]); 49 | } 50 | }); 51 | 52 | if (!requestOptions.target && !requestOptions.forward) { 53 | return this.emit("error", new Error("Must provide a proper URL as target")); 54 | } 55 | 56 | for (let i = 0; i < passes.length; i++) { 57 | /** 58 | * Call of passes functions 59 | * pass(req, res, options, head) 60 | * 61 | * In WebSockets case the `res` variable 62 | * refer to the connection socket 63 | * pass(req, socket, options, head) 64 | */ 65 | if (passes[i](req, res, requestOptions, head, this, cbl)) { 66 | break; 67 | } 68 | } 69 | }; 70 | }; 71 | } 72 | 73 | class ProxyServer { 74 | constructor(options) { 75 | EE3.call(this); 76 | 77 | options = options || {}; 78 | options.prependPath = options.prependPath !== false; 79 | 80 | this.web = this.proxyRequest = createRightProxy("web")(options); 81 | this.ws = this.proxyWebsocketRequest = createRightProxy("ws")(options); 82 | this.options = options; 83 | 84 | this.webPasses = Object.keys(web).map((pass) => web[pass]); 85 | this.wsPasses = Object.keys(ws).map((pass) => ws[pass]); 86 | 87 | this.on("error", this.onError.bind(this)); 88 | } 89 | 90 | onError(err) { 91 | if (this.listeners(this, "error") === 1) { 92 | throw err; 93 | } 94 | } 95 | 96 | listen(port, hostname) { 97 | const closure = (req, res) => this.web(req, res); 98 | 99 | const server = this.options.ssl ? https.createServer(this.options.ssl, closure) : http.createServer(closure); 100 | 101 | if (this.options.ws) { 102 | server.on("upgrade", (req, socket, head) => this.ws(req, socket, head)); 103 | } 104 | 105 | server.listen(port, hostname); 106 | this._server = server; 107 | 108 | return this; 109 | } 110 | 111 | close(callback) { 112 | if (this._server) { 113 | this._server.close((err) => { 114 | this._server = null; 115 | if (callback) { 116 | callback?.(err); 117 | } 118 | }); 119 | } 120 | } 121 | 122 | before(type, passName, callback) { 123 | addPass(type === "ws" ? this.wsPasses : this.webPasses, type, passName, callback, false); 124 | } 125 | 126 | after(type, passName, callback) { 127 | addPass(type === "ws" ? this.wsPasses : this.webPasses, type, passName, callback, true); 128 | } 129 | } 130 | 131 | require("node:util").inherits(ProxyServer, EE3); 132 | 133 | function addPass(passes, type, passName, callback, isAfter) { 134 | if (!["ws", "web"].includes(type)) { 135 | throw new Error("type must be `web` or `ws`"); 136 | } 137 | 138 | const passIndex = passes.findIndex((pass) => pass.name === passName); 139 | 140 | if (passIndex === -1) { 141 | throw new Error("No such pass"); 142 | } 143 | 144 | passes.splice(passIndex + (isAfter ? 1 : 0), 0, callback); 145 | } 146 | 147 | module.exports.Server = ProxyServer; 148 | module.exports.createRightProxy = createRightProxy; 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @squarecloud/http-proxy 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | 7 | A Full-Featured HTTP and WebSocket Proxy for Node.js forked from [http-party/node-http-proxy](https://github.com/http-party/node-http-proxy). 8 | 9 | This fork adds the following features: 10 | 11 | - Dependencies updates & security fixes. 12 | - Websocket close before response fixed. 13 | - Memory leak fixed when client closes prematurely. 14 | - Add support for Lookup option. Remove old followRedirects option. 15 | - Support for modifying content of websocket streams. 16 | - Respect NODE_TLS_REJECT_UNAUTHORIZED environment variable. 17 | - Fix for issues when using an Agent, fix the type error when req.socket is undefined. 18 | - Fixed bug when http:/ isn't replaced with: http:// 19 | - Fixed X-Forwarded-\* not capitalized. 20 | 21 | Inspired by the project [Ambassify project](https://github.com/ambassify/node-http-proxy). 22 | 23 | ## Usage 24 | 25 | Install package: 26 | 27 | ```sh 28 | # npm 29 | npm install @squarecloud/http-proxy 30 | 31 | # yarn 32 | yarn add @squarecloud/http-proxy 33 | 34 | # pnpm 35 | pnpm install @squarecloud/http-proxy 36 | 37 | # bun 38 | bun install @squarecloud/http-proxy 39 | ``` 40 | 41 | Create proxy: 42 | 43 | ```js 44 | const { createServer } = require("node:http"); 45 | const { createProxyServer } = require("@squarecloud/http-proxy"); 46 | 47 | const proxy = createProxyServer({}); 48 | const target = "http://example.com"; /* address of your proxy server here */ 49 | 50 | const server = createServer(async (req, res) => { 51 | try { 52 | await proxy.web(req, res, { target }); 53 | } catch (error) { 54 | console.error(error); 55 | res.statusCode = 500; 56 | res.end("Proxy error: " + error.toString()); 57 | } 58 | }); 59 | 60 | server.listen(80, () => console.log("Proxy is listening on http://localhost")); 61 | ``` 62 | 63 | Example with WebSocket: 64 | 65 | ```js 66 | const { createServer } = require("node:http"); 67 | const { createProxyServer } = require("@squarecloud/http-proxy"); 68 | 69 | const proxy = createProxyServer({ ws: true }); 70 | const target = "ws://example.com"; /* address of your proxy server here */ 71 | 72 | const server = createServer(async (req, res) => { 73 | /* ... */ 74 | }); 75 | 76 | server.on("upgrade", async (req, socket, head) => { 77 | try { 78 | // use proxy.ws() instead of proxy.web() for proxying WebSocket requests. 79 | await proxy.ws(req, socket, head, { target }); 80 | } catch (error) { 81 | console.error(error); 82 | socket.end(); 83 | } 84 | }); 85 | 86 | server.listen(80, () => console.log("Proxy is listening on http://localhost")); 87 | ``` 88 | 89 | Some options: 90 | 91 | ```js 92 | // Options most used in the proxy configuration: 93 | // * ws : 94 | // * xfwd : 95 | // * secure : 96 | // * prependPath: 97 | // * ignorePath: 98 | // * proxyTimeoutCustomError: true/false, default: false - specify whether you want to throw a custom `ETIMEDOUT` error when the `proxyTimeout` is reached. If false then the default `ECONNRESET` error will be thrown. 99 | ``` 100 | 101 | Checkout [http-party/node-http-proxy](https://github.com/http-party/node-http-proxy#options) for more options and examples. 102 | 103 | ## Development 104 | 105 | - Clone this repository 106 | - Install latest LTS version of [Node.js](https://nodejs.org/en/) 107 | - Install dependencies using `npm install` 108 | - Run interactive tests using `npm run test` 109 | 110 | ## License 111 | 112 | Published under [MIT License](./LICENSE). 113 | 114 | Made with 💙 & Supported by [Square Cloud | A hosting company](https://squarecloud.app). 115 | 116 | 117 | 118 | [npm-version-src]: https://img.shields.io/npm/v/@squarecloud/http-proxy?style=flat&colorA=18181B&colorB=2563eb 119 | [npm-version-href]: https://npmjs.com/package/@squarecloud/http-proxy 120 | [npm-downloads-src]: https://img.shields.io/npm/dm/@squarecloud/http-proxy?style=flat&colorA=18181B&colorB=2563eb 121 | [npm-downloads-href]: https://npmjs.com/package/@squarecloud/http-proxy 122 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@squarecloud/http-proxy?style=flat&colorA=18181B&colorB=2563eb 123 | [bundle-href]: https://bundlephobia.com/result?p=@squarecloud/http-proxy 124 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/web-outgoing.js: -------------------------------------------------------------------------------- 1 | const common = require("../common"); 2 | const redirectRegex = /^201|30(1|2|7|8)$/; 3 | 4 | /*! 5 | * Array of passes. 6 | * 7 | * A `pass` is just a function that is executed on `req, res, options` 8 | * so that you can easily add new checks while still keeping the base 9 | * flexible. 10 | */ 11 | 12 | module.exports = { 13 | /** 14 | * If is a HTTP 1.0 request, remove chunk headers 15 | * 16 | * @param {ClientRequest} Req Request object 17 | * @param {IncomingMessage} Res Response object 18 | * @param {proxyResponse} Res Response object from the proxy request 19 | * @private 20 | */ 21 | removeChunked: (req, res, proxyRes) => { 22 | if (req.httpVersion === "1.0" || proxyRes.statusCode === 204 || proxyRes.statusCode === 304) { 23 | delete proxyRes.headers["transfer-encoding"]; 24 | } 25 | }, 26 | 27 | /** 28 | * If is a HTTP 1.0 request, set the correct connection header 29 | * or if connection header not present, then use `keep-alive` 30 | * 31 | * @param {ClientRequest} Req Request object 32 | * @param {IncomingMessage} Res Response object 33 | * @param {proxyResponse} Res Response object from the proxy request 34 | * @private 35 | */ 36 | setConnection: (req, res, proxyRes) => { 37 | // Prioritize HTTP/2 handling for best performance: 38 | if (req.httpVersion === "2.0") { 39 | // No need to set Connection header for HTTP/2 40 | return; 41 | } 42 | 43 | if (req.httpVersion === "1.0") { 44 | proxyRes.headers.connection = req.headers.connection || "close"; 45 | } else if (!proxyRes.headers.connection) { 46 | proxyRes.headers.connection = req.headers.connection || "keep-alive"; 47 | } 48 | }, 49 | 50 | setRedirectHostRewrite: (req, res, proxyRes, options) => { 51 | if ( 52 | (options.hostRewrite || options.autoRewrite || options.protocolRewrite) && 53 | proxyRes.headers.location && 54 | redirectRegex.test(proxyRes.statusCode) 55 | ) { 56 | const target = new URL(options.target); 57 | const u = new URL(proxyRes.headers.location); 58 | 59 | if (target.host !== u.host) { 60 | return; 61 | } 62 | 63 | u.host = options.hostRewrite || (options.autoRewrite ? req.headers.host : u.host); 64 | u.protocol = options.protocolRewrite ?? u.protocol; 65 | 66 | proxyRes.headers.location = u.href; 67 | } 68 | }, 69 | /** 70 | * Copy headers from proxyResponse to response 71 | * set each header in response object. 72 | * 73 | * @param {ClientRequest} Req Request object 74 | * @param {IncomingMessage} Res Response object 75 | * @param {proxyResponse} Res Response object from the proxy request 76 | * @param {Object} Options options.cookieDomainRewrite: Config to rewrite cookie domain 77 | * @private 78 | */ 79 | writeHeaders: (req, res, proxyRes, options) => { 80 | // Remember, do not use 'const' for 'rewrite' since it needs to be mutable (to avoid TypeError: Assignment to constant variable). 81 | let rewriteCookieDomainConfig = options.cookieDomainRewrite; 82 | let rewriteCookiePathConfig = options.cookiePathRewrite; 83 | const preserveHeaderKeyCase = options.preserveHeaderKeyCase; 84 | 85 | let rawHeaderKeyMap; 86 | const setHeader = function (key, header) { 87 | if (header === undefined) return; 88 | 89 | if (key.toLowerCase() === "set-cookie") { 90 | if (rewriteCookieDomainConfig) { 91 | header = common.rewriteCookieProperty(header, rewriteCookieDomainConfig, "domain"); 92 | } 93 | if (rewriteCookiePathConfig) { 94 | header = common.rewriteCookieProperty(header, rewriteCookiePathConfig, "path"); 95 | } 96 | } 97 | 98 | try { 99 | res.setHeader(String(key).trim(), header); 100 | } catch (error) { 101 | console.warn(error, key, header); 102 | } 103 | }; 104 | 105 | if (typeof rewriteCookieDomainConfig === "string") { 106 | rewriteCookieDomainConfig = { "*": rewriteCookieDomainConfig }; 107 | } 108 | 109 | if (typeof rewriteCookiePathConfig === "string") { 110 | rewriteCookiePathConfig = { "*": rewriteCookiePathConfig }; 111 | } 112 | 113 | if (preserveHeaderKeyCase && proxyRes.rawHeaders) { 114 | rawHeaderKeyMap = {}; 115 | 116 | for (const [key] of proxyRes.rawHeaders) { 117 | if (typeof key === "string") { 118 | // Ensure key is a string for case conversion 119 | rawHeaderKeyMap[key.toLowerCase()] = key; 120 | } 121 | } 122 | } 123 | 124 | if (!preserveHeaderKeyCase || !rawHeaderKeyMap) { 125 | for (const [key, header] of Object.entries(proxyRes.headers)) { 126 | setHeader(key, header); 127 | } 128 | return; 129 | } 130 | 131 | for (const [key, header] of Object.entries(proxyRes.headers)) { 132 | const originalKey = rawHeaderKeyMap[key] || key; 133 | setHeader(originalKey, header); 134 | } 135 | }, 136 | 137 | /** 138 | * Set the statusCode from the proxyResponse 139 | * 140 | * @param {ClientRequest} Req Request object 141 | * @param {IncomingMessage} Res Response object 142 | * @param {proxyResponse} Res Response object from the proxy request 143 | * @private 144 | */ 145 | writeStatusCode: (req, res, proxyRes) => { 146 | res.statusCode = proxyRes.statusCode; 147 | if (proxyRes.statusMessage) { 148 | res.statusMessage = proxyRes.statusMessage; 149 | } 150 | }, 151 | }; 152 | -------------------------------------------------------------------------------- /test/lib-https-proxy-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const httpProxy = require("../lib/http-proxy"); 3 | const { readFileSync } = require("node:fs"); 4 | const { join } = require("node:path"); 5 | const expect = require("expect.js"); 6 | const https = require("node:https"); 7 | const http = require("node:http"); 8 | // 9 | // Expose a port number generator. 10 | // thanks to @3rd-Eden 11 | // 12 | let initialPort = 1024; 13 | const gen = {}; 14 | Object.defineProperty(gen, "port", { 15 | get: function get() { 16 | return initialPort++; 17 | }, 18 | }); 19 | 20 | describe("lib/http-proxy.js", () => { 21 | describe("HTTPS #createProxyServer", () => { 22 | describe("HTTPS to HTTP", () => { 23 | it("should proxy the request en send back the response", function (done) { 24 | const ports = { source: gen.port, proxy: gen.port }; 25 | const source = http.createServer(function (req, res) { 26 | expect(req.method).to.eql("GET"); 27 | expect(req.headers.host.split(":")[1]).to.eql(ports.proxy); 28 | res.writeHead(200, { "Content-Type": "text/plain" }); 29 | res.end("Hello from " + ports.source); 30 | }); 31 | 32 | source.listen(ports.source); 33 | 34 | const proxy = httpProxy 35 | .createProxyServer({ 36 | target: "http://127.0.0.1:" + ports.source, 37 | ssl: { 38 | key: readFileSync(join(__dirname, "fixtures", "agent2-key.pem")), 39 | cert: readFileSync(join(__dirname, "fixtures", "agent2-cert.pem")), 40 | ciphers: "AES128-GCM-SHA256", 41 | }, 42 | }) 43 | .listen(ports.proxy); 44 | 45 | https 46 | .request( 47 | { 48 | host: "localhost", 49 | port: ports.proxy, 50 | path: "/", 51 | method: "GET", 52 | rejectUnauthorized: false, 53 | }, 54 | function (res) { 55 | expect(res.statusCode).to.eql(200); 56 | 57 | res.on("data", function (data) { 58 | expect(data.toString()).to.eql("Hello from " + ports.source); 59 | }); 60 | 61 | res.once("end", () => { 62 | source.close(); 63 | proxy.close(); 64 | done(); 65 | }); 66 | } 67 | ) 68 | .end(); 69 | }); 70 | }); 71 | describe("HTTPS not allow SSL self-signed", () => { 72 | it("should fail with error", function (done) { 73 | const ports = { source: gen.port, proxy: gen.port }; 74 | https 75 | .createServer({ 76 | key: readFileSync(join(__dirname, "fixtures", "agent2-key.pem")), 77 | cert: readFileSync(join(__dirname, "fixtures", "agent2-cert.pem")), 78 | ciphers: "AES128-GCM-SHA256", 79 | }) 80 | .listen(ports.source); 81 | 82 | const proxy = httpProxy.createProxyServer({ 83 | target: "https://127.0.0.1:" + ports.source, 84 | secure: true, 85 | }); 86 | 87 | proxy.listen(ports.proxy); 88 | 89 | proxy.on("error", function (err, req, res) { 90 | expect(err).to.be.an(Error); 91 | expect(err.toString()).to.be("Error: self-signed certificate"); 92 | done(); 93 | }); 94 | 95 | http 96 | .request({ 97 | hostname: "127.0.0.1", 98 | port: ports.proxy, 99 | method: "GET", 100 | }) 101 | .end(); 102 | }); 103 | }); 104 | describe("HTTPS to HTTP using own server", () => { 105 | it("should proxy the request en send back the response", function (done) { 106 | const ports = { source: gen.port, proxy: gen.port }; 107 | const source = http.createServer(function (req, res) { 108 | expect(req.method).to.eql("GET"); 109 | expect(req.headers.host.split(":")[1]).to.eql(ports.proxy); 110 | res.writeHead(200, { "Content-Type": "text/plain" }); 111 | res.end("Hello from " + ports.source); 112 | }); 113 | 114 | source.listen(ports.source); 115 | 116 | const proxy = httpProxy.createServer({ 117 | agent: new http.Agent({ maxSockets: 2 }), 118 | }); 119 | 120 | const ownServer = https 121 | .createServer( 122 | { 123 | key: readFileSync(join(__dirname, "fixtures", "agent2-key.pem")), 124 | cert: readFileSync(join(__dirname, "fixtures", "agent2-cert.pem")), 125 | ciphers: "AES128-GCM-SHA256", 126 | }, 127 | function (req, res) { 128 | proxy.web(req, res, { 129 | target: "http://127.0.0.1:" + ports.source, 130 | }); 131 | } 132 | ) 133 | .listen(ports.proxy); 134 | 135 | https 136 | .request( 137 | { 138 | host: "localhost", 139 | port: ports.proxy, 140 | path: "/", 141 | method: "GET", 142 | rejectUnauthorized: false, 143 | }, 144 | function (res) { 145 | expect(res.statusCode).to.eql(200); 146 | 147 | res.on("data", function (data) { 148 | expect(data.toString()).to.eql("Hello from " + ports.source); 149 | }); 150 | 151 | res.once("end", () => { 152 | source.close(); 153 | ownServer.close(); 154 | done(); 155 | }); 156 | } 157 | ) 158 | .end(); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/ws-incoming.js: -------------------------------------------------------------------------------- 1 | const http = require("node:http"); 2 | const https = require("node:https"); 3 | const common = require("../common"); 4 | 5 | /*! 6 | * Array of passes. 7 | * 8 | * A `pass` is just a function that is executed on `req, socket, options` 9 | * so that you can easily add new checks while still keeping the base 10 | * flexible. 11 | */ 12 | 13 | /* 14 | * Websockets Passes 15 | * 16 | */ 17 | 18 | module.exports = { 19 | /** 20 | * WebSocket requests must have the `GET` method and 21 | * the `upgrade:websocket` header 22 | * 23 | * @param {ClientRequest} Req Request object 24 | * @param {Socket} Websocket 25 | * @private 26 | */ 27 | checkMethodAndHeader: (req, socket) => { 28 | if (req.method !== "GET" || !req.headers.upgrade || req.headers.upgrade.toLowerCase() !== "websocket") { 29 | socket.destroy(); 30 | return true; 31 | } 32 | }, 33 | 34 | /** 35 | * Sets `X-Forwarded-*` headers if specified in config. 36 | * 37 | * @param {ClientRequest} Req Request object 38 | * @param {Socket} Websocket 39 | * @param {Object} Options Config object passed to the proxy 40 | * @private 41 | */ 42 | XHeaders: (req, res, options) => { 43 | if (!options.xfwd) return; 44 | 45 | const remoteAddress = 46 | req.headers["cf-connecting-ip"] || 47 | req.headers["x-forwarded-for"] || 48 | req.connection?.remoteAddress || 49 | req.socket?.remoteAddress; 50 | 51 | const values = { 52 | For: remoteAddress, 53 | Port: common.getPort(req), 54 | Proto: common.hasEncryptedConnection(req) ? "wss" : "ws", 55 | }; 56 | 57 | for (const header of ["For", "Port", "Proto"]) { 58 | const headerName = `X-Forwarded-${header}`; 59 | const currentValue = req.headers[headerName]; 60 | req.headers[headerName] = currentValue ? `${currentValue}, ${values[header]}` : values[header]; 61 | } 62 | }, 63 | 64 | /** 65 | * Does the actual proxying. Make the request and upgrade it 66 | * send the Switching Protocols request and pipe the sockets. 67 | * 68 | * @param {ClientRequest} Req Request object 69 | * @param {Socket} Websocket 70 | * @param {Object} Options Config object passed to the proxy 71 | * @private 72 | */ 73 | stream: (req, socket, options, head, server, clb) => { 74 | const createHttpHeader = function (line, headers) { 75 | return ( 76 | Object.keys(headers) 77 | .reduce( 78 | function (head, key) { 79 | const value = headers[key]; 80 | 81 | if (!Array.isArray(value)) { 82 | head.push(key + ": " + value); 83 | return head; 84 | } 85 | 86 | for (let i = 0; i < value.length; i++) { 87 | head.push(key + ": " + value[i]); 88 | } 89 | return head; 90 | }, 91 | [line] 92 | ) 93 | .join("\r\n") + "\r\n\r\n" 94 | ); 95 | }; 96 | 97 | common.setupSocket(socket); 98 | 99 | if (head && head.length) socket.unshift(head); 100 | 101 | const proxyReq = (common.isSSL.test(options.target.protocol) ? https : http).request( 102 | common.setupOutgoing(options.ssl || {}, options, req) 103 | ); 104 | 105 | // Enable developers to modify the proxyReq before headers are sent 106 | if (server) { 107 | server.emit("proxyReqWs", proxyReq, req, socket, options, head); 108 | } 109 | 110 | // Error Handler 111 | proxyReq.on("error", onOutgoingError); 112 | proxyReq.on("response", function (res) { 113 | // if upgrade event isn"t going to happen, close the socket 114 | if (!res.upgrade && socket.readyState !== "closed") { 115 | socket.write(createHttpHeader(`HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`, res.headers)); 116 | res.pipe(socket); 117 | } 118 | }); 119 | 120 | proxyReq.on("upgrade", function (proxyRes, proxySocket, proxyHead) { 121 | proxySocket.on("error", onOutgoingError); 122 | 123 | // Allow us to listen when the websocket has completed 124 | proxySocket.on("end", () => server.emit("close", proxyRes, proxySocket, proxyHead)); 125 | 126 | // The pipe below will end proxySocket if socket closes cleanly, but not 127 | // if it errors (eg, vanishes from the net and starts returning 128 | // EHOSTUNREACH). We need to do that explicitly. 129 | socket.on("error", proxySocket.end); 130 | 131 | common.setupSocket(proxySocket); 132 | 133 | if (proxyHead && proxyHead.length) proxySocket.unshift(proxyHead); 134 | 135 | // 136 | // Remark: Handle writing the headers to the socket when switching protocols 137 | // Also handles when a header is an array 138 | // 139 | if (socket.writable) { 140 | socket.write(createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers)); 141 | } 142 | 143 | let proxyStream = proxySocket; 144 | 145 | if (options.createWsServerTransformStream) { 146 | const wsServerTransformStream = options.createWsServerTransformStream(req, proxyReq, proxyRes); 147 | 148 | wsServerTransformStream.on("error", onOutgoingError); 149 | proxyStream = proxyStream.pipe(wsServerTransformStream); 150 | } 151 | 152 | proxyStream = proxyStream.pipe(socket); 153 | 154 | if (options.createWsClientTransformStream) { 155 | const wsClientTransformStream = options.createWsClientTransformStream(req, proxyReq, proxyRes); 156 | 157 | wsClientTransformStream.on("error", onOutgoingError); 158 | proxyStream = proxyStream.pipe(wsClientTransformStream); 159 | } 160 | 161 | proxyStream.pipe(proxySocket); 162 | server.emit("open", proxySocket); 163 | }); 164 | 165 | return proxyReq.end(); 166 | 167 | function onOutgoingError(err) { 168 | if (clb) { 169 | clb(err, req, socket); 170 | } else { 171 | server.emit("error", err, req, socket); 172 | } 173 | socket.end(); 174 | } 175 | }, 176 | }; 177 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/web-incoming.js: -------------------------------------------------------------------------------- 1 | const common = require("../common"); 2 | const { pipeline } = require("node:stream"); 3 | let webO = require("./web-outgoing"); 4 | 5 | webO = Object.keys(webO).map(function (pass) { 6 | return webO[pass]; 7 | }); 8 | 9 | const nativeAgents = { 10 | http: require("node:http"), 11 | https: require("node:https"), 12 | }; 13 | 14 | /*! 15 | * Array of passes. 16 | * 17 | * A `pass` is just a function that is executed on `req, res, options` 18 | * so that you can easily add new checks while still keeping the base 19 | * flexible. 20 | */ 21 | 22 | module.exports = { 23 | /** 24 | * Sets `content-length` to "0" if request is of DELETE type. 25 | * 26 | * @param {ClientRequest} Req Request object 27 | * @param {IncomingMessage} Res Response object 28 | * @param {Object} Options Config object passed to the proxy 29 | * @private 30 | */ 31 | deleteLength: (req) => { 32 | if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"]) { 33 | req.headers["content-length"] = "0"; 34 | delete req.headers["transfer-encoding"]; 35 | } 36 | }, 37 | 38 | /** 39 | * Sets timeout in request socket if it was specified in options. 40 | * 41 | * @param {ClientRequest} Req Request object 42 | * @param {IncomingMessage} Res Response object 43 | * @param {Object} Options Config object passed to the proxy 44 | * @private 45 | */ 46 | 47 | timeout: (req, res, options) => { 48 | if (options.timeout) { 49 | req.socket.setTimeout(options.timeout); 50 | } 51 | }, 52 | 53 | /** 54 | * Sets `X-Forwarded-*` headers if specified in config. 55 | * 56 | * @param {ClientRequest} Req Request object 57 | * @param {IncomingMessage} Res Response object 58 | * @param {Object} Options Config object passed to the proxy 59 | * @private 60 | */ 61 | 62 | XHeaders: (req, res, options) => { 63 | if (!options.xfwd) return; 64 | 65 | const encrypted = req.isSpdy || common.hasEncryptedConnection(req); 66 | const values = { 67 | For: 68 | req.headers["cf-connecting-ip"] || 69 | req.headers["x-forwarded-for"] || 70 | req.connection.remoteAddress || 71 | req.socket.remoteAddress, 72 | Port: common.getPort(req), 73 | Proto: encrypted ? "https" : "http", 74 | }; 75 | 76 | for (const header of ["For", "Port", "Proto"]) { 77 | const headerName = `X-Forwarded-${header}`; 78 | if (req.headers?.[headerName]) { 79 | req.headers[headerName] += `, ${values[header]}`; 80 | } else { 81 | req.headers[headerName] = values[header]; 82 | } 83 | } 84 | 85 | req.headers["X-Forwarded-Host"] = req.headers["X-Forwarded-Host"] || req.headers.host || ""; 86 | }, 87 | 88 | /** 89 | * Does the actual proxying. If `forward` is enabled fires up 90 | * a ForwardStream, same happens for ProxyStream. The request 91 | * just dies otherwise. 92 | * 93 | * @param {ClientRequest} Req Request object 94 | * @param {IncomingMessage} Res Response object 95 | * @param {Object} Options Config object passed to the proxy 96 | * @private 97 | */ 98 | stream: (req, res, options, _, server, callback) => { 99 | server.emit("start", req, res, options.target || options.forward); 100 | 101 | const http = nativeAgents.http; 102 | const https = nativeAgents.https; 103 | 104 | if (options.forward) { 105 | // If forward enable, so just pipe the request 106 | const forwardReq = (options.forward.protocol === "https:" ? https : http).request( 107 | common.setupOutgoing(options.ssl || {}, options, req, "forward") 108 | ); 109 | // error handler (e.g. ECONNRESET, ECONNREFUSED) 110 | // Handle errors on incoming request as well as it makes sense to 111 | const forwardError = createErrorHandler(forwardReq, options.forward); 112 | req.on("error", forwardError); 113 | forwardReq.on("error", forwardError); 114 | 115 | pipeline(options.buffer || req, forwardReq, () => {}); 116 | 117 | if (!options.target) { 118 | return res.end(); 119 | } 120 | } 121 | 122 | // Request initalization 123 | const proxyReq = (options.target.protocol === "https:" ? https : http).request( 124 | common.setupOutgoing(options.ssl || {}, options, req) 125 | ); 126 | 127 | // Enable developers to modify the proxyReq before headers are sent 128 | proxyReq.on("socket", (socket) => { 129 | // Prevent possible bug: a pipeline with pending sockets not ending causing the process to hang 130 | if (socket.pending) { 131 | socket.on("connect", () => { 132 | if (server && !proxyReq.getHeader("expect")) { 133 | server.emit("proxyReq", proxyReq, req, res, options); 134 | } 135 | pipeline(options.buffer || req, proxyReq, () => {}); 136 | }); 137 | } else { 138 | if (server && !proxyReq.getHeader("expect")) { 139 | server.emit("proxyReq", proxyReq, req, res, options); 140 | } 141 | 142 | req.on("close", proxyReq.destroy); 143 | pipeline(options.buffer || req, proxyReq, () => {}); 144 | } 145 | }); 146 | 147 | // allow outgoing socket to timeout so that we could 148 | // show an error page at the initial request 149 | if (options.proxyTimeout) { 150 | proxyReq.setTimeout(options.proxyTimeout, () => { 151 | if (options.proxyTimeoutCustomError) { 152 | const timeoutError = new Error("The proxy request timed out"); 153 | timeoutError.code = "ETIMEDOUT"; 154 | return proxyReq.destroy(timeoutError); 155 | } 156 | proxyReq.destroy(); 157 | }); 158 | } 159 | 160 | res.on("close", () => { 161 | if (!req.destroyed && res.writableEnded === false) { 162 | proxyReq.destroy(); 163 | } 164 | }); 165 | 166 | // handle errors in proxy and incoming request, just like for forward proxy 167 | const proxyError = createErrorHandler(proxyReq, options.target); 168 | req.on("error", proxyError); 169 | proxyReq.on("error", proxyError); 170 | 171 | proxyReq.on("response", (proxyRes) => { 172 | if (server) { 173 | server.emit("proxyRes", proxyRes, req, res); 174 | } 175 | 176 | if (!res.headersSent) { 177 | for (let i = 0; i < webO.length; i++) { 178 | if (webO[i](req, res, proxyRes, options)) { 179 | break; 180 | } 181 | } 182 | } 183 | 184 | if (!res.writableEnded) { 185 | // Allow us to listen when the proxy has completed 186 | proxyRes.on("end", () => (server ? server.emit("end", req, res, proxyRes) : null)); 187 | pipeline(proxyRes, res, () => {}); 188 | } else { 189 | if (server) { 190 | server.emit("end", req, res, proxyRes); 191 | } 192 | } 193 | }); 194 | 195 | function createErrorHandler(proxyReq, url) { 196 | return function proxyError(err) { 197 | if ((req.destroyed || req.socket?.destroyed) && err.code === "ECONNRESET") { 198 | server.emit("econnreset", err, req, res, url); 199 | 200 | if (typeof proxyReq.destroy === "function" && !proxyReq.destroyed && !proxyReq.socket?.destroyed) { 201 | proxyReq.destroy(); 202 | } 203 | 204 | return; 205 | } 206 | 207 | if (callback) { 208 | callback(err, req, res, url); 209 | } else { 210 | server.emit("error", err, req, res, url); 211 | } 212 | }; 213 | } 214 | }, 215 | }; 216 | -------------------------------------------------------------------------------- /lib/http-proxy/common.js: -------------------------------------------------------------------------------- 1 | const common = exports; 2 | const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; 3 | const isSSL = /^https|wss/; 4 | 5 | /** 6 | * Check if we're required to add a port number. 7 | * 8 | * @see https://url.spec.whatwg.org/#default-port 9 | * @param {Number|String} port Port number we need to check 10 | * @param {String} protocol Protocol we need to check against. 11 | * @returns {Boolean} Is it a default port for the given protocol 12 | * @private 13 | */ 14 | // BEGIN: ed8c6549bwf9 (optimized) 15 | function isPortRequired(port, protocol) { 16 | const excludedPorts = { 17 | http: [80, 8080], 18 | https: [443], 19 | ws: [80], 20 | wss: [443], 21 | }; 22 | 23 | const [protocolName] = protocol.split(":"); 24 | const excluded = excludedPorts[protocolName]; 25 | const parsedPort = +port; 26 | 27 | return parsedPort && (!excluded || !excluded.includes(parsedPort)); 28 | } 29 | // END: ed8c6549bwf9 (optimized) 30 | 31 | /** 32 | * Simple Regex for testing if protocol is https 33 | */ 34 | common.isSSL = isSSL; 35 | /** 36 | * Copies the right headers from `options` and `req` to 37 | * `outgoing` which is then used to fire the proxied 38 | * request. 39 | * 40 | * Examples: 41 | * 42 | * common.setupOutgoing(outgoing, options, req) 43 | * // => { host: ..., hostname: ...} 44 | * 45 | * @param {Object} Outgoing Base object to be filled with required properties 46 | * @param {Object} Options Config object passed to the proxy 47 | * @param {ClientRequest} Req Request Object 48 | * @param {String} Forward String to select forward or target 49 | * 50 | * @return {Object} Outgoing Object with all required properties set 51 | * @private 52 | */ 53 | 54 | common.setupOutgoing = function (outgoing, options, req, forward) { 55 | outgoing.port = options[forward || "target"].port || (isSSL.test(options[forward || "target"].protocol) ? 443 : 80); 56 | ["host", "hostname", "socketPath", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "secureProtocol"].forEach( 57 | (e) => (outgoing[e] = outgoing[e] || options[forward || "target"][e]) 58 | ); 59 | 60 | outgoing.method = options.method || req.method; 61 | outgoing.headers = Object.assign({}, req.headers, options.headers || {}); 62 | 63 | if (options.auth) { 64 | outgoing.auth = options.auth; 65 | } 66 | 67 | if (options.ca) { 68 | outgoing.ca = options.ca; 69 | } 70 | 71 | if (options.lookup) { 72 | outgoing.lookup = options.lookup; 73 | } 74 | 75 | if (outgoing.port === 443) { 76 | // Respect `NODE_TLS_REJECT_UNAUTHORIZED` environment variable (https://nodejs.org/docs/latest/api/cli.html#node_tls_reject_unauthorizedvalue) 77 | const NODE_TLS_REJECT_UNAUTHORIZED = process.env.NODE_TLS_REJECT_UNAUTHORIZED; 78 | const rejectUnauthorizedEnv = 79 | typeof NODE_TLS_REJECT_UNAUTHORIZED !== "undefined" ? NODE_TLS_REJECT_UNAUTHORIZED.toString() : undefined; 80 | outgoing.rejectUnauthorized = 81 | typeof options.secure === "undefined" ? rejectUnauthorizedEnv !== "0" : options.secure; 82 | } 83 | 84 | outgoing.agent = options.agent || false; 85 | outgoing.localAddress = options.localAddress; 86 | 87 | // 88 | // Remark: If we are false and not upgrading, set the connection: close. This is the right thing to do 89 | // as node core doesn"t handle this COMPLETELY properly yet. 90 | // 91 | if (!outgoing.agent) { 92 | outgoing.headers || (outgoing.headers = {}); 93 | if (typeof outgoing.headers.connection !== "string" || !upgradeHeader.test(outgoing.headers.connection)) { 94 | outgoing.headers.connection = "close"; 95 | } 96 | } 97 | 98 | // The final path is target path + relative path requested by user: 99 | const target = options[forward || "target"]; 100 | const targetPath = target && options.prependPath !== false ? target.path || target.pathname || "" : ""; 101 | const pathToAppend = req.url.split("?")[0] || ""; 102 | const query = req.url.split("?").slice(1).join("?"); 103 | const outgoingPath = !options.ignorePath 104 | ? !options.toProxy 105 | ? pathToAppend + (query ? `?${query}` : "") 106 | : req.url 107 | : ""; 108 | 109 | outgoing.path = common.urlJoin(targetPath, outgoingPath); 110 | 111 | if (options.changeOrigin) { 112 | outgoing.headers.host = 113 | isPortRequired(outgoing.port, options[forward || "target"].protocol) && !hasPort(outgoing.host) 114 | ? `${outgoing.host}:${outgoing.port}` 115 | : outgoing.host; 116 | } 117 | 118 | return outgoing; 119 | }; 120 | 121 | /** 122 | * Set the proper configuration for sockets, 123 | * set no delay and set keep alive, also set 124 | * the timeout to 0. 125 | * 126 | * Examples: 127 | * 128 | * common.setupSocket(socket) 129 | * // => Socket 130 | * 131 | * @param {Socket} Socket instance to setup 132 | * 133 | * @return {Socket} Return the configured socket. 134 | * @private 135 | */ 136 | 137 | common.setupSocket = function (socket) { 138 | socket.setTimeout(0); 139 | socket.setNoDelay(true); 140 | socket.setKeepAlive(true, 0); 141 | 142 | return socket; 143 | }; 144 | 145 | /** 146 | * Get the port number from the host. Or guess it based on the connection type. 147 | * 148 | * @param {Request} req Incoming HTTP request. 149 | * 150 | * @return {String} The port number. 151 | * @private 152 | */ 153 | common.getPort = function (req) { 154 | // Get the port from the `host` header, if it exists. 155 | const portMatch = req.headers?.host?.match(/:(\d+)/); 156 | const port = portMatch ? Number(portMatch[1]) : undefined; 157 | 158 | // If the port is not specified in the `host` header, return the default port for the connection type. 159 | return port || (common.hasEncryptedConnection(req) ? 443 : 80); 160 | }; 161 | 162 | /** 163 | * Check if the request has an encrypted connection. 164 | * 165 | * @param {Request} req Incoming HTTP request. 166 | * 167 | * @return {Boolean} Whether the connection is encrypted or not. 168 | * @private 169 | */ 170 | common.hasEncryptedConnection = function (req) { 171 | return Boolean(req.connection?.encrypted || req.connection?.pair); 172 | }; 173 | 174 | /** 175 | * OS-agnostic join (doesn"t break on URLs like path.join does on Windows)> 176 | * 177 | * @return {String} The generated path. 178 | * @private 179 | */ 180 | 181 | common.urlJoin = function (...args) { 182 | const lastIndex = args.length - 1; 183 | const last = args[lastIndex]; 184 | const lastSegs = last.split("?"); 185 | args[lastIndex] = lastSegs.shift(); 186 | 187 | const path = args 188 | .filter((arg) => arg) 189 | .join("/") 190 | .replace(/\/+/g, "/") 191 | .replace(/(https?:)\/+/g, "$1//"); 192 | 193 | return [path, ...lastSegs].join("?"); 194 | }; 195 | 196 | /** 197 | * Rewrites or removes the domain of a cookie header 198 | * 199 | * @param {String|Array} Header 200 | * @param {Object} Config, mapping of domain to rewritten domain. 201 | * "*" key to match any domain, null value to remove the domain. 202 | * @private 203 | */ 204 | common.rewriteCookieProperty = (header, config, property) => { 205 | if (Array.isArray(header)) { 206 | return header.map((headerElement) => module.exports.rewriteCookieProperty(headerElement, config, property)); 207 | } 208 | 209 | return header.replace(new RegExp(`(;\\s*${property}=)([^;]+)`, "i"), (match, prefix, previousValue) => { 210 | const newValue = config[previousValue] ?? config["*"]; 211 | return newValue ? `${prefix}${newValue}` : ""; 212 | }); 213 | }; 214 | 215 | /** 216 | * Check the host and see if it potentially has a port in it (keep it simple) 217 | * 218 | * @returns {Boolean} Whether we have one or not 219 | * @private 220 | */ 221 | function hasPort(host) { 222 | return !!~host.indexOf(":"); 223 | } 224 | -------------------------------------------------------------------------------- /test/lib-http-proxy-passes-web-incoming-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const webPasses = require("../lib/http-proxy/passes/web-incoming"); 3 | const httpProxy = require("../lib/http-proxy"); 4 | const expect = require("expect.js"); 5 | const http = require("node:http"); 6 | 7 | describe("lib/http-proxy/passes/web.js", () => { 8 | describe("#deleteLength", () => { 9 | it("should change `content-length` for DELETE requests", () => { 10 | const stubRequest = { 11 | method: "DELETE", 12 | headers: {}, 13 | }; 14 | webPasses.deleteLength(stubRequest, {}, {}); 15 | expect(stubRequest.headers["content-length"]).to.eql("0"); 16 | }); 17 | 18 | it("should change `content-length` for OPTIONS requests", () => { 19 | const stubRequest = { 20 | method: "OPTIONS", 21 | headers: {}, 22 | }; 23 | webPasses.deleteLength(stubRequest, {}, {}); 24 | expect(stubRequest.headers["content-length"]).to.eql("0"); 25 | }); 26 | 27 | it("should remove `transfer-encoding` from empty DELETE requests", () => { 28 | const stubRequest = { 29 | method: "DELETE", 30 | headers: { 31 | "transfer-encoding": "chunked", 32 | }, 33 | }; 34 | webPasses.deleteLength(stubRequest, {}, {}); 35 | expect(stubRequest.headers["content-length"]).to.eql("0"); 36 | expect(stubRequest.headers).to.not.have.key("transfer-encoding"); 37 | }); 38 | }); 39 | 40 | describe("#timeout", () => { 41 | it("should set timeout on the socket", () => { 42 | let done = false; 43 | const stubRequest = { 44 | socket: { 45 | setTimeout: function (value) { 46 | done = value; 47 | }, 48 | }, 49 | }; 50 | 51 | webPasses.timeout(stubRequest, {}, { timeout: 5000 }); 52 | expect(done).to.eql(5000); 53 | }); 54 | }); 55 | 56 | describe("#XHeaders", () => { 57 | const stubRequest = { 58 | connection: { 59 | remoteAddress: "192.168.1.2", 60 | remotePort: "8080", 61 | }, 62 | headers: { 63 | host: "192.168.1.2:8080", 64 | }, 65 | }; 66 | 67 | it("set the correct X-Forwarded-* headers", () => { 68 | webPasses.XHeaders(stubRequest, {}, { xfwd: true }); 69 | expect(stubRequest.headers["X-Forwarded-For"]).to.be("192.168.1.2"); 70 | expect(stubRequest.headers["X-Forwarded-Port"]).to.be(8080); 71 | expect(stubRequest.headers["X-Forwarded-Proto"]).to.be("http"); 72 | }); 73 | }); 74 | }); 75 | 76 | describe("#createProxyServer.web() using own http server", () => { 77 | it("should proxy the request using the web proxy handler", function (done) { 78 | const proxy = httpProxy.createProxyServer({ 79 | target: "http://127.0.0.1:8080", 80 | }); 81 | 82 | function requestHandler(req, res) { 83 | proxy.web(req, res); 84 | } 85 | 86 | const proxyServer = http.createServer(requestHandler); 87 | 88 | const source = http.createServer(function (req, res) { 89 | source.close(); 90 | proxyServer.close(); 91 | expect(req.method).to.eql("GET"); 92 | expect(req.headers.host.split(":")[1]).to.eql("8081"); 93 | done(); 94 | }); 95 | 96 | proxyServer.listen("8081"); 97 | source.listen("8080"); 98 | 99 | http.request("http://127.0.0.1:8081", () => {}).end(); 100 | }); 101 | 102 | it("should detect a proxyReq event and modify headers", function (done) { 103 | const proxy = httpProxy.createProxyServer({ 104 | target: "http://127.0.0.1:8080", 105 | }); 106 | 107 | proxy.on("proxyReq", function (proxyReq, req, res, options) { 108 | proxyReq.setHeader("X-Special-Proxy-Header", "foobar"); 109 | }); 110 | 111 | function requestHandler(req, res) { 112 | proxy.web(req, res); 113 | } 114 | 115 | const proxyServer = http.createServer(requestHandler); 116 | 117 | const source = http.createServer(function (req, res) { 118 | source.close(); 119 | proxyServer.close(); 120 | expect(req.headers["x-special-proxy-header"]).to.eql("foobar"); 121 | done(); 122 | }); 123 | 124 | proxyServer.listen("8081"); 125 | source.listen("8080"); 126 | 127 | http.request("http://127.0.0.1:8081", () => {}).end(); 128 | }); 129 | 130 | it("should proxy the request and handle error via callback", function (done) { 131 | const proxy = httpProxy.createProxyServer({ 132 | target: "http://127.0.0.1:8080", 133 | }); 134 | 135 | const proxyServer = http.createServer(requestHandler); 136 | 137 | function requestHandler(req, res) { 138 | proxy.web(req, res, function (err) { 139 | proxyServer.close(); 140 | expect(err).to.be.an(Error); 141 | expect(err.code).to.be("ECONNREFUSED"); 142 | done(); 143 | }); 144 | } 145 | 146 | proxyServer.listen("8082"); 147 | 148 | http 149 | .request( 150 | { 151 | hostname: "127.0.0.1", 152 | port: "8082", 153 | method: "GET", 154 | }, 155 | () => {} 156 | ) 157 | .end(); 158 | }); 159 | 160 | it("should proxy the request and handle error via event listener", function (done) { 161 | const proxy = httpProxy.createProxyServer({ 162 | target: "http://127.0.0.1:8080", 163 | }); 164 | 165 | const proxyServer = http.createServer(requestHandler); 166 | 167 | function requestHandler(req, res) { 168 | proxy.once("error", function (err, errReq, errRes) { 169 | proxyServer.close(); 170 | expect(err).to.be.an(Error); 171 | expect(errReq).to.be.equal(req); 172 | expect(errRes).to.be.equal(res); 173 | expect(err.code).to.be("ECONNREFUSED"); 174 | done(); 175 | }); 176 | 177 | proxy.web(req, res); 178 | } 179 | 180 | proxyServer.listen("8083"); 181 | 182 | http 183 | .request( 184 | { 185 | hostname: "127.0.0.1", 186 | port: "8083", 187 | method: "GET", 188 | }, 189 | () => {} 190 | ) 191 | .end(); 192 | }); 193 | 194 | it("should forward the request and handle error via event listener", function (done) { 195 | const proxy = httpProxy.createProxyServer({ 196 | forward: "http://127.0.0.1:8080", 197 | }); 198 | 199 | const proxyServer = http.createServer(requestHandler); 200 | 201 | function requestHandler(req, res) { 202 | proxy.once("error", function (err, errReq, errRes) { 203 | proxyServer.close(); 204 | expect(err).to.be.an(Error); 205 | expect(errReq).to.be.equal(req); 206 | expect(errRes).to.be.equal(res); 207 | expect(err.code).to.be("ECONNREFUSED"); 208 | done(); 209 | }); 210 | 211 | proxy.web(req, res); 212 | } 213 | 214 | proxyServer.listen("8083"); 215 | 216 | http 217 | .request( 218 | { 219 | hostname: "127.0.0.1", 220 | port: "8083", 221 | method: "GET", 222 | }, 223 | () => {} 224 | ) 225 | .end(); 226 | }); 227 | 228 | it("should proxy the request and provide a proxyRes event with the request and response parameters", function (done) { 229 | const proxy = httpProxy.createProxyServer({ 230 | target: "http://127.0.0.1:8080", 231 | }); 232 | 233 | function requestHandler(req, res) { 234 | proxy.once("proxyRes", function (proxyRes, pReq, pRes) { 235 | source.close(); 236 | proxyServer.close(); 237 | expect(pReq).to.be.equal(req); 238 | expect(pRes).to.be.equal(res); 239 | done(); 240 | }); 241 | 242 | proxy.web(req, res); 243 | } 244 | 245 | const proxyServer = http.createServer(requestHandler); 246 | 247 | const source = http.createServer(function (req, res) { 248 | res.end("Response"); 249 | }); 250 | 251 | proxyServer.listen("8086"); 252 | source.listen("8080"); 253 | http.request("http://127.0.0.1:8086", () => {}).end(); 254 | }); 255 | 256 | it("should proxy the request and handle changeOrigin option", function (done) { 257 | const proxy = httpProxy.createProxyServer({ 258 | target: "http://127.0.0.1:8080", 259 | changeOrigin: true, 260 | }); 261 | 262 | function requestHandler(req, res) { 263 | proxy.web(req, res); 264 | } 265 | 266 | const proxyServer = http.createServer(requestHandler); 267 | 268 | const source = http.createServer(function (req, res) { 269 | source.close(); 270 | proxyServer.close(); 271 | expect(req.method).to.eql("GET"); 272 | expect(req.headers.host.split(":")[1]).to.eql("8080"); 273 | done(); 274 | }); 275 | 276 | proxyServer.listen("8081"); 277 | source.listen("8080"); 278 | 279 | http.request("http://127.0.0.1:8081", () => {}).end(); 280 | }); 281 | 282 | it("should proxy the request with the Authorization header set", function (done) { 283 | const proxy = httpProxy.createProxyServer({ 284 | target: "http://127.0.0.1:8080", 285 | auth: "user:pass", 286 | }); 287 | 288 | function requestHandler(req, res) { 289 | proxy.web(req, res); 290 | } 291 | 292 | const proxyServer = http.createServer(requestHandler); 293 | 294 | const source = http.createServer(function (req, res) { 295 | source.close(); 296 | proxyServer.close(); 297 | const auth = Buffer.from(req.headers.authorization.split(" ")[1], "base64"); 298 | expect(req.method).to.eql("GET"); 299 | expect(auth.toString()).to.eql("user:pass"); 300 | done(); 301 | }); 302 | 303 | proxyServer.listen("8081"); 304 | source.listen("8080"); 305 | 306 | http.request("http://127.0.0.1:8081", () => {}).end(); 307 | }); 308 | 309 | it("should proxy requests to multiple servers with different options", function (done) { 310 | const proxy = httpProxy.createProxyServer(); 311 | 312 | // proxies to two servers depending on url, rewriting the url as well 313 | // http://127.0.0.1:8080/s1/ -> http://127.0.0.1:8081/ 314 | // http://127.0.0.1:8080/ -> http://127.0.0.1:8082/ 315 | function requestHandler(req, res) { 316 | if (req.url.indexOf("/s1/") === 0) { 317 | proxy.web(req, res, { 318 | ignorePath: true, 319 | target: "http://127.0.0.1:8081" + req.url.substring(3), 320 | }); 321 | } else { 322 | proxy.web(req, res, { 323 | target: "http://127.0.0.1:8082", 324 | }); 325 | } 326 | } 327 | 328 | const proxyServer = http.createServer(requestHandler); 329 | 330 | const source1 = http.createServer(function (req, res) { 331 | expect(req.method).to.eql("GET"); 332 | expect(req.headers.host.split(":")[1]).to.eql("8080"); 333 | expect(req.url).to.eql("/test1"); 334 | }); 335 | 336 | const source2 = http.createServer(function (req, res) { 337 | source1.close(); 338 | source2.close(); 339 | proxyServer.close(); 340 | expect(req.method).to.eql("GET"); 341 | expect(req.headers.host.split(":")[1]).to.eql("8080"); 342 | expect(req.url).to.eql("/test2"); 343 | done(); 344 | }); 345 | 346 | proxyServer.listen("8080"); 347 | source1.listen("8081"); 348 | source2.listen("8082"); 349 | 350 | http.request("http://127.0.0.1:8080/s1/test1", () => {}).end(); 351 | http.request("http://127.0.0.1:8080/test2", () => {}).end(); 352 | }); 353 | }); 354 | -------------------------------------------------------------------------------- /test/lib-http-proxy-common-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const common = require("../lib/http-proxy/common"); 3 | const expect = require("expect.js"); 4 | 5 | describe("lib/http-proxy/common.js", () => { 6 | describe("#setupOutgoing", () => { 7 | it("should setup the correct headers", () => { 8 | const outgoing = {}; 9 | common.setupOutgoing( 10 | outgoing, 11 | { 12 | agent: "?", 13 | target: { 14 | host: "hey", 15 | hostname: "how", 16 | socketPath: "are", 17 | port: "you", 18 | }, 19 | headers: { fizz: "bang", overwritten: true }, 20 | localAddress: "local.address", 21 | auth: "username:pass", 22 | }, 23 | { 24 | method: "i", 25 | url: "am", 26 | headers: { pro: "xy", overwritten: false }, 27 | } 28 | ); 29 | 30 | expect(outgoing.host).to.eql("hey"); 31 | expect(outgoing.hostname).to.eql("how"); 32 | expect(outgoing.socketPath).to.eql("are"); 33 | expect(outgoing.port).to.eql("you"); 34 | expect(outgoing.agent).to.eql("?"); 35 | 36 | expect(outgoing.method).to.eql("i"); 37 | expect(outgoing.path).to.eql("am"); 38 | 39 | expect(outgoing.headers.pro).to.eql("xy"); 40 | expect(outgoing.headers.fizz).to.eql("bang"); 41 | expect(outgoing.headers.overwritten).to.eql(true); 42 | expect(outgoing.localAddress).to.eql("local.address"); 43 | expect(outgoing.auth).to.eql("username:pass"); 44 | }); 45 | 46 | it("should not override agentless upgrade header", () => { 47 | const outgoing = {}; 48 | common.setupOutgoing( 49 | outgoing, 50 | { 51 | agent: undefined, 52 | target: { 53 | host: "hey", 54 | hostname: "how", 55 | socketPath: "are", 56 | port: "you", 57 | }, 58 | headers: { connection: "upgrade" }, 59 | }, 60 | { 61 | method: "i", 62 | url: "am", 63 | headers: { pro: "xy", overwritten: false }, 64 | } 65 | ); 66 | expect(outgoing.headers.connection).to.eql("upgrade"); 67 | }); 68 | 69 | it("should not override agentless connection: contains upgrade", () => { 70 | const outgoing = {}; 71 | common.setupOutgoing( 72 | outgoing, 73 | { 74 | agent: undefined, 75 | target: { 76 | host: "hey", 77 | hostname: "how", 78 | socketPath: "are", 79 | port: "you", 80 | }, 81 | headers: { connection: "keep-alive, upgrade" }, // this is what Firefox sets 82 | }, 83 | { 84 | method: "i", 85 | url: "am", 86 | headers: { pro: "xy", overwritten: false }, 87 | } 88 | ); 89 | expect(outgoing.headers.connection).to.eql("keep-alive, upgrade"); 90 | }); 91 | 92 | it("should override agentless connection: contains improper upgrade", () => { 93 | // sanity check on upgrade regex 94 | const outgoing = {}; 95 | common.setupOutgoing( 96 | outgoing, 97 | { 98 | agent: undefined, 99 | target: { 100 | host: "hey", 101 | hostname: "how", 102 | socketPath: "are", 103 | port: "you", 104 | }, 105 | headers: { connection: "keep-alive, not upgrade" }, 106 | }, 107 | { 108 | method: "i", 109 | url: "am", 110 | headers: { pro: "xy", overwritten: false }, 111 | } 112 | ); 113 | expect(outgoing.headers.connection).to.eql("close"); 114 | }); 115 | 116 | it("should override agentless non-upgrade header to close", () => { 117 | const outgoing = {}; 118 | common.setupOutgoing( 119 | outgoing, 120 | { 121 | agent: undefined, 122 | target: { 123 | host: "hey", 124 | hostname: "how", 125 | socketPath: "are", 126 | port: "you", 127 | }, 128 | headers: { connection: "xyz" }, 129 | }, 130 | { 131 | method: "i", 132 | url: "am", 133 | headers: { pro: "xy", overwritten: false }, 134 | } 135 | ); 136 | expect(outgoing.headers.connection).to.eql("close"); 137 | }); 138 | 139 | it("should set the agent to false if none is given", () => { 140 | const outgoing = {}; 141 | common.setupOutgoing(outgoing, { target: "http://localhost" }, { url: "/" }); 142 | expect(outgoing.agent).to.eql(false); 143 | }); 144 | 145 | it("set the port according to the protocol", () => { 146 | const outgoing = {}; 147 | common.setupOutgoing( 148 | outgoing, 149 | { 150 | agent: "?", 151 | target: { 152 | host: "how", 153 | hostname: "are", 154 | socketPath: "you", 155 | protocol: "https:", 156 | }, 157 | }, 158 | { 159 | method: "i", 160 | url: "am", 161 | headers: { pro: "xy" }, 162 | } 163 | ); 164 | 165 | expect(outgoing.host).to.eql("how"); 166 | expect(outgoing.hostname).to.eql("are"); 167 | expect(outgoing.socketPath).to.eql("you"); 168 | expect(outgoing.agent).to.eql("?"); 169 | 170 | expect(outgoing.method).to.eql("i"); 171 | expect(outgoing.path).to.eql("am"); 172 | expect(outgoing.headers.pro).to.eql("xy"); 173 | 174 | expect(outgoing.port).to.eql(443); 175 | }); 176 | 177 | it("should keep the original target path in the outgoing path", () => { 178 | const outgoing = {}; 179 | common.setupOutgoing(outgoing, { target: { path: "some-path" } }, { url: "am" }); 180 | 181 | expect(outgoing.path).to.eql("some-path/am"); 182 | }); 183 | 184 | it("should keep the original forward path in the outgoing path", () => { 185 | const outgoing = {}; 186 | common.setupOutgoing( 187 | outgoing, 188 | { 189 | target: {}, 190 | forward: { 191 | path: "some-path", 192 | }, 193 | }, 194 | { 195 | url: "am", 196 | }, 197 | "forward" 198 | ); 199 | 200 | expect(outgoing.path).to.eql("some-path/am"); 201 | }); 202 | 203 | it("should properly detect https/wss protocol without the colon", () => { 204 | const outgoing = {}; 205 | common.setupOutgoing( 206 | outgoing, 207 | { 208 | target: { 209 | protocol: "https", 210 | host: "whatever.com", 211 | }, 212 | }, 213 | { url: "/" } 214 | ); 215 | 216 | expect(outgoing.port).to.eql(443); 217 | }); 218 | 219 | it("should not prepend the target path to the outgoing path with prependPath = false", () => { 220 | const outgoing = {}; 221 | common.setupOutgoing( 222 | outgoing, 223 | { 224 | target: { path: "hellothere" }, 225 | prependPath: false, 226 | }, 227 | { url: "hi" } 228 | ); 229 | 230 | expect(outgoing.path).to.eql("hi"); 231 | }); 232 | 233 | it("should properly join paths", () => { 234 | const outgoing = {}; 235 | common.setupOutgoing( 236 | outgoing, 237 | { 238 | target: { path: "/forward" }, 239 | }, 240 | { url: "/static/path" } 241 | ); 242 | 243 | expect(outgoing.path).to.eql("/forward/static/path"); 244 | }); 245 | 246 | it("should not modify the query string", () => { 247 | const outgoing = {}; 248 | common.setupOutgoing( 249 | outgoing, 250 | { 251 | target: { path: "/forward" }, 252 | }, 253 | { url: "/?foo=bar//&target=http://foobar.com/?a=1%26b=2&other=2" } 254 | ); 255 | 256 | expect(outgoing.path).to.eql("/forward/?foo=bar//&target=http://foobar.com/?a=1%26b=2&other=2"); 257 | }); 258 | 259 | // 260 | // This is the proper failing test case for the common.join problem 261 | // 262 | it("should correctly format the toProxy URL", () => { 263 | const outgoing = {}; 264 | const google = "https://google.com"; 265 | common.setupOutgoing( 266 | outgoing, 267 | { 268 | target: new URL("http://sometarget.com:80"), 269 | toProxy: true, 270 | }, 271 | { url: google } 272 | ); 273 | 274 | expect(outgoing.path).to.eql("/" + google); 275 | }); 276 | 277 | it("should not replace : to :\\ when no https word before", () => { 278 | const outgoing = {}; 279 | const google = "https://google.com:/join/join.js"; 280 | common.setupOutgoing( 281 | outgoing, 282 | { 283 | target: new URL("http://sometarget.com:80"), 284 | toProxy: true, 285 | }, 286 | { url: google } 287 | ); 288 | 289 | expect(outgoing.path).to.eql("/" + google); 290 | }); 291 | 292 | it("should not replace : to :\\ when no http word before", () => { 293 | const outgoing = {}; 294 | const google = "http://google.com:/join/join.js"; 295 | common.setupOutgoing( 296 | outgoing, 297 | { 298 | target: new URL("http://sometarget.com:80"), 299 | toProxy: true, 300 | }, 301 | { url: google } 302 | ); 303 | 304 | expect(outgoing.path).to.eql("/" + google); 305 | }); 306 | 307 | describe("when using ignorePath", () => { 308 | it("should ignore the path of the `req.url` passed in but use the target path", () => { 309 | const outgoing = {}; 310 | const myEndpoint = "https://whatever.com/some/crazy/path/whoooo"; 311 | common.setupOutgoing( 312 | outgoing, 313 | { 314 | target: new URL(myEndpoint), 315 | ignorePath: true, 316 | }, 317 | { url: "/more/crazy/pathness" } 318 | ); 319 | 320 | expect(outgoing.path).to.eql("/some/crazy/path/whoooo"); 321 | }); 322 | 323 | it("and prependPath: false, it should ignore path of target and incoming request", () => { 324 | const outgoing = {}; 325 | const myEndpoint = "https://whatever.com/some/crazy/path/whoooo"; 326 | common.setupOutgoing( 327 | outgoing, 328 | { 329 | target: new URL(myEndpoint), 330 | ignorePath: true, 331 | prependPath: false, 332 | }, 333 | { url: "/more/crazy/pathness" } 334 | ); 335 | 336 | expect(outgoing.path).to.eql(""); 337 | }); 338 | }); 339 | 340 | describe("when using changeOrigin", () => { 341 | it("should correctly set the port to the host when it is a non-standard port using url.parse", () => { 342 | const outgoing = {}; 343 | const myEndpoint = "https://myCouch.com:6984"; 344 | common.setupOutgoing( 345 | outgoing, 346 | { 347 | target: new URL(myEndpoint), 348 | changeOrigin: true, 349 | }, 350 | { url: "/" } 351 | ); 352 | 353 | expect(outgoing.headers.host).to.eql("mycouch.com:6984"); 354 | }); 355 | 356 | it("should correctly set the port to the host when it is a non-standard port when setting host and port manually (which ignores port)", () => { 357 | const outgoing = {}; 358 | common.setupOutgoing( 359 | outgoing, 360 | { 361 | target: { 362 | protocol: "https:", 363 | host: "mycouch.com", 364 | port: 6984, 365 | }, 366 | changeOrigin: true, 367 | }, 368 | { url: "/" } 369 | ); 370 | expect(outgoing.headers.host).to.eql("mycouch.com:6984"); 371 | }); 372 | }); 373 | 374 | it("should pass through https client parameters", () => { 375 | const outgoing = {}; 376 | common.setupOutgoing( 377 | outgoing, 378 | { 379 | agent: "?", 380 | target: { 381 | host: "how", 382 | hostname: "are", 383 | socketPath: "you", 384 | protocol: "https:", 385 | pfx: "my-pfx", 386 | key: "my-key", 387 | passphrase: "my-passphrase", 388 | cert: "my-cert", 389 | ca: "my-ca", 390 | ciphers: "my-ciphers", 391 | secureProtocol: "my-secure-protocol", 392 | }, 393 | }, 394 | { 395 | method: "i", 396 | url: "am", 397 | } 398 | ); 399 | 400 | expect(outgoing.pfx).eql("my-pfx"); 401 | expect(outgoing.key).eql("my-key"); 402 | expect(outgoing.passphrase).eql("my-passphrase"); 403 | expect(outgoing.cert).eql("my-cert"); 404 | expect(outgoing.ca).eql("my-ca"); 405 | expect(outgoing.ciphers).eql("my-ciphers"); 406 | expect(outgoing.secureProtocol).eql("my-secure-protocol"); 407 | }); 408 | 409 | it("should handle overriding the `method` of the http request", () => { 410 | const outgoing = {}; 411 | common.setupOutgoing( 412 | outgoing, 413 | { 414 | target: new URL("https://whooooo.com"), 415 | method: "POST", 416 | }, 417 | { method: "GET", url: "" } 418 | ); 419 | 420 | expect(outgoing.method).eql("POST"); 421 | }); 422 | 423 | // new URL('').path => null 424 | it("should not pass null as last arg to #urlJoin", () => { 425 | const outgoing = {}; 426 | common.setupOutgoing(outgoing, { target: { path: "" } }, { url: "" }); 427 | 428 | expect(outgoing.path).to.be(""); 429 | }); 430 | }); 431 | 432 | describe("#setupSocket", () => { 433 | it("should setup a socket", () => { 434 | const socketConfig = { 435 | timeout: null, 436 | nodelay: false, 437 | keepalive: false, 438 | }; 439 | const stubSocket = { 440 | setTimeout: function (num) { 441 | socketConfig.timeout = num; 442 | }, 443 | setNoDelay: function (bol) { 444 | socketConfig.nodelay = bol; 445 | }, 446 | setKeepAlive: function (bol) { 447 | socketConfig.keepalive = bol; 448 | }, 449 | }; 450 | returnValue = common.setupSocket(stubSocket); 451 | 452 | expect(socketConfig.timeout).to.eql(0); 453 | expect(socketConfig.nodelay).to.eql(true); 454 | expect(socketConfig.keepalive).to.eql(true); 455 | }); 456 | }); 457 | }); 458 | -------------------------------------------------------------------------------- /test/lib-http-proxy-passes-web-outgoing-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const httpProxy = require("../lib/http-proxy/passes/web-outgoing"); 3 | const expect = require("expect.js"); 4 | 5 | describe("lib/http-proxy/passes/web-outgoing.js", () => { 6 | describe("#setRedirectHostRewrite", () => { 7 | beforeEach(function () { 8 | this.req = { 9 | headers: { 10 | host: "ext-auto.com", 11 | }, 12 | }; 13 | this.proxyRes = { 14 | statusCode: 301, 15 | headers: { 16 | location: "http://backend.com/", 17 | }, 18 | }; 19 | this.options = { 20 | target: "http://backend.com", 21 | }; 22 | }); 23 | 24 | context("rewrites location host with hostRewrite", function () { 25 | beforeEach(function () { 26 | this.options.hostRewrite = "ext-manual.com"; 27 | }); 28 | [201, 301, 302, 307, 308].forEach(function (code) { 29 | it("on " + code, function () { 30 | this.proxyRes.statusCode = code; 31 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 32 | expect(this.proxyRes.headers.location).to.eql("http://ext-manual.com/"); 33 | }); 34 | }); 35 | 36 | it("not on 200", function () { 37 | this.proxyRes.statusCode = 200; 38 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 39 | expect(this.proxyRes.headers.location).to.eql("http://backend.com/"); 40 | }); 41 | 42 | it("not when hostRewrite is unset", function () { 43 | delete this.options.hostRewrite; 44 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 45 | expect(this.proxyRes.headers.location).to.eql("http://backend.com/"); 46 | }); 47 | 48 | it("takes precedence over autoRewrite", function () { 49 | this.options.autoRewrite = true; 50 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 51 | expect(this.proxyRes.headers.location).to.eql("http://ext-manual.com/"); 52 | }); 53 | 54 | it("not when the redirected location does not match target host", function () { 55 | this.proxyRes.statusCode = 302; 56 | this.proxyRes.headers.location = "http://some-other/"; 57 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 58 | expect(this.proxyRes.headers.location).to.eql("http://some-other/"); 59 | }); 60 | 61 | it("not when the redirected location does not match target port", function () { 62 | this.proxyRes.statusCode = 302; 63 | this.proxyRes.headers.location = "http://backend.com:8080/"; 64 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 65 | expect(this.proxyRes.headers.location).to.eql("http://backend.com:8080/"); 66 | }); 67 | }); 68 | 69 | context("rewrites location host with autoRewrite", function () { 70 | beforeEach(function () { 71 | this.options.autoRewrite = true; 72 | }); 73 | [201, 301, 302, 307, 308].forEach(function (code) { 74 | it("on " + code, function () { 75 | this.proxyRes.statusCode = code; 76 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 77 | expect(this.proxyRes.headers.location).to.eql("http://ext-auto.com/"); 78 | }); 79 | }); 80 | 81 | it("not on 200", function () { 82 | this.proxyRes.statusCode = 200; 83 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 84 | expect(this.proxyRes.headers.location).to.eql("http://backend.com/"); 85 | }); 86 | 87 | it("not when autoRewrite is unset", function () { 88 | delete this.options.autoRewrite; 89 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 90 | expect(this.proxyRes.headers.location).to.eql("http://backend.com/"); 91 | }); 92 | 93 | it("not when the redirected location does not match target host", function () { 94 | this.proxyRes.statusCode = 302; 95 | this.proxyRes.headers.location = "http://some-other/"; 96 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 97 | expect(this.proxyRes.headers.location).to.eql("http://some-other/"); 98 | }); 99 | 100 | it("not when the redirected location does not match target port", function () { 101 | this.proxyRes.statusCode = 302; 102 | this.proxyRes.headers.location = "http://backend.com:8080/"; 103 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 104 | expect(this.proxyRes.headers.location).to.eql("http://backend.com:8080/"); 105 | }); 106 | }); 107 | 108 | context("rewrites location protocol with protocolRewrite", function () { 109 | beforeEach(function () { 110 | this.options.protocolRewrite = "https"; 111 | }); 112 | [201, 301, 302, 307, 308].forEach(function (code) { 113 | it("on " + code, function () { 114 | this.proxyRes.statusCode = code; 115 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 116 | expect(this.proxyRes.headers.location).to.eql("https://backend.com/"); 117 | }); 118 | }); 119 | 120 | it("not on 200", function () { 121 | this.proxyRes.statusCode = 200; 122 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 123 | expect(this.proxyRes.headers.location).to.eql("http://backend.com/"); 124 | }); 125 | 126 | it("not when protocolRewrite is unset", function () { 127 | delete this.options.protocolRewrite; 128 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 129 | expect(this.proxyRes.headers.location).to.eql("http://backend.com/"); 130 | }); 131 | 132 | it("works together with hostRewrite", function () { 133 | this.options.hostRewrite = "ext-manual.com"; 134 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 135 | expect(this.proxyRes.headers.location).to.eql("https://ext-manual.com/"); 136 | }); 137 | 138 | it("works together with autoRewrite", function () { 139 | this.options.autoRewrite = true; 140 | httpProxy.setRedirectHostRewrite(this.req, {}, this.proxyRes, this.options); 141 | expect(this.proxyRes.headers.location).to.eql("https://ext-auto.com/"); 142 | }); 143 | }); 144 | }); 145 | 146 | describe("#setConnection", function () { 147 | it("set the right connection with 1.0 - `close`", function () { 148 | const proxyRes = { headers: {} }; 149 | httpProxy.setConnection( 150 | { 151 | httpVersion: "1.0", 152 | headers: { 153 | connection: null, 154 | }, 155 | }, 156 | {}, 157 | proxyRes 158 | ); 159 | 160 | expect(proxyRes.headers.connection).to.eql("close"); 161 | }); 162 | 163 | it("set the right connection with 1.0 - req.connection", function () { 164 | const proxyRes = { headers: {} }; 165 | httpProxy.setConnection( 166 | { 167 | httpVersion: "1.0", 168 | headers: { 169 | connection: "hey", 170 | }, 171 | }, 172 | {}, 173 | proxyRes 174 | ); 175 | 176 | expect(proxyRes.headers.connection).to.eql("hey"); 177 | }); 178 | 179 | it("set the right connection - req.connection", function () { 180 | const proxyRes = { headers: {} }; 181 | httpProxy.setConnection( 182 | { 183 | httpVersion: null, 184 | headers: { 185 | connection: "hola", 186 | }, 187 | }, 188 | {}, 189 | proxyRes 190 | ); 191 | 192 | expect(proxyRes.headers.connection).to.eql("hola"); 193 | }); 194 | 195 | it("set the right connection - `keep-alive`", function () { 196 | const proxyRes = { headers: {} }; 197 | httpProxy.setConnection( 198 | { 199 | httpVersion: null, 200 | headers: { 201 | connection: null, 202 | }, 203 | }, 204 | {}, 205 | proxyRes 206 | ); 207 | 208 | expect(proxyRes.headers.connection).to.eql("keep-alive"); 209 | }); 210 | 211 | it("don`t set connection with 2.0 if exist", function () { 212 | const proxyRes = { headers: {} }; 213 | httpProxy.setConnection( 214 | { 215 | httpVersion: "2.0", 216 | headers: { 217 | connection: "namstey", 218 | }, 219 | }, 220 | {}, 221 | proxyRes 222 | ); 223 | 224 | expect(proxyRes.headers.connection).to.eql(undefined); 225 | }); 226 | 227 | it("don`t set connection with 2.0 if doesn`t exist", function () { 228 | const proxyRes = { headers: {} }; 229 | httpProxy.setConnection( 230 | { 231 | httpVersion: "2.0", 232 | headers: {}, 233 | }, 234 | {}, 235 | proxyRes 236 | ); 237 | 238 | expect(proxyRes.headers.connection).to.eql(undefined); 239 | }); 240 | }); 241 | 242 | describe("#writeStatusCode", function () { 243 | it("should write status code", function () { 244 | const res = { 245 | writeHead: function (n) { 246 | expect(n).to.eql(200); 247 | }, 248 | }; 249 | 250 | httpProxy.writeStatusCode({}, res, { statusCode: 200 }); 251 | }); 252 | }); 253 | 254 | describe("#writeHeaders", function () { 255 | beforeEach(function () { 256 | this.proxyRes = { 257 | headers: { 258 | hey: "hello", 259 | how: "are you?", 260 | "set-cookie": ["hello; domain=my.domain; path=/", "there; domain=my.domain; path=/"], 261 | }, 262 | }; 263 | this.rawProxyRes = { 264 | headers: { 265 | hey: "hello", 266 | how: "are you?", 267 | "set-cookie": ["hello; domain=my.domain; path=/", "there; domain=my.domain; path=/"], 268 | }, 269 | rawHeaders: [ 270 | "Hey", 271 | "hello", 272 | "How", 273 | "are you?", 274 | "Set-Cookie", 275 | "hello; domain=my.domain; path=/", 276 | "Set-Cookie", 277 | "there; domain=my.domain; path=/", 278 | ], 279 | }; 280 | this.res = { 281 | setHeader: function (k, v) { 282 | // https://nodejs.org/api/http.html#http_message_headers 283 | // Header names are lower-cased 284 | this.headers[k.toLowerCase()] = v; 285 | }, 286 | headers: {}, 287 | }; 288 | }); 289 | 290 | it("writes headers", function () { 291 | const options = {}; 292 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 293 | 294 | expect(this.res.headers.hey).to.eql("hello"); 295 | expect(this.res.headers.how).to.eql("are you?"); 296 | 297 | expect(this.res.headers).to.have.key("set-cookie"); 298 | expect(this.res.headers["set-cookie"]).to.be.an(Array); 299 | expect(this.res.headers["set-cookie"]).to.have.length(2); 300 | }); 301 | 302 | it("writes raw headers", function () { 303 | const options = {}; 304 | httpProxy.writeHeaders({}, this.res, this.rawProxyRes, options); 305 | 306 | expect(this.res.headers.hey).to.eql("hello"); 307 | expect(this.res.headers.how).to.eql("are you?"); 308 | 309 | expect(this.res.headers).to.have.key("set-cookie"); 310 | expect(this.res.headers["set-cookie"]).to.be.an(Array); 311 | expect(this.res.headers["set-cookie"]).to.have.length(2); 312 | }); 313 | 314 | it("rewrites path", function () { 315 | const options = { 316 | cookiePathRewrite: "/dummyPath", 317 | }; 318 | 319 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 320 | 321 | expect(this.res.headers["set-cookie"]).to.contain("hello; domain=my.domain; path=/dummyPath"); 322 | }); 323 | 324 | it("does not rewrite path", function () { 325 | const options = {}; 326 | 327 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 328 | 329 | expect(this.res.headers["set-cookie"]).to.contain("hello; domain=my.domain; path=/"); 330 | }); 331 | 332 | it("removes path", function () { 333 | const options = { 334 | cookiePathRewrite: "", 335 | }; 336 | 337 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 338 | 339 | expect(this.res.headers["set-cookie"]).to.contain("hello; domain=my.domain"); 340 | }); 341 | 342 | it("does not rewrite domain", function () { 343 | const options = {}; 344 | 345 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 346 | 347 | expect(this.res.headers["set-cookie"]).to.contain("hello; domain=my.domain; path=/"); 348 | }); 349 | 350 | it("rewrites domain", function () { 351 | const options = { 352 | cookieDomainRewrite: "my.new.domain", 353 | }; 354 | 355 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 356 | 357 | expect(this.res.headers["set-cookie"]).to.contain("hello; domain=my.new.domain; path=/"); 358 | }); 359 | 360 | it("removes domain", function () { 361 | const options = { 362 | cookieDomainRewrite: "", 363 | }; 364 | 365 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 366 | 367 | expect(this.res.headers["set-cookie"]).to.contain("hello; path=/"); 368 | }); 369 | 370 | it("rewrites headers with advanced configuration", function () { 371 | const options = { 372 | cookieDomainRewrite: { 373 | "*": "", 374 | "my.old.domain": "my.new.domain", 375 | "my.special.domain": "my.special.domain", 376 | }, 377 | }; 378 | this.proxyRes.headers["set-cookie"] = [ 379 | "hello-on-my.domain; domain=my.domain; path=/", 380 | "hello-on-my.old.domain; domain=my.old.domain; path=/", 381 | "hello-on-my.special.domain; domain=my.special.domain; path=/", 382 | ]; 383 | httpProxy.writeHeaders({}, this.res, this.proxyRes, options); 384 | 385 | expect(this.res.headers["set-cookie"]).to.contain("hello-on-my.domain; path=/"); 386 | expect(this.res.headers["set-cookie"]).to.contain("hello-on-my.old.domain; domain=my.new.domain; path=/"); 387 | expect(this.res.headers["set-cookie"]).to.contain("hello-on-my.special.domain; domain=my.special.domain; path=/"); 388 | }); 389 | 390 | it("rewrites raw headers with advanced configuration", function () { 391 | const options = { 392 | cookieDomainRewrite: { 393 | "*": "", 394 | "my.old.domain": "my.new.domain", 395 | "my.special.domain": "my.special.domain", 396 | }, 397 | }; 398 | this.rawProxyRes.headers["set-cookie"] = [ 399 | "hello-on-my.domain; domain=my.domain; path=/", 400 | "hello-on-my.old.domain; domain=my.old.domain; path=/", 401 | "hello-on-my.special.domain; domain=my.special.domain; path=/", 402 | ]; 403 | this.rawProxyRes.rawHeaders = this.rawProxyRes.rawHeaders.concat([ 404 | "Set-Cookie", 405 | "hello-on-my.domain; domain=my.domain; path=/", 406 | "Set-Cookie", 407 | "hello-on-my.old.domain; domain=my.old.domain; path=/", 408 | "Set-Cookie", 409 | "hello-on-my.special.domain; domain=my.special.domain; path=/", 410 | ]); 411 | httpProxy.writeHeaders({}, this.res, this.rawProxyRes, options); 412 | 413 | expect(this.res.headers["set-cookie"]).to.contain("hello-on-my.domain; path=/"); 414 | expect(this.res.headers["set-cookie"]).to.contain("hello-on-my.old.domain; domain=my.new.domain; path=/"); 415 | expect(this.res.headers["set-cookie"]).to.contain("hello-on-my.special.domain; domain=my.special.domain; path=/"); 416 | }); 417 | }); 418 | 419 | describe("#removeChunked", function () { 420 | const proxyRes = { 421 | headers: { 422 | "transfer-encoding": "hello", 423 | }, 424 | }; 425 | 426 | httpProxy.removeChunked({ httpVersion: "1.0" }, {}, proxyRes); 427 | 428 | expect(proxyRes.headers["transfer-encoding"]).to.eql(undefined); 429 | }); 430 | }); 431 | -------------------------------------------------------------------------------- /test/lib-http-proxy-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const httpProxy = require("../lib/http-proxy"); 3 | const { randomBytes } = require("node:crypto"); 4 | const ioClient = require("socket.io-client"); 5 | const expect = require("expect.js"); 6 | const http = require("node:http"); 7 | const net = require("node:net"); 8 | const io = require("socket.io"); 9 | const ws = require("ws"); 10 | const SSE = require("sse"); 11 | // 12 | // Expose a port number generator. 13 | // thanks to @3rd-Eden 14 | // 15 | let initialPort = 1024; 16 | const gen = {}; 17 | Object.defineProperty(gen, "port", { 18 | get: function get() { 19 | return initialPort++; 20 | }, 21 | }); 22 | 23 | describe("lib/http-proxy.js", () => { 24 | describe("#createProxyServer", () => { 25 | it.skip("should throw without options", () => { 26 | let error; 27 | try { 28 | httpProxy.createProxyServer(); 29 | } catch (e) { 30 | error = e; 31 | } 32 | 33 | expect(error).to.be.an(Error); 34 | }); 35 | 36 | it("should return an object otherwise", () => { 37 | const obj = httpProxy.createProxyServer({ 38 | target: "http://www.google.com:80", 39 | }); 40 | 41 | expect(obj.web).to.be.a(Function); 42 | expect(obj.ws).to.be.a(Function); 43 | expect(obj.listen).to.be.a(Function); 44 | }); 45 | }); 46 | 47 | describe("#createProxyServer with forward options and using web-incoming passes", () => { 48 | it("should pipe the request using web-incoming#stream method", function (done) { 49 | const ports = { source: gen.port, proxy: gen.port }; 50 | const proxy = httpProxy 51 | .createProxyServer({ 52 | forward: "http://127.0.0.1:" + ports.source, 53 | }) 54 | .listen(ports.proxy); 55 | 56 | const source = http.createServer(function (req, res) { 57 | expect(req.method).to.eql("GET"); 58 | expect(req.headers.host.split(":")[1]).to.eql(ports.proxy); 59 | source.close(); 60 | proxy.close(); 61 | done(); 62 | }); 63 | 64 | source.listen(ports.source); 65 | http.request("http://127.0.0.1:" + ports.proxy, () => {}).end(); 66 | }); 67 | }); 68 | 69 | describe("#createProxyServer using the web-incoming passes", () => { 70 | it("should proxy sse", function (done) { 71 | const ports = { source: gen.port, proxy: gen.port }; 72 | const proxy = httpProxy.createProxyServer({ 73 | target: "http://localhost:" + ports.source, 74 | }); 75 | proxy.listen(ports.proxy); 76 | const source = http.createServer(); 77 | const sse = new SSE(source, { path: "/" }); 78 | 79 | sse.on("connection", function (client) { 80 | client.send("Hello over SSE"); 81 | client.close(); 82 | }); 83 | 84 | source.listen(ports.source); 85 | 86 | const options = { 87 | hostname: "localhost", 88 | port: ports.proxy, 89 | }; 90 | 91 | http 92 | .request(options, function (res) { 93 | let streamData = ""; 94 | res.on("data", function (chunk) { 95 | streamData += chunk.toString("utf8"); 96 | }); 97 | res.once("end", () => { 98 | expect(streamData).to.equal(":ok\n\ndata: Hello over SSE\n\n"); 99 | source.close(); 100 | proxy.close(); 101 | done(); 102 | }); 103 | }) 104 | .end(); 105 | }); 106 | 107 | it("should make the request on pipe and finish it", function (done) { 108 | const ports = { source: gen.port, proxy: gen.port }; 109 | const proxy = httpProxy 110 | .createProxyServer({ 111 | target: "http://127.0.0.1:" + ports.source, 112 | }) 113 | .listen(ports.proxy); 114 | 115 | const source = http.createServer(function (req) { 116 | expect(req.method).to.eql("POST"); 117 | expect(req.headers["x-forwarded-for"]).to.eql("127.0.0.1"); 118 | expect(req.headers.host.split(":")[1]).to.eql(ports.proxy); 119 | source.close(); 120 | proxy.close(); 121 | done(); 122 | }); 123 | 124 | source.listen(ports.source); 125 | 126 | http 127 | .request( 128 | { 129 | hostname: "127.0.0.1", 130 | port: ports.proxy, 131 | method: "POST", 132 | headers: { 133 | "X-Forwarded-for": "127.0.0.1", 134 | }, 135 | }, 136 | () => {} 137 | ) 138 | .end(); 139 | }); 140 | }); 141 | 142 | describe("#createProxyServer using the web-incoming passes", () => { 143 | it("should make the request, handle response and finish it", function (done) { 144 | const ports = { source: gen.port, proxy: gen.port }; 145 | const proxy = httpProxy 146 | .createProxyServer({ 147 | target: "http://127.0.0.1:" + ports.source, 148 | preserveHeaderKeyCase: true, 149 | }) 150 | .listen(ports.proxy); 151 | 152 | const source = http.createServer(function (req, res) { 153 | expect(req.method).to.eql("GET"); 154 | expect(req.headers.host.split(":")[1]).to.eql(ports.proxy); 155 | res.writeHead(200, { "Content-Type": "text/plain" }); 156 | res.end("Hello from " + source.address().port); 157 | }); 158 | 159 | source.listen(ports.source); 160 | 161 | http 162 | .request( 163 | { 164 | hostname: "127.0.0.1", 165 | port: ports.proxy, 166 | method: "GET", 167 | }, 168 | function (res) { 169 | expect(res.statusCode).to.eql(200); 170 | expect(res.headers["content-type"]).to.eql("text/plain"); 171 | if (res.rawHeaders !== undefined) { 172 | expect(res.rawHeaders.indexOf("content-type")).not.to.eql(-1); 173 | expect(res.rawHeaders.indexOf("text/plain")).not.to.eql(-1); 174 | } 175 | 176 | res.on("data", function (data) { 177 | expect(data.toString()).to.eql("Hello from " + ports.source); 178 | }); 179 | 180 | res.once("end", () => { 181 | source.close(); 182 | proxy.close(); 183 | done(); 184 | }); 185 | } 186 | ) 187 | .end(); 188 | }); 189 | }); 190 | 191 | describe("#createProxyServer() method with error response", () => { 192 | it("should make the request and emit the error event", function (done) { 193 | const ports = { source: gen.port, proxy: gen.port }; 194 | const proxy = httpProxy.createProxyServer({ 195 | target: "http://127.0.0.1:" + ports.source, 196 | }); 197 | 198 | proxy.on("error", function (err) { 199 | expect(err).to.be.an(Error); 200 | expect(err.code).to.be("ECONNREFUSED"); 201 | proxy.close(); 202 | done(); 203 | }); 204 | 205 | proxy.listen(ports.proxy); 206 | 207 | http 208 | .request( 209 | { 210 | hostname: "127.0.0.1", 211 | port: ports.proxy, 212 | method: "GET", 213 | }, 214 | () => {} 215 | ) 216 | .end(); 217 | }); 218 | }); 219 | 220 | describe("#createProxyServer setting the correct timeout value", () => { 221 | it("should hang up the socket at the timeout", function (done) { 222 | this.timeout(30); 223 | const ports = { source: gen.port, proxy: gen.port }; 224 | const proxy = httpProxy 225 | .createProxyServer({ 226 | target: "http://127.0.0.1:" + ports.source, 227 | timeout: 3, 228 | }) 229 | .listen(ports.proxy); 230 | 231 | proxy.on("error", function (e) { 232 | expect(e).to.be.an(Error); 233 | expect(e.code).to.be.eql("ECONNRESET"); 234 | }); 235 | 236 | const source = http.createServer(function (req, res) { 237 | setTimeout(() => { 238 | res.end("At this point the socket should be closed"); 239 | }, 5); 240 | }); 241 | 242 | source.listen(ports.source); 243 | 244 | const testReq = http.request( 245 | { 246 | hostname: "127.0.0.1", 247 | port: ports.proxy, 248 | method: "GET", 249 | }, 250 | () => {} 251 | ); 252 | 253 | testReq.on("error", function (e) { 254 | expect(e).to.be.an(Error); 255 | expect(e.code).to.be.eql("ECONNRESET"); 256 | proxy.close(); 257 | source.close(); 258 | done(); 259 | }); 260 | 261 | testReq.end(); 262 | }); 263 | }); 264 | 265 | describe("#createProxyServer with xfwd option", () => { 266 | it("should not throw on empty http host header", function (done) { 267 | const ports = { source: gen.port, proxy: gen.port }; 268 | const proxy = httpProxy 269 | .createProxyServer({ 270 | forward: "http://127.0.0.1:" + ports.source, 271 | xfwd: true, 272 | }) 273 | .listen(ports.proxy); 274 | 275 | const source = http.createServer(function (req, res) { 276 | expect(req.method).to.eql("GET"); 277 | expect(req.headers.host.split(":")[1]).to.eql(ports.source); 278 | source.close(); 279 | proxy.close(); 280 | done(); 281 | }); 282 | 283 | source.listen(ports.source); 284 | 285 | const socket = net.connect({ port: ports.proxy }, () => { 286 | socket.write("GET / HTTP/1.0\r\n\r\n"); 287 | }); 288 | 289 | // handle errors 290 | socket.on("error", () => { 291 | expect.fail("Unexpected socket error"); 292 | }); 293 | 294 | socket.on("data", function (data) { 295 | socket.end(); 296 | }); 297 | 298 | socket.on("end", () => { 299 | expect("Socket to finish").to.be.ok(); 300 | }); 301 | 302 | // http.request('http://127.0.0.1:' + ports.proxy, function() {}).end(); 303 | }); 304 | }); 305 | 306 | // describe('#createProxyServer using the web-incoming passes', () => { 307 | // it('should emit events correctly', function(done) { 308 | // var proxy = httpProxy.createProxyServer({ 309 | // target: 'http://127.0.0.1:8080' 310 | // }), 311 | 312 | // proxyServer = proxy.listen('8081'), 313 | 314 | // source = http.createServer(function(req, res) { 315 | // expect(req.method).to.eql('GET'); 316 | // expect(req.headers.host.split(':')[1]).to.eql('8081'); 317 | // res.writeHead(200, {'Content-Type': 'text/plain'}) 318 | // res.end('Hello from ' + source.address().port); 319 | // }), 320 | 321 | // events = []; 322 | 323 | // source.listen('8080'); 324 | 325 | // proxy.ee.on('http-proxy:**', function (uno, dos, tres) { 326 | // events.push(this.event); 327 | // }) 328 | 329 | // http.request({ 330 | // hostname: '127.0.0.1', 331 | // port: '8081', 332 | // method: 'GET', 333 | // }, function(res) { 334 | // expect(res.statusCode).to.eql(200); 335 | 336 | // res.on('data', function (data) { 337 | // expect(data.toString()).to.eql('Hello from 8080'); 338 | // }); 339 | 340 | // res.on('end', () => { 341 | // expect(events).to.contain('http-proxy:outgoing:web:begin'); 342 | // expect(events).to.contain('http-proxy:outgoing:web:end'); 343 | // source.close(); 344 | // proxyServer.close(); 345 | // done(); 346 | // }); 347 | // }).end(); 348 | // }); 349 | // }); 350 | 351 | describe("#createProxyServer using the ws-incoming passes", () => { 352 | it("should proxy the websockets stream", function (done) { 353 | const ports = { source: gen.port, proxy: gen.port }; 354 | const proxy = httpProxy.createProxyServer({ 355 | target: "ws://127.0.0.1:" + ports.source, 356 | ws: true, 357 | }); 358 | const proxyServer = proxy.listen(ports.proxy); 359 | const destiny = new ws.Server({ port: ports.source }, () => { 360 | const client = new ws("ws://127.0.0.1:" + ports.proxy); 361 | 362 | client.on("open", () => { 363 | client.send("hello there"); 364 | }); 365 | 366 | client.on("message", function (msg) { 367 | expect(msg.toString()).to.be("Hello over websockets"); 368 | client.close(); 369 | proxyServer.close(); 370 | destiny.close(); 371 | done(); 372 | }); 373 | }); 374 | 375 | destiny.on("connection", function (socket) { 376 | socket.on("message", function (msg) { 377 | expect(msg.toString()).to.be("hello there"); 378 | socket.send("Hello over websockets"); 379 | }); 380 | }); 381 | }); 382 | 383 | it("should emit error on proxy error", function (done) { 384 | const ports = { source: gen.port, proxy: gen.port }; 385 | const proxy = httpProxy.createProxyServer({ 386 | // note: we don't ever listen on this port 387 | target: "ws://127.0.0.1:" + ports.source, 388 | ws: true, 389 | }); 390 | const proxyServer = proxy.listen(ports.proxy); 391 | const client = new ws("ws://127.0.0.1:" + ports.proxy); 392 | 393 | client.on("open", () => { 394 | client.send("hello there"); 395 | }); 396 | 397 | let count = 0; 398 | function maybeDone() { 399 | count += 1; 400 | if (count === 2) done(); 401 | } 402 | 403 | client.on("error", function (err) { 404 | expect(err).to.be.an(Error); 405 | expect(err.code).to.be("ECONNRESET"); 406 | maybeDone(); 407 | }); 408 | 409 | proxy.on("error", function (err) { 410 | expect(err).to.be.an(Error); 411 | expect(err.code).to.be("ECONNREFUSED"); 412 | proxyServer.close(); 413 | maybeDone(); 414 | }); 415 | }); 416 | 417 | it("should close client socket if upstream is closed before upgrade", function (done) { 418 | const ports = { source: gen.port, proxy: gen.port }; 419 | const server = http.createServer(); 420 | server.on("upgrade", function (req, socket, head) { 421 | const response = ["HTTP/1.1 404 Not Found", "Content-type: text/html", "", ""]; 422 | socket.write(response.join("\r\n")); 423 | socket.end(); 424 | }); 425 | server.listen(ports.source); 426 | 427 | const proxy = httpProxy.createProxyServer({ 428 | // note: we don't ever listen on this port 429 | target: "ws://127.0.0.1:" + ports.source, 430 | ws: true, 431 | }); 432 | const proxyServer = proxy.listen(ports.proxy); 433 | const client = new ws("ws://127.0.0.1:" + ports.proxy); 434 | 435 | client.on("open", () => { 436 | client.send("hello there"); 437 | }); 438 | 439 | client.on("error", function (err) { 440 | expect(err).to.be.an(Error); 441 | proxyServer.close(); 442 | done(); 443 | }); 444 | }); 445 | 446 | it("should proxy a socket.io stream", function (done) { 447 | const ports = { source: gen.port, proxy: gen.port }; 448 | const proxy = httpProxy.createProxyServer({ 449 | target: "ws://127.0.0.1:" + ports.source, 450 | ws: true, 451 | }); 452 | const proxyServer = proxy.listen(ports.proxy); 453 | const server = http.createServer(); 454 | const destiny = new io.Server(server); 455 | 456 | function startSocketIo() { 457 | const client = ioClient.connect("ws://127.0.0.1:" + ports.proxy); 458 | 459 | client.on("connect", () => { 460 | client.emit("incoming", "hello there"); 461 | }); 462 | 463 | client.on("outgoing", function (data) { 464 | expect(data).to.be("Hello over websockets"); 465 | proxyServer.close(); 466 | server.close(); 467 | done(); 468 | }); 469 | } 470 | server.listen(ports.source); 471 | server.on("listening", startSocketIo); 472 | 473 | destiny.sockets.on("connection", function (socket) { 474 | socket.on("incoming", function (msg) { 475 | expect(msg).to.be("hello there"); 476 | socket.emit("outgoing", "Hello over websockets"); 477 | }); 478 | }); 479 | }); 480 | 481 | it("should emit open and close events when socket.io client connects and disconnects", function (done) { 482 | const ports = { source: gen.port, proxy: gen.port }; 483 | const proxy = httpProxy.createProxyServer({ 484 | target: "ws://127.0.0.1:" + ports.source, 485 | ws: true, 486 | }); 487 | const proxyServer = proxy.listen(ports.proxy); 488 | const server = http.createServer(); 489 | const destiny = new io.Server(server); 490 | 491 | function startSocketIo() { 492 | const client = ioClient.connect(`ws://127.0.0.1:${ports.proxy}`); 493 | client.once("connect", client.disconnect); 494 | } 495 | 496 | let count = 0; 497 | proxyServer.once("open", () => { 498 | count += 1; 499 | }); 500 | 501 | proxyServer.once("close", () => { 502 | proxyServer.close(); 503 | server.close(); 504 | destiny.close(); 505 | if (count === 1) { 506 | done(); 507 | } 508 | }); 509 | 510 | server.listen(ports.source); 511 | server.on("listening", startSocketIo); 512 | }); 513 | 514 | it("should pass all set-cookie headers to client", function (done) { 515 | const ports = { source: gen.port, proxy: gen.port }; 516 | const proxy = httpProxy.createProxyServer({ 517 | target: "ws://127.0.0.1:" + ports.source, 518 | ws: true, 519 | }); 520 | proxy.listen(ports.proxy); 521 | const destiny = new ws.Server({ port: ports.source }, () => { 522 | const key = randomBytes(16).toString("base64"); 523 | 524 | const requestOptions = { 525 | port: ports.proxy, 526 | host: "127.0.0.1", 527 | headers: { 528 | Connection: "Upgrade", 529 | Upgrade: "websocket", 530 | Host: "ws://127.0.0.1", 531 | "Sec-WebSocket-Version": 13, 532 | "Sec-WebSocket-Key": key, 533 | }, 534 | }; 535 | 536 | const req = http.request(requestOptions); 537 | 538 | req.on("upgrade", function (res, socket, upgradeHead) { 539 | expect(res.headers["set-cookie"].length).to.be(2); 540 | done(); 541 | }); 542 | 543 | req.end(); 544 | }); 545 | 546 | destiny.on("headers", function (headers) { 547 | headers.push("Set-Cookie: test1=test1"); 548 | headers.push("Set-Cookie: test2=test2"); 549 | }); 550 | }); 551 | 552 | it("should detect a proxyReq event and modify headers", function (done) { 553 | const ports = { source: gen.port, proxy: gen.port }; 554 | const proxy = httpProxy.createProxyServer({ 555 | target: "ws://127.0.0.1:" + ports.source, 556 | ws: true, 557 | }); 558 | 559 | proxy.on("proxyReqWs", function (proxyReq, req, socket, options, head) { 560 | proxyReq.setHeader("X-Special-Proxy-Header", "foobar"); 561 | }); 562 | 563 | const proxyServer = proxy.listen(ports.proxy); 564 | 565 | const destiny = new ws.Server({ port: ports.source }, () => { 566 | const client = new ws("ws://127.0.0.1:" + ports.proxy); 567 | 568 | client.on("open", () => { 569 | client.send("hello there"); 570 | }); 571 | 572 | client.on("message", function (msg) { 573 | expect(msg.toString()).to.be("Hello over websockets"); 574 | client.close(); 575 | proxyServer.close(); 576 | destiny.close(); 577 | done(); 578 | }); 579 | }); 580 | 581 | destiny.on("connection", function (socket, upgradeReq) { 582 | expect(upgradeReq.headers["x-special-proxy-header"]).to.eql("foobar"); 583 | 584 | socket.on("message", function (msg) { 585 | expect(msg.toString()).to.be("hello there"); 586 | socket.send("Hello over websockets"); 587 | }); 588 | }); 589 | }); 590 | 591 | it("should forward frames with single frame payload (including on node 4.x)", function (done) { 592 | const payload = Array(65529).join("0"); 593 | const ports = { source: gen.port, proxy: gen.port }; 594 | const proxy = httpProxy.createProxyServer({ 595 | target: "ws://127.0.0.1:" + ports.source, 596 | ws: true, 597 | }); 598 | const proxyServer = proxy.listen(ports.proxy); 599 | const destiny = new ws.Server({ port: ports.source }, () => { 600 | const client = new ws("ws://127.0.0.1:" + ports.proxy); 601 | 602 | client.on("open", () => { 603 | client.send(payload); 604 | }); 605 | 606 | client.on("message", function (msg) { 607 | expect(msg.toString()).to.be("Hello over websockets"); 608 | client.close(); 609 | proxyServer.close(); 610 | destiny.close(); 611 | done(); 612 | }); 613 | }); 614 | 615 | destiny.on("connection", function (socket) { 616 | socket.on("message", function (msg) { 617 | expect(msg.toString()).to.be(payload); 618 | socket.send("Hello over websockets"); 619 | }); 620 | }); 621 | }); 622 | 623 | it("should forward continuation frames with big payload (including on node 4.x)", function (done) { 624 | const payload = Array(65530).join("0"); 625 | const ports = { source: gen.port, proxy: gen.port }; 626 | const proxy = httpProxy.createProxyServer({ 627 | target: "ws://127.0.0.1:" + ports.source, 628 | ws: true, 629 | }); 630 | const proxyServer = proxy.listen(ports.proxy); 631 | const destiny = new ws.Server({ port: ports.source }, () => { 632 | const client = new ws("ws://127.0.0.1:" + ports.proxy); 633 | 634 | client.on("open", () => { 635 | client.send(payload); 636 | }); 637 | 638 | client.on("message", function (msg) { 639 | expect(msg.toString()).to.be("Hello over websockets"); 640 | client.close(); 641 | proxyServer.close(); 642 | destiny.close(); 643 | done(); 644 | }); 645 | }); 646 | 647 | destiny.on("connection", function (socket) { 648 | socket.on("message", function (msg) { 649 | expect(msg.toString()).to.be(payload); 650 | socket.send("Hello over websockets"); 651 | }); 652 | }); 653 | }); 654 | }); 655 | }); 656 | --------------------------------------------------------------------------------