├── test ├── .gitkeep ├── namespaces.js └── request.js ├── .gitignore ├── .eslintrc ├── lib ├── namespaces.js ├── auth.js ├── index.js ├── routes.js └── request.js ├── package.json ├── .travis.yml ├── LICENSE └── README.md /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | 6 | "rules": { 7 | "no-underscore-dangle": 0, 8 | 9 | "quotes": [2, "single"], 10 | "strict": [2, "global"], 11 | 12 | "consistent-return": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/namespaces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(io, namespaces) { 4 | 5 | var nsps = {}; 6 | 7 | nsps['/'] = io.of('/'); 8 | 9 | if (Array.isArray(namespaces)) { 10 | namespaces.forEach(function(namespace) { 11 | nsps[namespace] = io.of(namespace); 12 | }); 13 | } 14 | 15 | return nsps; 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-io", 3 | "license": "MIT", 4 | "author": "Simon Bartlett ", 5 | "version": "0.2.1", 6 | "description": "Awesome socket.io plugin for hapi", 7 | "homepage": "https://github.com/sibartlett/hapi-io", 8 | "repository": "git://github.com/sibartlett/hapi-io", 9 | "main": "lib/index.js", 10 | "scripts": { 11 | "test": "lab --verbose --lint --coverage" 12 | }, 13 | "engines": { 14 | "node": ">=4.0.0" 15 | }, 16 | "dependencies": { 17 | "async": "^2.5.0", 18 | "socket.io": "^2.0.3" 19 | }, 20 | "peerDependencies": { 21 | "hapi": ">=11.1.0", 22 | "hoek": ">=3.0.0" 23 | }, 24 | "devDependencies": { 25 | "code": "^4.1.0", 26 | "hoek": "^4.1.1", 27 | "lab": "^14.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - "4.0.0" 5 | deploy: 6 | provider: npm 7 | email: simon@sibartlett.com 8 | api_key: 9 | secure: m/lAYnseUn89iDrm2CJv3XlVbMzUjBzOmzs0AH0X3IyqKBz/y4CLq94hq1gRNZ2auhIUXIx1QOwkW7nqhpBdmo/eagjCe/V3FAdlR1UaLyCAkcw713Gt7cLmUE9p0Frq7Xc54tZlIoqT87toEHwIdcwFG/SAMOlFMe7a5FnfFNQ9hnJY2DUAeCGtlE8LWevZdfsjtUXSQshHDwCG4666rDUPeudOz4rBG8b+3bizRIJz2MGTShwdkWSISySrriDXYWbE7Lk0xNIePQBoELdsgcPJxSrg+V4OGj53CZ16WCghbZ/GXv4FkZQ09ku0I93Ju13b6iDpyhej9Kgq1j+QD6y78S+IK6DirslvhxOxkE3uaYKQNJBg8SuSDMuMyP+nf72VZh2zFSnKpJNAh7vbMeFc+xeX0QaF7xRIZFMhUsKVd5LJNm2w2xiBapHMkax5+TENIOwV4b9Cy6G8wAGZlPgjT81k9Me94QIiPHIBqCVZsYSd0Ebao8YANKdEbdjCFy2n7vJDIi9hpv9qvm9oc2+YwISB9M5SlWGBh1e0q1fo0v3DaQ8sSJKJJJ+5Te2ZES6WUJMaDkw8eKwRj24+NhXVrI55chOzHYfcbZcxfRRThKBc3nBI6yzpbyaCzJl1CqA/tYfil8wTGsvazqnkN/aY7RxEEyCcYRaVYHm1RaQ= 10 | on: 11 | tags: true 12 | repo: sibartlett/hapi-io 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Simon Bartlett 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/namespaces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Code = require('code'); 4 | var Lab = require('lab'); 5 | var lab = exports.lab = Lab.script(); 6 | 7 | var expect = Code.expect; 8 | var describe = lab.describe; 9 | var it = lab.it; 10 | 11 | describe('request', function() { 12 | 13 | var namespaces = require('../lib/namespaces'); 14 | 15 | describe('namespaces(io, namespaces)', function() { 16 | 17 | it('returns object containing namespaces', function(done) { 18 | var io = { of: function(namespace) { return namespace; }}; 19 | var names = ['/hello', '/bye']; 20 | var nsps = namespaces(io, names); 21 | 22 | expect(nsps).to.equal({ 23 | '/': io.of('/'), 24 | '/hello': io.of('/hello'), 25 | '/bye': io.of('/bye') 26 | }); 27 | 28 | done(); 29 | }); 30 | 31 | it('always returns default namespace', function(done) { 32 | var io = { of: function(namespace) { return namespace; }}; 33 | var names = 'blah'; 34 | var nsps = namespaces(io, names); 35 | 36 | expect(nsps).to.equal({ 37 | '/': io.of('/') 38 | }); 39 | 40 | done(); 41 | }); 42 | 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var request = require('./request'); 5 | 6 | module.exports = function(server, io, options) { 7 | var strategies = options.auth.strategies; 8 | 9 | // if a raw string is passed in, use that (as per README.md) 10 | if (typeof options.auth === 'string') { 11 | strategies = [options.auth]; 12 | } 13 | 14 | if (!strategies && options.auth.strategy) { 15 | strategies = [options.auth.strategy]; 16 | } 17 | 18 | // This route purposely mirrors socket.io's path 19 | server.route({ 20 | method: 'GET', 21 | path: options.socketio.path, 22 | config: { 23 | id: 'socket.io', 24 | plugins: { 25 | lout: false 26 | } 27 | }, 28 | handler: function(req, reply) { 29 | reply(); 30 | } 31 | }); 32 | 33 | io.use(function(socket, next) { 34 | var route = server.lookup('socket.io'); 35 | var req = request({ socket: socket, route: route}); 36 | 37 | server.inject(req, function(res) { 38 | // We need to call server.inject, in order to call server.auth.test 39 | 40 | async.some(strategies, function(strategy, cb) { 41 | server.auth.test(strategy, res.request, function(err, credentials) { 42 | if (err) { 43 | return cb(null, false); 44 | } 45 | 46 | socket.credentials = credentials; 47 | next(); 48 | cb(null, true); 49 | }); 50 | }, 51 | 52 | function(err, result) { 53 | if (!result) { 54 | next(new Error('Authentication Failed')); 55 | socket.disconnect(); 56 | } 57 | }); 58 | 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hoek = require('hoek'); 4 | var socketio = require('socket.io'); 5 | var auth = require('./auth'); 6 | var routes = require('./routes'); 7 | var namespaces = require('./namespaces'); 8 | 9 | // Declare internals 10 | 11 | var internals = { 12 | defaults: { 13 | socketio: { 14 | path: '/socket.io' 15 | } 16 | } 17 | }; 18 | 19 | exports.register = function(server, options, next) { 20 | 21 | options = Hoek.applyToDefaults(internals.defaults, options); 22 | 23 | var s = options.connectionLabel ? 24 | server.select(options.connectionLabel) : server; 25 | 26 | if (!s) { 27 | return next('hapi-io - no server'); 28 | } 29 | 30 | if (!s.connections.length) { 31 | return next('hapi-io - no connection'); 32 | } 33 | 34 | if (s.connections.length !== 1) { 35 | return next('hapi-io - multiple connections'); 36 | } 37 | 38 | var connection = s && s.connections.length && s.connections[0]; 39 | 40 | if (!connection) { 41 | return next('No connection/listener found'); 42 | } 43 | 44 | var io = socketio(connection.listener, options.socketio); 45 | 46 | var nsps = namespaces(io, options.namespaces); 47 | 48 | s.expose('io', io); 49 | 50 | s.ext('onRequest', function(request, reply) { 51 | if (!request.plugins['hapi-io']) { 52 | request.plugins['hapi-io'] = {}; 53 | } 54 | 55 | request.plugins['hapi-io'].io = request.server.plugins['hapi-io'].io; 56 | return reply.continue(); 57 | }); 58 | 59 | if (options.auth) { 60 | auth(s, io, options); 61 | } 62 | 63 | Object.keys(nsps).forEach(function(namespace) { 64 | nsps[namespace].on('connection', function(socket) { 65 | routes(s, socket, namespace); 66 | }); 67 | }); 68 | 69 | next(); 70 | }; 71 | 72 | exports.register.attributes = { 73 | pkg: require('../package.json') 74 | }; 75 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('./request'); 4 | 5 | module.exports = function(server, socket, socketNamespace) { 6 | var routingTable = server.table(); 7 | 8 | routingTable.forEach(function(connection) { 9 | var routes = connection.table.filter(function(item) { 10 | return item.settings && 11 | item.settings.plugins && 12 | item.settings.plugins['hapi-io']; 13 | }); 14 | 15 | routes.forEach(function(route) { 16 | var hapiio = route.settings.plugins['hapi-io']; 17 | var isBasic = typeof hapiio === 'string'; 18 | 19 | var event = isBasic ? hapiio : hapiio.event; 20 | var namespace = !isBasic && hapiio.namespace ? hapiio.namespace : '/'; 21 | 22 | if (namespace !== socketNamespace) { 23 | return; 24 | } 25 | 26 | socket.on(event, function(data, respond) { 27 | if (typeof data === 'function') { 28 | respond = data; 29 | data = undefined; 30 | } 31 | 32 | var req = request({ socket: socket, route: route, data: data }); 33 | 34 | server.inject(req, function(res) { 35 | 36 | var responder = function(err, result) { 37 | if (!respond) { 38 | return; 39 | } 40 | 41 | if (err) { 42 | // Should we be responding with the error? 43 | return respond(err); 44 | } 45 | 46 | respond(result || res.result); 47 | }; 48 | 49 | var context = { 50 | io: server.plugins['hapi-io'].io, 51 | socket: socket, 52 | event: event, 53 | data: data, 54 | req: req, 55 | res: res, 56 | result: res.result, 57 | trigger: function(_event, _data, nsp) { 58 | var packet = { 59 | type: 2, 60 | nsp: nsp || '/', 61 | id: -1, 62 | data: [_event, _data] 63 | }; 64 | 65 | socket.onevent(packet); 66 | } 67 | }; 68 | 69 | if (hapiio.post) { 70 | return hapiio.post(context, responder); 71 | } 72 | 73 | return responder(); 74 | }); 75 | }); 76 | }); 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('url'); 4 | var Hoek = require('hoek'); 5 | 6 | var getPlugins = function(options) { 7 | if (!options.socket) { 8 | return; 9 | } 10 | 11 | return { 12 | 'hapi-io': { 13 | socket: options.socket 14 | } 15 | }; 16 | }; 17 | 18 | module.exports = function(options) { 19 | var socket = options.socket || {}; 20 | var socketRequest = socket.request || {}; 21 | var route = options.route || {}; 22 | var data = options.data || {}; 23 | 24 | var method = (route.method || 'get').toLowerCase(); 25 | var path = route.path || '/'; 26 | var settings = route.settings || {}; 27 | var validate = settings.validate || {}; 28 | var plugins = settings.plugins || {}; 29 | var hapiio = plugins['hapi-io'] || {}; 30 | var mapping = hapiio.mapping || {}; 31 | var dataKeys = Object.keys(data); 32 | 33 | var get = method === 'get'; 34 | 35 | var newPath = path.replace(/(?:\{(\w+)(\??)\})/g, function(group, key, type) { 36 | var index = dataKeys.indexOf(key); 37 | var optional = type === '?'; 38 | 39 | if (index === -1) { 40 | if (optional) { 41 | return ''; 42 | } 43 | 44 | return group; 45 | } 46 | 47 | dataKeys.splice(index, 1); 48 | return data[key]; 49 | }); 50 | 51 | var headers = Hoek.clone(socketRequest.headers) || {}; 52 | delete headers['accept-encoding']; 53 | 54 | var payload = {}; 55 | var query = {}; 56 | 57 | dataKeys.forEach(function(key) { 58 | 59 | if (mapping.query && mapping.query.indexOf(key) !== -1) { 60 | query[key] = data[key]; 61 | return; 62 | } 63 | 64 | if (mapping.payload && mapping.payload.indexOf(key) !== -1) { 65 | payload[key] = data[key]; 66 | return; 67 | } 68 | 69 | if (mapping.headers && mapping.headers.indexOf(key) !== -1) { 70 | headers[key] = data[key]; 71 | return; 72 | } 73 | 74 | if (validate.query && validate.query[key]) { 75 | query[key] = data[key]; 76 | return; 77 | } 78 | 79 | if (validate.payload && validate.payload[key]) { 80 | payload[key] = data[key]; 81 | return; 82 | } 83 | 84 | if (validate.headers && validate.headers[key]) { 85 | headers[key] = data[key]; 86 | return; 87 | } 88 | 89 | if (get) { 90 | query[key] = data[key]; 91 | return; 92 | } 93 | 94 | payload[key] = data[key]; 95 | }); 96 | 97 | var uri = url.parse(newPath, true); 98 | 99 | var newQuery = {}; 100 | Hoek.merge(newQuery, socketRequest._query); 101 | Hoek.merge(newQuery, query); 102 | Hoek.merge(newQuery, uri.query); 103 | 104 | uri.query = newQuery; 105 | delete uri.search; 106 | 107 | // Auto map "Authorization" attribute to Authorization header 108 | // TODO: Make this configurable? 109 | var headerNames = ['Authorization']; 110 | headerNames.some(function(value) { 111 | return [value, value.toLowerCase()].some(function(header) { 112 | if (headers[header]) { 113 | return true; 114 | } 115 | if (payload[header]) { 116 | headers[header] = payload[header]; 117 | return true; 118 | } 119 | if (uri.query[header]) { 120 | headers[header] = uri.query[header]; 121 | return true; 122 | } 123 | 124 | return false; 125 | }); 126 | }); 127 | 128 | var pluginData = getPlugins(options); 129 | 130 | var result = { 131 | // credentials: socket.credentials, 132 | method: method, 133 | url: url.format(uri), 134 | headers: headers, 135 | payload: JSON.stringify(payload) 136 | }; 137 | 138 | if (pluginData) { 139 | result.plugins = pluginData; 140 | } 141 | 142 | return result; 143 | }; 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-io 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/sibartlett/hapi-io.svg)](https://greenkeeper.io/) 4 | [![npm](https://img.shields.io/npm/v/hapi-io.svg)](https://www.npmjs.com/package/hapi-io) 5 | [![Build Status](https://travis-ci.org/sibartlett/hapi-io.svg?branch=master)](https://travis-ci.org/sibartlett/hapi-io) 6 | [![Dependency Status](https://david-dm.org/sibartlett/hapi-io.svg)](https://david-dm.org/sibartlett/hapi-io) 7 | [![devDependency Status](https://david-dm.org/sibartlett/hapi-io/dev-status.svg)](https://david-dm.org/sibartlett/hapi-io#info=devDependencies) 8 | 9 | Awesome socket.io plugin for [hapi](http://hapijs.com/) (inspired by [express.oi](https://github.com/sibartlett/express.oi) and [express.io](https://github.com/techpines/express.io)). 10 | 11 | ##### Table of Contents 12 | 13 | * [Installation and Configuration](#installation-and-configuration) 14 | * [Raw access to socket.io](#raw-access-to-socketio) 15 | * [Forward socket.io events to hapi routes](#forward-events-to-hapi-routes) 16 | 17 | 18 | ### Installation and Configuration 19 | 20 | ```sh 21 | npm install hapi-io --save 22 | ``` 23 | 24 | ```js 25 | server.register({ 26 | register: require('hapi-io'), 27 | options: { 28 | ... 29 | } 30 | }); 31 | ``` 32 | 33 | ##### Options 34 | 35 | * `connectionLabel` (optional) 36 | * `namespaces` (optional) - an array of strings representing namespaces. Namespaces must always begin with a slash `'/'` (e.g. `'/mynamespace'`. _The default `'/'` namespace is always available irrespective if explicitly specified_, and will be the only namespace available to routes if this option is not set upon plugin initialization. 37 | * `socketio` (optional) - an object which is passed through to socket.io 38 | * `auth` (optional) - authorization configuration. Socket.io connections will be refused if they fail authorization. If this option is omitted: all socket.io connections will be accepted, but route level authorization will still be enforced. Value can be: 39 | * a string with the name of an authentication strategy registered with `server.auth.strategy()`. 40 | * an object with: 41 | * `strategies` - a string array of strategy names in order they should be attempted. If only one strategy is used, `strategy` can be used instead with the single string value. 42 | 43 | 44 | ### Raw access to socket.io 45 | 46 | You can get raw access to the [socket.io server](http://socket.io/docs/server-api/) as follows: 47 | 48 | ```js 49 | exports.register = function(server, options, next) { 50 | 51 | var io = server.plugins['hapi-io'].io; 52 | 53 | }; 54 | ``` 55 | 56 | 57 | ### Forward events to hapi routes 58 | 59 | _Perfect for exposing HTTP API endpoints over websockets!_ 60 | 61 | socket.io events can be mapped to hapi routes; reusing the same authentication, validation, plugins and handler logic. 62 | 63 | ##### Example 64 | 65 | ###### Server 66 | 67 | ```js 68 | exports.register = function(server, options, next) { 69 | 70 | server.route([ 71 | 72 | { 73 | method: 'GET', 74 | path: '/users/{id}', 75 | config: { 76 | plugins: { 77 | 'hapi-io': 'get-user' 78 | } 79 | }, 80 | handler: function(request, reply) { 81 | db.users.get(request.params.id, function(err, user) { 82 | reply(err, user); 83 | }); 84 | } 85 | }, 86 | 87 | { 88 | method: 'POST', 89 | path: '/users', 90 | config: { 91 | plugins: { 92 | 'hapi-io': { 93 | event: 'create-user', 94 | mapping: { 95 | headers: ['accept'], 96 | query: ['returnType'] 97 | } 98 | } 99 | } 100 | }, 101 | handler: function(request, reply) { 102 | db.users.create(request.payload, function(err, user) { 103 | if (err) { 104 | return reply(err).code(201); 105 | } 106 | 107 | if (request.headers.accept === 'application/hal+json') { 108 | addMeta(user); 109 | } 110 | 111 | if (request.query.returnType !== 'full') { 112 | delete user.favoriteColor; 113 | } 114 | 115 | reply(err, user); 116 | }); 117 | } 118 | }, 119 | 120 | // '/admin' namespace 121 | { 122 | method: 'GET', 123 | path: '/users/{id}', 124 | config: { 125 | plugins: { 126 | 'hapi-io': { 127 | event: 'create-user', 128 | namespace: '/admin' 129 | } 130 | } 131 | }, 132 | handler: function(request, reply) { 133 | db.adminUsers.get(request.params.id, function(err, user) { 134 | reply(err, user); 135 | }); 136 | } 137 | }, 138 | 139 | ]); 140 | }; 141 | ``` 142 | 143 | ###### Client 144 | 145 | Reference socket.io as per https://socket.io/docs/ 146 | 147 | ```js 148 | 149 | 173 | ``` 174 | 175 | ##### How it works 176 | 177 | Each time an event is received, a fake HTTP request is created and injected into the hapi server. 178 | 179 | The fake HTTP request is constructed as follows: 180 | 181 | 1. The headers and querystring parameters from the socket.io handshake are added to the fake request. 182 | 183 | This allows you to use the route's auth stategy - to authenticate the socket.io event. 184 | 185 | 2. Each field in the event payload is mapped to one of the following hapi param types: headers, path, query or payload. The mapping is determined on a per field basis: 186 | 187 | 1. If the field is a parameter in the route's path, it's mapped as a path parameter. 188 | 189 | 2. If the hapi-io config is an object and has a `mapping` property, then the field is checked against the mapping. Allowed mappings are headers, query, and payload. 190 | 191 | 3. If the field exists in the route's validate object, the value is mapped to the corresponding param type. 192 | 193 | 4. If the route is a 'GET' method, the field is mapped as a query param. 194 | 195 | 5. Otherwise it's mapped as a payload field. 196 | 197 | 3. Maps "Authorization" attribute from query or data object if possible and not already mapped. 198 | 199 | ##### Access socket during hapi request 200 | 201 | You can access both the socket.io server and socket within the hapi route. 202 | 203 | ```js 204 | exports.register = function(server, options, next) { 205 | 206 | server.route({ 207 | method: 'GET', 208 | path: '/users/{id}', 209 | config: { 210 | plugins: { 211 | 'hapi-io': 'get-user' 212 | } 213 | }, 214 | handler: function(request, reply) { 215 | var io = request.plugins['hapi-io'].io; 216 | var socket = request.plugins['hapi-io'].socket; 217 | 218 | reply({ success: true }); 219 | 220 | if (socket) { 221 | // socket is only defined during a hapi-io/socket.io request 222 | } 223 | } 224 | }); 225 | }; 226 | ``` 227 | 228 | ##### Post event hook 229 | 230 | You can do further processing on a socket.io event, after it has been processed by hapi. 231 | 232 | You can use the `post` option to specify a function, with two parameters: `ctx` and `next`. `ctx` has the following properties: 233 | 234 | * `io` - the socket.io Server object 235 | * `socket` - the socket.io Socket object 236 | * `event` - the socket.io event 237 | * `data` - the event's data object 238 | * `req` - the request object that was injected into hapi 239 | * `res` - the result object that was returned by hapi 240 | * `result` - the res.result 241 | * `trigger` - a method that allows you to trigger another socket.io event 242 | 243 | ```js 244 | server.route({ 245 | method: 'POST', 246 | path: '/rooms/{roomId}/join', 247 | config: { 248 | plugins: { 249 | 'hapi-io': { 250 | event: 'join-room', 251 | post: function(ctx, next) { 252 | ctx.socket.join(ctx.data.roomId); 253 | next(); 254 | } 255 | } 256 | } 257 | }, 258 | ... 259 | }); 260 | ``` 261 | -------------------------------------------------------------------------------- /test/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Code = require('code'); 4 | var Lab = require('lab'); 5 | var lab = exports.lab = Lab.script(); 6 | 7 | var expect = Code.expect; 8 | var describe = lab.describe; 9 | var it = lab.it; 10 | 11 | describe('request', function() { 12 | 13 | var request = require('../lib/request'); 14 | 15 | describe('request(socket, route, data)', function() { 16 | 17 | it('handles empty request', function(done) { 18 | var req = request({}); 19 | 20 | expect(req).to.equal({ 21 | method: 'get', 22 | url: '/', 23 | headers: {}, 24 | payload: JSON.stringify({}) 25 | }); 26 | 27 | done(); 28 | }); 29 | 30 | it('maps socket param to request.plugins["hapi-io"]', function(done) { 31 | var req = request({ 32 | route: { 33 | method: 'get', 34 | path: '/' 35 | }, 36 | socket: 'MY_SOCKET' 37 | }); 38 | 39 | expect(req).to.equal({ 40 | method: 'get', 41 | url: '/', 42 | headers: {}, 43 | payload: JSON.stringify({}), 44 | plugins: { 45 | 'hapi-io': { 46 | socket: 'MY_SOCKET' 47 | } 48 | } 49 | }); 50 | 51 | done(); 52 | }); 53 | 54 | it('maps data param to query object when GET', function(done) { 55 | var req = request({ 56 | route: { 57 | method: 'get', 58 | path: '/' 59 | }, 60 | data: { myparam: 'hello world'} 61 | }); 62 | 63 | expect(req).to.equal({ 64 | method: 'get', 65 | url: '/?myparam=hello%20world', 66 | headers: {}, 67 | payload: JSON.stringify({}) 68 | }); 69 | 70 | done(); 71 | }); 72 | 73 | it('maps data param to payload object when POST', function(done) { 74 | var req = request({ 75 | route: { 76 | method: 'post', 77 | path: '/' 78 | }, 79 | data: { myparam: 'hello world'} 80 | }); 81 | 82 | expect(req).to.equal({ 83 | method: 'post', 84 | url: '/', 85 | headers: {}, 86 | payload: JSON.stringify({myparam: 'hello world'}) 87 | }); 88 | 89 | done(); 90 | }); 91 | 92 | it('maps data param to payload with validate mapping', function(done) { 93 | var req = request({ 94 | route: { 95 | method: 'post', 96 | path: '/', 97 | settings: { 98 | validate: { 99 | payload: { 100 | myparam: true 101 | } 102 | } 103 | } 104 | }, 105 | data: { myparam: 'hello world'} 106 | }); 107 | 108 | expect(req).to.equal({ 109 | method: 'post', 110 | url: '/', 111 | headers: {}, 112 | payload: JSON.stringify({ myparam: 'hello world'}) 113 | }); 114 | 115 | done(); 116 | }); 117 | 118 | it('maps data param to query with validate mapping', function(done) { 119 | var req = request({ 120 | route: { 121 | method: 'post', 122 | path: '/', 123 | settings: { 124 | validate: { 125 | query: { 126 | myparam: true 127 | } 128 | } 129 | } 130 | }, 131 | data: { myparam: 'hello world'} 132 | }); 133 | 134 | expect(req).to.equal({ 135 | method: 'post', 136 | url: '/?myparam=hello%20world', 137 | headers: {}, 138 | payload: JSON.stringify({}) 139 | }); 140 | 141 | done(); 142 | }); 143 | 144 | it('maps data param to headers with validate mapping', function(done) { 145 | var req = request({ 146 | route: { 147 | method: 'post', 148 | path: '/', 149 | settings: { 150 | validate: { 151 | headers: { 152 | myparam: true 153 | } 154 | } 155 | } 156 | }, 157 | data: { myparam: 'hello world'} 158 | }); 159 | 160 | expect(req).to.equal({ 161 | method: 'post', 162 | url: '/', 163 | headers: { myparam: 'hello world'}, 164 | payload: JSON.stringify({}) 165 | }); 166 | 167 | done(); 168 | }); 169 | 170 | it('maps data param to query when validate mapping specifies both query and headers', function(done) { 171 | var req = request({ 172 | route: { 173 | method: 'post', 174 | path: '/', 175 | settings: { 176 | validate: { 177 | query: { 178 | myparam: true 179 | }, 180 | headers: { 181 | myparam: true 182 | } 183 | } 184 | } 185 | }, 186 | data: { myparam: 'hello world'} 187 | }); 188 | 189 | expect(req).to.equal({ 190 | method: 'post', 191 | url: '/?myparam=hello%20world', 192 | headers: {}, 193 | payload: JSON.stringify({}) 194 | }); 195 | 196 | done(); 197 | }); 198 | 199 | it('maps data param to query when validate mapping specifies both query and payload', function(done) { 200 | var req = request({ 201 | route: { 202 | method: 'post', 203 | path: '/', 204 | settings: { 205 | validate: { 206 | query: { 207 | myparam: true 208 | }, 209 | payload: { 210 | myparam: true 211 | } 212 | } 213 | } 214 | }, 215 | data: { myparam: 'hello world'} 216 | }); 217 | 218 | expect(req).to.equal({ 219 | method: 'post', 220 | url: '/?myparam=hello%20world', 221 | headers: {}, 222 | payload: JSON.stringify({}) 223 | }); 224 | 225 | done(); 226 | }); 227 | 228 | it('maps data param to payload with custom mapping', function(done) { 229 | var req = request({ 230 | route: { 231 | method: 'post', 232 | path: '/', 233 | settings: { 234 | plugins: { 235 | 'hapi-io': { 236 | mapping: { 237 | payload: ['myparam'] 238 | } 239 | } 240 | } 241 | } 242 | }, 243 | data: { myparam: 'hello world'} 244 | }); 245 | 246 | expect(req).to.equal({ 247 | method: 'post', 248 | url: '/', 249 | headers: {}, 250 | payload: JSON.stringify({ myparam: 'hello world'}) 251 | }); 252 | 253 | done(); 254 | }); 255 | 256 | it('maps data param to query with custom mapping', function(done) { 257 | var req = request({ 258 | route: { 259 | method: 'post', 260 | path: '/', 261 | settings: { 262 | plugins: { 263 | 'hapi-io': { 264 | mapping: { 265 | query: ['myparam'] 266 | } 267 | } 268 | } 269 | } 270 | }, 271 | data: { myparam: 'hello world'} 272 | }); 273 | 274 | expect(req).to.equal({ 275 | method: 'post', 276 | url: '/?myparam=hello%20world', 277 | headers: {}, 278 | payload: JSON.stringify({}) 279 | }); 280 | 281 | done(); 282 | }); 283 | 284 | it('maps data param to headers with custom mapping', function(done) { 285 | var req = request({ 286 | route: { 287 | method: 'post', 288 | path: '/', 289 | settings: { 290 | plugins: { 291 | 'hapi-io': { 292 | mapping: { 293 | headers: ['myparam'] 294 | } 295 | } 296 | } 297 | } 298 | }, 299 | data: { myparam: 'hello world'} 300 | }); 301 | 302 | expect(req).to.equal({ 303 | method: 'post', 304 | url: '/', 305 | headers: { myparam: 'hello world'}, 306 | payload: JSON.stringify({}) 307 | }); 308 | 309 | done(); 310 | }); 311 | 312 | it('maps Authorization header from query', function(done) { 313 | var socket = { 314 | request: { 315 | _query: { Authorization: 'MyToken'}, 316 | headers: {} 317 | } 318 | }; 319 | 320 | var req = request({ 321 | socket: socket, 322 | route: { 323 | method: 'get', 324 | path: '/' 325 | }, 326 | data: {} 327 | }); 328 | 329 | expect(req).to.equal({ 330 | method: 'get', 331 | url: '/?Authorization=MyToken', 332 | headers: { Authorization: 'MyToken'}, 333 | payload: JSON.stringify({}), 334 | plugins: { 335 | 'hapi-io': { 336 | socket: socket 337 | } 338 | } 339 | }); 340 | 341 | done(); 342 | }); 343 | 344 | it('maps Authorization header from data', function(done) { 345 | var req = request({ 346 | route: { 347 | method: 'get', 348 | path: '/' 349 | }, 350 | data: { Authorization: 'MyToken'} 351 | }); 352 | 353 | expect(req).to.equal({ 354 | method: 'get', 355 | url: '/?Authorization=MyToken', 356 | headers: { Authorization: 'MyToken'}, 357 | payload: JSON.stringify({}) 358 | }); 359 | 360 | done(); 361 | }); 362 | 363 | it('maps Authorization header case-insensitive', function(done) { 364 | var req = request({ 365 | route: { 366 | method: 'post', 367 | path: '/' 368 | }, 369 | data: { authorization: 'MyToken'} 370 | }); 371 | 372 | expect(req).to.equal({ 373 | method: 'post', 374 | url: '/', 375 | headers: { authorization: 'MyToken'}, 376 | payload: JSON.stringify({ authorization: 'MyToken'}) 377 | }); 378 | 379 | done(); 380 | }); 381 | 382 | it('does not map Authorization header when it already exists', function(done) { 383 | var socket = { 384 | request: { 385 | headers: { Authorization: 'MyToken'} 386 | } 387 | }; 388 | 389 | var req = request({ 390 | socket: socket, 391 | route: { 392 | method: 'get', 393 | path: '/' 394 | } 395 | }); 396 | 397 | expect(req).to.equal({ 398 | method: 'get', 399 | url: '/', 400 | headers: { Authorization: 'MyToken'}, 401 | payload: JSON.stringify({}), 402 | plugins: { 403 | 'hapi-io': { 404 | socket: socket 405 | } 406 | } 407 | }); 408 | 409 | done(); 410 | }); 411 | 412 | it('maps data param to path param', function(done) { 413 | var req = request({ 414 | route: { 415 | method: 'get', 416 | path: '/blog-post/{blogId}' 417 | }, 418 | data: { 419 | blogId: 1 420 | } 421 | }); 422 | 423 | expect(req).to.equal({ 424 | method: 'get', 425 | url: '/blog-post/1', 426 | headers: {}, 427 | payload: JSON.stringify({}) 428 | }); 429 | 430 | done(); 431 | }); 432 | 433 | it('does not map missing data param to path param', function(done) { 434 | var req = request({ 435 | route: { 436 | method: 'get', 437 | path: '/blog-post/{blogId}' 438 | } 439 | }); 440 | 441 | expect(req).to.equal({ 442 | method: 'get', 443 | url: '/blog-post/{blogId}', 444 | headers: {}, 445 | payload: JSON.stringify({}) 446 | }); 447 | 448 | done(); 449 | }); 450 | 451 | it('maps data param to optional path param', function(done) { 452 | var req = request({ 453 | route: { 454 | method: 'get', 455 | path: '/blog-post/{blogId?}' 456 | }, 457 | data: { 458 | blogId: 1 459 | } 460 | }); 461 | 462 | expect(req).to.equal({ 463 | method: 'get', 464 | url: '/blog-post/1', 465 | headers: {}, 466 | payload: JSON.stringify({}) 467 | }); 468 | 469 | done(); 470 | }); 471 | 472 | it('does not map missing data param to optional path param', function(done) { 473 | var req = request({ 474 | route: { 475 | method: 'get', 476 | path: '/blog-post/{blogId?}' 477 | } 478 | }); 479 | 480 | expect(req).to.equal({ 481 | method: 'get', 482 | url: '/blog-post/', 483 | headers: {}, 484 | payload: JSON.stringify({}) 485 | }); 486 | 487 | done(); 488 | }); 489 | 490 | }); 491 | 492 | }); 493 | --------------------------------------------------------------------------------