diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..22e308d --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + presets: ["es2015"] +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d2a7f03 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f644a4b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": "airbnb/base", + "rules": { + "comma-dangle": 0, + "no-param-reassign": 0 + } +} diff --git a/.gitignore b/.gitignore index 72c3a8e..3d3f08d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ +lib/ *.sw[po] diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..8eba6c8 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +src/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..be1fc49 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +### v2.0 + +* __[BREAKING]__ Now supports Express Routers as well. A side-effect of this is that the Router prototype is modified to add a `.ws` method - in rare cases, this may interfere with existing code that relies on the Router prototype either remaining unchanged, or adding its own `.ws` method to it. + + A `leaveRouterUntouched` option has been added to prevent this behaviour; see the API documentation for more information. + +* [minor] You can now add `.ws` functionality to any custom Router object that follows the Express Router API. diff --git a/README.md b/README.md index 7261e66..141809a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,25 @@ # express-ws [![Dependency Status](https://www.versioneye.com/nodejs/express-ws/badge?style=flat)](https://www.versioneye.com/nodejs/express-ws) -WebSocket endpoints for express applications. Gives WebSocket connections access to functionality from express middlewares. + +[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) endpoints for [Express](http://expressjs.com/) applications. Lets you define WebSocket endpoints like any other type of route, and applies regular Express midddleware like for anything else. + +Version 2.0 of this library contains a breaking change. Please make sure to read the CHANGELOG.md before upgrading. ## Installation -`npm install express-ws` + +`npm install --save express-ws` ## Usage -Add this line to your express application: + +__Full documentation can be found in the API section below. This section only shows a brief example.__ + +Add this line to your Express application: + ```javascript -var expressWs = require('express-ws')(app); //app = express app +var expressWs = require('express-ws')(app); ``` Now you will be able to add WebSocket routes (almost) the same way you add other routes. The following snippet sets up a simple echo server at `/echo`. + ```javascript app.ws('/echo', function(ws, req) { ws.on('message', function(msg) { @@ -19,7 +28,22 @@ app.ws('/echo', function(ws, req) { }); ``` -## Example +It works with routers, too, this time at `/ws-stuff/echo`: + +```javascript +var router = express.Router(); + +router.ws('/echo', function(ws, req) { + ws.on('message', function(msg) { + ws.send(msg); + }); +}); + +app.use("/ws-stuff", router); +``` + +## Full example + ```javascript var express = require('express'); var app = express(); @@ -45,3 +69,42 @@ app.ws('/', function(ws, req) { app.listen(3000); ``` + +## API + +### expressWs(app, *server*, *options*) + +Sets up `express-ws` on the specified `app`. This will modify the global Router prototype for Express as well - see the `leaveRouterUntouched` option for more information on disabling this. + +* __app__: The Express application to set up `express-ws` on. +* __server__: *Optional.* When using a custom `http.Server`, you should pass it in here, so that `express-ws` can use it to set up the WebSocket upgrade handlers. If you don't specify a `server`, you will only be able to use it with the server that is created automatically when you call `app.listen`. +* __options__: *Optional.* An object containing further options. + * __leaveRouterUntouched:__ Set this to `true` to keep `express-ws` from modifying the Router prototype. You will have to manually `applyTo` every Router that you wish to make `.ws` available on, when this is enabled. + +This function will return a new `express-ws` API object, which will be referred to as `wsInstance` in the rest of the documentation. + +### wsInstance.app + +This property contains the `app` that `express-ws` was set up on. + +### wsInstance.getWss() + +Returns the underlying WebSocket server/handler. You can use `wsInstance.getWss().clients` to obtain a list of all the connected WebSocket clients for this server. + +Note that this list will include *all* clients, not just those for a specific route - this means that it's often *not* a good idea to use this for broadcasts, for example. + +### wsInstance.applyTo(router) + +Sets up `express-ws` on the given `router` (or other Router-like object). You will only need this in two scenarios: + +1. You have enabled `options.leaveRouterUntouched`, or +2. You are using a custom router that is not based on the express.Router prototype. + +In most cases, you won't need this at all. + +## Development + +This module is written in ES6, and uses Babel for compilation. What this means in practice: + +* The source code lives in the `src/` directory. +* After changing this code, make sure to run `npm run build` to compile it. diff --git a/index.js b/index.js index df94092..8a13083 100644 --- a/index.js +++ b/index.js @@ -1,69 +1 @@ -var url = require('url'); -var http = require('http'); -var ServerResponse = http.ServerResponse; -var WebSocketServer = require('ws').Server; - -var wsServer; - -var wrapWsHandler = function(handler) { - return function(req, res, next) { - if (req.ws) { - req.wsHandled = true; - handler(req.ws, req, next); - } else { - next(); - } - }; -}; - -/** - * @param {express.Application} app - * @param {http.Server} [server] - */ -module.exports = function (app, server) { - if(!server) { - server = http.createServer(app); - - app.listen = function() - { - return server.listen.apply(server, arguments) - } - } - - wsServer = new WebSocketServer({ server: server }); - - wsServer.on('connection', function(ws) { - var response = new ServerResponse(ws.upgradeReq); - response.writeHead = function (statusCode) { - if (statusCode > 200) ws.close(); - }; - ws.upgradeReq.ws = ws; - ws.upgradeReq.url = '/.websocket/' + ws.upgradeReq.url; - - app.handle(ws.upgradeReq, response, function() { - if (!ws.upgradeReq.wsHandled) { - ws.close(); - } - }); - }); - - function addSocketRoute(route, middleware) { - var args = [].splice.call(arguments, 0); - route = '/.websocket/' + route; - - var middlewares = args.slice(1).map(wrapWsHandler); - var routeArgs = [route].concat(middlewares); - app.get.apply(app, routeArgs); - - return app; - }; - - app.ws = addSocketRoute; - - return { - app: app, - getWss: function (route) { - return wsServer; - } - }; -}; +module.exports = require("./lib").expressWs; diff --git a/package.json b/package.json index 94005e7..287ef04 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,23 @@ { "name": "express-ws", "version": "1.0.0", - "description": "WebSocket endpoints for express applications", + "description": "WebSocket endpoints for Express applications", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "prepublish": "npm run build", + "build": "babel src/ -d lib/", + "lint": "eslint src/" }, "author": "Henning Morud ", "contributors": [ "Jesús Leganés Combarro ", + "Sven Slootweg ", "Andrew Phillips " ], "license": "BSD-2-Clause", "dependencies": { "ws": "^1.0.0" }, - "devDependencies": { - "express": "^4.0.0" - }, "peerDependencies": { "express": "^4.0.0" }, @@ -36,5 +36,11 @@ "bugs": { "url": "https://github.com/HenningM/express-ws/issues" }, - "homepage": "https://github.com/HenningM/express-ws" + "homepage": "https://github.com/HenningM/express-ws", + "devDependencies": { + "babel-cli": "^6.5.1", + "babel-preset-es2015": "^6.5.0", + "eslint": "^1.10.3", + "eslint-config-airbnb": "^5.0.0" + } } diff --git a/src/add-ws-method.js b/src/add-ws-method.js new file mode 100644 index 0000000..545925e --- /dev/null +++ b/src/add-ws-method.js @@ -0,0 +1,25 @@ +import wrapMiddleware from './wrap-middleware'; +import websocketUrl from './websocket-url'; + +export default function addWsMethod(target) { + /* This prevents conflict with other things setting `.ws`. */ + if (target.ws === null || target.ws === undefined) { + target.ws = function addWsRoute(route, ...middlewares) { + const wrappedMiddlewares = middlewares.map(wrapMiddleware); + + /* We append `/.websocket` to the route path here. Why? To prevent conflicts when + * a non-WebSocket request is made to the same GET route - after all, we are only + * interested in handling WebSocket requests. + * + * Whereas the original `express-ws` prefixed this path segment, we suffix it - + * this makes it possible to let requests propagate through Routers like normal, + * which allows us to specify WebSocket routes on Routers as well \o/! */ + const wsRoute = websocketUrl(route); + + /* Here we configure our new GET route. It will never get called by a client + * directly, it's just to let our request propagate internally, so that we can + * leave the regular middleware execution and error handling to Express. */ + this.get.apply(this, [wsRoute].concat(wrappedMiddlewares)); + }; + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cefc000 --- /dev/null +++ b/src/index.js @@ -0,0 +1,80 @@ +/* This module does a lot of monkeypatching, but unfortunately that appears to be the only way to + * accomplish this kind of stuff in Express. + * + * Here be dragons. */ + +import http from 'http'; +import express from 'express'; +import ws from 'ws'; + +import websocketUrl from './websocket-url'; +import addWsMethod from './add-ws-method'; + +export function expressWs(app, httpServer, options = {}) { + let server = httpServer; + + if (server === null || server === undefined) { + /* No HTTP server was explicitly provided, create one for our Express application. */ + server = http.createServer(app); + + app.listen = function serverListen() { + server.listen.apply(server, arguments); + }; + } + + /* Make our custom `.ws` method available directly on the Express application. You should + * really be using Routers, though. */ + addWsMethod(app); + + /* Monkeypatch our custom `.ws` method into Express' Router prototype. This makes it possible, + * when using the standard Express Router, to use the `.ws` method without any further calls + * to `makeRouter`. When using a custom router, the use of `makeRouter` may still be necessary. + * + * This approach works, because Express does a strange mixin hack - the Router factory + * function is simultaneously the prototype that gets assigned to the resulting Router + * object. */ + if (!options.leaveRouterUntouched) { + addWsMethod(express.Router); + } + + const wsServer = new ws.Server({ server }); + + wsServer.on('connection', (socket) => { + const request = socket.upgradeReq; + + request.ws = socket; + request.wsHandled = false; + + /* By setting this fake `.url` on the request, we ensure that it will end up in the fake + * `.get` handler that we defined above - where the wrapper will then unpack the `.ws` + * property, indicate that the WebSocket has been handled, and call the actual handler. */ + request.url = websocketUrl(request.url); + + const dummyResponse = new http.ServerResponse(request); + + dummyResponse.writeHead = function writeHead(statusCode) { + if (statusCode > 200) { + /* Something in the middleware chain signalled an error. */ + socket.close(); + } + }; + + app.handle(request, dummyResponse, () => { + if (!request.wsHandled) { + /* There was no matching WebSocket-specific route for this request. We'll close + * the connection, as no endpoint was able to handle the request anyway... */ + socket.close(); + } + }); + }); + + return { + app, + getWss: function getWss() { + return wsServer; + }, + applyTo: function applyTo(router) { + addWsMethod(router); + } + }; +} diff --git a/src/trailing-slash.js b/src/trailing-slash.js new file mode 100644 index 0000000..c7cba79 --- /dev/null +++ b/src/trailing-slash.js @@ -0,0 +1,7 @@ +export default function addTrailingSlash(string) { + let suffixed = string; + if (suffixed.charAt(suffixed.length - 1) !== '/') { + suffixed = `${suffixed}/`; + } + return suffixed; +} diff --git a/src/websocket-url.js b/src/websocket-url.js new file mode 100644 index 0000000..64b845f --- /dev/null +++ b/src/websocket-url.js @@ -0,0 +1,11 @@ +import trailingSlash from './trailing-slash'; + +/* The following fixes HenningM/express-ws#17, correctly. */ +export default function websocketUrl(url) { + if (url.indexOf('?') !== -1) { + const [baseUrl, query] = url.split('?'); + + return `${trailingSlash(baseUrl)}.websocket?${query}`; + } + return `${trailingSlash(url)}.websocket`; +} diff --git a/src/wrap-middleware.js b/src/wrap-middleware.js new file mode 100644 index 0000000..2c40af3 --- /dev/null +++ b/src/wrap-middleware.js @@ -0,0 +1,12 @@ +export default function wrapMiddleware(middleware) { + return (req, res, next) => { + if (req.ws !== null && req.ws !== undefined) { + req.wsHandled = true; + /* Unpack the `.ws` property and call the actual handler. */ + middleware(req.ws, req, next); + } else { + /* This wasn't a WebSocket request, so skip this middleware. */ + next(); + } + }; +}