Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add route available check #161

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
examples/*.js
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
*.sw[po]
yarn.lock
package-lock.json
.vscode
5 changes: 4 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
src/
/node_modules
npm-debug.log
/src
/examples
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable-next-line no-underscore-dangle */
const _require = require('esm')(module);

module.exports = _require('./src/index').default;
module.exports = _require('./lib/index').default;
72 changes: 72 additions & 0 deletions lib/add-ws-method.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }

Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = addWsMethod;

var _wrapMiddleware = _interopRequireDefault(require("./wrap-middleware"));

var _websocketUrl = _interopRequireWildcard(require("./websocket-url"));

function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }

function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }

function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

function addWsMethod(target) {
/* This prevents conflict with other things setting `.ws`. */
if (target.ws === null || target.ws === undefined) {
target.ws = function addWsRoute(route) {
for (var _len = arguments.length, middlewares = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
middlewares[_key - 1] = arguments[_key];
}

var wrappedMiddlewares = middlewares.map(_wrapMiddleware["default"]);
/* 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/! */

var wsRoute = (0, _websocketUrl["default"])(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, _toConsumableArray([wsRoute].concat(wrappedMiddlewares)));
/* This is a trick. Just as what it is above, we use a trick url appending with
* `/.websocket_check` to config a route. when we handle the upgrade the event,
* we pipeline the req and res to let the Express check whether the url is avaliable.
*/

var wsRouteCheck = (0, _websocketUrl.websocketUrlCheck)(route);
this.get(wsRouteCheck, function (req, res, next) {
req.wsUrlChecked = true;
next();
});
/*
* Return `this` to allow for chaining:
*/

return this;
};
}
}
123 changes: 123 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }

Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = expressWs;

var _http = _interopRequireDefault(require("http"));

var _express = _interopRequireDefault(require("express"));

var _ws2 = _interopRequireDefault(require("ws"));

var _websocketUrl = _interopRequireWildcard(require("./websocket-url"));

var _addWsMethod = _interopRequireDefault(require("./add-ws-method"));

function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

/* 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. */
function expressWs(app, httpServer) {
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var server = httpServer;

if (server === null || server === undefined) {
/* No HTTP server was explicitly provided, create one for our Express application. */
server = _http["default"].createServer(app);

app.listen = function serverListen() {
var _server;

return (_server = server).listen.apply(_server, arguments);
};
}
/* Make our custom `.ws` method available directly on the Express application. You should
* really be using Routers, though. */


(0, _addWsMethod["default"])(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) {
(0, _addWsMethod["default"])(_express["default"].Router);
} // Handle the server's event `upgrade` to check the url's accessablility.


var wsServer = new _ws2["default"].Server({
noServer: true
});
server.on('upgrade', function (request, socket, head) {
var wsOriginalURL = request.url;
request.wsUrlChecked = false;
request.url = (0, _websocketUrl.websocketUrlCheck)(request.url);
var dummyResponse = new _http["default"].ServerResponse(request);
app.handle(request, dummyResponse, function () {
if (!request.wsUrlChecked) {
/* 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.destroy();
} else {
wsServer.handleUpgrade(request, socket, head, function (_ws) {
request.url = wsOriginalURL;
wsServer.emit('connection', _ws, request);
});
}
});
});
wsServer.on('connection', function (socket, request) {
if ('upgradeReq' in socket) {
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 = (0, _websocketUrl["default"])(request.url);
var dummyResponse = new _http["default"].ServerResponse(request);

dummyResponse.writeHead = function writeHead(statusCode) {
if (statusCode > 200) {
/* Something in the middleware chain signalled an error. */
dummyResponse._header = ''; // eslint-disable-line no-underscore-dangle

socket.close();
}
};

app.handle(request, dummyResponse, function () {
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: app,
getWss: function getWss() {
return wsServer;
},
applyTo: function applyTo(router) {
(0, _addWsMethod["default"])(router);
}
};
}
16 changes: 16 additions & 0 deletions lib/trailing-slash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = addTrailingSlash;

function addTrailingSlash(string) {
var suffixed = string;

if (suffixed.charAt(suffixed.length - 1) !== '/') {
suffixed = "".concat(suffixed, "/");
}

return suffixed;
}
45 changes: 45 additions & 0 deletions lib/websocket-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = websocketUrl;
exports.websocketUrlCheck = void 0;

var _trailingSlash = _interopRequireDefault(require("./trailing-slash"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }

function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }

function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }

/* The following fixes HenningM/express-ws#17, correctly. */
function websocketUrl(url) {
if (url.indexOf('?') !== -1) {
var _url$split = url.split('?'),
_url$split2 = _slicedToArray(_url$split, 2),
baseUrl = _url$split2[0],
query = _url$split2[1];

return "".concat((0, _trailingSlash["default"])(baseUrl), ".websocket?").concat(query);
}

return "".concat((0, _trailingSlash["default"])(url), ".websocket");
}
/* used to check the route avaliable */


var websocketUrlCheck = function websocketUrlCheck(url) {
return "".concat(websocketUrl(url), "_check");
};

exports.websocketUrlCheck = websocketUrlCheck;
25 changes: 25 additions & 0 deletions lib/wrap-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = wrapMiddleware;

function wrapMiddleware(middleware) {
return function (req, res, next) {
if (req.ws !== null && req.ws !== undefined) {
req.wsHandled = true;

try {
/* Unpack the `.ws` property and call the actual handler. */
middleware(req.ws, req, next);
} catch (err) {
/* If an error is thrown, let's send that on to any error handling */
next(err);
}
} else {
/* This wasn't a WebSocket request, so skip this middleware. */
next();
}
};
}
23 changes: 17 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"version": "5.0.1",
"description": "WebSocket endpoints for Express applications",
"main": "index",
"module": "src/index",
"module": "lib/index",
"scripts": {
"lint": "eslint src/"
"lint": "eslint src/",
"build": "babel src -d lib"
},
"author": "Henning Morud <[email protected]>",
"contributors": [
Expand All @@ -19,18 +20,25 @@
],
"license": "BSD-2-Clause",
"dependencies": {
"esm": "^3.0.84",
"ws": "^7.4.6"
"esm": "^3.2.25",
"ws": "^8.8.1"
},
"peerDependencies": {
"express": "^4.0.0 || ^5.0.0-alpha.1"
"express": "^4.0.0 || ^5.0.0-beta.1"
},
"engines": {
"node": ">=4.5.0"
},
"directories": {
"example": "examples"
},
"files": [
"index.js",
"LICENSE",
"README.md",
"index.js",
"lib/*.js"
],
"repository": {
"type": "git",
"url": "https://github.com/HenningM/express-ws"
Expand All @@ -45,9 +53,12 @@
},
"homepage": "https://github.com/HenningM/express-ws",
"devDependencies": {
"@babel/cli": "^7.18.10",
"@babel/core": "^7.18.13",
"@babel/preset-env": "^7.18.10",
"eslint": "^7.27.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-import": "^2.12.0",
"express": "^5.0.0-alpha.6"
"express": "^5.0.0-beta.1"
}
}
12 changes: 11 additions & 1 deletion src/add-ws-method.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import wrapMiddleware from './wrap-middleware';
import websocketUrl from './websocket-url';
import websocketUrl, { websocketUrlCheck } from './websocket-url';

export default function addWsMethod(target) {
/* This prevents conflict with other things setting `.ws`. */
Expand All @@ -21,6 +21,16 @@ export default function addWsMethod(target) {
* leave the regular middleware execution and error handling to Express. */
this.get(...[wsRoute].concat(wrappedMiddlewares));

/* This is a trick. Just as what it is above, we use a trick url appending with
* `/.websocket_check` to config a route. when we handle the upgrade the event,
* we pipeline the req and res to let the Express check whether the url is avaliable.
*/
const wsRouteCheck = websocketUrlCheck(route);
this.get(wsRouteCheck, (req, res, next) => {
req.wsUrlChecked = true;
next();
});

/*
* Return `this` to allow for chaining:
*/
Expand Down
Loading