From fcc45433597b7f47bab0e475307eaff91e715461 Mon Sep 17 00:00:00 2001 From: Xiaoxin Lu Date: Fri, 7 Apr 2017 16:28:15 -0400 Subject: [PATCH] feat: update engine.io to capture and forward sticky session header --- .editorconfig | 12 +++++++++++ .travis.yml | 17 +++++++++++++++ README.md | 29 +++++++++++++++++++++++++ index.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 40 ++++++++++++++++++++++++++++++++++ test/test.js | 28 ++++++++++++++++++++++++ 6 files changed, 185 insertions(+) create mode 100644 .editorconfig create mode 100644 .travis.yml create mode 100644 index.js create mode 100644 package.json create mode 100644 test/test.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /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] +trim_trailing_whitespace = false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..730edbe --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: node_js +cache: + directories: + - node_modules +notifications: + email: false +node_js: + - '7' + - '6' + - '4' +before_script: + - npm prune +after_success: + - npm run semantic-release +branches: + except: + - /^v\d+\.\d+\.\d+$/ diff --git a/README.md b/README.md index 35829c6..e7f1589 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,31 @@ # socket.io-sticky-headers + Use custom header to maintain sticky sessions with socket.io + +## Problem + +For cluster of socket.io servers Socket.io connections made by the client requires a handshake process that involves sending several XHR requests. If any of those requests fail to connect to the same instance the handshake will fail and socket.io will be in a reconnect loop (failing with 401 errors). + +Sticky sessions are need to successfully connect to a cluster of socket.io servers. Usually this can be managed with a Cookie. However browsers such as Safari has very strict third party cookie rules which could prevent this from working. An example could be a chat client placed on a customers site. + +This module patches the engine.io XHR polling to capture a custom http header response of your choosing. Any future requests made will use the same http header. Future requests will update the header if that header ever changed. + +## Install + +```bash +npm install socket.io-sticky-headers --save +``` + +## Usage + +### Socket.io + +```javascript +require('socket.io-sticky-headers')(require('socket.io-client/node_modules/engine.io-client/lib/transports/polling-xhr'), 'My-Session-Id'); +``` + +### Engine.io + +```javascript +require('socket.io-sticky-headers')(require('engine.io-client/lib/transports/polling-xhr'), 'My-Session-Id'); +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..467d4c6 --- /dev/null +++ b/index.js @@ -0,0 +1,59 @@ +'use strict' + +const debug = require('debug')('engine.io-sticky-headers') + +let initialized = false + +function initialize (XHR, stickyHeader) { + if (initialized) return + + if (stickyHeader == null) { + stickyHeader = 'Session-Id' + } + + if (XHR == null || typeof XHR !== 'function') { + throw new Error('Please provide XHR function constructor ie require("engine.io-client/lib/transports/polling-xhr"') + } + + const Request = XHR.Request + + const originalXHRRequest = XHR.prototype.request + if (originalXHRRequest == null) { + throw new Error('XHR.prototype.request is not set') + } + + const originalOnLoad = Request.prototype.onLoad + + if (originalOnLoad == null) { + throw new Error('Request.prototype.onLoad is not set') + } + + Request.prototype.onLoad = function () { + this.emit('onLoad') + originalOnLoad.call(this) + } + + XHR.prototype.request = function (opts) { + let request = originalXHRRequest.call(this, opts) + request.once('onLoad', createStickyUpdater(this)) + return request + } + + // Update sticky header after every successful request + function createStickyUpdater (xhr) { + return function updateSticky () { + let newStickyValue = this.xhr.getResponseHeader(stickyHeader) + if (newStickyValue != null) { + debug('setting header %s to %s', stickyHeader, newStickyValue) + if (xhr.extraHeaders == null) { + xhr.extraHeaders = {} + } + xhr.extraHeaders[stickyHeader] = newStickyValue + } + } + } + + initialized = true +} + +module.exports = initialize diff --git a/package.json b/package.json new file mode 100644 index 0000000..cbbabb8 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "socket.io-sticky-headers", + "version": "0.0.0-development", + "description": "Use custom header to maintain sticky sessions with socket.io", + "main": "index.js", + "scripts": { + "test": "standard && mocha", + "semantic-release": "semantic-release pre && npm publish && semantic-release post" + }, + "repository": { + "type": "git", + "url": "https://github.com/hyperlink/socket.io-sticky-headers.git" + }, + "files": [ + "index.js" + ], + "keywords": [ + "socket.io", + "sticky sessions", + "headers" + ], + "author": "Xiaoxin Lu ", + "license": "MIT", + "bugs": { + "url": "https://github.com/hyperlink/socket.io-sticky-headers/issues" + }, + "homepage": "https://github.com/hyperlink/socket.io-sticky-headers#readme", + "devDependencies": { + "mocha": "^3.2.0", + "semantic-release": "^6.3.2", + "engine.io-client": "^2.0.2", + "standard": "^10.0.0" + }, + "dependencies": { + "debug": "^2.6.3" + }, + "peerDependencies": { + "engine.io-client": "2" + } +} diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..85f9588 --- /dev/null +++ b/test/test.js @@ -0,0 +1,28 @@ +/* eslint-env node, mocha */ + +'use strict' + +const stickyHeaders = require('../') +const assert = require('assert') + +describe('sticky-headers', function () { + describe('verify patching', function () { + it('should throw exception if passing in undefined XHR polling', function () { + assert.throws(function () { + stickyHeaders() + }, Error) + }) + + it('should throw exception if passing in null XHR polling', function () { + assert.throws(function () { + stickyHeaders(null) + }, Error) + }) + + it('should throw exception if missing prototype methods to patch', function () { + assert.throws(function () { + stickyHeaders(function () {}) + }, Error) + }) + }) +})