From e5fa13ddf442033880c6c9f59e8055045321fa6a Mon Sep 17 00:00:00 2001 From: Steven Chim Date: Mon, 27 Jul 2015 23:03:11 +0200 Subject: [PATCH 1/2] feat: added websocket support --- README.md | 5 ++++- examples/websocket/index.js | 38 ++++++++++++++++++++++++++++++++ index.js | 43 +++++++++++++++++++++++++++++++------ lib/handlers.js | 8 +------ lib/path-rewriter.js | 5 +++++ package.json | 3 ++- test/path-rewriter.spec.js | 9 +++++--- 7 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 examples/websocket/index.js diff --git a/README.md b/README.md index a6f04b0b..cb4a3b6a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # http-proxy-middleware [![Build Status](https://img.shields.io/travis/chimurai/http-proxy-middleware/master.svg?style=flat-square)](https://travis-ci.org/chimurai/http-proxy-middleware) [![Coveralls](https://img.shields.io/coveralls/chimurai/http-proxy-middleware.svg?style=flat-square)](https://coveralls.io/r/chimurai/http-proxy-middleware) -[![dependency Status](https://img.shields.io/david/chimurai/http-proxy-middleware.svg?style=flat-square)](https://david-dm.org/chimurai/http-proxy-middleware#info=devDependencies) +[![dependency Status](https://img.shields.io/david/chimurai/http-proxy-middleware.svg?style=flat-square)](https://david-dm.org/chimurai/http-proxy-middleware#info=dependencies) [![devDependency Status](https://img.shields.io/david/dev/chimurai/http-proxy-middleware.svg?style=flat-square)](https://david-dm.org/chimurai/http-proxy-middleware#info=devDependencies) The one-liner proxy middleware for [connect](https://github.com/senchalabs/connect), [express](https://github.com/strongloop/express) and [browser-sync](https://github.com/BrowserSync/browser-sync) @@ -86,6 +86,8 @@ The following options are provided by the underlying [http-proxy](https://www.np * **option.xfwd**: true/false, adds x-forward headers * **option.toProxy**: passes the absolute URL as the `path` (useful for proxying to proxies) * **option.hostRewrite**: rewrites the location hostname on (301/302/307/308) redirects. + * **option.ssl: object to be passed to https.createServer() + * **option.ws: true/false**: if you want to proxy websockets Undocumented options are provided by the underlying [http-proxy](https://github.com/nodejitsu/node-http-proxy/blob/master/lib/http-proxy.js#L32). * **option.headers**: object, adds [request headers](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields). (Example: `{host:'www.example.org'}` @@ -140,6 +142,7 @@ $ node examples/connect * `examples/connect` - [connect proxy middleware example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/connect) * `examples/express` - [express proxy middleware example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/express) * `examples/browser-sync` - [browser-sync proxy middleware example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/browser-sync) + * `examples/websocket` - [websocket proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/websocket) with express ## Tests diff --git a/examples/websocket/index.js b/examples/websocket/index.js new file mode 100644 index 00000000..9255a629 --- /dev/null +++ b/examples/websocket/index.js @@ -0,0 +1,38 @@ +/** + * Module dependencies. + */ +var express = require('../../node_modules/express/index'); // require('express'); +var proxyMiddleware = require('../../index'); // require('http-proxy-middleware'); + +// configure proxy middleware +// context: '/' will proxy all requests +var proxy = proxyMiddleware('/', { + target: 'http://echo.websocket.org', + // target: 'ws://echo.websocket.org', // alternative way to provide target with ws:// protocol + // pathRewrite: { + // '^/websocket' : '/socket', // rewrite path. + // '^/removepath' : '' // remove path. + // }, + changeOrigin: true, // for vhosted sites, changes host header to match to target's host + ws: true // enable websocket proxy + + }); + +var app = express(); +app.use(proxy); // add the proxy to express + +app.listen(3000); + +console.log('listening on port 3000'); +console.log('try:'); +console.log(' ws://localhost:3000 requests will be proxied to ws://echo.websocket.org'); + +/** + * Example: + * Open http://localhost:3000 in WebSocket compatible browser. // don't mind the 404 page... + * In browser console: + * 1. `var socket = new WebSocket('ws://localhost:3000');` // create new WebSocket + * 2. `socket.onmessage = function (msg) {console.log(msg)};` // listen to socket messages + * 3. `socket.send('hello world')`; // send message + * > {data: "hello world"} // server should echo back your message. + **/ diff --git a/index.js b/index.js index 42c60498..fde597ec 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,12 @@ var httpProxy = require('http-proxy'); var handlers = require('./lib/handlers'); var contextMatcher = require('./lib/context-matcher'); -var pathRewriter = require('./lib/path-rewriter'); +var PathRewriter = require('./lib/path-rewriter'); var httpProxyMiddleware = function (context, opts) { - + var isWsUpgradeListened = false; var proxyOptions = opts || {}; + var pathRewriter; // Legacy option.proxyHost // set options.headers.host when option.proxyHost is provided @@ -22,13 +23,14 @@ var httpProxyMiddleware = function (context, opts) { // create proxy var proxy = httpProxy.createProxyServer(proxyOptions); + console.log('[HPM] Proxy created:', context, ' -> ', proxyOptions.target); - // handle option.pathRewrite - if (proxyOptions.pathRewrite) { - var rewriter = pathRewriter.create(proxyOptions.pathRewrite); + pathRewriter = PathRewriter.create(proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided + // handle option.pathRewrite + if (pathRewriter) { proxy.on('proxyReq', function (proxyReq, req, res, options) { - handlers.proxyPathRewrite(proxyReq, rewriter); + proxyReq.path = pathRewriter(proxyReq.path); }); } @@ -37,7 +39,11 @@ var httpProxyMiddleware = function (context, opts) { handlers.proxyError(err, req, res, proxyOptions); }); - console.log('[HPM] Proxy created:', context, ' -> ', proxyOptions.target); + // Listen for the `close` event on `proxy`. + proxy.on('close', function (req, socket, head) { + // view disconnected websocket connections + console.log('[HPM] Client disconnected'); + }); return middleware; @@ -47,6 +53,29 @@ var httpProxyMiddleware = function (context, opts) { } else { next(); } + + if (proxyOptions.ws === true) { + catchUpgradeRequest(req); + } + } + + function catchUpgradeRequest (req) { + // make sure only 1 handle listens to server's upgrade request. + if (isWsUpgradeListened === true) { + return; + } + + isWsUpgradeListened = true; + + req.connection.server.on('upgrade', function (req, socket, head) { + if (contextMatcher.match(context, req.url)) { + if (pathRewriter) { + req.url = pathRewriter(req.url); + } + proxy.ws(req, socket, head); + console.log('[HPM] Upgrading to WebSocket'); + } + }); } }; diff --git a/lib/handlers.js b/lib/handlers.js index 8fe64a94..299ac733 100644 --- a/lib/handlers.js +++ b/lib/handlers.js @@ -1,6 +1,5 @@ module.exports = { - proxyError : proxyError, - proxyPathRewrite : proxyPathRewrite + proxyError : proxyError } function proxyError (err, req, res, proxyOptions) { @@ -11,8 +10,3 @@ function proxyError (err, req, res, proxyOptions) { console.log('[HPM] Proxy error:', err.code, targetUri); }; - -function proxyPathRewrite(proxyReq, fnRewriter) { - proxyReq.path = fnRewriter(proxyReq.path); -} - diff --git a/lib/path-rewriter.js b/lib/path-rewriter.js index ac27b562..3a95383d 100644 --- a/lib/path-rewriter.js +++ b/lib/path-rewriter.js @@ -6,6 +6,11 @@ module.exports = { * Create rewrite function, to cache parsed rewrite rules. */ function createPathRewriter (config) { + + if (config === undefined || config === null) { + return; + } + var rules = parsePathRewriteRules(config); return rewritePath; diff --git a/package.json b/package.json index 8b8f9e76..4f74f9d0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "connect", "express", "browser-sync", - "gulp" + "gulp", + "websocket" ], "author": "Steven Chim", "license": "MIT", diff --git a/test/path-rewriter.spec.js b/test/path-rewriter.spec.js index 4595c47e..00512005 100644 --- a/test/path-rewriter.spec.js +++ b/test/path-rewriter.spec.js @@ -54,10 +54,13 @@ describe('Path rewriting', function () { }; }); + it('should return undefined when no config is provided', function () { + expect((badFn())()).to.equal(undefined); + expect((badFn(null)())).to.equal(undefined); + expect((badFn(undefined)())).to.equal(undefined); + }); + it('should throw when bad config is provided', function () { - expect(badFn()).to.throw(Error); - expect(badFn(null)).to.throw(Error); - expect(badFn(undefined)).to.throw(Error); expect(badFn(123)).to.throw(Error); expect(badFn("abc")).to.throw(Error); expect(badFn(function(){})).to.throw(Error); From d752cd0367a603ba6640db3944003a12c8aedfc0 Mon Sep 17 00:00:00 2001 From: Steven Chim Date: Tue, 28 Jul 2015 23:00:59 +0200 Subject: [PATCH 2/2] tests: websocket proxy --- package.json | 3 +- test/websocket.spec.js | 73 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 test/websocket.spec.js diff --git a/package.json b/package.json index 4f74f9d0..adc09051 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "istanbul": "^0.3.17", "istanbul-coveralls": "^1.0.3", "mocha": "^2.2.5", - "mocha-lcov-reporter": "0.0.2" + "mocha-lcov-reporter": "0.0.2", + "ws": "^0.7.2" }, "dependencies": { "http-proxy": "^1.11.1", diff --git a/test/websocket.spec.js b/test/websocket.spec.js new file mode 100644 index 00000000..f8de24c1 --- /dev/null +++ b/test/websocket.spec.js @@ -0,0 +1,73 @@ +var expect = require('chai').expect; +var proxyMiddleware = require('../index'); +var http = require('http'); +var express = require('express'); +var WebSocket = require('ws'); +var WebSocketServer = require('ws').Server; + +describe('websocket proxy', function () { + var proxyServer, ws, wss; + var targetHeaders; + var responseMessage; + + beforeEach(function () { + proxyServer = createServer(3000, proxyMiddleware('/', { + target:'http://localhost:8000', + ws: true, + pathRewrite: { + '^/socket' : '' + } + })); + + wss = new WebSocketServer({ port: 8000 }); + wss.on('connection', function connection(ws) { + ws.on('message', function incoming(message) { + ws.send(message); // echo received message + }); + }); + }); + + beforeEach(function (done) { + // need to make a normal http request, + // so http-proxy-middleware can catch the upgrade request + http.get('http://localhost:3000/', function () { + // do a second http request to make + // sure only 1 listener subscribes to upgrade request + http.get('http://localhost:3000/', function () { + ws = new WebSocket('ws://localhost:3000/socket'); + + ws.on('message', function incoming(message) { + responseMessage = message; + done(); + }); + + ws.on('open', function open() { + ws.send('foobar'); + }); + }); + }); + + }); + + afterEach(function () { + proxyServer.close(); + wss.close(); + ws = null; + }); + + it('should proxy to path', function () { + expect(responseMessage).to.equal('foobar'); + }); +}); + +function createServer (portNumber, middleware) { + var app = express(); + + if (middleware) { + app.use(middleware); + } + + var server = app.listen(portNumber); + + return server; +}