├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── example ├── serverTest.js └── udp-discovery.js ├── index.js ├── lib ├── coap-shepherd.js ├── components │ ├── coap-node.js │ ├── constants.js │ ├── cutils.js │ ├── nedb-storage.js │ ├── reqHandler.js │ └── storage-interface.js ├── config.js └── init.js ├── package.json └── test ├── coap-node.test.js ├── coap-shepherd.test.js ├── cutils.test.js ├── fixture.js └── nedb-storage.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Development 7 | .idea/ 8 | 9 | # Test data 10 | test/database_test/ 11 | 12 | # Runtime data 13 | lib/database/ 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directory 34 | node_modules 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | # Package lock 43 | package-lock.json 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1.0" 4 | - "6.9.2" 5 | - "8.11.1" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | Peter Yi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test-all: 2 | @./node_modules/.bin/mocha -u bdd --reporter spec 3 | 4 | .PHONY: test-all -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coap-shepherd 2 | Network server and manager for lightweight M2M (LWM2M). 3 | 4 | [![NPM](https://nodei.co/npm/coap-shepherd.png?downloads=true)](https://nodei.co/npm/coap-shepherd/) 5 | 6 | [![Build Status](https://travis-ci.org/PeterEB/coap-shepherd.svg?branch=develop)](https://travis-ci.org/PeterEB/coap-shepherd) 7 | [![npm](https://img.shields.io/npm/v/coap-shepherd.svg?maxAge=2592000)](https://www.npmjs.com/package/coap-shepherd) 8 | [![npm](https://img.shields.io/npm/l/coap-shepherd.svg?maxAge=2592000)](https://www.npmjs.com/package/coap-shepherd) 9 | 10 |
11 | 12 | ## Documentation 13 | 14 | Please visit the [Wiki](https://github.com/PeterEB/coap-shepherd/wiki). 15 | 16 |
17 | 18 | ## Overview 19 | 20 | [**OMA Lightweight M2M**](http://technical.openmobilealliance.org/Technical/technical-information/release-program/current-releases/oma-lightweightm2m-v1-0) (LWM2M) is a resource constrained device management protocol relies on [**CoAP**](https://tools.ietf.org/html/rfc7252). And **CoAP** is an application layer protocol that allows devices to communicate with each other RESTfully over the Internet. 21 | 22 | **coap-shepherd**, **coap-node** and **lwm2m-bs-server** modules aim to provide a simple way to build and manage a **LWM2M** network. 23 | * Server-side library: **coap-shepherd** (this module) 24 | * Client-side library: [**coap-node**](https://github.com/PeterEB/coap-node) 25 | * Bootstrap server library: [**lwm2m-bs-server**](https://github.com/PeterEB/lwm2m-bs-server) 26 | * [**A simple demo webapp**](https://github.com/PeterEB/quick-demo) 27 | 28 | ![coap-shepherd net](https://raw.githubusercontent.com/PeterEB/documents/master/coap-shepherd/media/lwm2m_net.png) 29 | 30 | ### LWM2M Server: coap-shepherd 31 | 32 | * It is a **LWM2M** Server application framework running on node.js. 33 | * It follows most parts of **LWM2M** specification to meet the requirements of a machine network and devices management. 34 | * It works well with [**Leshan**](https://github.com/eclipse/leshan) and [**Wakaama**](https://github.com/eclipse/wakaama). 35 | * Supports functionalities, such as permission of device joining, reading resources, writing resources, observing resources, and executing a procedure on a remote device. 36 | * It follows [**IPSO**](http://www.ipso-alliance.org/smart-object-guidelines/) data model to let you allocate and query resources on remote devices with semantic URIs in a comprehensive manner. 37 | 38 |
39 | 40 | ## Installation 41 | 42 | > $ npm install coap-shepherd --save 43 | 44 |
45 | 46 | ## Usage 47 | 48 | This example shows how to start a server and allow devices to join the network within 180 seconds after the server is ready: 49 | 50 | ```js 51 | var cserver = require('coap-shepherd'); 52 | 53 | cserver.on('ready', function () { 54 | console.log('Server is ready.'); 55 | 56 | // when server is ready, allow devices to join the network within 180 secs 57 | cserver.permitJoin(180); 58 | }); 59 | 60 | cserver.start(function (err) { // start the server 61 | if (err) 62 | console.log(err); 63 | }); 64 | 65 | // That's all to start a LWM2M server. 66 | // Now cserver is going to automatically tackle most of the network managing things. 67 | ``` 68 | 69 | Or you can pass a config object as an argument to the CoapShepherd constructor and instance the CoapShepherd by yourself: 70 | 71 | ```js 72 | var CoapShepherd = require('coap-shepherd').constructor; 73 | var cshepherd = new CoapShepherd({ 74 | connectionType: 'udp6', 75 | port: 5500, 76 | defaultDbPath: __dirname + '/../lib/database/myShepherd.db' 77 | }); 78 | ``` 79 | 80 |
81 | 82 | ## License 83 | 84 | Licensed under [MIT](https://github.com/PeterEB/coap-shepherd/blob/master/LICENSE). 85 | -------------------------------------------------------------------------------- /example/serverTest.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'); 3 | 4 | var shepherd = require('../index.js'); 5 | 6 | try { 7 | fs.unlinkSync(path.resolve('../lib/database/coap.db')); 8 | } catch (e) { 9 | console.log(e); 10 | } 11 | 12 | shepherd.on('ready', function () { 13 | console.log('>> coap-shepherd server start!'); 14 | 15 | shepherd.permitJoin(180); 16 | }); 17 | 18 | shepherd.on('ind', handler); 19 | 20 | shepherd.on('error', errHandler); 21 | 22 | shepherd.start(function (err) { 23 | if (err) throw err; 24 | }); 25 | 26 | // // stop test 27 | // setTimeout(function () { 28 | // shepherd.stop(function (err, rsp) { 29 | // if (err) throw err; 30 | // }); 31 | // }, 5000); 32 | 33 | // // reset test 34 | // setTimeout(function () { 35 | // shepherd.reset(function (err) { 36 | // if (err) throw err; 37 | // }); 38 | // }, 10000); 39 | 40 | // // announce test 41 | // setTimeout(function () { 42 | // shepherd.announce('Awesome!', function (err, rsp) { 43 | // if (err) throw err; 44 | // }); 45 | // }, 15000); 46 | 47 | function reqHandler (err, rsp) { 48 | if (err) console.log(err); 49 | else console.log(rsp); 50 | } 51 | 52 | function errHandler (err) { 53 | throw err; 54 | } 55 | 56 | function handler (msg) { 57 | var cnode; 58 | console.log(msg.type + ': ' + msg.data); 59 | 60 | if (msg.type === 'devIncoming') { 61 | cnode = msg.cnode; 62 | cnode.observeReq('/presence/0/dInState', reqHandler); 63 | 64 | // read test 65 | // setTimeout(function () { cnode.readReq('/3303/0/5700', reqHandler); }, 5000); 66 | // setTimeout(function () { cnode.readReq('/3303/0/5701', reqHandler); }, 10000); 67 | // setTimeout(function () { cnode.readReq('/3303/0/5702', reqHandler); }, 15000); 68 | // setTimeout(function () { cnode.readReq('/3303/0/5703', reqHandler); }, 20000); 69 | // setTimeout(function () { cnode.readReq('/3303/0/5704', reqHandler); }, 25000); 70 | // setTimeout(function () { cnode.readReq('/3303/0', reqHandler); }, 30000); 71 | 72 | // discover test 73 | // setTimeout(function () { cnode.discoverReq('/3303/0/5700', reqHandler); }, 5000); 74 | // setTimeout(function () { cnode.discoverReq('/3303/0/5701', reqHandler); }, 10000); 75 | // setTimeout(function () { cnode.discoverReq('/3303/0/5702', reqHandler); }, 15000); 76 | // setTimeout(function () { cnode.discoverReq('/3303/0/5703', reqHandler); }, 20000); 77 | // setTimeout(function () { cnode.discoverReq('/3303/0/5704', reqHandler); }, 25000); 78 | // setTimeout(function () { cnode.discoverReq('/3303/0', reqHandler); }, 30000); 79 | // setTimeout(function () { cnode.discoverReq('/3303', reqHandler); }, 35000); 80 | 81 | // write test 82 | // setTimeout(function () { cnode.writeReq('/3303/0/5700', 19, reqHandler); }, 3000); 83 | // setTimeout(function () { cnode.writeReq('/3303/0/5701', 'C', reqHandler); }, 8000); 84 | // setTimeout(function () { cnode.writeReq('/3303/0/5702', 'Hum', reqHandler); }, 13000); 85 | // setTimeout(function () { cnode.writeReq('/3303/0/5703', 'Hum', reqHandler); }, 18000); 86 | // setTimeout(function () { cnode.writeReq('/3303/0/5704', 'Hum', reqHandler); }, 23000); 87 | // setTimeout(function () { cnode.writeReq('/3303/0', { 5700: 87, 5701: 'F' }, reqHandler); }, 28000); 88 | 89 | // writeAttr test 90 | // setTimeout(function () { cnode.writeAttrsReq('/3303/0/5700', { 'pmin': 10, 'pmax': 30, 'gt': 0 }, reqHandler); }, 3000); 91 | // setTimeout(function () { cnode.writeAttrsReq('/3303/0/5701', { 'pmin': 10, 'pmax': 30, 'gt': 0 }, reqHandler); }, 8000); 92 | // setTimeout(function () { cnode.writeAttrsReq('/3303/0/5702', { 'pmin': 10, 'pmax': 30, 'gt': 0 }, reqHandler); }, 13000); 93 | // setTimeout(function () { cnode.writeAttrsReq('/3303/0/5703', { 'pmin': 10, 'pmax': 30, 'gt': 0 }, reqHandler); }, 18000); 94 | // setTimeout(function () { cnode.writeAttrsReq('/3303/0/5704', { 'pmin': 10, 'pmax': 30, 'gt': 0 }, reqHandler); }, 23000); 95 | // setTimeout(function () { cnode.writeAttrsReq('/3303/0', { 'pmin': 10, 'pmax': 30 }, reqHandler); }, 28000); 96 | // setTimeout(function () { cnode.writeAttrsReq('/3303', { 'pmin': 10, 'pmax': 30 }, reqHandler); }, 33000); 97 | 98 | // exec test 99 | // setTimeout(function () { cnode.executeReq('/3303/0/5700', ['Peter', 'world'], reqHandler); }, 5000); 100 | // setTimeout(function () { cnode.executeReq('/3303/0/5701', ['Peter', 'world'], reqHandler); }, 10000); 101 | // setTimeout(function () { cnode.executeReq('/3303/0/5702', ['Peter', 'world'], reqHandler); }, 15000); 102 | // setTimeout(function () { cnode.executeReq('/3303/0/5703', ['Peter', 'world'], reqHandler); }, 20000); 103 | // setTimeout(function () { cnode.executeReq('/3303/0/5704', ['Peter', 'world'], reqHandler); }, 25000); 104 | 105 | // observe test 106 | // setTimeout(function () { cnode.observeReq('/3303/0/5700', reqHandler); }, 5000); 107 | // setTimeout(function () { cnode.observeReq('/3303/0/5701', reqHandler); }, 10000); 108 | // setTimeout(function () { cnode.observeReq('/3303/0/5702', reqHandler); }, 15000); 109 | // setTimeout(function () { cnode.observeReq('/3303/0/5703', reqHandler); }, 20000); 110 | // setTimeout(function () { cnode.observeReq('/3303/0/5704', reqHandler); }, 25000); 111 | // setTimeout(function () { cnode.observeReq('/3303/0', reqHandler); }, 30000); 112 | 113 | // cancelObserve test 114 | // setTimeout(function () { cnode.cancelObserveReq('/3303/0/5702', reqHandler); }, 10000); 115 | 116 | // ping test 117 | // setTimeout(function () { cnode.pingReq(reqHandler); }, 3000); 118 | 119 | // remove test 120 | // setTimeout(function () { shepherd.remove('nodeTest'); }, 10000); 121 | } else if (msg.type === 'devNotify') { 122 | console.log(msg.data); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /example/udp-discovery.js: -------------------------------------------------------------------------------- 1 | var shepherd = require('../index.js'); 2 | var Discovery = require('udp-discovery').Discovery; 3 | var discover = new Discovery(); 4 | 5 | var cnode; 6 | 7 | var name = 'freebird-demo-ip-broadcast', 8 | interval = 500, 9 | available = true, 10 | serv = { 11 | port: 80, 12 | proto: 'tcp', 13 | addrFamily: 'IPv4' 14 | }; 15 | 16 | shepherd.on('ready', function () { 17 | console.log('>> coap-shepherd server start!'); 18 | shepherd.permitJoin(180); 19 | 20 | discover.announce(name, serv, interval, available); 21 | 22 | discover.on('MessageBus', function(event, data) { 23 | console.log(data); 24 | }); 25 | }); 26 | 27 | shepherd.on('ind', handler); 28 | 29 | shepherd.on('error', errHandler); 30 | 31 | shepherd.start(function (err) { 32 | if (err) throw err; 33 | }); 34 | 35 | function reqHandler (err, rsp) { 36 | if (err) console.log(err); 37 | else console.log(rsp); 38 | } 39 | 40 | function errHandler (err) { 41 | throw err; 42 | } 43 | 44 | function handler (msg) { 45 | console.log(msg.data); 46 | } 47 | 48 | function handler (msg) { 49 | var cnode; 50 | console.log(msg.data); 51 | 52 | switch (msg.type) { 53 | case 'devStatus': 54 | cnode = msg.cnode; 55 | if (msg.data === 'online') { 56 | setTimeout(function () { cnode.observeReq('/buzzer/0/onOff', reqHandler); }, 2000); 57 | setTimeout(function () { cnode.writeReq('/buzzer/0/onOff', true, reqHandler); }, 4000); 58 | setTimeout(function () { cnode.writeReq('/buzzer/0/onOff', false, reqHandler); }, 6000); 59 | } 60 | break; 61 | 62 | default: 63 | // Not deal with other msg.type in this example 64 | break; 65 | } 66 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/coap-shepherd.js'); 4 | -------------------------------------------------------------------------------- /lib/coap-shepherd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'), 4 | util = require('util'), 5 | EventEmitter = require('events').EventEmitter, 6 | Readable = require('stream').Readable, 7 | network = require('network'); 8 | 9 | var proving = require('proving'), 10 | _ = require('busyman'), 11 | coap = require('coap'), 12 | debug = require('debug')('coap-shepherd'); 13 | 14 | var StorageInterface = require('./components/storage-interface'), 15 | NedbStorage = require('./components/nedb-storage'), 16 | cutils = require('./components/cutils'), 17 | CNST = require('./components/constants'), 18 | defaultConfig = require('./config'), 19 | init = require('./init'); 20 | 21 | /**** Code Enumerations ****/ 22 | var RSP = CNST.RSP; 23 | 24 | function CoapShepherd(config) { 25 | EventEmitter.call(this); 26 | 27 | this.clientIdCount = 1; 28 | 29 | this._setConfig(config); 30 | 31 | this._net = { 32 | intf: '', 33 | ip: this._config.ip, 34 | port: this._config.port, 35 | mac: '', 36 | routerIp: '' 37 | }; 38 | 39 | this._agent = coap.globalAgent; 40 | this._registry = {}; 41 | this._server = null; 42 | 43 | this._enabled = false; 44 | this._joinable = false; 45 | this._hbChecker = null; 46 | this._permitJoinTime = 0; 47 | this._permitJoinTimer = null; 48 | 49 | coap.updateTiming({ 50 | maxLatency: (this._config.reqTimeout - 47) / 2 51 | }); 52 | 53 | this._acceptDevIncoming = function (devInfo, callback) { // Override at will. 54 | setImmediate(function () { 55 | var accepted = true; 56 | callback(null, accepted); 57 | }); 58 | }; 59 | } 60 | 61 | util.inherits(CoapShepherd, EventEmitter); 62 | 63 | CoapShepherd.prototype.start = function (callback) { 64 | var deferred = Q.defer(), 65 | shepherd = this; 66 | 67 | if (!this._enabled) { 68 | init.setupShepherd(shepherd).then(function () { 69 | hbCheck(shepherd, true); 70 | shepherd._fire('ready'); 71 | deferred.resolve(); 72 | }).fail(function (err) { 73 | shepherd._server = null; 74 | shepherd._enabled = false; 75 | deferred.reject(err); 76 | }).done(); 77 | } else { 78 | deferred.resolve(); 79 | } 80 | 81 | return deferred.promise.nodeify(callback); 82 | }; 83 | 84 | CoapShepherd.prototype.stop = function (callback) { 85 | var deferred = Q.defer(), 86 | shepherd = this; 87 | 88 | if (!shepherd._enabled) { 89 | deferred.resolve(); 90 | } else { 91 | if (!shepherd._server) { 92 | deferred.reject(new Error('server does not exist.')); 93 | } else { 94 | shepherd._server.close(function () { 95 | shepherd._server = null; 96 | shepherd._enabled = false; 97 | shepherd._agent._doClose(); // [FIXIT] 98 | hbCheck(shepherd, false); 99 | deferred.resolve(); 100 | }); 101 | } 102 | } 103 | 104 | return deferred.promise.nodeify(callback); 105 | }; 106 | 107 | CoapShepherd.prototype.reset = function (mode, callback) { 108 | var shepherd = this, 109 | deferred = Q.defer(); 110 | 111 | if (_.isFunction(mode)) { 112 | callback = mode; 113 | mode = false; 114 | } 115 | 116 | mode = !!mode; 117 | 118 | this.stop().then(function () { 119 | if (mode === true) { 120 | return shepherd._storage.reset().then(function () { 121 | debug('Database cleared.'); 122 | return shepherd.start(); 123 | }); 124 | } 125 | else 126 | return shepherd.start(); 127 | }).done(deferred.resolve, deferred.reject); 128 | 129 | return deferred.promise.nodeify(callback); 130 | }; 131 | 132 | CoapShepherd.prototype.find = function (clientName) { 133 | proving.string(clientName, 'clientName should be a string.'); 134 | 135 | return this._registry[clientName]; 136 | }; 137 | 138 | CoapShepherd.prototype.findByMacAddr = function (macAddr) { 139 | proving.string(macAddr, 'macAddr should be a string.'); 140 | 141 | return _.filter(this._registry, function (cnode) { 142 | return cnode.mac === macAddr; 143 | }); 144 | }; 145 | 146 | CoapShepherd.prototype._findByClientId = function (id) { 147 | proving.stringOrNumber(id, 'id should be a string or a number.'); 148 | 149 | return _.find(this._registry, function (cnode) { 150 | return cnode.clientId == id; 151 | }); 152 | }; 153 | 154 | CoapShepherd.prototype._findByLocationPath = function (path) { 155 | proving.string(path, 'path should be a string.'); 156 | 157 | return _.find(this._registry, function (cnode) { 158 | return cnode.locationPath === path; 159 | }); 160 | }; 161 | 162 | CoapShepherd.prototype.permitJoin = function (time) { 163 | if (!_.isUndefined(time)) 164 | proving.number(time, 'time should be a number if given.'); 165 | 166 | var shepherd = this; 167 | 168 | if (!this._enabled) { 169 | this._permitJoinTime = 0; 170 | return false; 171 | } 172 | 173 | time = time || 0; 174 | 175 | if (!time) { 176 | this._joinable = false; 177 | this._permitJoinTime = 0; 178 | this._fire('permitJoining', this._permitJoinTime); 179 | 180 | if (this._permitJoinTimer) { 181 | clearInterval(this._permitJoinTimer); 182 | this._permitJoinTimer = null; 183 | } 184 | 185 | return true; 186 | } 187 | 188 | if (this._joinable && this._permitJoinTimer && this._permitJoinTimer._idleTimeout !== -1) { 189 | clearInterval(this._permitJoinTimer); 190 | this._permitJoinTimer = null; 191 | } 192 | 193 | this._joinable = true; 194 | this._permitJoinTime = Math.floor(time); 195 | this._fire('permitJoining', shepherd._permitJoinTime); 196 | 197 | this._permitJoinTimer = setInterval(function () { 198 | shepherd._permitJoinTime -= 1; 199 | 200 | if (shepherd._permitJoinTime === 0) { 201 | shepherd._joinable = false; 202 | clearInterval(shepherd._permitJoinTimer); 203 | shepherd._permitJoinTimer = null; 204 | } 205 | 206 | shepherd._fire('permitJoining', shepherd._permitJoinTime); 207 | }, 1000); 208 | 209 | return true; 210 | }; 211 | 212 | CoapShepherd.prototype.alwaysPermitJoin = function (permit) { 213 | proving.boolean(permit, 'permit should be a boolean.'); 214 | 215 | if (!this._enabled) 216 | return false; 217 | 218 | this._joinable = permit; 219 | 220 | if (this._permitJoinTimer) { 221 | clearInterval(this._permitJoinTimer); 222 | this._permitJoinTimer = null; 223 | } 224 | 225 | return true; 226 | }; 227 | 228 | CoapShepherd.prototype.list = function () { 229 | var devList = []; 230 | 231 | _.forEach(this._registry, function (dev, clientName) { 232 | var rec = dev._dumpSummary(); 233 | rec.status = dev.status; 234 | devList.push(rec); 235 | }); 236 | 237 | return devList; 238 | }; 239 | 240 | CoapShepherd.prototype.request = function (reqObj, callback) { 241 | proving.object(reqObj, 'reqObj should be an object.'); 242 | 243 | var deferred = Q.defer(), 244 | socket; 245 | 246 | if (!reqObj.hostname || !reqObj.port || !reqObj.method) { 247 | deferred.reject(new Error('bad reqObj.')); 248 | return deferred.promise.nodeify(callback); 249 | } 250 | 251 | if (!this._enabled) { 252 | deferred.reject(new Error('server does not enabled.')); 253 | } else { 254 | if (!_.isNil(reqObj.payload)) 255 | reqObj.payload = reqObj.payload; 256 | else 257 | reqObj.payload = null; 258 | 259 | coapRequest(reqObj, this._agent).done(deferred.resolve, deferred.reject); 260 | } 261 | 262 | return deferred.promise.nodeify(callback); 263 | }; 264 | 265 | CoapShepherd.prototype.announce = function (msg, callback) { 266 | proving.string(msg, 'msg should be an string.'); 267 | 268 | var deferred = Q.defer(), 269 | shepherd = this, 270 | announceAllClient = [], 271 | count = Object.keys(this._registry).length, 272 | reqObj = { 273 | hostname: null, 274 | port: null, 275 | pathname: '/announce', 276 | method: 'POST', 277 | payload: msg 278 | }; 279 | 280 | function reqWithoutRsp(reqObj) { 281 | var req = shepherd._agent.request(reqObj); 282 | 283 | req.end(reqObj.payload); 284 | shepherd._agent.abort(req); 285 | count -= 1; 286 | 287 | if (count === 0) 288 | deferred.resolve(msg); 289 | } 290 | 291 | _.forEach(this._registry, function (cnode, clientName) { 292 | reqObj.hostname = cnode.ip; 293 | reqObj.port = cnode.port; 294 | 295 | setImmediate(function () { 296 | reqWithoutRsp(reqObj); 297 | }); 298 | }); 299 | 300 | return deferred.promise.nodeify(callback); 301 | }; 302 | 303 | CoapShepherd.prototype.remove = function (clientName, callback) { 304 | var deferred = Q.defer(), 305 | shepherd = this, 306 | cnode = this.find(clientName), 307 | mac; 308 | 309 | if (cnode) { 310 | mac = cnode.mac; 311 | cnode._setStatus('offline'); 312 | cnode.lifeCheck(false); 313 | this._storage.remove(cnode).done(function () { 314 | cnode._registered = false; 315 | cnode.so = null; 316 | cnode._cancelAllObservers(); 317 | shepherd._registry[cnode.clientName] = null; 318 | delete shepherd._registry[cnode.clientName]; 319 | shepherd.clientIdCount -= 1; 320 | shepherd._fire('ind', { 321 | type: 'devLeaving', 322 | cnode: clientName, 323 | data: mac 324 | }); 325 | 326 | deferred.resolve(clientName); 327 | }, function (err) { 328 | deferred.reject(err); 329 | }); 330 | } else { 331 | deferred.resolve(); 332 | } 333 | 334 | return deferred.promise.nodeify(callback); 335 | }; 336 | 337 | CoapShepherd.prototype.acceptDevIncoming = function (predicate) { 338 | proving.fn(predicate, 'predicate must be a function'); 339 | 340 | this._acceptDevIncoming = predicate; 341 | return true; 342 | }; 343 | 344 | CoapShepherd.prototype._newClientId = function (id) { 345 | if (!_.isUndefined(id)) 346 | proving.number(id, 'id should be a number.'); 347 | 348 | var clientId = id || this.clientIdCount; 349 | 350 | if (this._findByClientId(clientId)) { 351 | this.clientIdCount += 1; 352 | return this._newClientId(); 353 | } else { 354 | this.clientIdCount += 1; 355 | return clientId; 356 | } 357 | }; 358 | 359 | CoapShepherd.prototype._fire = function (msg, data) { 360 | var shepherd = this; 361 | 362 | setImmediate(function () { 363 | shepherd.emit(msg, data); 364 | }); 365 | }; 366 | 367 | CoapShepherd.prototype._setConfig = function (config) { 368 | if (undefined !== config) 369 | proving.object(config, 'config should be an object if given.'); 370 | 371 | this._config = config ? Object.assign({}, defaultConfig, config) : defaultConfig; 372 | this._config.ip = this._config.ip || ''; 373 | this._config.port = this._config.port || 5683; 374 | this._config.reqTimeout = this._config.reqTimeout || 60; 375 | this._config.hbTimeout = this._config.hbTimeout || 60; 376 | 377 | if ((this._config.storage !== undefined) && (this._config.storage !== null) && !(this._config.storage instanceof StorageInterface)) 378 | throw new TypeError('config.storage should be an StorageInterface if given.'); 379 | if (this._config.storage) 380 | this._storage = this._config.storage; 381 | else 382 | this._storage = this._createDefaultStorage(this._config.defaultDbPath); 383 | }; 384 | 385 | CoapShepherd.prototype._createDefaultStorage = function (dbPath) { 386 | return new NedbStorage(dbPath); 387 | }; 388 | 389 | /********************************************************* 390 | * Private function * 391 | *********************************************************/ 392 | function coapRequest(reqObj) { 393 | var deferred = Q.defer(), 394 | req = coap.request(reqObj); 395 | 396 | if (!_.isNil(reqObj.observe) && reqObj.observe === false) 397 | req.setOption('Observe', 1); 398 | 399 | req.on('response', function (rsp) { 400 | debug('RSP <-- %s, token: %s, status: %s', reqObj.method, req._packet ? req._packet.token.toString('hex') : undefined, rsp.code); 401 | 402 | if (!_.isEmpty(rsp.payload) && rsp.headers['Content-Format'] === 'application/json') { 403 | rsp.payload = cutils.decodeJson(reqObj.pathname, rsp.payload); 404 | } else if (!_.isEmpty(rsp.payload) && rsp.headers['Content-Format'] === 'application/tlv') { 405 | rsp.payload = cutils.decodeTlv(reqObj.pathname, rsp.payload); 406 | } else if (!_.isEmpty(rsp.payload) && rsp.headers['Content-Format'] === 'application/link-format') { 407 | rsp.payload = cutils.decodeLinkFormat(rsp.payload.toString()); 408 | } else if (!_.isEmpty(rsp.payload)) { 409 | rsp.payload = cutils.checkRescType(reqObj.pathname, rsp.payload.toString()); 410 | } 411 | 412 | deferred.resolve(rsp); 413 | }); 414 | 415 | req.on('error', function(err) { 416 | if (err.retransmitTimeout) 417 | deferred.resolve({ code: RSP.timeout }); 418 | else 419 | deferred.reject(err); 420 | }); 421 | 422 | req.end(reqObj.payload); 423 | debug('REQ --> %s, token: %s', reqObj.method, req._packet ? req._packet.token.toString('hex') : undefined); 424 | return deferred.promise; 425 | } 426 | 427 | function hbCheck (shepherd, enabled) { 428 | clearInterval(shepherd._hbChecker); 429 | shepherd._hbChecker = null; 430 | 431 | if (enabled) { 432 | shepherd._hbChecker = setInterval(function () { 433 | _.forEach(shepherd._registry, function (cn) { 434 | var now = cutils.getTime(); 435 | 436 | if (cn.status === 'online' && cn.heartbeatEnabled && ((now - cn._heartbeat) > shepherd._config.hbTimeout)) { 437 | cn._setStatus('offline'); 438 | 439 | cn.pingReq().done(function (rspObj) { 440 | if (rspObj.status === RSP.content) { 441 | cn._heartbeat = now; 442 | } else if (cn.status !== 'online') { 443 | cn._cancelAllObservers(); 444 | } 445 | }, function (err) { 446 | cn._cancelAllObservers(); 447 | shepherd._fire('error', err); 448 | }); 449 | } 450 | }); 451 | }, shepherd._config.hbTimeout * 1000); 452 | } 453 | } 454 | 455 | var coapShepherd = new CoapShepherd(); 456 | 457 | module.exports = coapShepherd; 458 | -------------------------------------------------------------------------------- /lib/components/coap-node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'), 4 | _ = require('busyman'), 5 | proving = require('proving'), 6 | SmartObject = require('smartobject'); 7 | 8 | var cutils = require('./cutils.js'), 9 | CNST = require('./constants'); 10 | 11 | /**** Code Enumerations ****/ 12 | var RSP = CNST.RSP; 13 | 14 | function CoapNode (shepherd, devAttrs) { 15 | 16 | this.shepherd = shepherd; 17 | this.dataFormat = []; 18 | // [TODO] clientName locationPath check 19 | this.so = new SmartObject(); 20 | this._assignAttrs(devAttrs); 21 | this.locationPath = '/rd/' + this.clientId.toString(); 22 | 23 | this.status = 'offline'; 24 | this.observedList = []; 25 | 26 | this._registered = false; 27 | this._streamObservers = {}; 28 | this._lifeChecker = null; 29 | this._sleepChecker = null; 30 | 31 | this._heartbeat = null; 32 | } 33 | 34 | CoapNode.prototype.lifeCheck = function (enable) { 35 | proving.boolean(enable, 'enable should be a boolean.'); 36 | 37 | var self = this; 38 | 39 | if (this._lifeChecker) 40 | clearTimeout(this._lifeChecker); 41 | 42 | if (enable) { 43 | this._lifeChecker = setTimeout(function () { 44 | self.shepherd.remove(self.clientName); 45 | }, (this.lifetime * 1000) + 500); 46 | } else { 47 | this._lifeChecker = null; 48 | } 49 | 50 | return this; 51 | }; 52 | 53 | CoapNode.prototype.sleepCheck = function (enable, duration) { 54 | proving.boolean(enable, 'enable should be a boolean.'); 55 | if (!_.isUndefined(duration)) 56 | proving.number(duration, 'duration should be a number.'); 57 | 58 | var self = this; 59 | 60 | if (this._sleepChecker) 61 | clearTimeout(this._sleepChecker); 62 | 63 | if (enable) { 64 | if (duration) { 65 | this._sleepChecker = setTimeout(function () { 66 | self._setStatus('offline'); 67 | }, (duration * 1000) + 500); 68 | } 69 | } else { 70 | this._sleepChecker = null; 71 | } 72 | 73 | return this; 74 | }; 75 | 76 | /********************************************************* 77 | * Request function * 78 | *********************************************************/ 79 | CoapNode.prototype.readReq = function (path, callback) { 80 | var deferred = Q.defer(), 81 | self = this, 82 | chkErr = this._reqCheck('read', path), 83 | reqObj, 84 | rspObj; 85 | 86 | if (chkErr) { 87 | deferred.reject(chkErr); 88 | } else { 89 | reqObj = this._reqObj('GET', cutils.getNumPath(path)); 90 | if (this.dataFormat.includes('application/json')) 91 | reqObj.options = { Accept: 'application/json' }; 92 | else 93 | reqObj.options = { Accept: 'application/tlv' }; // Default format is tlv 94 | 95 | this.shepherd.request(reqObj).then(function (rsp) { 96 | rspObj = { status: rsp.code }; 97 | 98 | isRspTimeout(rsp, self); 99 | if (rsp.code === RSP.content) { // only 2.05 is with data 100 | rspObj.data = rsp.payload; 101 | return self._updateSoAndDb(path, rspObj.data); 102 | } else if (rsp.code === RSP.notallowed) { 103 | rspObj.data = rsp.payload; 104 | } 105 | 106 | return 'notUpdate'; 107 | }).done(function (diff) { 108 | deferred.resolve(rspObj); 109 | }, function (err) { 110 | deferred.reject(err); 111 | }); 112 | } 113 | return deferred.promise.nodeify(callback); 114 | }; 115 | 116 | CoapNode.prototype.discoverReq = function (path, callback) { 117 | var deferred = Q.defer(), 118 | self = this, 119 | chkErr = this._reqCheck('discover', path), 120 | reqObj, 121 | rspObj; 122 | 123 | if (chkErr) { 124 | deferred.reject(chkErr); 125 | } else { 126 | reqObj = this._reqObj('GET', cutils.getNumPath(path)); 127 | reqObj.options = { Accept: 'application/link-format' }; 128 | 129 | this.shepherd.request(reqObj).done(function (rsp) { 130 | rspObj = { status: rsp.code }; 131 | 132 | isRspTimeout(rsp, self); 133 | if (rsp.code === RSP.content) { // only 2.05 is with data 134 | rspObj.data = rsp.payload; 135 | } 136 | 137 | deferred.resolve(rspObj); 138 | }, function (err) { 139 | deferred.reject(err); 140 | }); 141 | } 142 | return deferred.promise.nodeify(callback); 143 | }; 144 | 145 | CoapNode.prototype.writeReq = function (path, value, callback) { 146 | var deferred = Q.defer(), 147 | self = this, 148 | chkErr = this._reqCheck('write', path, value), 149 | reqObj, 150 | rspObj; 151 | 152 | if (chkErr) { 153 | deferred.reject(chkErr); 154 | } else { 155 | reqObj = this._reqObj('PUT', cutils.getNumPath(path)); 156 | if (this.dataFormat.includes('application/json')) { 157 | reqObj.payload = cutils.encodeJson(path, value); 158 | reqObj.options = { 'Content-Format': 'application/json' }; 159 | } else { 160 | reqObj.payload = cutils.encodeTlv(path, value); 161 | reqObj.options = { 'Content-Format': 'application/tlv' }; // Default format is tlv 162 | } 163 | 164 | this.shepherd.request(reqObj).then(function (rsp) { 165 | rspObj = { status: rsp.code }; 166 | 167 | isRspTimeout(rsp, self); 168 | if (rsp.code === RSP.changed) { // consider only 2.04 with the written value 169 | return self._updateSoAndDb(path, value); 170 | } 171 | return 'notUpdate'; 172 | }).done(function (diff) { 173 | deferred.resolve(rspObj); 174 | }, function (err) { 175 | deferred.reject(err); 176 | }); 177 | } 178 | 179 | return deferred.promise.nodeify(callback); 180 | }; 181 | 182 | CoapNode.prototype.writeAttrsReq = function (path, attrs, callback) { 183 | var deferred = Q.defer(), 184 | self = this, 185 | chkErr = this._reqCheck('writeAttrs', path, attrs), 186 | reqObj; 187 | 188 | if (chkErr) { 189 | deferred.reject(chkErr); 190 | } else { 191 | reqObj = this._reqObj('PUT', cutils.getNumPath(path)); 192 | reqObj.query = getAttrQuery(attrs); 193 | 194 | this.shepherd.request(reqObj).done(function (rsp) { 195 | isRspTimeout(rsp, self); 196 | deferred.resolve({ status: rsp.code }); 197 | }, function (err) { 198 | deferred.reject(err); 199 | }); 200 | } 201 | 202 | return deferred.promise.nodeify(callback); 203 | }; 204 | 205 | CoapNode.prototype.executeReq = function (path, argus, callback) { 206 | var deferred = Q.defer(), 207 | self = this, 208 | chkErr, 209 | reqObj, 210 | argusInPlain = null; 211 | 212 | if ((arguments.length === 2) && _.isFunction(argus)) { 213 | callback = argus; 214 | argus = []; 215 | } 216 | 217 | chkErr = this._reqCheck('execute', path, argus); 218 | 219 | if (!_.isEmpty(argus)) 220 | argusInPlain = getPlainTextArgus(argus); // argus to plain text format 221 | 222 | if (chkErr) { 223 | deferred.reject(chkErr); 224 | } else { 225 | reqObj = this._reqObj('POST', cutils.getNumPath(path)); 226 | reqObj.payload = argusInPlain; 227 | 228 | this.shepherd.request(reqObj).done(function (rsp) { 229 | isRspTimeout(rsp, self); 230 | deferred.resolve({ status: rsp.code }); 231 | }, function (err) { 232 | deferred.reject(err); 233 | }); 234 | } 235 | 236 | return deferred.promise.nodeify(callback); 237 | }; 238 | 239 | CoapNode.prototype.observeReq = function (path, callback) { 240 | var deferred = Q.defer(), 241 | self = this, 242 | chkErr = this._reqCheck('observe', path), 243 | reqObj, 244 | rspObj, 245 | type; 246 | 247 | if (chkErr && path !== '/heartbeat') { 248 | deferred.reject(chkErr); 249 | } else { 250 | reqObj = this._reqObj('GET', cutils.getNumPath(path)); 251 | reqObj.observe = true; 252 | if (this.dataFormat.includes('application/json')) 253 | reqObj.options = { Accept: 'application/json' }; 254 | else 255 | reqObj.options = { Accept: 'application/tlv' }; // Default format is tlv 256 | 257 | this.shepherd.request(reqObj).done(function (observeStream) { 258 | rspObj = { status: observeStream.code }; 259 | isRspTimeout(observeStream, self); 260 | 261 | if (observeStream.code === RSP.content) { 262 | rspObj.data = observeStream.payload; 263 | observeStream._disableFiltering = self.shepherd._config.disableFiltering; 264 | 265 | if (path !== '/heartbeat') 266 | self.observedList.push(cutils.getKeyPath(reqObj.pathname)); 267 | 268 | self._streamObservers[cutils.getKeyPath(reqObj.pathname)] = observeStream; 269 | 270 | observeStream.once('data', function (value) { 271 | observeStream.on('data', function (value) { 272 | type = observeStream.headers['Content-Format']; 273 | notifyHandler(self, path, value, type); 274 | }); 275 | }); 276 | } 277 | 278 | deferred.resolve(rspObj); 279 | }, function (err) { 280 | deferred.reject(err); 281 | }); 282 | } 283 | 284 | return deferred.promise.nodeify(callback); 285 | }; 286 | 287 | CoapNode.prototype.cancelObserveReq = function (path, callback) { 288 | var deferred = Q.defer(), 289 | self = this, 290 | chkErr = this._reqCheck('cancelObserve', path), 291 | reqObj; 292 | 293 | if (chkErr) { 294 | deferred.reject(chkErr); 295 | } else { 296 | reqObj = this._reqObj('GET', cutils.getNumPath(path)); 297 | reqObj.observe = false; 298 | 299 | this.shepherd.request(reqObj).done(function (rsp) { 300 | isRspTimeout(rsp, self); 301 | 302 | if (rsp.code === RSP.content) 303 | self._cancelObserver(cutils.getKeyPath(reqObj.pathname)); 304 | 305 | deferred.resolve({ status: rsp.code }); 306 | }, function (err) { 307 | deferred.reject(err); 308 | }); 309 | } 310 | 311 | return deferred.promise.nodeify(callback); 312 | }; 313 | 314 | CoapNode.prototype.pingReq = function (callback) { 315 | var deferred = Q.defer(), 316 | self = this, 317 | reqObj, 318 | txTime = new Date().getTime(); 319 | 320 | reqObj = this._reqObj('POST', '/ping'); 321 | 322 | if (!this._registered) { 323 | deferred.reject(new Error(this.clientName + ' was deregistered.')); 324 | } else { 325 | this.shepherd.request(reqObj).done(function (rsp) { 326 | var rspObj = { status: rsp.code }; 327 | 328 | isRspTimeout(rsp, self); 329 | if (rsp.code === RSP.content) { 330 | rspObj.data = new Date().getTime() - txTime; 331 | } 332 | 333 | deferred.resolve(rspObj); 334 | }, function (err) { 335 | deferred.reject(err); 336 | }); 337 | } 338 | 339 | return deferred.promise.nodeify(callback); 340 | }; 341 | 342 | CoapNode.prototype.dump = function () { 343 | var dumped = this._dumpSummary(); 344 | dumped['so'] = this.so.dumpSync(); 345 | return dumped; 346 | }; 347 | 348 | CoapNode.prototype._dumpSummary = function () { 349 | var self = this, 350 | dumped = {}, 351 | includedKeys = [ 'clientName', 'clientId', 'lifetime', 'version', 'ip', 'mac', 'port', 'objList', 'observedList', 'heartbeatEnabled' ]; 352 | 353 | _.forEach(includedKeys, function (key) { 354 | dumped[key] = _.cloneDeep(self[key]); 355 | }); 356 | 357 | return dumped; 358 | }; 359 | 360 | /********************************************************* 361 | * Protected function * 362 | *********************************************************/ 363 | CoapNode.prototype._assignAttrs = function (devAttrs) { 364 | proving.object(devAttrs, 'devAttrs should be an object.'); 365 | 366 | this.clientName = devAttrs.clientName; 367 | this.clientId = this.shepherd._newClientId(devAttrs.clientId); 368 | this.version = devAttrs.version || '1.0.0'; 369 | this.lifetime = devAttrs.lifetime || 86400; 370 | 371 | this.ip = devAttrs.ip || 'unknown'; 372 | this.mac = devAttrs.mac || 'unknown'; 373 | this.port = devAttrs.port || 'unknown'; 374 | 375 | this.joinTime = devAttrs.joinTime || Date.now(); 376 | this.objList = devAttrs.objList; 377 | 378 | if ((devAttrs.ct == '11543') && (this.dataFormat.indexOf('application/json') < 0)) 379 | this.dataFormat.push('application/json'); 380 | 381 | this.heartbeatEnabled = !!devAttrs.heartbeatEnabled; 382 | 383 | if (typeof devAttrs.so === 'object') 384 | this._assignSo(devAttrs.so); 385 | }; 386 | 387 | CoapNode.prototype._assignSo = function (src) { 388 | var so = new SmartObject(); 389 | _.forEach(src, function (obj, oid) { 390 | _.forEach(obj, function (iObj, iid) { 391 | so.init(oid, iid, iObj); 392 | }); 393 | }); 394 | this.so = so; 395 | }; 396 | 397 | CoapNode.prototype._setStatus = function (status) { 398 | if (status !== 'online' && status !== 'offline' && status !== 'sleep') 399 | throw new TypeError('bad status.'); 400 | 401 | var self = this, 402 | shepherd = this.shepherd; 403 | 404 | if (this.status !== status) { 405 | this.status = status; 406 | 407 | setImmediate(function () { 408 | shepherd.emit('ind', { 409 | type: 'devStatus', 410 | cnode: self, 411 | data: status 412 | }); 413 | }); 414 | } 415 | }; 416 | 417 | CoapNode.prototype._reqObj = function (method, pathname) { 418 | proving.string(method, 'method should be a string.'); 419 | proving.string(pathname, 'pathname should be a string.'); 420 | 421 | return { 422 | hostname: this.ip, 423 | port: this.port, 424 | pathname: pathname, 425 | method: method 426 | }; 427 | }; 428 | 429 | CoapNode.prototype._reqCheck = function (type, path, data) { 430 | var chkErr = null, 431 | allowedAttrs = [ 'pmin', 'pmax', 'gt', 'lt', 'stp', 'step' ], 432 | pathItems; 433 | 434 | proving.string(path, 'path should be a string.'); 435 | 436 | switch (type) { 437 | case 'read': 438 | break; 439 | 440 | case 'write': 441 | pathItems = cutils.getPathArray(path); 442 | 443 | if (!pathItems[1]) 444 | throw Error('path should contain Object ID and Object Instance ID.'); 445 | else if (pathItems.length === 2 && !_.isObject(data)) 446 | throw TypeError('value should be an object.'); 447 | else if (_.isFunction(data) || _.isNil(data)) 448 | throw TypeError('value is undefined.'); 449 | break; 450 | 451 | case 'execute': 452 | pathItems = cutils.getPathArray(path); 453 | 454 | if (!pathItems[1] || !pathItems[2]) 455 | throw Error('path should contain Object ID, Object Instance ID and Resource ID.'); 456 | else if (!_.isArray(data) && !_.isNil(data)) 457 | chkErr = new TypeError('argus should be an array.'); 458 | break; 459 | 460 | case 'discover': 461 | break; 462 | 463 | case 'writeAttrs': 464 | proving.object(data, 'data should be an object.'); 465 | 466 | _.forEach(data, function (val, key) { 467 | if (!_.includes(allowedAttrs, key)) 468 | chkErr = chkErr || new TypeError(key + ' is not allowed.'); 469 | }); 470 | break; 471 | 472 | case 'observe': 473 | break; 474 | 475 | case 'cancelObserve': 476 | break; 477 | 478 | default: 479 | chkErr = new Error('unknown method.'); 480 | } 481 | 482 | if (!this._registered) 483 | chkErr = chkErr || new Error(this.clientName + ' was deregistered.'); 484 | else if (this.status === 'offline') 485 | chkErr = chkErr || new Error(this.clientName + ' is offline.'); 486 | else if (this.status === 'sleep') 487 | chkErr = chkErr || new Error(this.clientName + ' is sleeping.'); 488 | 489 | return chkErr; 490 | }; 491 | 492 | CoapNode.prototype._readAllResource = function (callback) { 493 | var deferred = Q.defer(), 494 | self = this, 495 | oids = [], 496 | reqObj, 497 | readAllResourcePromises = []; 498 | 499 | _.forEach(this.objList, function (iids, oid) { 500 | reqObj = self._reqObj('GET', cutils.getNumPath(oid)); 501 | // [TODO] 502 | reqObj.options = { Accept: 'application/tlv' }; 503 | readAllResourcePromises.push(self.shepherd.request(reqObj)); 504 | oids.push(oid); 505 | }); 506 | 507 | Q.all(readAllResourcePromises).then(function (rsps) { 508 | var isAnyFail = false, 509 | rspObj = {}; 510 | 511 | _.forEach(rsps, function (rsp, idx) { 512 | var obj = rsp.payload, 513 | oid = oids[idx]; 514 | 515 | if (rsp.code === RSP.content) { 516 | _.forEach(obj, function (iObj, iid) { 517 | var rsc = {}; 518 | 519 | _.forEach(iObj, function (val, rid) { 520 | rsc[rid] = val; 521 | }); 522 | 523 | self.so.init(oid, iid, rsc); 524 | }); 525 | } else { 526 | rspObj.status = rspObj.status || rsp.code; 527 | rspObj.data = rspObj.data || '/' + oids[idx]; 528 | isAnyFail = true; 529 | } 530 | }); 531 | 532 | if (isAnyFail) { 533 | deferred.reject(new Error('object requests fail.')); 534 | } else { 535 | rspObj.status = RSP.content; 536 | rspObj.data = self.so; 537 | deferred.resolve(rspObj); 538 | } 539 | }).fail(function (err) { 540 | deferred.reject(err); 541 | }).done(); 542 | 543 | return deferred.promise.nodeify(callback); 544 | }; 545 | 546 | CoapNode.prototype._reinitiateObserve = function (callback) { 547 | var deferred = Q.defer(), 548 | self = this, 549 | paths = [], 550 | reinitiateObservePromises = []; 551 | 552 | if (_.isEmpty(this.observedList)) { 553 | deferred.resolve(); 554 | return deferred.promise.nodeify(callback); 555 | } 556 | 557 | _.forEach(this.observedList, function (path) { 558 | paths.push(path); 559 | reinitiateObservePromises.push(self.observeReq(path)); 560 | }); 561 | 562 | Q.all(reinitiateObservePromises).then(function (rsps) { 563 | _.forEach(rsps, function (rsp, idx) { 564 | if (rsp.code !== RSP.content) 565 | _.remove(self.observedList, paths[idx]); 566 | }); 567 | 568 | deferred.resolve(); 569 | }).fail(function (err) { 570 | deferred.reject(err); 571 | }).done(); 572 | 573 | return deferred.promise.nodeify(callback); 574 | }; 575 | 576 | CoapNode.prototype._cancelObserver = function (path) { 577 | var streamObservers = this._streamObservers; 578 | 579 | streamObservers[path].close(); 580 | streamObservers[path] = null; 581 | delete streamObservers[path]; 582 | 583 | if (path !== '/heartbeat') 584 | _.remove(this.observedList, path); 585 | }; 586 | 587 | CoapNode.prototype._cancelAllObservers = function () { 588 | var self = this; 589 | 590 | _.forEach(this._streamObservers, function (observeStream, path) { 591 | self._cancelObserver(path); 592 | }); 593 | }; 594 | 595 | /********************************************************* 596 | * CoapNode Database Access Methods 597 | *********************************************************/ 598 | CoapNode.prototype._updateSoAndDb = function (path, data, callback) { 599 | var deferred = Q.defer(), 600 | dataType = cutils.getPathDateType(path), 601 | dataObj = cutils.getPathIdKey(path), 602 | diff = null; 603 | 604 | if (!_.isNil(data)) { 605 | switch (dataType) { 606 | case 'object': 607 | diff = {}; 608 | diff[dataObj.oid] = data; 609 | break; 610 | case 'instance': 611 | diff = {}; 612 | diff[dataObj.oid] = {}; 613 | diff[dataObj.oid][dataObj.iid] = data; 614 | break; 615 | case 'resource': 616 | diff = {}; 617 | diff[dataObj.oid] = {}; 618 | diff[dataObj.oid][dataObj.iid] = {}; 619 | diff[dataObj.oid][dataObj.iid][dataObj.rid] = data; 620 | break; 621 | default: 622 | deferred.resolve(); 623 | break; 624 | } 625 | 626 | if (diff) 627 | this.shepherd._storage.patchSo(this, diff).done(function (diff) { 628 | deferred.resolve(diff); 629 | }, function (err) { 630 | deferred.reject(err); 631 | }); 632 | } else { 633 | deferred.resolve(); 634 | } 635 | 636 | return deferred.promise.nodeify(callback); 637 | }; 638 | 639 | CoapNode.prototype._updateAttrs = function (attrs, callback) { 640 | var deferred = Q.defer(), 641 | self = this, 642 | diff = {}, 643 | chkErr = null; 644 | 645 | if (!_.isPlainObject(attrs)) 646 | chkErr = new TypeError('attrs to update should be an object.'); 647 | 648 | if (chkErr) { 649 | deferred.reject(chkErr); 650 | } else { 651 | _.forEach(attrs, function (value, key) { 652 | if (!_.isEqual(self[key], value) && key !== 'hb' && key !== 'ct') { 653 | diff[key] = value; 654 | } 655 | }); 656 | 657 | if (_.isEmpty(diff)) { 658 | deferred.resolve(diff); 659 | } else { 660 | this.shepherd._storage.updateAttrs(this, diff).done(function () { 661 | _.merge(self, diff); 662 | deferred.resolve(diff); 663 | }, function (err) { 664 | deferred.reject(err); 665 | }); 666 | } 667 | } 668 | 669 | return deferred.promise.nodeify(callback); 670 | }; 671 | 672 | /********************************************************* 673 | * Notify handler function * 674 | *********************************************************/ 675 | function notifyHandler (cnode, path, value, type) { 676 | var shepherd = cnode.shepherd, 677 | data; 678 | 679 | switch (type) { 680 | case 'application/json': 681 | data = cutils.decodeJson(path, value); 682 | break; 683 | case 'application/tlv': 684 | data = cutils.decodeTlv(path, value); 685 | break; 686 | default: 687 | data = cutils.checkRescType(path, value.toString()); 688 | break; 689 | } 690 | 691 | if (shepherd._enabled && cutils.getPathArray(path)[0] === 'heartbeat') { 692 | cnode._setStatus('online'); 693 | cnode._heartbeat = cutils.getTime(); 694 | } else if (shepherd._enabled) { 695 | cnode._setStatus('online'); 696 | cnode._updateSoAndDb(path, data).done(function () { 697 | setImmediate(function () { 698 | shepherd.emit('ind', { 699 | type: 'devNotify', 700 | cnode: cnode, 701 | data: { 702 | path: cutils.getKeyPath(path), 703 | value: data 704 | } 705 | }); 706 | }); 707 | }, function (err) { 708 | shepherd.emit('error', err); 709 | }); 710 | } 711 | } 712 | 713 | /********************************************************* 714 | * Private function * 715 | *********************************************************/ 716 | function getAttrQuery(attr) { 717 | var query = ''; 718 | 719 | _.forEach(attr, function (value, key) { 720 | if (key === 'step') 721 | query = query + 'stp=' + value + '&'; 722 | else 723 | query = query + key + '=' + value + '&'; 724 | }); 725 | 726 | return query.slice(0, query.length - 1); // take off the last '&' 727 | } 728 | 729 | function getPlainTextArgus(argus) { 730 | var plainTextArgus = ''; 731 | 732 | _.forEach(argus, function (argu) { 733 | if (_.isString(argu)) 734 | plainTextArgus += "'" + argu + "'" + ','; 735 | else if (_.isNumber(argu)) 736 | plainTextArgus += argu + ','; 737 | }); 738 | 739 | return plainTextArgus.slice(0, plainTextArgus.length - 1); // take off the last ',' 740 | } 741 | 742 | function isRspTimeout(rsp, cn) { 743 | if (rsp.code !== '4.08') { 744 | cn._setStatus('online'); 745 | } else if (cn.status !== 'sleep') 746 | cn._setStatus('offline'); 747 | } 748 | 749 | module.exports = CoapNode; 750 | -------------------------------------------------------------------------------- /lib/components/constants.js: -------------------------------------------------------------------------------- 1 | var CONSTANTS = { 2 | TTYPE: { 3 | root: 0, 4 | obj: 1, 5 | inst: 2, 6 | rsc: 3 7 | }, 8 | TAG: { 9 | notfound: '_notfound_', 10 | unreadable: '_unreadable_', 11 | exec: '_exec_', 12 | unwritable: '_unwritable_', 13 | unexecutable: '_unexecutable_' 14 | }, 15 | ERR: { 16 | success: 0, 17 | notfound: 1, 18 | unreadable: 2, 19 | unwritable: 3, 20 | unexecutable: 4, 21 | timeout: 5, 22 | badtype: 6 23 | }, 24 | RSP: { 25 | created: '2.01', 26 | deleted: '2.02', 27 | changed: '2.04', 28 | content: '2.05', 29 | badreq: '4.00', 30 | unauth: '4.01', 31 | forbid: '4.03', 32 | notfound: '4.04', 33 | notallowed: '4.05', 34 | timeout: '4.08', 35 | serverError: '5.00' 36 | } 37 | }; 38 | 39 | module.exports = CONSTANTS; 40 | -------------------------------------------------------------------------------- /lib/components/cutils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('busyman'), 4 | proving = require('proving'), 5 | lwm2mId = require('lwm2m-id'), 6 | lwm2mCodec = require('lwm2m-codec'); 7 | 8 | var cutils = {}; 9 | 10 | cutils.getTime = function () { 11 | return Math.round(new Date().getTime()/1000); 12 | }; 13 | 14 | /********************************************************* 15 | * lwm2m-id utils * 16 | *********************************************************/ 17 | cutils.oidKey = function (oid) { 18 | var oidItem = lwm2mId.getOid(oid); 19 | return oidItem ? oidItem.key : oid; 20 | }; 21 | 22 | cutils.oidNumber = function (oid) { 23 | var oidItem = lwm2mId.getOid(oid); 24 | 25 | oidItem = oidItem ? oidItem.value : parseInt(oid); 26 | 27 | if (_.isNaN(oidItem)) 28 | oidItem = oid; 29 | 30 | return oidItem; 31 | }; 32 | 33 | cutils.ridKey = function (oid, rid) { 34 | var ridItem = lwm2mId.getRid(oid, rid); 35 | 36 | if (_.isUndefined(rid)) 37 | rid = oid; 38 | 39 | return ridItem ? ridItem.key : rid; 40 | }; 41 | 42 | cutils.ridNumber = function (oid, rid) { 43 | var ridItem = lwm2mId.getRid(oid, rid); 44 | 45 | if (_.isUndefined(rid)) 46 | rid = oid; 47 | 48 | ridItem = ridItem ? ridItem.value : parseInt(rid); 49 | 50 | if (_.isNaN(ridItem)) 51 | ridItem = rid; 52 | 53 | return ridItem; 54 | }; 55 | 56 | /********************************************************* 57 | * req utils * 58 | *********************************************************/ 59 | cutils.getObjListOfSo = function (objList) { 60 | var objListOfSo = {}, 61 | arrayOfObjList = objList.split(','), // [';ct=11543;hb', '', '', ''] 62 | opts = {}; 63 | 64 | _.forEach(arrayOfObjList, function (obj, idx) { 65 | if (obj.startsWith('')) { 66 | obj = obj.split(';').slice(1); 67 | 68 | _.forEach(obj, function (attr, idx) { 69 | obj[idx] = obj[idx].split('='); 70 | 71 | if(obj[idx][0] === 'ct') { 72 | opts.ct = obj[idx][1]; 73 | } else if (obj[idx][0] === 'hb') { 74 | opts.hb = true; 75 | } else { 76 | opts[obj[idx][0]] = obj[idx][1]; 77 | } 78 | }); 79 | } else { 80 | obj = obj.trim().slice(1, -1).split('/'); 81 | 82 | if (obj[0] === '') 83 | obj = obj.slice(1); 84 | 85 | if (obj[0] && !_.has(objListOfSo, obj[0])) 86 | objListOfSo[obj[0]] = []; 87 | 88 | if (obj[1]) 89 | objListOfSo[obj[0]].push(obj[1]); 90 | } 91 | }); 92 | 93 | return { opts: opts, list: objListOfSo }; // { '0':[] '1': ['2', '3'], '2':['0'] } 94 | }; 95 | 96 | /********************************************************* 97 | * path utils * 98 | *********************************************************/ 99 | cutils.urlParser = function (url) { 100 | var urlObj = { 101 | pathname: url.split('?')[0], 102 | query: url.split('?')[1] 103 | }; 104 | 105 | return urlObj; 106 | }; 107 | 108 | cutils.getPathArray = function (url) { 109 | var path = this.urlParser(url).pathname, 110 | pathArray = path.split('/'); // '/x/y/z' 111 | 112 | if (pathArray[0] === '') 113 | pathArray = pathArray.slice(1); 114 | 115 | if (pathArray[pathArray.length-1] === '') 116 | pathArray = pathArray.slice(0, pathArray.length-1); 117 | 118 | return pathArray; // ['x', 'y', 'z'] 119 | }; 120 | 121 | cutils.getPathIdKey = function (url) { 122 | var pathArray = this.getPathArray(url), // '/1/2/3' 123 | pathObj = {}, 124 | oid, 125 | rid; 126 | 127 | if (pathArray[0]) { //oid 128 | oid = this.oidKey(pathArray[0]); 129 | pathObj.oid = oid; 130 | 131 | if (pathArray[1]) { //iid 132 | pathObj.iid = pathArray[1]; 133 | 134 | if (pathArray[2]) { //rid 135 | rid = this.ridKey(oid, pathArray[2]); 136 | pathObj.rid = rid; 137 | } 138 | } 139 | } 140 | 141 | return pathObj; // {oid:'lwm2mServer', iid: '2', rid: 'defaultMaxPeriod'} 142 | }; 143 | 144 | cutils.getKeyPath = function (url) { 145 | var pathArray = this.getPathArray(url), // '/1/2/3' 146 | soPath = '', 147 | oid, 148 | rid; 149 | 150 | if (pathArray[0]) { //oid 151 | oid = this.oidKey(pathArray[0]); 152 | soPath += '/' + oid; 153 | 154 | if (pathArray[1]) { //iid 155 | soPath += '/' + pathArray[1]; 156 | 157 | if (pathArray[2]) { //rid 158 | rid = this.ridKey(oid, pathArray[2]); 159 | soPath += '/' + rid; 160 | } 161 | } 162 | } 163 | 164 | return soPath; // '/lwm2mServer/2/defaultMaxPeriod' 165 | }; 166 | 167 | cutils.getNumPath = function (url) { 168 | var pathArray = this.getPathArray(url), // '/lwm2mServer/2/defaultMaxPeriod' 169 | soPath = '', 170 | oid, 171 | rid; 172 | 173 | if (pathArray[0]) { //oid 174 | oid = this.oidNumber(pathArray[0]); 175 | soPath += '/' + oid; 176 | 177 | if (pathArray[1]) { //iid 178 | soPath += '/' + pathArray[1]; 179 | 180 | if (pathArray[2]) { //rid 181 | rid = this.ridNumber(oid, pathArray[2]); 182 | soPath += '/' + rid; 183 | } 184 | } 185 | } 186 | 187 | return soPath; // '/1/2/3' 188 | }; 189 | 190 | cutils.getPathDateType = function (path) { 191 | var pathArray = this.getPathArray(path), 192 | dataType = [ 'so', 'object', 'instance', 'resource' ][pathArray.length]; 193 | return dataType; 194 | }; 195 | 196 | cutils.checkRescType = function (path, value) { 197 | var pathArray = this.getPathArray(path), 198 | oid, 199 | rid, 200 | dataDef, 201 | dataType, 202 | data; 203 | 204 | if (pathArray.length < 3 || _.isObject(value)) 205 | return value; 206 | 207 | oid = cutils.oidKey(pathArray[0]); 208 | rid = cutils.ridKey(pathArray[0], pathArray[2]); 209 | dataDef = lwm2mId.getRdef(oid, rid); 210 | 211 | if (dataDef) 212 | dataType = dataDef.type; 213 | 214 | switch (dataType) { 215 | case 'string': 216 | data = value; 217 | break; 218 | case 'integer': 219 | case 'float': 220 | data = Number(value); 221 | break; 222 | case 'boolean': 223 | if (value === '0') { 224 | data = false; 225 | } else { 226 | data = true; 227 | } 228 | break; 229 | case 'time': 230 | data = value; 231 | break; 232 | default: 233 | if (Number(value)) 234 | data = Number(value); 235 | else 236 | data = value; 237 | break; 238 | } 239 | 240 | return data; 241 | }; 242 | 243 | /********************************************************* 244 | * Link utils * 245 | *********************************************************/ 246 | cutils.decodeLinkFormat = function (value) { 247 | return lwm2mCodec.decode('link', value); 248 | }; 249 | /********************************************************* 250 | * TLV utils * 251 | *********************************************************/ 252 | cutils.encodeTlv = function (basePath, value) { 253 | return lwm2mCodec.encode('tlv', basePath, value); 254 | }; 255 | 256 | cutils.decodeTlv = function (basePath, value) { 257 | return lwm2mCodec.decode('tlv', basePath, value); 258 | }; 259 | 260 | /********************************************************* 261 | * JSON utils * 262 | *********************************************************/ 263 | cutils.encodeJson = function (basePath, value) { 264 | return lwm2mCodec.encode('json', basePath, value); 265 | }; 266 | 267 | cutils.decodeJson = function (basePath, value) { 268 | return lwm2mCodec.decode('json', basePath, value); 269 | }; 270 | 271 | /********************************************************* 272 | * Diff utils * 273 | *********************************************************/ 274 | cutils.dotPath = function (path) { 275 | path = path.replace(/\//g, '.'); // '/1/2/3' 276 | 277 | if (path[0] === '.') 278 | path = path.slice(1); 279 | 280 | if (path[path.length-1] === '.') 281 | path = path.slice(0, path.length-1); 282 | 283 | return path; // 1.2.3 284 | }; 285 | 286 | cutils.createPath = function () { 287 | var connector = arguments[0], 288 | path = ''; 289 | 290 | proving.string(connector, 'arguments[0] should be a string.'); 291 | 292 | _.forEach(arguments, function (arg, i) { 293 | if (i > 0) path = path + arg + connector; 294 | }); 295 | 296 | if (path[path.length-1] === connector) 297 | path = path.slice(0, path.length-1); 298 | 299 | return path; 300 | }; 301 | 302 | cutils.buildPathValuePairs = function (rootPath, obj) { 303 | var result = {}; 304 | 305 | rootPath = cutils.dotPath(rootPath); 306 | 307 | if (_.isObject(obj)) { 308 | if (rootPath !== '' && rootPath !== '.' && rootPath !== '/' && !_.isUndefined(rootPath)) 309 | rootPath = rootPath + '.'; 310 | 311 | _.forEach(obj, function (n, key) { 312 | // Tricky: objList is an array, don't buid its full path, or updating new list will fail 313 | if (_.isObject(n) && key !== 'objList') 314 | _.assign(result, cutils.buildPathValuePairs(rootPath + key, n)); 315 | else 316 | result[rootPath + key] = n; 317 | }); 318 | } else { 319 | result[rootPath] = obj; 320 | } 321 | 322 | return result; 323 | }; 324 | 325 | cutils.invalidPathOfTarget = function (target, objToUpdata) { 326 | var invalidPath = []; 327 | 328 | _.forEach(objToUpdata, function (n, p) { 329 | if (!_.has(target, p)) { 330 | invalidPath.push(p); 331 | } 332 | }); 333 | 334 | return invalidPath; 335 | }; 336 | 337 | cutils.objectInstanceDiff = function (oldInst, newInst) { 338 | var badPath = cutils.invalidPathOfTarget(oldInst, newInst); 339 | 340 | if (badPath.length !== 0) 341 | throw new Error('No such property ' + badPath[0] + ' in targeting object instance.'); 342 | else 343 | return cutils.objectDiff(oldInst, newInst); 344 | }; 345 | 346 | cutils.resourceDiff = function (oldVal, newVal) { 347 | var badPath; 348 | 349 | if (typeof oldVal !== typeof newVal) { 350 | return newVal; 351 | } else if (_.isPlainObject(oldVal)) { 352 | // object diff 353 | badPath = cutils.invalidPathOfTarget(oldVal, newVal); 354 | if (badPath.length !== 0) 355 | throw new Error('No such property ' + badPath[0] + ' in targeting object.'); 356 | else 357 | return cutils.objectDiff(oldVal, newVal); 358 | } else if (oldVal !== newVal) { 359 | return newVal; 360 | } else { 361 | return null; 362 | } 363 | }; 364 | 365 | cutils.objectDiff = function (oldObj, newObj) { 366 | var pvp = cutils.buildPathValuePairs('/', newObj), 367 | diff = {}; 368 | 369 | _.forEach(pvp, function (val, path) { 370 | if (!_.has(oldObj, path) || _.get(oldObj, path) !== val) 371 | _.set(diff, path, val); 372 | }); 373 | 374 | return diff; 375 | }; 376 | 377 | module.exports = cutils; 378 | -------------------------------------------------------------------------------- /lib/components/nedb-storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | path = require('path'), 5 | util = require('util'), 6 | proving = require('proving'), 7 | Q = require('q'), 8 | _ = require('busyman'), 9 | Datastore = require('nedb'), 10 | StorageInterface = require('./storage-interface'); 11 | 12 | function NedbStorage(dbPath) { 13 | proving.string(dbPath, 'dbPath should be a string.'); 14 | 15 | var fullPath, dir; 16 | 17 | if (dbPath) { 18 | fullPath = path.resolve(dbPath); 19 | dir = path.dirname(fullPath); 20 | if (!fs.existsSync(dir)) fs.mkdirSync(dir); 21 | } 22 | else { 23 | fullPath = null; // implies inMemoryOnly 24 | } 25 | 26 | this._db = this._createDatabase(fullPath); 27 | } 28 | 29 | util.inherits(NedbStorage, StorageInterface); 30 | 31 | NedbStorage.prototype.save = function (cnode) { 32 | _provingCnode(cnode); 33 | 34 | var deferred = Q.defer(); 35 | 36 | this._db.update({ clientName: cnode.clientName }, cnode.dump(), { upsert: true }, function (err, count, upserted) { 37 | if (err) 38 | deferred.reject(err); 39 | else { 40 | if (upserted) delete upserted._id; 41 | deferred.resolve(upserted); 42 | } 43 | }); 44 | 45 | return deferred.promise; 46 | }; 47 | 48 | NedbStorage.prototype.load = function (cnode) { 49 | _provingCnode(cnode); 50 | 51 | var deferred = Q.defer(); 52 | 53 | this._db.findOne({ clientName: cnode.clientName }, { _id: 0 }, function (err, doc) { 54 | if (err) return deferred.reject(err); 55 | if (!doc) return deferred.reject(new Error('coap node data not found')); 56 | cnode._assignAttrs(doc); 57 | deferred.resolve(doc); 58 | }); 59 | 60 | return deferred.promise; 61 | }; 62 | 63 | NedbStorage.prototype.loadAll = function () { 64 | var deferred = Q.defer(); 65 | 66 | this._db.find({}, { _id: 0 }, function (err, docs) { 67 | if (err) return deferred.reject(err); 68 | deferred.resolve(docs); 69 | }); 70 | 71 | return deferred.promise; 72 | }; 73 | 74 | NedbStorage.prototype.remove = function (cnode) { 75 | _provingCnode(cnode); 76 | 77 | var deferred = Q.defer(); 78 | 79 | this._db.remove({ clientName: cnode.clientName }, { multi: true }, function (err, numRemoved) { 80 | if (err) return deferred.reject(err); 81 | deferred.resolve(numRemoved > 0); 82 | }); 83 | 84 | return deferred.promise; 85 | }; 86 | 87 | NedbStorage.prototype.updateAttrs = function (cnode, diff) { 88 | _provingCnode(cnode); 89 | if (diff !== null && !_.isPlainObject(diff)) throw new TypeError('diff should be an object if not null.'); 90 | 91 | var deferred = Q.defer(); 92 | 93 | if (diff === null || Object.keys(diff).length === 0) 94 | deferred.resolve(null); 95 | else 96 | if (diff.clientName) 97 | deferred.reject(new Error('clientName can not be modified.')); 98 | else 99 | this._db.update({ clientName: cnode.clientName }, { $set: diff }, { 100 | returnUpdatedDocs: true, 101 | multi: false 102 | }, function (err, count, doc) { 103 | if (err) return deferred.reject(err); 104 | deferred.resolve(diff); 105 | }); 106 | 107 | return deferred.promise; 108 | }; 109 | 110 | NedbStorage.prototype.patchSo = function (cnode, diff) { 111 | _provingCnode(cnode); 112 | if (diff !== null && !_.isPlainObject(diff)) throw new TypeError('diff should be an object if not null.'); 113 | 114 | var deferred = Q.defer(); 115 | 116 | if (diff === null || Object.keys(diff).length === 0) 117 | deferred.resolve(null); 118 | else 119 | this._db.update({ clientName: cnode.clientName }, { $set: _flatten(diff, 'so') }, { 120 | returnUpdatedDocs: true, 121 | multi: false 122 | }, function (err, count, doc) { 123 | if (err) return deferred.reject(err); 124 | deferred.resolve(diff); 125 | }); 126 | 127 | return deferred.promise; 128 | }; 129 | 130 | NedbStorage.prototype.reset = function () { 131 | var deferred = Q.defer(); 132 | 133 | this._db.remove({}, { multi: true }, function (err, numRemoved) { 134 | if (err) return deferred.reject(err); 135 | this._db.loadDatabase(function (err) { 136 | if (err) return deferred.reject(err); 137 | deferred.resolve(numRemoved); 138 | }); 139 | }.bind(this)); 140 | 141 | return deferred.promise; 142 | }; 143 | 144 | NedbStorage.prototype._createDatabase = function (fullPath) { 145 | var store = new Datastore({ 146 | filename: fullPath, 147 | autoload: true 148 | }); 149 | store.ensureIndex({ 150 | fieldName: 'clientName', 151 | unique: true 152 | }, function (err) { 153 | if (err) throw err; 154 | }); 155 | return store; 156 | }; 157 | 158 | function _flatten (diff, path) { 159 | var result = {}, prefix = path ? (path + '.') : '', subObj; 160 | Object.keys(diff).forEach(function (key) { 161 | if (!_.isPlainObject(diff[key])) 162 | result[prefix + key] = diff[key]; 163 | else { 164 | subObj = _flatten(diff[key], prefix + key); 165 | Object.keys(subObj).forEach(function (subKey) { 166 | result[subKey] = subObj[subKey]; 167 | }); 168 | } 169 | }); 170 | return result; 171 | } 172 | 173 | function _provingCnode (cnode) { 174 | proving.object(cnode, 'cnode should be a CoapNode instance.'); 175 | proving.string(cnode.clientName, 'cnode should be a CoapNode instance.'); 176 | proving.fn(cnode.dump, 'cnode should be a CoapNode instance.'); 177 | } 178 | 179 | module.exports = NedbStorage; 180 | -------------------------------------------------------------------------------- /lib/components/reqHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'), 4 | _ = require('busyman'), 5 | debug = require('debug')('coap-shepherd:reqHdlr'); 6 | 7 | var CoapNode = require('./coap-node.js'), 8 | cutils = require('./cutils'), 9 | CNST = require('./constants'); 10 | 11 | /**** Code Enumerations ****/ 12 | var RSP = CNST.RSP; 13 | 14 | /********************************************************* 15 | * Handler function * 16 | *********************************************************/ 17 | function clientReqHandler(shepherd, req, rsp) { 18 | var optType = clientReqParser(req), 19 | reqHdlr; 20 | 21 | switch (optType) { 22 | case 'register': 23 | reqHdlr = clientRegisterHandler; 24 | break; 25 | case 'update': 26 | reqHdlr = clientUpdateHandler; 27 | break; 28 | case 'deregister': 29 | reqHdlr = clientDeregisterHandler; 30 | break; 31 | case 'check': 32 | reqHdlr = clientCheckHandler; 33 | break; 34 | case 'lookup': 35 | reqHdlr = clientLookupHandler; 36 | break; 37 | case 'test': 38 | reqHdlr = clientTestHandler; 39 | break; 40 | case 'empty': 41 | rsp.reset(); 42 | break; 43 | default: 44 | break; 45 | } 46 | 47 | if (reqHdlr) 48 | setImmediate(function () { 49 | reqHdlr(shepherd, req, rsp); 50 | }); 51 | } 52 | 53 | function clientRegisterHandler (shepherd, req, rsp) { 54 | var devAttrs = buildDevAttrs(shepherd, req), 55 | cnode = (devAttrs && devAttrs.clientName) ? shepherd.find(devAttrs.clientName) : null, 56 | errCount = 0; 57 | 58 | debug('REQ <-- register, token: %s', req._packet ? req._packet.token.toString('hex') : undefined); 59 | 60 | if (devAttrs === false || !devAttrs.clientName || !devAttrs.objList) 61 | return sendRsp(rsp, RSP.badreq, '', 'register'); 62 | else if (shepherd._joinable === false) 63 | return sendRsp(rsp, RSP.notallowed, '', 'register'); 64 | 65 | function getClientDetails(notFire) { 66 | setTimeout(function() { 67 | cnode = shepherd.find(devAttrs.clientName); 68 | if (cnode) { 69 | var promise; 70 | if (shepherd._config.autoReadResources) 71 | promise = cnode._readAllResource().then(function (rspObj) { 72 | return shepherd._storage.save(cnode); 73 | }); 74 | else 75 | promise = Q.fcall(function () {}); 76 | promise.then(function () { 77 | if (cnode.heartbeatEnabled) 78 | return cnode.observeReq('/heartbeat'); 79 | }).then(function () { 80 | if (!notFire) { 81 | fireImmediate(shepherd, 'ind', { 82 | type: 'devIncoming', 83 | cnode: cnode 84 | }); 85 | } 86 | // [TODO] else 87 | cnode._setStatus('online'); 88 | if (shepherd._config.dontReinitiateObserve) 89 | cnode.observedList = []; 90 | return cnode._reinitiateObserve(); 91 | }).fail(function (err) { 92 | if (errCount < 2) { 93 | errCount += 1; 94 | return getClientDetails(); 95 | } else { 96 | errCount = 0; 97 | return shepherd.emit('error', err); 98 | } 99 | }).done(); 100 | } else { 101 | // [TODO] 102 | } 103 | }, 100); 104 | } 105 | 106 | if (!cnode) { 107 | Q.fcall(function () { 108 | var allowDevIncoming; 109 | if (_.isFunction(shepherd._acceptDevIncoming)) { 110 | allowDevIncoming = Q.nbind(shepherd._acceptDevIncoming, shepherd); 111 | return allowDevIncoming(devAttrs); 112 | } else { 113 | return true; 114 | } 115 | }).then(function (accepted) { 116 | var extra = undefined; 117 | if (Array.isArray(accepted)) { 118 | extra = accepted[1]; 119 | accepted = accepted[0]; 120 | } 121 | if (accepted) { 122 | cnode = new CoapNode(shepherd, devAttrs); 123 | cnode._extra = extra; 124 | shepherd._registry[devAttrs.clientName] = cnode; 125 | cnode._registered = true; 126 | cnode._heartbeat = cutils.getTime(); 127 | cnode.lifeCheck(true); 128 | rsp.setOption('Location-Path', [new Buffer('rd'), new Buffer(cnode.clientId.toString())]); 129 | sendRsp(rsp, RSP.created, '', 'register'); 130 | return getClientDetails(); 131 | } else { 132 | sendRsp(rsp, RSP.notallowed, '', 'register'); 133 | return accepted; 134 | } 135 | }, function (err) { 136 | sendRsp(rsp, RSP.serverError, '', 'register'); 137 | shepherd.emit('error', err); 138 | }).done(); 139 | } else { // [TODO] delete cnode and add a new cnode 140 | cnode._updateAttrs(devAttrs).then(function (diff) { 141 | cnode._registered = true; 142 | cnode._heartbeat = cutils.getTime(); 143 | cnode.lifeCheck(true); 144 | rsp.setOption('Location-Path', [new Buffer('rd'), new Buffer(cnode.clientId.toString())]); 145 | sendRsp(rsp, RSP.created, '', 'register'); 146 | return getClientDetails(!shepherd._config.alwaysFireDevIncoming); 147 | }, function (err) { 148 | sendRsp(rsp, RSP.serverError, '', 'register'); 149 | shepherd.emit('error', err); 150 | }).done(); 151 | } 152 | } 153 | 154 | function clientUpdateHandler (shepherd, req, rsp) { 155 | var devAttrs = buildDevAttrs(shepherd, req), 156 | locationPath = cutils.urlParser(req.url).pathname, 157 | cnode = shepherd._findByLocationPath(locationPath), 158 | diff, 159 | msg = {}; 160 | 161 | debug('REQ <-- update, token: %s', req._packet ? req._packet.token.toString('hex') : undefined); 162 | 163 | if (devAttrs === false) 164 | return sendRsp(rsp, RSP.badreq, '', 'update'); 165 | 166 | if (cnode) { 167 | cnode._updateAttrs(devAttrs).then(function (diffAttrs) { 168 | diff = diffAttrs; 169 | cnode._setStatus('online'); 170 | cnode._heartbeat = cutils.getTime(); 171 | cnode.lifeCheck(true); 172 | if (shepherd._config.autoReadResources && diff.objList) { 173 | return cnode._readAllResource().then(function (rspObj) { 174 | return shepherd._storage.save(cnode); 175 | }); 176 | } 177 | }).then(function () { 178 | sendRsp(rsp, RSP.changed, '', 'update'); 179 | 180 | _.forEach(diff, function (val, key) { 181 | msg[key] = val; 182 | }); 183 | 184 | fireImmediate(shepherd, 'ind', { 185 | type: 'devUpdate', 186 | cnode: cnode, 187 | data: msg 188 | }); 189 | }, function (err) { 190 | sendRsp(rsp, RSP.serverError, '', 'update'); 191 | shepherd.emit('error', err); 192 | }).done(); 193 | } else { 194 | sendRsp(rsp, RSP.notfound, '', 'update'); 195 | } 196 | } 197 | 198 | function clientDeregisterHandler (shepherd, req, rsp) { 199 | var locationPath = cutils.urlParser(req.url).pathname, 200 | cnode = shepherd._findByLocationPath(locationPath), 201 | clientName = cnode.clientName, 202 | mac = cnode.mac; 203 | 204 | debug('REQ <-- deregister, token: %s', req._packet ? req._packet.token.toString('hex') : undefined); 205 | 206 | if (cnode) { 207 | shepherd.remove(clientName).then(function () { 208 | sendRsp(rsp, RSP.deleted, '', 'deregister'); 209 | }, function (err) { 210 | sendRsp(rsp, RSP.serverError, '', 'deregister'); 211 | }).done(); 212 | } else { 213 | sendRsp(rsp, RSP.notfound, '', 'deregister'); 214 | } 215 | } 216 | 217 | function clientCheckHandler (shepherd, req, rsp) { 218 | var locationPath = cutils.urlParser(req.url).pathname, 219 | cnode = shepherd._findByLocationPath(locationPath), 220 | chkAttrs = buildChkAttrs(req), 221 | devAttrs = {}, 222 | errCount = 0; 223 | 224 | debug('REQ <-- check, token: %s', req._packet ? req._packet.token.toString('hex') : undefined); 225 | 226 | if (chkAttrs === false) 227 | return sendRsp(rsp, RSP.badreq, '', 'check'); 228 | 229 | function startHeartbeat() { 230 | _.delay(function() { 231 | cnode.observeReq('/heartbeat').then(function () { 232 | cnode._setStatus('online'); 233 | }).fail(function (err) { 234 | if (errCount < 2) { 235 | errCount += 1; 236 | startHeartbeat(); 237 | } else { 238 | errCount = 0; 239 | shepherd.emit('error', err); 240 | } 241 | }).done(); 242 | }, 50); 243 | } 244 | 245 | if (cnode) { 246 | if (chkAttrs.sleep) { // check out 247 | cnode._cancelAllObservers(); 248 | cnode.sleepCheck(true, chkAttrs.duration); 249 | sendRsp(rsp, RSP.changed, '', 'check'); 250 | cnode._setStatus('sleep'); 251 | } else { // check in 252 | cnode.lifeCheck(true); 253 | cnode.sleepCheck(false); 254 | devAttrs.ip = req.rsinfo.address; 255 | devAttrs.port = req.rsinfo.port; 256 | cnode._heartbeat = cutils.getTime(); 257 | cnode._updateAttrs(devAttrs).then(function () { 258 | sendRsp(rsp, RSP.changed, '', 'check'); 259 | startHeartbeat(); 260 | }, function (err) { 261 | sendRsp(rsp, RSP.serverError, '', 'check'); 262 | shepherd.emit('error', err); 263 | }).done(); 264 | } 265 | } else { 266 | sendRsp(rsp, RSP.notfound, '', 'check'); 267 | } 268 | } 269 | 270 | function clientLookupHandler (shepherd, req, rsp) { 271 | var lookupType = cutils.getPathArray(req.url)[1], 272 | devAttrs = buildDevAttrs(shepherd, req), 273 | clientName = devAttrs ? devAttrs.clientName : '', 274 | cnode = clientName ? shepherd.find(devAttrs.clientName) : null, 275 | data; 276 | 277 | debug('REQ <-- lookup, token: %s', req._packet ? req._packet.token.toString('hex') : undefined); 278 | // [TODO] check pathname & lookupType 279 | if (cnode) { 280 | data = ';ep=' + cnode.clientName; 281 | sendRsp(rsp, RSP.content, data, 'lookup'); 282 | fireImmediate(shepherd, 'ind', { type: 'lookup' , data: clientName }); 283 | } else { 284 | sendRsp(rsp, clientName ? RSP.notfound : RSP.badreq, '', 'lookup'); 285 | } 286 | } 287 | 288 | function clientTestHandler (shepherd, req, rsp) { 289 | debug('REQ <-- test, token: %s', req._packet ? req._packet.token.toString('hex') : undefined); 290 | sendRsp(rsp, RSP.content, '_test', 'test'); 291 | } 292 | 293 | /********************************************************* 294 | * Private function * 295 | *********************************************************/ 296 | function clientReqParser (req) { 297 | var optType, 298 | lookupType, 299 | pathArray; 300 | 301 | if (req.code === '0.00' && req._packet.confirmable && req.payload.length === 0) 302 | return 'empty'; 303 | 304 | switch (req.method) { 305 | case 'POST': 306 | pathArray = cutils.getPathArray(req.url); 307 | if (pathArray.length === 1 && pathArray[0] === 'rd') 308 | optType = 'register'; 309 | else 310 | optType = 'update'; 311 | break; 312 | case 'PUT': 313 | optType = 'check'; 314 | break; 315 | case 'DELETE': 316 | optType = 'deregister'; 317 | break; 318 | case 'GET': 319 | pathArray = cutils.getPathArray(req.url); 320 | if (pathArray[0] === 'test') 321 | optType = 'test'; 322 | else 323 | optType = 'lookup'; 324 | break; 325 | default: 326 | break; 327 | } 328 | 329 | return optType; 330 | } 331 | 332 | function sendRsp(rsp, code, data, optType) { 333 | rsp.code = code; 334 | rsp.end(data); 335 | debug('RSP --> %s, token: %s', optType, rsp._packet ? rsp._packet.token.toString('hex') : undefined); 336 | } 337 | 338 | function buildDevAttrs(shepherd, req) { 339 | var devAttrs = {}, 340 | query = req.url ? req.url.split('?')[1] : undefined, // 'ep=clientName<=86400&lwm2m=1.0.0' 341 | queryParams = query ? query.split('&') : undefined, 342 | invalidAttrs = [], 343 | obj; 344 | 345 | _.forEach(queryParams, function (queryParam, idx) { 346 | queryParams[idx] = queryParam.split('='); 347 | }); 348 | 349 | _.forEach(queryParams, function(queryParam) { 350 | if(queryParam[0] === 'ep') { 351 | devAttrs.clientName = queryParam[1]; 352 | } else if (queryParam[0] === 'lt') { 353 | devAttrs.lifetime = parseInt(queryParam[1]); 354 | } else if (queryParam[0] === 'lwm2m') { 355 | devAttrs.version = queryParam[1]; 356 | } else if (queryParam[0] === 'mac') { 357 | devAttrs.mac = queryParam[1]; 358 | } else if (queryParam[0] === 'b') { 359 | // [TODO] 360 | } else { 361 | invalidAttrs.push(queryParam[0]); 362 | } 363 | }); 364 | 365 | devAttrs.ip = req.rsinfo.address; 366 | devAttrs.port = req.rsinfo.port; 367 | 368 | if (req.payload.length !== 0) { 369 | obj = cutils.getObjListOfSo(req.payload); 370 | devAttrs.objList = obj.list; 371 | devAttrs.ct = obj.opts.ct; 372 | devAttrs.heartbeatEnabled = obj.opts.hb; 373 | } 374 | 375 | if (devAttrs.clientName && ('function' === typeof shepherd._config.clientNameParser)) { 376 | devAttrs.clientName = shepherd._config.clientNameParser(devAttrs.clientName); 377 | } 378 | 379 | if (invalidAttrs.length > 0) { 380 | devAttrs = false; 381 | } 382 | 383 | return devAttrs; // { clientName: 'clientName', lifetime: 86400, version: '1.0.0', objList: { "1": [] }} 384 | } 385 | 386 | function buildChkAttrs(req) { 387 | var chkAttrs = {}, 388 | query = req.url ? req.url.split('?')[1] : undefined, // 'chk=out&t=300' 389 | queryParams = query ? query.split('&') : undefined, 390 | invalidAttrs = []; 391 | 392 | _.forEach(queryParams, function (queryParam, idx) { 393 | queryParams[idx] = queryParam.split('='); 394 | }); 395 | 396 | _.forEach(queryParams, function(queryParam) { 397 | if(queryParam[0] === 'chk') { 398 | if (queryParam[1] === 'in') 399 | chkAttrs.sleep = false; 400 | else if (queryParam[1] === 'out') 401 | chkAttrs.sleep = true; 402 | } else if (queryParam[0] === 't') { 403 | chkAttrs.duration = Number(queryParam[1]); 404 | } else { 405 | invalidAttrs.push(queryParam[0]); 406 | } 407 | }); 408 | 409 | if (invalidAttrs.length > 0) { 410 | chkAttrs = false; 411 | } 412 | 413 | return chkAttrs; 414 | } 415 | 416 | function fireImmediate(shepherd, evt, msg) { // (shepherd, evt, ...) 417 | setImmediate(function () { 418 | shepherd.emit(evt, msg); 419 | }); 420 | } 421 | 422 | /********************************************************* 423 | * Module Exports * 424 | *********************************************************/ 425 | module.exports = clientReqHandler; 426 | -------------------------------------------------------------------------------- /lib/components/storage-interface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the interface of Storage implementations. Any implementation should be a sub-class of this. 3 | */ 4 | function StorageInterface (options) {} 5 | 6 | /** 7 | * Save device attributes and smart objects of a specific cnode to backend storage. 8 | * Cnode.`clientName` is used as PK. 9 | * All existing device attributes and smart objects of this cnode in backend storage will be replaced. 10 | * @param cnode This should be an instance of `CoapNode`. 11 | * @return Promise: resolved to device attributes and smart objects of the cnode. 12 | */ 13 | StorageInterface.prototype.save = function (cnode) {}; 14 | 15 | /** 16 | * Load device attributes and smart objects of a specific cnode from backend storage. 17 | * Cnode.`clientName` is used as PK. 18 | * @param cnode This should be an instance of `CoapNode`. 19 | * @return Promise: resolved to device attributes and smart objects of the cnode. 20 | */ 21 | StorageInterface.prototype.load = function (cnode) {}; 22 | 23 | /** 24 | * Load device attributes and smart objects of all cnodes from backend storage. 25 | * @return Promise: resolved to array of device attributes and smart objects of a cnode, 26 | * each item can be passed as the `devAttrs` argument to create a CoapNode. 27 | */ 28 | StorageInterface.prototype.loadAll = function () {}; 29 | 30 | /** 31 | * Remove device attributes and smart objects of a specific cnode from backend storage. 32 | * Cnode.`clientName` is used as PK. 33 | * @param cnode This should be an instance of `CoapNode`. 34 | * @return Promise: resolved to if device attributes and smart objects of the cnode is removed. 35 | */ 36 | StorageInterface.prototype.remove = function (cnode) {}; 37 | 38 | /** 39 | * Update some device attributes of a specific cnode from backend storage. 40 | * Cnode.`clientName` is used as PK. 41 | * @param diff Properties NOT specified in `diff` will NOT be changed. 42 | * Properties specified in `diff` will be REPLACED as a whole part, this is different from `patchSo`. 43 | * @param cnode This should be an instance of `CoapNode`. 44 | * @return Promise: resolved to the passed in `diff` argument. 45 | */ 46 | StorageInterface.prototype.updateAttrs = function (cnode, diff) {}; 47 | 48 | /** 49 | * Update some smart objects of a specific cnode from backend storage. 50 | * Cnode.`clientName` is used as PK. 51 | * @param diff Properties NOT specified in `diff` will NOT be changed. 52 | * Properties specified in `diff` will be PATCHED into existing smart objects, this is different from `updateAttrs`. 53 | * @param cnode This should be an instance of `CoapNode` 54 | * @return Promise: resolved to the passed in `diff` argument. 55 | */ 56 | StorageInterface.prototype.patchSo = function (cnode, diff) {}; 57 | 58 | /** 59 | * Clear the whole backend storage, this will remove ALL device attributes and smart objects. 60 | * @return Promise: resolved to number of cnode infos removed from backend storage. 61 | */ 62 | StorageInterface.prototype.reset = function () {}; 63 | 64 | module.exports = StorageInterface; 65 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // indicates if the server should create IPv4 connections (udp4) or IPv6 connections (udp6). 5 | // default is udp4. 6 | connectionType: 'udp4', 7 | 8 | // the cserver's ip address. 9 | ip: '127.0.0.1', 10 | 11 | // the cserver's COAP server will start listening. 12 | // default is 5683. 13 | port: 5683, 14 | 15 | // storage for cnode persistence, should be an instance of StorageInterface if specified. 16 | // default is an instance of NedbStorage with `this.defaultDbPath` as storage file. 17 | storage: null, 18 | 19 | // request should get response in the time. 20 | // default is 60 secs. 21 | reqTimeout: 60, 22 | 23 | // how often to check heartbeat. 24 | // it must be greater than client device heartbeatTime. 25 | // default is 60 secs. 26 | hbTimeout: 60, 27 | 28 | // auto read client resources when it's registering. 29 | autoReadResources: true, 30 | 31 | // disable filtering for observed client packets. details: 32 | // https://github.com/mcollina/node-coap/issues/200 33 | disableFiltering: false, 34 | 35 | // a function to parse clientName. some clients may send 'urn:123456' as clientName, 36 | // you can use your own clientNameParser to keep just the '123456' part as clientName. 37 | clientNameParser: function (clientName) { return clientName; }, 38 | 39 | // always fire devIncoming event, even if client is already online when registering. 40 | alwaysFireDevIncoming: false, 41 | 42 | // do not call cnode._reinitiateObserve() when when registering. 43 | dontReinitiateObserve: false, 44 | 45 | // path to the file where the data is persisted, if default NedbStorage is used. 46 | // default is ./lib/database/coap.db. 47 | defaultDbPath: __dirname + '/database/coap.db' 48 | }; 49 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'), 4 | _ = require('busyman'), 5 | coap = require('coap'), 6 | network = require('network'), 7 | debug = require('debug')('coap-shepherd:init'); 8 | 9 | var reqHandler = require('./components/reqHandler'), 10 | CoapNode = require('./components/coap-node'), 11 | cutils = require('./components/cutils'), 12 | CNST = require('./components/constants'); 13 | 14 | /**** Code Enumerations ****/ 15 | var RSP = CNST.RSP; 16 | 17 | var init = {}; 18 | 19 | init.setupShepherd = function (shepherd, callback) { 20 | var deferred = Q.defer(), 21 | self = this; 22 | 23 | debug('coap-shepherd booting...'); 24 | 25 | coap.registerFormat('application/tlv', 11542); // Leshan TLV binary Content-Formats 26 | coap.registerFormat('application/json', 11543); // Leshan JSON Numeric Content-Formats 27 | 28 | coap.updateTiming({ 29 | ackTimeout:0.25, 30 | ackRandomFactor: 1.0, 31 | maxRetransmit: 3, 32 | maxLatency: 2, 33 | }); 34 | 35 | this._coapServerStart(shepherd).then(function (server) { 36 | debug('Create a coap server for shepherd.'); 37 | shepherd._enabled = true; 38 | shepherd._server = server; 39 | return self._testRequestServer(shepherd); 40 | }).then(function () { 41 | debug('Coap server testing done.'); 42 | return self._loadNodesFromDb(shepherd); 43 | }).then(function () { 44 | debug('Loading cnodes from database done.'); 45 | return self._updateNetInfo(shepherd); 46 | }).then(function () { 47 | debug('coap-shepherd is up and ready.'); 48 | deferred.resolve(); 49 | }).fail(function (err) { 50 | debug(err); 51 | deferred.reject(err); 52 | }).done(); 53 | 54 | return deferred.promise.nodeify(callback); 55 | }; 56 | 57 | /********************************************************* 58 | * Private function * 59 | *********************************************************/ 60 | init._coapServerStart = function (shepherd, callback) { 61 | var deferred = Q.defer(), 62 | server = coap.createServer({ 63 | type: shepherd._config.connectionType 64 | }); 65 | 66 | server.on('request', function (req, rsp) { 67 | if (!_.isEmpty(req.payload)) 68 | req.payload = req.payload.toString(); 69 | 70 | reqHandler(shepherd, req, rsp); 71 | }); 72 | 73 | server.listen(shepherd._net.port, function (err) { 74 | if (err) 75 | deferred.reject(err); 76 | else 77 | deferred.resolve(server); 78 | }); 79 | 80 | if (shepherd._config.connectionType === 'udp6') { 81 | coap.globalAgentIPv6 = new coap.Agent({ 82 | type: shepherd._config.connectionType, 83 | socket: server._sock 84 | }); 85 | 86 | shepherd._agent = coap.globalAgentIPv6; 87 | } else { 88 | coap.globalAgent = new coap.Agent({ 89 | type: shepherd._config.connectionType, 90 | socket: server._sock 91 | }); 92 | 93 | shepherd._agent = coap.globalAgent; 94 | } 95 | 96 | 97 | return deferred.promise.nodeify(callback); 98 | }; 99 | 100 | init._testRequestServer = function (shepherd) { 101 | var deferred = Q.defer(), 102 | reqOdj = { 103 | hostname: shepherd._net.ip, 104 | port: shepherd._net.port, 105 | pathname: '/test', 106 | method: 'GET' 107 | }; 108 | 109 | debug('Coap server testing start.'); 110 | shepherd.request(reqOdj).then(function (rsp) { 111 | debug('Coap server testing request done.'); 112 | if (rsp.code === RSP.content && rsp.payload === '_test') { 113 | reqOdj = null; 114 | deferred.resolve(); 115 | } else { 116 | deferred.reject(new Error('shepherd client test error')); 117 | } 118 | }).fail(function (err) { 119 | deferred.reject(err); 120 | }).done(); 121 | 122 | return deferred.promise; 123 | }; 124 | 125 | init._loadNodesFromDb = function (shepherd, callback) { 126 | var deferred = Q.defer(); 127 | 128 | shepherd._storage.loadAll().then(function (devAttrsList) { 129 | devAttrsList.forEach(function (devAttrs) { 130 | shepherd._registry[devAttrs.clientName] = new CoapNode(shepherd, devAttrs); 131 | }); 132 | }).done(function () { 133 | deferred.resolve(shepherd); 134 | }, function (err) { 135 | deferred.reject(err); 136 | }); 137 | 138 | return deferred.promise.nodeify(callback); 139 | }; 140 | 141 | init._updateNetInfo = function (shepherd, callback) { 142 | var deferred = Q.defer(); 143 | 144 | network.get_active_interface(function(err, obj) { 145 | if (err) { 146 | deferred.reject(err); 147 | } else { 148 | shepherd._net.intf = obj.name; 149 | shepherd._net.ip = obj.ip_address; 150 | shepherd._net.mac = obj.mac_address; 151 | shepherd._net.routerIp = obj.gateway_ip; 152 | deferred.resolve(_.cloneDeep(shepherd._net)); 153 | } 154 | }); 155 | 156 | return deferred.promise.nodeify(callback); 157 | }; 158 | 159 | module.exports = init; 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coap-shepherd", 3 | "version": "0.3.1", 4 | "description": "Network server and manager for lightweight M2M (LWM2M).", 5 | "main": "index.js", 6 | "dependencies": { 7 | "busyman": "^0.3.0", 8 | "coap": "^0.23.1", 9 | "debug": "^2.2.0", 10 | "firmata": "^0.10.1", 11 | "lwm2m-codec": "0.1.1", 12 | "lwm2m-id": "^1.6.1", 13 | "nedb": "^1.8.0", 14 | "network": "^0.2.1", 15 | "proving": "^0.1.0", 16 | "q": "^1.4.1", 17 | "smartobject": "^1.4.0" 18 | }, 19 | "devDependencies": { 20 | "chai": "^3.5.0", 21 | "mocha": "^2.5.3", 22 | "sinon": "^1.17.5", 23 | "sinon-chai": "^2.8.0" 24 | }, 25 | "scripts": { 26 | "test": "make test-all" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/PeterEB/coap-shepherd.git" 31 | }, 32 | "keywords": [ 33 | "coap" 34 | ], 35 | "author": "", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/PeterEB/coap-shepherd/issues" 39 | }, 40 | "homepage": "https://github.com/PeterEB/coap-shepherd" 41 | } 42 | -------------------------------------------------------------------------------- /test/coap-node.test.js: -------------------------------------------------------------------------------- 1 | var _ = require('busyman'), 2 | Q = require('q'), 3 | chai = require('chai'), 4 | sinon = require('sinon'), 5 | sinonChai = require('sinon-chai'), 6 | expect = chai.expect; 7 | 8 | chai.use(sinonChai); 9 | 10 | var NedbStorage = require('../lib/components/nedb-storage'), 11 | CoapNode = require('../lib/components/coap-node'), 12 | cutils = require('../lib/components/cutils'), 13 | defaultConfig = require('../lib/config'), 14 | fixture = require('./fixture'), 15 | _verifySignatureSync = fixture._verifySignatureSync, 16 | _verifySignatureAsync = fixture._verifySignatureAsync; 17 | 18 | var devAttrs = { 19 | clientName: 'coap-client', 20 | lifetime: 86400, 21 | ip: '192.168.1.100', 22 | port: '5685', 23 | mac: 'AA:BB:CC:DD:EE:00', 24 | version: '1.0.0', 25 | objList: { x: [0, 1] }, 26 | ct: '11543', 27 | heartbeatEnabled: true 28 | }; 29 | 30 | var sObj = { 31 | 0: { 32 | x0: 10, 33 | x1: 20 34 | }, 35 | 1: { 36 | x0: 100, 37 | x1: 200 38 | } 39 | }; 40 | 41 | var fakeShp, 42 | node, 43 | reqObj, 44 | rspObj; 45 | 46 | describe('coap-node', function () { 47 | before(function () { 48 | fakeShp = { 49 | emit: function () {}, 50 | request: function (req, callback) { 51 | var deferred = Q.defer(); 52 | if (_.isEqual(req, reqObj)) 53 | deferred.resolve(rspObj); 54 | 55 | return deferred.promise.nodeify(callback); 56 | }, 57 | _newClientId: function () { return 1; }, 58 | _config: Object.assign({}, defaultConfig), 59 | _storage: new NedbStorage('') 60 | }; 61 | node = new CoapNode(fakeShp, devAttrs); 62 | 63 | node.so.init('x', 0, sObj[0]); 64 | node.so.init('x', 1, sObj[1]); 65 | }); 66 | 67 | describe('Constructor Check', function () { 68 | it('new CoapNode()', function () { 69 | expect(node.shepherd).to.be.equal(fakeShp); 70 | expect(node.clientName).to.be.eql('coap-client'); 71 | expect(node.ip).to.be.eql('192.168.1.100'); 72 | expect(node.version).to.be.eql('1.0.0'); 73 | expect(node.lifetime).to.be.eql(86400); 74 | expect(node.status).to.be.eql('offline'); 75 | expect(node.objList).to.be.eql({ x: [0, 1] }); 76 | expect(node.observedList).to.be.eql([]); 77 | expect(node._registered).to.be.false; 78 | expect(node._streamObservers).to.be.eql({}); 79 | expect(node._lifeChecker).to.be.eql(null); 80 | expect(node._heartbeat).to.be.eql(null); 81 | }); 82 | }); 83 | 84 | describe('Signature Check', function () { 85 | it('new CoapNode()', function () { 86 | _verifySignatureSync(function (arg) { return new CoapNode(arg, {}); }, [fakeShp]); 87 | _verifySignatureSync(function (arg) { return new CoapNode(fakeShp, arg); }, ['object']); 88 | }); 89 | 90 | it('#.lifeCheck()', function () { 91 | _verifySignatureSync(function (arg) { node.lifeCheck(arg); }, ['boolean']); 92 | }); 93 | 94 | it('#.sleepCheck()', function () { 95 | _verifySignatureSync(function (arg) { node.sleepCheck(arg); }, ['boolean']); 96 | }); 97 | 98 | it('#._reqObj()', function () { 99 | _verifySignatureSync(function (arg) { node._reqObj(arg, arg); }, ['string']); 100 | }); 101 | 102 | it('#._setStatus()', function () { 103 | _verifySignatureSync(function (arg) { node._setStatus(arg); }, [['online', 'offline', 'sleep']]); 104 | }); 105 | 106 | // Asynchronous APIs 107 | describe('#.readReq()', function () { 108 | it('should throw err if path is not a string', function () { 109 | _verifySignatureSync(function (arg) { node.readReq(arg).done(); }, ['string']); 110 | }); 111 | 112 | it('should return err if not registered', function (done) { 113 | node._registered = false; 114 | node.readReq('x').fail(function (err) { 115 | done(); 116 | }); 117 | }); 118 | 119 | it('should return err if status is offline', function (done) { 120 | node._registered = true; 121 | node.readReq('x').fail(function (err) { 122 | done(); 123 | }); 124 | }); 125 | }); 126 | 127 | describe('#.writeReq()', function () { 128 | it('should throw err if path is not a string', function () { 129 | _verifySignatureSync(function (arg) { node.writeReq(arg, 1).done(); }, ['string']); 130 | }); 131 | 132 | it('should throw err if value is undefined', function () { 133 | expect(function () { return node.writeReq('x/y/z', undefined); }).to.throw(); 134 | }); 135 | 136 | it('should return err if not registered', function (done) { 137 | node._registered = false; 138 | node.writeReq('x/y', {}).fail(function (err) { 139 | done(); 140 | }); 141 | }); 142 | 143 | it('should return err if status is offline', function (done) { 144 | node._registered = true; 145 | node.writeReq('x/y', {}).fail(function (err) { 146 | done(); 147 | }); 148 | }); 149 | }); 150 | 151 | describe('#.executeReq()', function () { 152 | it('should throw err if path is not a string', function () { 153 | _verifySignatureSync(function (arg) { node.executeReq(arg, []).done(); }, ['string']); 154 | }); 155 | 156 | it('should return err if not registered', function (done) { 157 | node._registered = false; 158 | node.executeReq('x/y/z', []).fail(function (err) { 159 | done(); 160 | }); 161 | }); 162 | 163 | it('should return err if status is offline', function (done) { 164 | node._registered = true; 165 | node.executeReq('x/y/z', []).fail(function (err) { 166 | done(); 167 | }); 168 | }); 169 | 170 | it('should return err if args is not an array', function () { 171 | return _verifySignatureAsync(function (arg) { 172 | return node.executeReq('x/y/z', arg, undefined); 173 | }, ['undefined', 'null', 'array']); 174 | }); 175 | }); 176 | 177 | describe('#.discoverReq()', function () { 178 | it('should throw err if path is not a string', function () { 179 | _verifySignatureSync(function (arg) { node.discoverReq(arg).done(); }, ['string']); 180 | }); 181 | 182 | it('should return err if not registered', function (done) { 183 | node._registered = false; 184 | node.discoverReq('x/y').fail(function (err) { 185 | done(); 186 | }); 187 | }); 188 | 189 | it('should return err if status is offline', function (done) { 190 | node._registered = true; 191 | node.discoverReq('x/y').fail(function (err) { 192 | done(); 193 | }); 194 | }); 195 | }); 196 | 197 | describe('#.writeAttrReq()', function () { 198 | it('should throw err if path is not a string', function () { 199 | _verifySignatureSync(function (arg) { node.writeAttrsReq(arg, {}).done(); }, ['string']); 200 | }); 201 | 202 | it('should throw err if attrs is not an object', function () { 203 | _verifySignatureSync(function (arg) { node.writeAttrsReq('x/y', arg).done(); }, ['object']); 204 | }); 205 | 206 | it('should return err if not registered', function (done) { 207 | node._registered = false; 208 | node.writeAttrsReq('x/y', {}).fail(function (err) { 209 | done(); 210 | }); 211 | }); 212 | 213 | it('should return err if status is offline', function (done) { 214 | node._registered = true; 215 | node.writeAttrsReq('x/y', {}).fail(function (err) { 216 | done(); 217 | }); 218 | }); 219 | }); 220 | 221 | describe('#.observeReq()', function () { 222 | it('should throw err if path is not a string', function () { 223 | _verifySignatureSync(function (arg) { node.observeReq(arg).done(); }, ['string']); 224 | }); 225 | 226 | it('should return err if not registered', function (done) { 227 | node._registered = false; 228 | node.observeReq('x/y/z').fail(function (err) { 229 | done(); 230 | }); 231 | }); 232 | 233 | it('should return err if status is offline', function (done) { 234 | node._registered = true; 235 | node.observeReq('x/y/z').fail(function (err) { 236 | done(); 237 | }); 238 | }); 239 | }); 240 | 241 | describe('#.cancelObserveReq()', function () { 242 | it('should throw err if path is not a string', function () { 243 | _verifySignatureSync(function (arg) { node.cancelObserveReq(arg).done(); }, ['string']); 244 | }); 245 | 246 | it('should return err if not registered', function (done) { 247 | node._registered = false; 248 | node.cancelObserveReq('x/y/z').fail(function (err) { 249 | done(); 250 | }); 251 | }); 252 | 253 | it('should return err if status is offline', function (done) { 254 | node._registered = true; 255 | node.cancelObserveReq('x/y/z').fail(function (err) { 256 | done(); 257 | }); 258 | }); 259 | }); 260 | 261 | describe('#.pingReq()', function () { 262 | it('should return err if not registered', function (done) { 263 | node._registered = false; 264 | node.pingReq().fail(function (err) { 265 | done(); 266 | }); 267 | }); 268 | }); 269 | 270 | describe('#._updateAttrs()', function () { 271 | it('should return err if attrs is not an object', function () { 272 | return _verifySignatureAsync(function (arg) { return node._updateAttrs(arg); }, ['object']); 273 | }); 274 | }); 275 | }); 276 | 277 | describe('Functional Check', function () { 278 | before(function () { 279 | node._registered = true; 280 | node.status = 'online'; 281 | }); 282 | 283 | describe('#.lifeCheck()', function () { 284 | it('should open lifeCheck', function (done) { 285 | node.lifeCheck(true); 286 | if (node._lifeChecker !== null) done(); 287 | }); 288 | 289 | it('should close lifeCheck', function (done) { 290 | node.lifeCheck(false); 291 | if (node._lifeChecker === null) done(); 292 | }); 293 | }); 294 | 295 | describe('#.sleepCheck()', function () { 296 | it('should open sleepCheck', function (done) { 297 | node.sleepCheck(true); 298 | if (node._sleepChecker === null) done(); 299 | }); 300 | 301 | it('should open sleepCheck', function (done) { 302 | node.sleepCheck(true, 10); 303 | if (node._sleepChecker !== null) done(); 304 | }); 305 | 306 | it('should close sleepCheck', function (done) { 307 | node.sleepCheck(false); 308 | if (node._sleepChecker === null) done(); 309 | }); 310 | }); 311 | 312 | describe('#._reqObj()', function () { 313 | it('should return reqObj', function () { 314 | var obj = { 315 | hostname: '192.168.1.100', 316 | port: '5685', 317 | pathname: 'x', 318 | method: 'GET' 319 | }; 320 | 321 | expect(node._reqObj('GET', 'x')).to.be.eql(obj); 322 | }); 323 | }); 324 | 325 | describe('#.readReq()', function () { 326 | it('should read Resource and return status 2.05', function (done) { 327 | reqObj = { 328 | hostname: '192.168.1.100', 329 | port: '5685', 330 | pathname: '/x/0/x0', 331 | method: 'GET', 332 | options: { Accept: 'application/json' } 333 | }; 334 | rspObj = { 335 | code: '2.05', 336 | payload: 10 337 | }; 338 | 339 | node.readReq('/x/0/x0').then(function (rsp) { 340 | expect(rsp.status).to.equal('2.05'); 341 | expect(rsp.data).to.equal(10); 342 | done(); 343 | }).done(); 344 | }); 345 | 346 | it('should read Object Instance and return status 2.05', function (done) { 347 | var obj = { 348 | x0: 10, 349 | x1: 20 350 | }; 351 | 352 | reqObj = { 353 | hostname: '192.168.1.100', 354 | port: '5685', 355 | pathname: '/x/0', 356 | method: 'GET', 357 | options: { Accept: 'application/json' } 358 | }; 359 | rspObj = { 360 | code: '2.05', 361 | payload: obj 362 | }; 363 | 364 | node.readReq('/x/0').then(function (rsp) { 365 | if (rsp.status === '2.05' && rsp.data === obj) 366 | done(); 367 | }); 368 | }); 369 | 370 | it('should read Object and return status 2.05', function (done) { 371 | var obj = { 372 | 0: { 373 | x0: 10, 374 | x1: 20 375 | }, 376 | 1: { 377 | x0: 100, 378 | x1: 200 379 | } 380 | }; 381 | 382 | reqObj = { 383 | hostname: '192.168.1.100', 384 | port: '5685', 385 | pathname: '/x', 386 | method: 'GET', 387 | options: { Accept: 'application/json' } 388 | }; 389 | rspObj = { 390 | code: '2.05', 391 | payload: obj 392 | }; 393 | 394 | node.readReq('/x').then(function (rsp) { 395 | if (rsp.status === '2.05' && rsp.data === obj) 396 | done(); 397 | }); 398 | }); 399 | }); 400 | 401 | describe('#.writeReq()', function () { 402 | it('should write Resource and return status 2.04', function (done) { 403 | reqObj = { 404 | hostname: '192.168.1.100', 405 | port: '5685', 406 | pathname: '/x/0/x0', 407 | method: 'PUT', 408 | payload: new Buffer([0x7b, 0x22, 0x62, 0x6e, 0x22, 0x3a, 0x22, 0x2f, 0x78, 0x2f, 0x30, 0x2f, 0x78, 0x30, 0x22, 0x2c, 0x22, 0x65, 0x22, 0x3a, 0x5b, 0x7b, 0x22, 0x6e, 0x22, 0x3a, 0x22, 0x22, 0x2c, 0x22, 0x76, 0x22, 0x3a, 0x31, 0x30, 0x7d, 0x5d, 0x7d]), 409 | options: { 410 | 'Content-Format': 'application/json' 411 | } 412 | }; 413 | rspObj = { 414 | code: '2.04' 415 | }; 416 | 417 | node.writeReq('/x/0/x0', 10).then(function (rsp) { 418 | if (rsp.status === '2.04') 419 | done(); 420 | }); 421 | }); 422 | 423 | it('should write Object Instance and return status 2.04', function (done) { 424 | reqObj = { 425 | hostname: '192.168.1.100', 426 | port: '5685', 427 | pathname: '/x/0', 428 | method: 'PUT', 429 | payload: new Buffer([0x7b, 0x22, 0x62, 0x6e, 0x22, 0x3a, 0x22, 0x2f, 0x78, 0x2f, 0x30, 0x22, 0x2c, 0x22, 0x65, 0x22, 0x3a, 0x5b, 0x7b, 0x22, 0x6e, 0x22, 0x3a, 0x22, 0x78, 0x30, 0x22, 0x2c, 0x22, 0x76, 0x22, 0x3a, 0x31, 0x30, 0x7d, 0x2c, 0x7b, 0x22, 0x6e, 0x22, 0x3a, 0x22, 0x78, 0x31, 0x22, 0x2c, 0x22, 0x76, 0x22, 0x3a, 0x32, 0x30, 0x7d, 0x5d, 0x7d]), 430 | options: { 431 | 'Content-Format': 'application/json' 432 | } 433 | }; 434 | rspObj = { 435 | code: '2.04' 436 | }; 437 | 438 | node.writeReq('/x/0', { x0: 10, x1: 20 }).then(function (rsp) { 439 | if (rsp.status === '2.04') 440 | done(); 441 | }); 442 | }); 443 | }); 444 | 445 | describe('#.executeReq()', function () { 446 | it('should execute Resource and return status 2.04', function (done) { 447 | reqObj = { 448 | hostname: '192.168.1.100', 449 | port: '5685', 450 | pathname: '/x/0/x0', 451 | method: 'POST', 452 | payload: '10,20' 453 | }; 454 | rspObj = { 455 | code: '2.04' 456 | }; 457 | 458 | node.executeReq('/x/0/x0', [ 10, 20 ]).then(function (rsp) { 459 | if (rsp.status === '2.04') 460 | done(); 461 | }); 462 | }); 463 | }); 464 | 465 | describe('#.discoverReq()', function () { 466 | it('should discover Resource and return status 2.05', function (done) { 467 | var obj = { 468 | path: '/x/0/x0', 469 | attrs: { 470 | pmin: 10, 471 | pmax: 60 472 | } 473 | }; 474 | 475 | reqObj = { 476 | hostname: '192.168.1.100', 477 | port: '5685', 478 | pathname: '/x/0/x0', 479 | method: 'GET', 480 | options: { 481 | Accept: 'application/link-format' 482 | } 483 | }; 484 | rspObj = { 485 | headers: { 486 | 'Content-Format': 'application/link-format' 487 | }, 488 | code: '2.05', 489 | payload: obj 490 | }; 491 | 492 | node.discoverReq('/x/0/x0').then(function (rsp) { 493 | if (rsp.status === '2.05' && _.isEqual(rsp.data, obj)) 494 | done(); 495 | }); 496 | }); 497 | 498 | it('should discover Object Instance and return status 2.05', function (done) { 499 | var obj = { 500 | path: '/x/0', 501 | attrs: { 502 | pmin: 10, 503 | pmax: 60 504 | }, 505 | resrcList: ['/x/0/x0', '/x/0/x1'] 506 | }; 507 | 508 | reqObj = { 509 | hostname: '192.168.1.100', 510 | port: '5685', 511 | pathname: '/x/0', 512 | method: 'GET', 513 | options: { 514 | Accept: 'application/link-format' 515 | } 516 | }; 517 | rspObj = { 518 | headers: { 519 | 'Content-Format': 'application/link-format' 520 | }, 521 | code: '2.05', 522 | payload: obj 523 | }; 524 | 525 | node.discoverReq('/x/0').then(function (rsp) { 526 | if (rsp.status === '2.05' && _.isEqual(rsp.data, obj)) 527 | done(); 528 | }); 529 | }); 530 | 531 | it('should discover Object and return status 2.05', function (done) { 532 | var obj = { 533 | path: '/x', 534 | attrs: { 535 | pmin: 10, 536 | pmax: 60 537 | }, 538 | resrcList: [ '/x/0/x0', '/x/0/x1', '/x/1/x0', '/x/1/x1' ] 539 | }; 540 | 541 | reqObj = { 542 | hostname: '192.168.1.100', 543 | port: '5685', 544 | pathname: '/x', 545 | method: 'GET', 546 | options: { 547 | Accept: 'application/link-format' 548 | } 549 | }; 550 | rspObj = { 551 | headers: { 552 | 'Content-Format': 'application/link-format' 553 | }, 554 | code: '2.05', 555 | payload: obj 556 | }; 557 | 558 | node.discoverReq('/x').then(function (rsp) { 559 | if (rsp.status === '2.05' && _.isEqual(rsp.data, obj)) 560 | done(); 561 | }); 562 | }); 563 | }); 564 | 565 | describe('#.writeAttrsReq()', function () { 566 | it('should write Resource Attrs and return status 2.05', function (done) { 567 | reqObj = { 568 | hostname: '192.168.1.100', 569 | port: '5685', 570 | pathname: '/x/0/x0', 571 | method: 'PUT', 572 | query: 'pmin=10&pmax=60' 573 | }; 574 | rspObj = { 575 | code: '2.04' 576 | }; 577 | 578 | node.writeAttrsReq('/x/0/x0', { pmin: 10, pmax: 60 }).then(function (rsp) { 579 | if (rsp.status === '2.04') 580 | done(); 581 | }); 582 | }); 583 | 584 | it('should write Object Instance Attrs and return status 2.05', function (done) { 585 | reqObj = { 586 | hostname: '192.168.1.100', 587 | port: '5685', 588 | pathname: '/x/0', 589 | method: 'PUT', 590 | query: 'pmin=10&pmax=60' 591 | }; 592 | rspObj = { 593 | code: '2.04' 594 | }; 595 | 596 | node.writeAttrsReq('/x/0', { pmin: 10, pmax: 60 }).then(function (rsp) { 597 | if (rsp.status === '2.04') 598 | done(); 599 | }); 600 | }); 601 | 602 | it('should write Object Attrs and return status 2.05', function (done) { 603 | reqObj = { 604 | hostname: '192.168.1.100', 605 | port: '5685', 606 | pathname: '/x', 607 | method: 'PUT', 608 | query: 'pmin=10&pmax=60' 609 | }; 610 | rspObj = { 611 | code: '2.04' 612 | }; 613 | 614 | node.writeAttrsReq('/x', { pmin: 10, pmax: 60 }).then(function (rsp) { 615 | if (rsp.status === '2.04') 616 | done(); 617 | }); 618 | }); 619 | }); 620 | 621 | describe('#.observeReq()', function () { 622 | it('should observe Resource and return status 2.05 for number data', function (done) { 623 | reqObj = { 624 | hostname: '192.168.1.100', 625 | port: '5685', 626 | pathname: '/x/0/x0', 627 | method: 'GET', 628 | options: { Accept: 'application/json' }, 629 | observe: true 630 | }; 631 | rspObj.headers = { 'Content-Format': 'application/tlv' }; 632 | rspObj.code = '2.05'; 633 | rspObj.payload = 10; 634 | rspObj.close = function () {}; 635 | rspObj.once = function () {}; 636 | 637 | node.observeReq('/x/0/x0').then(function (rsp) { 638 | if (rsp.status === '2.05' && rsp.data === 10) 639 | done(); 640 | }); 641 | }); 642 | 643 | it('should observe Resource and return status 2.05 for object data', function (done) { 644 | var obj = { 645 | x0: 10, 646 | x1: 20 647 | }; 648 | 649 | reqObj = { 650 | hostname: '192.168.1.100', 651 | port: '5685', 652 | pathname: '/x/0', 653 | method: 'GET', 654 | options: { Accept: 'application/json' }, 655 | observe: true 656 | }; 657 | rspObj.headers = { 'Content-Format': 'application/tlv' }; 658 | rspObj.code = '2.05'; 659 | rspObj.payload = obj; 660 | rspObj.close = function () {}; 661 | rspObj.once = function () {}; 662 | 663 | node.observeReq('/x/0').then(function (rsp) { 664 | if (rsp.status === '2.05' && _.isEqual(rsp.data, obj)) 665 | done(); 666 | }); 667 | }); 668 | 669 | it('should set observeStream._disableFiltering to false by default', function (done) { 670 | reqObj = { 671 | hostname: '192.168.1.100', 672 | port: '5685', 673 | pathname: '/x/0/x0', 674 | method: 'GET', 675 | options: { Accept: 'application/json' }, 676 | observe: true 677 | }; 678 | rspObj.headers = { 'Content-Format': 'application/tlv' }; 679 | rspObj.code = '2.05'; 680 | rspObj.payload = 10; 681 | rspObj.close = function () {}; 682 | rspObj.once = function () {}; 683 | 684 | node.observeReq('/x/0/x0').then(function (rsp) { 685 | expect(rspObj._disableFiltering).to.equal(false); 686 | if (rsp.status === '2.05' && rsp.data === 10) 687 | done(); 688 | }); 689 | }); 690 | 691 | it('should set observeStream._disableFiltering to true when shepherd config is set so', function (done) { 692 | reqObj = { 693 | hostname: '192.168.1.100', 694 | port: '5685', 695 | pathname: '/x/0/x0', 696 | method: 'GET', 697 | options: { Accept: 'application/json' }, 698 | observe: true 699 | }; 700 | rspObj.headers = { 'Content-Format': 'application/tlv' }; 701 | rspObj.code = '2.05'; 702 | rspObj.payload = 10; 703 | rspObj.close = function () {}; 704 | rspObj.once = function () {}; 705 | node.shepherd._config.disableFiltering = true; 706 | 707 | node.observeReq('/x/0/x0').then(function (rsp) { 708 | node.shepherd._config.disableFiltering = defaultConfig.disableFiltering; 709 | expect(rspObj._disableFiltering).to.equal(true); 710 | if (rsp.status === '2.05' && rsp.data === 10) 711 | done(); 712 | }); 713 | }); 714 | 715 | it('should call notifyHandler with latest Content-Format', function (done) { 716 | var value = { hello: 'world' }, 717 | _updateSoAndDbStub = sinon.stub(node, '_updateSoAndDb', function (path, data) { 718 | _updateSoAndDbStub.restore(); 719 | expect(data).to.eql(value); 720 | return { done: function () {} }; 721 | }), 722 | decodeJsonStub = sinon.stub(cutils, 'decodeJson', function (path, value) { 723 | decodeJsonStub.restore(); 724 | return JSON.parse(value); 725 | }); 726 | reqObj = { 727 | hostname: '192.168.1.100', 728 | port: '5685', 729 | pathname: '/x/0/x0', 730 | method: 'GET', 731 | options: { Accept: 'application/json' }, 732 | observe: true 733 | }; 734 | rspObj.headers = { 'Content-Format': 'text/plain' }; 735 | rspObj.code = '2.05'; 736 | rspObj.payload = 10; 737 | rspObj.close = function () {}; 738 | rspObj.once = function (event, handler) { handler(); }; 739 | rspObj.on = function (event, handler) { 740 | rspObj.headers['Content-Format'] = 'application/json'; 741 | handler(JSON.stringify(value)); 742 | }; 743 | node.shepherd._enabled = true; 744 | 745 | node.observeReq('/x/0/x0').then(function (rsp) { 746 | delete node.shepherd._enabled; 747 | expect(rsp.status).to.eql('2.05'); 748 | expect(rsp.data).to.eql(10); 749 | done(); 750 | }); 751 | }); 752 | 753 | }); 754 | 755 | describe('#.cancelObserveReq()', function () { 756 | it('should cancel Resource observe and return status 2.05', function (done) { 757 | reqObj = { 758 | hostname: '192.168.1.100', 759 | port: '5685', 760 | pathname: '/x/0/x0', 761 | method: 'GET', 762 | observe: false 763 | }; 764 | rspObj = { 765 | code: '2.05' 766 | }; 767 | 768 | node.cancelObserveReq('/x/0/x0').then(function (rsp) { 769 | if (rsp.status === '2.05') 770 | done(); 771 | }); 772 | }); 773 | 774 | it('should cancel Object Instance observe and return status 2.05', function (done) { 775 | reqObj = { 776 | hostname: '192.168.1.100', 777 | port: '5685', 778 | pathname: '/x/0', 779 | method: 'GET', 780 | observe: false 781 | }; 782 | rspObj = { 783 | code: '2.05' 784 | }; 785 | 786 | node.cancelObserveReq('/x/0').then(function (rsp) { 787 | if (rsp.status === '2.05') 788 | done(); 789 | }); 790 | }); 791 | }); 792 | 793 | describe('#.pingReq()', function () { 794 | it('should ping cnode and return status 2.05', function (done) { 795 | reqObj = { 796 | hostname: '192.168.1.100', 797 | port: '5685', 798 | pathname: '/ping', 799 | method: 'POST' 800 | }; 801 | rspObj = { 802 | code: '2.05' 803 | }; 804 | 805 | node.pingReq().then(function (rsp) { 806 | if (rsp.status === '2.05') 807 | done(); 808 | }); 809 | }); 810 | }); 811 | 812 | describe('#.dump()', function () { 813 | it('should return node record', function () { 814 | var dumper = { 815 | clientName: 'coap-client', 816 | clientId: 1, 817 | ip: '192.168.1.100', 818 | port: '5685', 819 | mac: 'AA:BB:CC:DD:EE:00', 820 | lifetime: 86400, 821 | version: '1.0.0', 822 | objList: { x: [0, 1] }, 823 | observedList: [], 824 | heartbeatEnabled: true, 825 | so: { 826 | x: sObj 827 | } 828 | }, 829 | nDump = node.dump(); 830 | 831 | delete nDump.joinTime; 832 | 833 | expect(nDump).to.be.eql(dumper); 834 | }); 835 | }); 836 | 837 | describe('#._setStatus()', function () { 838 | it('should set node status to online', function (done) { 839 | node._setStatus('online'); 840 | if (node.status === 'online') done(); 841 | }); 842 | 843 | it('should set node status to offline', function (done) { 844 | node._setStatus('offline'); 845 | if (node.status === 'offline') done(); 846 | }); 847 | }); 848 | 849 | describe('#._updateAttrs()', function () { 850 | before(function (done) { 851 | var dumper = { 852 | clientName: 'coap-client', 853 | clientId: 1, 854 | ip: '192.168.1.100', 855 | port: '5685', 856 | mac: 'AA:BB:CC:DD:EE:00', 857 | lifetime: 86400, 858 | version: '1.0.0', 859 | objList: { x: [0, 1] }, 860 | observedList: [], 861 | heartbeatEnabled: true, 862 | so: { 863 | x: sObj 864 | } 865 | }; 866 | 867 | node.shepherd._storage.save(node).then(function (data) { 868 | expect(data).to.deep.equal(dumper); 869 | done(); 870 | }).done(); 871 | }); 872 | 873 | it('should update node attrs, and return diff', function (done) { 874 | var attrs = { lifetime: 60000, version: '1.0.1' }, oldClientName = node.clientName; 875 | node._updateAttrs(attrs).then(function (diff) { 876 | expect(diff).to.deep.equal(attrs); 877 | expect(node.lifetime).to.equal(attrs.lifetime); 878 | expect(node.version).to.equal(attrs.version); 879 | expect(node.clientName).to.equal(oldClientName); 880 | done(); 881 | }).done(); 882 | }); 883 | }); 884 | 885 | describe('#._updateSoAndDb()', function () { 886 | it('should update Object and db, and return diff', function (done) { 887 | var data = { 888 | 1: { 889 | x0: 33, 890 | x1: 333 891 | } 892 | }, 893 | expected = { 894 | x: { 895 | 0: { x0: 10, x1: 20 }, 896 | 1: { x0: 33, x1: 333 } 897 | } 898 | }; 899 | node._updateSoAndDb('/x', data).then(function (diff) { 900 | expect(diff).to.deep.equal({ x: data }); 901 | var loaded = new CoapNode(node.shepherd, { clientName: node.clientName }); 902 | node.shepherd._storage.load(loaded).then(function () { 903 | expect(loaded.dump().so).to.deep.equal(expected); 904 | done(); 905 | }).done(); 906 | }).done(); 907 | }); 908 | 909 | it('should update Object Instance and db, and return diff', function (done) { 910 | var data = { 911 | x0: 109, 912 | x1: 209 913 | }, 914 | expected = { 915 | x: { 916 | 0: { x0: 10, x1: 20 }, 917 | 1: { x0: 109, x1: 209 } 918 | } 919 | }; 920 | node._updateSoAndDb('/x/1', data).then(function (diff) { 921 | expect(diff).to.deep.equal({ x: { 1: data } }); 922 | var loaded = new CoapNode(node.shepherd, { clientName: node.clientName }); 923 | node.shepherd._storage.load(loaded).then(function () { 924 | expect(loaded.dump().so).to.deep.equal(expected); 925 | done(); 926 | }).done(); 927 | }).done(); 928 | }); 929 | 930 | it('should update Resource and db, and return diff', function (done) { 931 | var data = 199, 932 | expected = { 933 | x: { 934 | 0: { x0: 10, x1: 20 }, 935 | 1: { x0: 199, x1: 209 } 936 | } 937 | }; 938 | node._updateSoAndDb('/x/1/x0', data).then(function (diff) { 939 | expect(diff).to.deep.equal({ x: { 1: { x0: data } } }); 940 | var loaded = new CoapNode(node.shepherd, { clientName: node.clientName }); 941 | node.shepherd._storage.load(loaded).then(function () { 942 | expect(loaded.dump().so).to.deep.equal(expected); 943 | done(); 944 | }).done(); 945 | }).done(); 946 | }); 947 | }); 948 | }); 949 | }); 950 | -------------------------------------------------------------------------------- /test/coap-shepherd.test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | Q = require('q'), 4 | _ = require('busyman'), 5 | chai = require('chai'), 6 | sinon = require('sinon'), 7 | sinonChai = require('sinon-chai'), 8 | expect = chai.expect, 9 | coap = require('coap'); 10 | 11 | chai.use(sinonChai); 12 | 13 | var CoapNode = require('../lib/components/coap-node'), 14 | StorageInterface = require('../lib/components/storage-interface'), 15 | NedbStorage = require('../lib/components/nedb-storage'), 16 | shepherd = require('../lib/coap-shepherd'), 17 | CoapShepherd = shepherd.constructor, 18 | init = require('../lib/init'), 19 | fixture = require('./fixture'), 20 | _verifySignatureSync = fixture._verifySignatureSync, 21 | _verifySignatureAsync = fixture._verifySignatureAsync, 22 | _fireSetTimeoutCallbackEarlier = fixture._fireSetTimeoutCallbackEarlier; 23 | 24 | var interface6 = { 25 | ip_address: '::1', 26 | gateway_ip: '::c0a8:0101', 27 | mac_address: '00:00:00:00:00:00' 28 | }, 29 | interface4 = { 30 | ip_address: '127.0.0.1', 31 | gateway_ip: '192.168.1.1', 32 | mac_address: '00:00:00:00:00:00' 33 | }; 34 | 35 | describe('coap-shepherd', function () { 36 | before(function (done) { 37 | fs.unlink(path.resolve('./lib/database/coap.db'), function (err) { 38 | expect(err).to.equal(null); 39 | done(); 40 | }); 41 | }); 42 | 43 | describe('Constructor Check', function () { 44 | it('coapShepherd', function () { 45 | expect(shepherd.clientIdCount).to.be.eql(1); 46 | expect(shepherd._registry).to.be.eql({}); 47 | expect(shepherd._enabled).to.be.false; 48 | expect(shepherd._server).to.be.eql(null); 49 | expect(shepherd._hbChecker).to.be.eql(null); 50 | expect(shepherd._storage).to.be.instanceOf(NedbStorage); 51 | }); 52 | }); 53 | 54 | describe('Signature Check', function () { 55 | describe('#.constructor()', function () { 56 | it('should throw TypeError if config is given but not an object', function () { 57 | _verifySignatureSync(function (arg) { return new CoapShepherd(arg); }, ['undefined', 'object']); 58 | }); 59 | 60 | it('should throw TypeError if config.storage is given but not an instance of StorageInterface', function () { 61 | _verifySignatureSync(function (arg) { 62 | var options = { storage: arg }; 63 | return new CoapShepherd(options); 64 | }, ['undefined', 'null', new StorageInterface()]); 65 | }); 66 | }); 67 | 68 | describe('#.find()', function () { 69 | it('should throw TypeError if clientName is not a string', function () { 70 | _verifySignatureSync(function (arg) { shepherd.find(arg); }, ['string']); 71 | }); 72 | }); 73 | 74 | describe('#.findByMacAddr()', function () { 75 | it('should throw TypeError if macAddr is not a string', function () { 76 | _verifySignatureSync(function (arg) { shepherd.findByMacAddr(arg); }, ['string']); 77 | }); 78 | }); 79 | 80 | describe('#._findByClientId()', function () { 81 | it('should throw TypeError if clientId is not a string or a number', function () { 82 | _verifySignatureSync(function (arg) { shepherd._findByClientId(arg); }, ['string', 'number']); 83 | }); 84 | }); 85 | 86 | describe('#._findByLocationPath()', function () { 87 | it('should throw TypeError if clientId is not a string', function () { 88 | _verifySignatureSync(function (arg) { shepherd._findByLocationPath(arg); }, ['string']); 89 | }); 90 | }); 91 | 92 | describe('#.permitJoin()', function () { 93 | it('should throw TypeError if time is not a number', function () { 94 | _verifySignatureSync(function (arg) { shepherd.permitJoin(arg); }, ['undefined', 'number']); 95 | }); 96 | }); 97 | 98 | describe('#.alwaysPermitJoin()', function () { 99 | it('should throw TypeError if permit is not a boolean', function () { 100 | _verifySignatureSync(function (arg) { shepherd.alwaysPermitJoin(arg); }, ['boolean']); 101 | }); 102 | }); 103 | 104 | describe('#.request()', function () { 105 | it('should throw TypeError if reqObj is not an object', function () { 106 | _verifySignatureSync(function (arg) { shepherd.request(arg); }, ['object']); 107 | }); 108 | }); 109 | 110 | describe('#.announce()', function () { 111 | it('should throw TypeError if msg is not a string', function () { 112 | _verifySignatureSync(function (arg) { shepherd.announce(arg); }, ['string']); 113 | }); 114 | }); 115 | 116 | describe('#.remove()', function () { 117 | it('should throw TypeError if clientName is not a string', function () { 118 | _verifySignatureSync(function (arg) { shepherd.remove(arg); }, ['string']); 119 | }); 120 | }); 121 | 122 | describe('#.acceptDevIncoming()', function () { 123 | it('should throw TypeError if predicate is not a function', function () { 124 | var savedPredicate = shepherd._acceptDevIncoming; 125 | _verifySignatureSync(function (arg) { shepherd.acceptDevIncoming(arg); }, ['function']); 126 | shepherd._acceptDevIncoming = savedPredicate; 127 | }); 128 | }); 129 | 130 | describe('#._newClientId()', function () { 131 | it('should throw TypeError if id is not a number', function () { 132 | _verifySignatureSync(function (arg) { shepherd._newClientId(arg); }, ['undefined', 'number']); 133 | }); 134 | }); 135 | }); 136 | 137 | describe('Functional Check', function () { 138 | var _updateNetInfoStub, testDbPath = __dirname + '/../lib/database/test.db'; 139 | 140 | before(function () { 141 | _updateNetInfoStub = sinon.stub(init, '_updateNetInfo', function (shepherd, callback) { 142 | var deferred = Q.defer(); 143 | 144 | setTimeout(function () { 145 | var intf = (shepherd._config.connectionType === 'udp6') ? interface6 : interface4; 146 | shepherd._net.intf = intf.name; 147 | shepherd._net.ip = intf.ip_address; 148 | shepherd._net.mac = intf.mac_address; 149 | shepherd._net.routerIp = intf.gateway_ip; 150 | deferred.resolve(_.cloneDeep(shepherd._net)); 151 | }, 10); 152 | 153 | return deferred.promise.nodeify(callback); 154 | }); 155 | 156 | }); 157 | 158 | after(function (done) { 159 | _updateNetInfoStub.restore(); 160 | fs.unlink(testDbPath, function (err) { 161 | expect(err).to.equal(null); 162 | done(); 163 | }); 164 | }); 165 | 166 | describe('#.constructor()', function () { 167 | it('should create an instance when passing no arguments', function () { 168 | var created = new CoapShepherd(); 169 | expect(created).to.be.not.null; 170 | expect(created).to.be.instanceOf(CoapShepherd); 171 | expect(created._storage).to.be.instanceOf(NedbStorage); 172 | expect(created._config).to.be.an('object'); 173 | expect(created._config.connectionType).to.be.eql('udp4'); 174 | expect(created._config.ip).to.be.eql('127.0.0.1'); 175 | expect(created._config.port).to.be.eql(5683); 176 | expect(created._config.reqTimeout).to.be.eql(60); 177 | expect(created._config.hbTimeout).to.be.eql(60); 178 | expect(created._config.defaultDbPath).to.be.a('string'); 179 | expect(created._config.defaultDbPath.split('/').pop()).to.be.eql('coap.db'); 180 | }); 181 | 182 | it('should create an instance when passing config argument', function () { 183 | var myStorage = new StorageInterface(); 184 | myStorage._myFlag = 'customized'; 185 | var created = new CoapShepherd({ 186 | connectionType: 'udp6', 187 | ip: '::2', 188 | port: 1234, 189 | hbTimeout: 45, 190 | storage: myStorage, 191 | defaultDbPath: testDbPath 192 | }); 193 | expect(created).to.be.not.null; 194 | expect(created).to.be.instanceOf(CoapShepherd); 195 | expect(created._storage).to.equal(myStorage); 196 | expect(created._storage._myFlag).to.equal('customized'); 197 | expect(created._config).to.be.an('object'); 198 | expect(created._config.connectionType).to.be.eql('udp6'); 199 | expect(created._config.ip).to.be.eql('::2'); 200 | expect(created._config.port).to.be.eql(1234); 201 | expect(created._config.reqTimeout).to.be.eql(60); 202 | expect(created._config.hbTimeout).to.be.eql(45); 203 | expect(created._config.defaultDbPath).to.be.eql(testDbPath); 204 | }); 205 | }); 206 | 207 | describe('#.start()', function () { 208 | before(function () { 209 | return Q.all([1, 2, 3] 210 | .map(function (index) { return new CoapNode(shepherd, { clientName: 'myCoapNode' + index }); }) 211 | .map(function (cnode) { return shepherd._storage.save(cnode); }) 212 | ); 213 | }); 214 | 215 | it('should start shepherd', function () { 216 | return shepherd.start().then(function () { 217 | expect(Object.keys(shepherd._registry)).to.have.lengthOf(3); 218 | expect(shepherd._registry).to.have.property('myCoapNode1'); 219 | expect(shepherd._registry['myCoapNode1']).to.be.instanceOf(CoapNode); 220 | expect(shepherd._registry['myCoapNode1'].clientName).to.equal('myCoapNode1'); 221 | expect(shepherd._registry).to.have.property('myCoapNode2'); 222 | expect(shepherd._registry['myCoapNode2']).to.be.instanceOf(CoapNode); 223 | expect(shepherd._registry['myCoapNode2'].clientName).to.equal('myCoapNode2'); 224 | expect(shepherd._registry).to.have.property('myCoapNode3'); 225 | expect(shepherd._registry['myCoapNode3']).to.be.instanceOf(CoapNode); 226 | expect(shepherd._registry['myCoapNode3'].clientName).to.equal('myCoapNode3'); 227 | expect(shepherd._enabled).to.equal(true); 228 | }); 229 | }); 230 | 231 | after(function () { 232 | shepherd._registry = {}; 233 | return shepherd._storage.reset(); 234 | }); 235 | }); 236 | 237 | describe('#.permitJoin()', function () { 238 | it('should open permitJoin when time > 0', function () { 239 | shepherd.permitJoin(180); 240 | expect(shepherd._joinable).to.be.eql(true); 241 | }); 242 | 243 | it('should close permitJoin when time == 0', function () { 244 | shepherd.permitJoin(0); 245 | expect(shepherd._joinable).to.be.eql(false); 246 | }); 247 | 248 | it('should open permitJoin when time > 0 after alwaysPermitJoin(false)', function () { 249 | shepherd.alwaysPermitJoin(false); 250 | shepherd.permitJoin(180); 251 | expect(shepherd._joinable).to.be.eql(true); 252 | }); 253 | 254 | it('should close permitJoin when time == 0 after alwaysPermitJoin(true)', function () { 255 | shepherd.alwaysPermitJoin(true); 256 | shepherd.permitJoin(0); 257 | expect(shepherd._joinable).to.be.eql(false); 258 | }); 259 | }); 260 | 261 | describe('#.alwaysPermitJoin()', function () { 262 | it('should open permitJoin when permit is true', function () { 263 | var result = shepherd.alwaysPermitJoin(true); 264 | expect(result).to.be.eql(true); 265 | expect(shepherd._joinable).to.be.eql(true); 266 | }); 267 | 268 | it('should close permitJoin when permit is false', function () { 269 | shepherd.alwaysPermitJoin(false); 270 | expect(shepherd._joinable).to.be.eql(false); 271 | }); 272 | 273 | it('should clear _permitJoinTimer when permit is true', function () { 274 | shepherd.permitJoin(180); 275 | var result = shepherd.alwaysPermitJoin(true); 276 | expect(result).to.be.eql(true); 277 | expect(shepherd._joinable).to.be.eql(true); 278 | expect(shepherd._permitJoinTimer).to.be.eql(null); 279 | }); 280 | 281 | it('should clear _permitJoinTimer when permit is false', function () { 282 | shepherd.permitJoin(180); 283 | var result = shepherd.alwaysPermitJoin(false); 284 | expect(result).to.be.eql(true); 285 | expect(shepherd._joinable).to.be.eql(false); 286 | expect(shepherd._permitJoinTimer).to.be.eql(null); 287 | }); 288 | 289 | it('should not open permitJoin when server is not enabled', function () { 290 | shepherd._joinable = false; 291 | shepherd._enabled = false; 292 | var result = shepherd.alwaysPermitJoin(true); 293 | expect(result).to.be.eql(false); 294 | expect(shepherd._joinable).to.be.eql(false); 295 | }); 296 | 297 | after(function () { 298 | shepherd._enabled = true; 299 | shepherd.alwaysPermitJoin(true); 300 | }); 301 | }); 302 | 303 | describe('#register new cnode', function () { 304 | before(function () { 305 | shepherd.clientIdCount = 1; 306 | }); 307 | 308 | it('should not crash if "ep" not passed in', function () { 309 | var rsp = {}, 310 | req = { 311 | code: '0.01', 312 | method: 'POST', 313 | url: '/rd?lt=86400&lwm2m=1.0.0&mac=AA:AA:AA', 314 | rsinfo: { 315 | address: '127.0.0.1', 316 | port: '5686' 317 | }, 318 | payload: ',,,', 319 | headers: {} 320 | }, 321 | oldSetImmediate = global.setImmediate, 322 | reqHandler; 323 | rsp.setOption = sinon.spy(); 324 | rsp.end = sinon.spy(); 325 | global.setImmediate = sinon.spy(); 326 | emitClintReqMessage(shepherd, req, rsp); 327 | expect(global.setImmediate).to.have.been.called; 328 | reqHandler = global.setImmediate.args[0][0]; 329 | global.setImmediate = oldSetImmediate; 330 | 331 | expect(reqHandler).not.to.throw(); 332 | 333 | expect(rsp.setOption).not.to.have.been.called; 334 | expect(rsp.end).to.have.been.calledWith(''); 335 | expect(rsp.code).to.eql('4.00'); 336 | expect(shepherd.find('')).to.be.falsy; 337 | }); 338 | 339 | it('should register new cnode', function (done) { 340 | var _readAllResourceStub = sinon.stub(CoapNode.prototype, '_readAllResource', function (path, callback) { 341 | return Q.resolve({ 342 | status: '2.05', 343 | data: { x: {'0': { 'x0': 10, 'x1': 20 }, '1':{ 'x0': 11, 'x1': 21 }}, 344 | y: {'0': { 'y0': 20, 'y1': 40 }, '1':{ 'y0': 22, 'y1': 44 }}} 345 | }); 346 | }), 347 | observeReqStub = sinon.stub(CoapNode.prototype, 'observeReq', function (callback) { 348 | return Q.resolve({ 349 | status: '2.05', 350 | data: 'hb' 351 | }); 352 | }), 353 | rsp = {}, 354 | cnode, 355 | regCallback = function (msg) { 356 | if (msg.type === 'devIncoming') { 357 | cnode = msg.cnode; 358 | _readAllResourceStub.restore(); 359 | observeReqStub.restore(); 360 | expect(rsp.setOption).to.have.been.calledWith('Location-Path', [new Buffer('rd'),new Buffer(cnode.clientId.toString())]); 361 | expect(rsp.end).to.have.been.calledWith(''); 362 | if (shepherd.find('cnode01') === cnode) { 363 | shepherd.removeListener('ind', regCallback); 364 | done(); 365 | } 366 | } 367 | }; 368 | 369 | rsp.setOption = sinon.spy(); 370 | rsp.end = sinon.spy(); 371 | _fireSetTimeoutCallbackEarlier(2); 372 | 373 | shepherd.on('ind', regCallback); 374 | 375 | emitClintReqMessage(shepherd, { 376 | code: '0.01', 377 | method: 'POST', 378 | url: '/rd?ep=cnode01<=86400&lwm2m=1.0.0&mac=AA:AA:AA', 379 | rsinfo: { 380 | address: '127.0.0.1', 381 | port: '5686' 382 | }, 383 | payload: ',,,', 384 | headers: {} 385 | }, rsp); 386 | }); 387 | 388 | it('should register 2nd new cnode', function (done) { 389 | var _readAllResourceStub = sinon.stub(CoapNode.prototype, '_readAllResource', function (path, callback) { 390 | return Q.resolve({ 391 | status: '2.05', 392 | data: { a: {'0': { 'a0': 10, 'a1': 20 }, '1':{ 'a0': 11, 'a1': 21 }}, 393 | b: {'0': { 'b0': 20, 'b1': 40 }, '1':{ 'b0': 22, 'b1': 44 }}} 394 | }); 395 | }), 396 | observeReqStub = sinon.stub(CoapNode.prototype, 'observeReq', function (callback) { 397 | return Q.resolve({ 398 | status: '2.05', 399 | data: 'hb' 400 | }); 401 | }), 402 | rsp = {}, 403 | cnode, 404 | regCallback = function (msg) { 405 | if (msg.type === 'devIncoming') { 406 | cnode = msg.cnode; 407 | expect(rsp.setOption).to.have.been.calledWith('Location-Path', [new Buffer('rd'),new Buffer(cnode.clientId.toString())]); 408 | expect(rsp.end).to.have.been.calledWith(''); 409 | if (shepherd.find('cnode02') === cnode) { 410 | _readAllResourceStub.restore(); 411 | observeReqStub.restore(); 412 | shepherd.removeListener('ind', regCallback); 413 | done(); 414 | } 415 | } 416 | }; 417 | 418 | rsp.setOption = sinon.spy(); 419 | rsp.end = sinon.spy(); 420 | _fireSetTimeoutCallbackEarlier(2); 421 | 422 | shepherd.on('ind', regCallback); 423 | 424 | emitClintReqMessage(shepherd, { 425 | code: '0.01', 426 | method: 'POST', 427 | url: '/rd?ep=cnode02<=86400&lwm2m=1.0.0&mac=BB:BB:BB', 428 | rsinfo: { 429 | address: '127.0.0.1', 430 | port: '5687' 431 | }, 432 | payload: ',,,', 433 | headers: {} 434 | }, rsp); 435 | }); 436 | }); 437 | 438 | describe('#config.clientNameParser', function () { 439 | before(function () { 440 | shepherd._config.clientNameParser = function (clientName) { 441 | return clientName.split(':')[1]; 442 | } 443 | }); 444 | 445 | after(function (done) { 446 | shepherd._config.clientNameParser = function (clientName) { 447 | return clientName; 448 | }; 449 | shepherd.remove('cnode0X', done); 450 | }); 451 | 452 | it('should keep the last part of clientName', function (done) { 453 | var _readAllResourceStub = sinon.stub(CoapNode.prototype, '_readAllResource', function (path, callback) { 454 | return Q.resolve({ 455 | status: '2.05', 456 | data: { a: {'0': { 'a0': 10, 'a1': 20 }, '1':{ 'a0': 11, 'a1': 21 }}, 457 | b: {'0': { 'b0': 20, 'b1': 40 }, '1':{ 'b0': 22, 'b1': 44 }}} 458 | }); 459 | }), 460 | observeReqStub = sinon.stub(CoapNode.prototype, 'observeReq', function (callback) { 461 | return Q.resolve({ 462 | status: '2.05', 463 | data: 'hb' 464 | }); 465 | }), 466 | rsp = {}, 467 | cnode, 468 | regCallback = function (msg) { 469 | if (msg.type === 'devIncoming') { 470 | _readAllResourceStub.restore(); 471 | observeReqStub.restore(); 472 | shepherd.removeListener('ind', regCallback); 473 | cnode = msg.cnode; 474 | expect(cnode.clientName).to.eql('cnode0X'); 475 | expect(rsp.setOption).to.have.been.calledWith('Location-Path', [new Buffer('rd'),new Buffer(cnode.clientId.toString())]); 476 | expect(rsp.end).to.have.been.calledWith(''); 477 | expect(shepherd.find('cnode0X')).to.be.truthy; 478 | done(); 479 | } 480 | }; 481 | 482 | rsp.setOption = sinon.spy(); 483 | rsp.end = sinon.spy(); 484 | _fireSetTimeoutCallbackEarlier(2); 485 | 486 | shepherd.on('ind', regCallback); 487 | 488 | emitClintReqMessage(shepherd, { 489 | code: '0.01', 490 | method: 'POST', 491 | url: '/rd?ep=urn:cnode0X<=86400&lwm2m=1.0.0&mac=FF:FF:FF', 492 | rsinfo: { 493 | address: '127.0.0.1', 494 | port: '5687' 495 | }, 496 | payload: ',,,', 497 | headers: {} 498 | }, rsp); 499 | }); 500 | }); 501 | 502 | describe('#config.alwaysFireDevIncoming', function () { 503 | before(function () { 504 | shepherd._config.alwaysFireDevIncoming = true; 505 | }); 506 | 507 | after(function () { 508 | shepherd._config.alwaysFireDevIncoming = false; 509 | }); 510 | 511 | it('should fire devIncoming', function (done) { 512 | var _readAllResourceStub = sinon.stub(CoapNode.prototype, '_readAllResource', function (path, callback) { 513 | return Q.resolve({ 514 | status: '2.05', 515 | data: { a: {'0': { 'a0': 10, 'a1': 20 }, '1':{ 'a0': 11, 'a1': 21 }}, 516 | b: {'0': { 'b0': 20, 'b1': 40 }, '1':{ 'b0': 22, 'b1': 44 }}} 517 | }); 518 | }), 519 | observeReqStub = sinon.stub(CoapNode.prototype, 'observeReq', function (callback) { 520 | return Q.resolve({ 521 | status: '2.05', 522 | data: 'hb' 523 | }); 524 | }), 525 | rsp = {}, 526 | cnode, 527 | regCallback = function (msg) { 528 | _readAllResourceStub.restore(); 529 | observeReqStub.restore(); 530 | shepherd.removeListener('ind', regCallback); 531 | expect(msg.type).to.eql('devIncoming'); 532 | cnode = msg.cnode; 533 | expect(rsp.setOption).to.have.been.calledWith('Location-Path', [new Buffer('rd'),new Buffer(cnode.clientId.toString())]); 534 | expect(rsp.end).to.have.been.calledWith(''); 535 | expect(shepherd.find('cnode02')).to.eql(cnode); 536 | done(); 537 | }; 538 | expect(shepherd.find('cnode02')).to.be.truthy; 539 | rsp.setOption = sinon.spy(); 540 | rsp.end = sinon.spy(); 541 | _fireSetTimeoutCallbackEarlier(2); 542 | 543 | shepherd.on('ind', regCallback); 544 | 545 | emitClintReqMessage(shepherd, { 546 | code: '0.01', 547 | method: 'POST', 548 | url: '/rd?ep=cnode02<=86400&lwm2m=1.0.0&mac=BB:BB:BB', 549 | rsinfo: { 550 | address: '127.0.0.1', 551 | port: '5687' 552 | }, 553 | payload: ',,,', 554 | headers: {} 555 | }, rsp); 556 | }); 557 | }); 558 | 559 | describe('#update cnode', function () { 560 | it('should update cnode lifetime', function (done) { 561 | var rsp = {}, 562 | cnode, 563 | upCallback = function (msg) { 564 | if (msg.type === 'devUpdate') { 565 | diff = msg.data; 566 | expect(rsp.end).to.have.been.calledWith(''); 567 | if (diff.lifetime == 87654) { 568 | shepherd.removeListener('ind', upCallback); 569 | done(); 570 | } 571 | } 572 | }; 573 | 574 | rsp.end = sinon.spy(); 575 | 576 | shepherd.on('ind', upCallback); 577 | 578 | emitClintReqMessage(shepherd, { 579 | code: '0.02', 580 | method: 'POST', 581 | url: '/rd/1?lt=87654', 582 | rsinfo: { 583 | address: '127.0.0.1', 584 | port: '5688' 585 | }, 586 | payload: '', 587 | headers: {} 588 | }, rsp); 589 | }); 590 | }); 591 | 592 | describe('#config.autoReadResources', function () { 593 | var shepherd; 594 | 595 | before(function (done) { 596 | shepherd = new CoapShepherd({port: 5684, defaultDbPath: testDbPath, autoReadResources: false}); 597 | shepherd.start().then(function () { 598 | shepherd.alwaysPermitJoin(true); 599 | done(); 600 | }); 601 | }); 602 | 603 | it('should not call cnode._readAllResource when autoReadResources is false for register', function (done) { 604 | var _readAllResourceStub = sinon.stub(CoapNode.prototype, '_readAllResource', function (path, callback) { 605 | return null; 606 | }), 607 | observeReqStub = sinon.stub(CoapNode.prototype, 'observeReq', function (callback) { 608 | return Q.resolve({ 609 | status: '2.05', 610 | data: 'hb' 611 | }); 612 | }), 613 | rsp = {}, 614 | cnode, 615 | regCallback = function (msg) { 616 | if (msg.type === 'devIncoming') { 617 | cnode = msg.cnode; 618 | expect(rsp.setOption).to.have.been.calledWith('Location-Path', [new Buffer('rd'),new Buffer(cnode.clientId.toString())]); 619 | expect(rsp.end).to.have.been.calledWith(''); 620 | expect(_readAllResourceStub).to.have.not.been.called; 621 | if (shepherd.find('cnode02') === cnode) { 622 | _readAllResourceStub.restore(); 623 | observeReqStub.restore(); 624 | shepherd.removeListener('ind', regCallback); 625 | done(); 626 | } 627 | } 628 | }; 629 | 630 | rsp.setOption = sinon.spy(); 631 | rsp.end = sinon.spy(); 632 | _fireSetTimeoutCallbackEarlier(2); 633 | 634 | shepherd.on('ind', regCallback); 635 | 636 | emitClintReqMessage(shepherd, { 637 | code: '0.01', 638 | method: 'POST', 639 | url: '/rd?ep=cnode02<=86400&lwm2m=1.0.0&mac=BB:BB:BB', 640 | rsinfo: { 641 | address: '127.0.0.1', 642 | port: '5687' 643 | }, 644 | payload: ',,,', 645 | headers: {} 646 | }, rsp); 647 | }); 648 | 649 | it('should not call cnode._readAllResource when autoReadResources is false for update', function (done) { 650 | var _readAllResourceStub = sinon.stub(CoapNode.prototype, '_readAllResource', function (path, callback) { 651 | return null; 652 | }), 653 | _updateAttrsStub = sinon.stub(CoapNode.prototype, '_updateAttrs', function () { 654 | return Q.fcall(function () { 655 | return { lifetime: 87654, objList: {} }; 656 | }); 657 | }), 658 | rsp = {}, 659 | cnode, 660 | upCallback = function (msg) { 661 | if (msg.type === 'devUpdate') { 662 | diff = msg.data; 663 | expect(rsp.end).to.have.been.calledWith(''); 664 | expect(_readAllResourceStub).to.have.not.been.called; 665 | if (diff.lifetime == 87654) { 666 | _readAllResourceStub.restore(); 667 | _updateAttrsStub.restore(); 668 | shepherd.removeListener('ind', upCallback); 669 | done(); 670 | } 671 | } 672 | }; 673 | 674 | rsp.end = sinon.spy(); 675 | 676 | shepherd.on('ind', upCallback); 677 | 678 | emitClintReqMessage(shepherd, { 679 | code: '0.02', 680 | method: 'POST', 681 | url: '/rd/1?lt=87654', 682 | rsinfo: { 683 | address: '127.0.0.1', 684 | port: '5688' 685 | }, 686 | payload: '', 687 | headers: {} 688 | }, rsp); 689 | }); 690 | }); 691 | 692 | describe('#deregister cnode', function () { 693 | it('should deregister 2nd cnode ', function (done) { 694 | var rsp = {}, 695 | cnode, 696 | deCallback = function (msg) { 697 | if (msg.type === 'devLeaving') { 698 | clientName = msg.cnode; 699 | expect(rsp.end).to.have.been.calledWith(''); 700 | if (clientName === 'cnode02' && !shepherd.find('cnode02')) { 701 | shepherd.removeListener('ind', deCallback); 702 | done(); 703 | } 704 | } 705 | }; 706 | 707 | rsp.end = sinon.spy(); 708 | 709 | shepherd.on('ind', deCallback); 710 | 711 | emitClintReqMessage(shepherd, { 712 | code: '0.03', 713 | method: 'DELETE', 714 | url: '/rd/2', 715 | rsinfo: { 716 | address: '127.0.0.1', 717 | port: '5687' 718 | }, 719 | payload: '', 720 | headers: {} 721 | }, rsp); 722 | }); 723 | }); 724 | 725 | describe('#checkOut cnode', function () { 726 | it('should check out and cnode status changed to sleep', function (done) { 727 | var rsp = {}, 728 | cnode, 729 | outCallback = function (msg) { 730 | if (msg.type === 'devStatus' || msg.data === 'sleep') { 731 | clientName = msg.cnode.clientName; 732 | expect(rsp.end).to.have.been.calledWith(''); 733 | if (clientName === 'cnode01') { 734 | expect(shepherd.find('cnode01').status).to.be.eql('sleep'); 735 | shepherd.removeListener('ind', outCallback); 736 | done(); 737 | } 738 | } 739 | }; 740 | 741 | rsp.end = sinon.spy(); 742 | _fireSetTimeoutCallbackEarlier(); 743 | 744 | shepherd.on('ind', outCallback); 745 | 746 | emitClintReqMessage(shepherd, { 747 | code: '0.04', 748 | method: 'PUT', 749 | url: '/rd/1?chk=out', 750 | rsinfo: { 751 | address: '127.0.0.1', 752 | port: '5688' 753 | }, 754 | payload: '', 755 | headers: {} 756 | }, rsp); 757 | }); 758 | 759 | it('should return error when device is sleeping', function (done) { 760 | var cnode = shepherd.find('cnode01'); 761 | 762 | cnode.readReq('/x/0/x0', function (err) { 763 | if (err) done(); 764 | }); 765 | }); 766 | 767 | it ('should check out and cnode status changed to sleep with duration', function (done) { 768 | var rsp = {}, 769 | cnode, 770 | outCallback = function (msg) { 771 | if (msg.type === 'devStatus' || msg.data === 'offline') { 772 | clientName = msg.cnode.clientName; 773 | expect(rsp.end).to.have.been.calledWith(''); 774 | if (clientName === 'cnode01') { 775 | expect(shepherd.find('cnode01').status).to.be.eql('offline'); 776 | shepherd.removeListener('ind', outCallback); 777 | done(); 778 | } 779 | } 780 | }; 781 | 782 | rsp.end = sinon.spy(); 783 | 784 | shepherd.on('ind', outCallback); 785 | 786 | emitClintReqMessage(shepherd, { 787 | code: '0.04', 788 | method: 'PUT', 789 | url: '/rd/1?chk=out&t=1', 790 | rsinfo: { 791 | address: '127.0.0.1', 792 | port: '5688' 793 | }, 794 | payload: '', 795 | headers: {} 796 | }, rsp); 797 | }); 798 | }); 799 | 800 | describe('#checkIn cnode', function () { 801 | it('should check out and cnode status changed to online', function (done) { 802 | var observeReqStub = sinon.stub(CoapNode.prototype, 'observeReq', function (callback) { 803 | return Q.resolve({ 804 | status: '2.05', 805 | data: 'hb' 806 | }); 807 | }), 808 | delayStub = sinon.stub(_, 'delay', function (cb, time) { 809 | setImmediate(cb); 810 | }), 811 | rsp = {}, 812 | cnode, 813 | inCallback = function (msg) { 814 | if (msg.type === 'devStatus' || msg.data === 'online') { 815 | clientName = msg.cnode.clientName; 816 | expect(rsp.end).to.have.been.calledWith(''); 817 | if (clientName === 'cnode01') { 818 | observeReqStub.restore(); 819 | delayStub.restore(); 820 | expect(shepherd.find('cnode01').status).to.be.eql('online'); 821 | expect(shepherd.find('cnode01').port).to.be.eql('5690'); 822 | shepherd.removeListener('ind', inCallback); 823 | done(); 824 | } 825 | } 826 | }; 827 | 828 | rsp.end = sinon.spy(); 829 | 830 | shepherd.on('ind', inCallback); 831 | 832 | emitClintReqMessage(shepherd, { 833 | code: '0.04', 834 | method: 'PUT', 835 | url: '/rd/1?chk=in', 836 | rsinfo: { 837 | address: '127.0.0.1', 838 | port: '5690' 839 | }, 840 | payload: '', 841 | headers: {} 842 | }, rsp); 843 | }); 844 | }); 845 | 846 | describe('#.lookup', function () { 847 | it('should not crash if "ep" not passed in', function () { 848 | var rsp = {}, 849 | req = { 850 | code: '0.01', 851 | method: 'GET', 852 | url: '/lookup?lt=86400&lwm2m=1.0.0&mac=AA:AA:AA', 853 | rsinfo: { 854 | address: '127.0.0.1', 855 | port: '5686' 856 | }, 857 | payload: ',,,', 858 | headers: {} 859 | }, 860 | oldSetImmediate = global.setImmediate, 861 | reqHandler; 862 | rsp.setOption = sinon.spy(); 863 | rsp.end = sinon.spy(); 864 | global.setImmediate = sinon.spy(); 865 | emitClintReqMessage(shepherd, req, rsp); 866 | expect(global.setImmediate).to.have.been.called; 867 | reqHandler = global.setImmediate.args[0][0]; 868 | global.setImmediate = oldSetImmediate; 869 | 870 | expect(reqHandler).not.to.throw(); 871 | 872 | expect(rsp.setOption).not.to.have.been.called; 873 | expect(rsp.end).to.have.been.calledWith(''); 874 | expect(rsp.code).to.eql('4.00'); 875 | }); 876 | }); 877 | 878 | describe('#.find()', function () { 879 | it('should find cnode01 by clientName and return cnode01', function () { 880 | var cnode01 = shepherd.find('cnode01'); 881 | expect(cnode01.clientName).to.be.eql('cnode01'); 882 | }); 883 | 884 | it('should not find cnode02 and return undefined', function () { 885 | var cnode02 = shepherd.find('cnode02'); 886 | expect(cnode02).to.be.eql(undefined); 887 | }); 888 | }); 889 | 890 | describe('#.findByMacAddr()', function () { 891 | it('should find cnode01 by MacAddr and return cnode01', function () { 892 | var cnode01 = shepherd.findByMacAddr('AA:AA:AA')[0]; 893 | expect(cnode01.clientName).to.be.eql('cnode01'); 894 | }); 895 | 896 | it('should not find cnode02 and return undefined', function () { 897 | var cnode02 = shepherd.findByMacAddr('BB:BB:BB'); 898 | expect(cnode02).to.be.eql([]); 899 | }); 900 | }); 901 | 902 | describe('#._findByClientId()', function () { 903 | it('should find cnode01 by ClientId and return cnode01', function () { 904 | var cnode01 = shepherd._findByClientId(1); 905 | expect(cnode01.clientName).to.be.eql('cnode01'); 906 | }); 907 | 908 | it('should not find cnode02 and return undefined', function () { 909 | var cnode02 = shepherd._findByClientId(2); 910 | expect(cnode02).to.be.eql(undefined); 911 | }); 912 | }); 913 | 914 | describe('#._findByLocationPath()', function () { 915 | it('should find cnode01 by LocationPath and return cnode01', function () { 916 | var cnode01 = shepherd._findByLocationPath('/rd/1'); 917 | expect(cnode01.clientName).to.be.eql('cnode01'); 918 | }); 919 | 920 | it('should not find cnode02 and return undefined', function () { 921 | var cnode02 = shepherd._findByLocationPath('/rd/2'); 922 | expect(cnode02).to.be.eql(undefined); 923 | }); 924 | }); 925 | 926 | describe('#.list()', function () { 927 | it('should return devices list', function () { 928 | var list = shepherd.list(); 929 | expect(list[0].clientName).to.be.eql('cnode01'); 930 | expect(list[0].mac).to.be.eql('AA:AA:AA'); 931 | }); 932 | }); 933 | 934 | describe('#.request()', function () { 935 | it('should announce a message', function () { 936 | var server = coap.createServer(); 937 | 938 | server.on('request', function (req, rsp) { 939 | if (req.payload.method === 'PUT') 940 | done(); 941 | }); 942 | 943 | server.listen('5690'); 944 | shepherd.request({ 945 | hostname: '127.0.0.1', 946 | port: '5690', 947 | method: 'PUT' 948 | }); 949 | }); 950 | }); 951 | 952 | describe('#.announce()', function (done) { 953 | it('should announce a message', function () { 954 | var server = coap.createServer(); 955 | 956 | server.on('request', function (req, rsp) { 957 | if (req.payload.toString() === 'Hum') 958 | done(); 959 | }); 960 | 961 | server.listen('5688'); 962 | shepherd.announce('Hum'); 963 | }); 964 | }); 965 | 966 | describe('#.remove()', function () { 967 | it('should remove cnode01', function () { 968 | shepherd.remove('cnode01', function () { 969 | expect(shepherd.find('shepherd')).to.be.eql(undefined); 970 | }); 971 | }); 972 | }); 973 | 974 | describe('#.acceptDevIncoming()', function () { 975 | it('should implement acceptDevIncoming and get not allow rsp', function (done) { 976 | var rsp = {}; 977 | 978 | rsp.end = function (msg) { 979 | expect(rsp.code).to.be.eql('4.05'); 980 | expect(msg).to.be.eql(''); 981 | expect(shepherd.find('cnode03')).to.equal(undefined); 982 | done(); 983 | }; 984 | 985 | shepherd.acceptDevIncoming(function (devInfo, callback) { 986 | if (devInfo.clientName === 'cnode03') { 987 | callback(null, false); 988 | } else { 989 | callback(null, true); 990 | } 991 | }); 992 | 993 | emitClintReqMessage(shepherd, { 994 | code: '0.01', 995 | method: 'POST', 996 | url: '/rd?ep=cnode03<=86400&lwm2m=1.0.0&mac=BB:BB:BB', 997 | rsinfo: { 998 | address: '127.0.0.1', 999 | port: '5687' 1000 | }, 1001 | payload: ',,,', 1002 | headers: {} 1003 | }, rsp); 1004 | }); 1005 | 1006 | it('should implement acceptDevIncoming and create dev', function (done) { 1007 | var rsp = {}, extra = { businessKey: 'hello_world' }; 1008 | 1009 | rsp.setOption = sinon.spy(); 1010 | rsp.end = function (msg) { 1011 | expect(rsp.code).to.be.eql('2.01'); 1012 | var node = shepherd.find('cnode03'); 1013 | expect(node).to.be.instanceOf(CoapNode); 1014 | expect(node.clientName).to.equal('cnode03'); 1015 | expect(node._extra).to.equal(extra); 1016 | done(); 1017 | }; 1018 | 1019 | shepherd.acceptDevIncoming(function (devInfo, callback) { 1020 | callback(null, true, extra); 1021 | }); 1022 | 1023 | emitClintReqMessage(shepherd, { 1024 | code: '0.01', 1025 | method: 'POST', 1026 | url: '/rd?ep=cnode03<=86400&lwm2m=1.0.0&mac=BB:BB:BB', 1027 | rsinfo: { 1028 | address: '127.0.0.1', 1029 | port: '5687' 1030 | }, 1031 | payload: ',,,', 1032 | headers: {} 1033 | }, rsp); 1034 | }); 1035 | }); 1036 | 1037 | describe('#.stop()', function () { 1038 | it('should stop shepherd', function () { 1039 | return shepherd.stop().then(function () { 1040 | expect(shepherd._enabled).to.equal(false); 1041 | expect(shepherd._server).to.equal(null); 1042 | }); 1043 | }); 1044 | }); 1045 | 1046 | describe('#.reset()', function () { 1047 | it('should reset shepherd', function () { 1048 | var storageResetStub = sinon.stub(NedbStorage.prototype, 'reset', function () {}); 1049 | return shepherd.reset().then(function () { 1050 | storageResetStub.restore(); 1051 | expect(shepherd._enabled).to.equal(true); 1052 | expect(storageResetStub).not.have.been.called; 1053 | }); 1054 | }); 1055 | 1056 | it('should remove db and reset shepherd', function () { 1057 | var storageResetStub = sinon.stub(NedbStorage.prototype, 'reset', function () { return Q.fcall(function () {}); }); 1058 | return shepherd.reset(1).then(function () { 1059 | storageResetStub.restore(); 1060 | expect(shepherd._enabled).to.equal(true); 1061 | expect(storageResetStub).have.been.called; 1062 | }); 1063 | }); 1064 | }); 1065 | }); 1066 | }); 1067 | 1068 | function emitClintReqMessage(shepherd, req, rsp) { 1069 | shepherd._server.emit('request', req, rsp); 1070 | } 1071 | -------------------------------------------------------------------------------- /test/cutils.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | cutils = require('../lib/components/cutils.js'); 3 | 4 | describe('cutils', function () { 5 | describe('Signature Check', function () { 6 | 7 | it('#.getTime()', function () { 8 | expect(function () { cutils.getTime(); }).not.to.throw(); 9 | }); 10 | 11 | it('#.oidKey()', function () { 12 | expect(function () { cutils.oidKey({}); }).to.throw(); 13 | expect(function () { cutils.oidKey([]); }).to.throw(); 14 | expect(function () { cutils.oidKey(); }).to.throw(); 15 | 16 | expect(function () { cutils.oidKey('x'); }).not.to.throw(); 17 | expect(function () { cutils.oidKey(5); }).not.to.throw(); 18 | }); 19 | 20 | it('#.oidNumber()', function () { 21 | expect(function () { cutils.oidNumber({}); }).to.throw(); 22 | expect(function () { cutils.oidNumber([]); }).to.throw(); 23 | expect(function () { cutils.oidNumber(); }).to.throw(); 24 | 25 | expect(function () { cutils.oidNumber('x'); }).not.to.throw(); 26 | expect(function () { cutils.oidNumber(5); }).not.to.throw(); 27 | }); 28 | 29 | it('#.ridKey()', function () { 30 | expect(function () { cutils.ridNumber({}, 'x'); }).to.throw(); 31 | expect(function () { cutils.ridNumber([], 'x'); }).to.throw(); 32 | expect(function () { cutils.ridNumber('x', []); }).to.throw(); 33 | expect(function () { cutils.ridNumber('x', {}); }).to.throw(); 34 | expect(function () { cutils.ridNumber(); }).to.throw(); 35 | 36 | expect(function () { cutils.ridNumber('x', 'y'); }).not.to.throw(); 37 | expect(function () { cutils.ridNumber(5, 'y'); }).not.to.throw(); 38 | expect(function () { cutils.ridNumber('x', 5); }).not.to.throw(); 39 | expect(function () { cutils.ridNumber(1, 5); }).not.to.throw(); 40 | }); 41 | 42 | it('#.ridNumber()', function () { 43 | expect(function () { cutils.ridNumber({}, 'x'); }).to.throw(); 44 | expect(function () { cutils.ridNumber([], 'x'); }).to.throw(); 45 | expect(function () { cutils.ridNumber('x', []); }).to.throw(); 46 | expect(function () { cutils.ridNumber('x', {}); }).to.throw(); 47 | expect(function () { cutils.ridNumber(); }).to.throw(); 48 | 49 | expect(function () { cutils.ridNumber('x', 'y'); }).not.to.throw(); 50 | expect(function () { cutils.ridNumber(5, 'y'); }).not.to.throw(); 51 | expect(function () { cutils.ridNumber('x', 5); }).not.to.throw(); 52 | expect(function () { cutils.ridNumber(1, 5); }).not.to.throw(); 53 | }); 54 | 55 | it('#.getPathArray()', function () { 56 | expect(function () { cutils.getPathArray(5); }).to.throw(); 57 | expect(function () { cutils.getPathArray({}); }).to.throw(); 58 | expect(function () { cutils.getPathArray([]); }).to.throw(); 59 | expect(function () { cutils.getPathArray(); }).to.throw(); 60 | 61 | expect(function () { cutils.getPathArray('x'); }).not.to.throw(); 62 | }); 63 | 64 | it('#.getPathIdKey()', function () { 65 | expect(function () { cutils.getPathIdKey(5); }).to.throw(); 66 | expect(function () { cutils.getPathIdKey({}); }).to.throw(); 67 | expect(function () { cutils.getPathIdKey([]); }).to.throw(); 68 | expect(function () { cutils.getPathIdKey(); }).to.throw(); 69 | 70 | expect(function () { cutils.getPathIdKey('x'); }).not.to.throw(); 71 | }); 72 | 73 | it('#.getNumPath()', function () { 74 | expect(function () { cutils.getNumPath(5); }).to.throw(); 75 | expect(function () { cutils.getNumPath({}); }).to.throw(); 76 | expect(function () { cutils.getNumPath([]); }).to.throw(); 77 | expect(function () { cutils.getNumPath(); }).to.throw(); 78 | 79 | expect(function () { cutils.getNumPath('x'); }).not.to.throw(); 80 | }); 81 | 82 | it('#.decodeLinkFormat()', function () { 83 | expect(function () { cutils.decodeLinkFormat(5); }).to.throw(); 84 | expect(function () { cutils.decodeLinkFormat({}); }).to.throw(); 85 | expect(function () { cutils.decodeLinkFormat([]); }).to.throw(); 86 | expect(function () { cutils.decodeLinkFormat(); }).to.throw(); 87 | 88 | expect(function () { cutils.decodeLinkFormat('x'); }).not.to.throw(); 89 | }); 90 | 91 | it('#.dotPath()', function () { 92 | expect(function () { cutils.dotPath(5); }).to.throw(); 93 | expect(function () { cutils.dotPath({}); }).to.throw(); 94 | expect(function () { cutils.dotPath([]); }).to.throw(); 95 | expect(function () { cutils.dotPath(); }).to.throw(); 96 | 97 | expect(function () { cutils.dotPath('xyz'); }).not.to.throw(); 98 | }); 99 | 100 | it('#.createPath()', function () { 101 | expect(function () { cutils.createPath(5); }).to.throw(); 102 | expect(function () { cutils.createPath({}); }).to.throw(); 103 | expect(function () { cutils.createPath([]); }).to.throw(); 104 | expect(function () { cutils.createPath(); }).to.throw(); 105 | 106 | expect(function () { cutils.createPath('xyz'); }).not.to.throw(); 107 | }); 108 | 109 | it('#.buildPathValuePairs()', function () { 110 | expect(function () { cutils.buildPathValuePairs(3, { a: { b: 1 } }); }).to.throw(); 111 | expect(function () { cutils.buildPathValuePairs([], { a: { b: 1 } }); }).to.throw(); 112 | expect(function () { cutils.buildPathValuePairs({}, { a: { b: 1 } }); }).to.throw(); 113 | expect(function () { cutils.buildPathValuePairs(undefined, { a: { b: 1 } }); }).to.throw(); 114 | expect(function () { cutils.buildPathValuePairs(null, { a: { b: 1 } }); }).to.throw(); 115 | 116 | expect(function () { cutils.buildPathValuePairs('/xyz', { a: { b: 1 } }); }).not.to.throw(); 117 | }); 118 | 119 | }); 120 | 121 | describe('Functional Check', function () { 122 | it('#.oidKey()', function () { 123 | expect(cutils.oidKey('x')).to.be.eql('x'); 124 | expect(cutils.oidKey(9999)).to.be.eql(9999); 125 | expect(cutils.oidKey(2051)).to.be.eql('cmdhDefEcValues'); 126 | expect(cutils.oidKey('2051')).to.be.eql('cmdhDefEcValues'); 127 | expect(cutils.oidKey('cmdhDefEcValues')).to.be.eql('cmdhDefEcValues'); 128 | }); 129 | 130 | it('#.oidNumber()', function () { 131 | expect(cutils.oidNumber('x')).to.be.eql('x'); 132 | expect(cutils.oidNumber(9999)).to.be.eql(9999); 133 | expect(cutils.oidNumber(2051)).to.be.eql(2051); 134 | expect(cutils.oidNumber('2051')).to.be.eql(2051); 135 | expect(cutils.oidNumber('cmdhDefEcValues')).to.be.eql(2051); 136 | }); 137 | 138 | it('#.ridKey()', function () { 139 | expect(cutils.ridKey('x', 1)).to.be.eql(1); 140 | expect(cutils.ridKey('x', 1)).to.be.eql(1); 141 | expect(cutils.ridKey(9999)).to.be.eql(9999); 142 | expect(cutils.ridKey(9999, 1)).to.be.eql(1); 143 | expect(cutils.ridKey(1, 9999)).to.be.eql(9999); 144 | expect(cutils.ridKey(1, 'xxx')).to.be.eql('xxx'); 145 | 146 | expect(cutils.ridKey(5602)).to.be.eql('maxMeaValue'); 147 | expect(cutils.ridKey('5602')).to.be.eql('maxMeaValue'); 148 | expect(cutils.ridKey('maxMeaValue')).to.be.eql('maxMeaValue'); 149 | expect(cutils.ridKey('lwm2mServer', 5)).to.be.eql('disableTimeout'); 150 | expect(cutils.ridKey('lwm2mServer', '5')).to.be.eql('disableTimeout'); 151 | expect(cutils.ridKey(1, 5)).to.be.eql('disableTimeout'); 152 | expect(cutils.ridKey(1, '5')).to.be.eql('disableTimeout'); 153 | expect(cutils.ridKey(1, 'disableTimeout')).to.be.eql('disableTimeout'); 154 | expect(cutils.ridKey('1', 'disableTimeout')).to.be.eql('disableTimeout'); 155 | }); 156 | 157 | it('#.ridNumber()', function () { 158 | expect(cutils.ridNumber('x', 1)).to.be.eql(1); 159 | expect(cutils.ridNumber('x', 1)).to.be.eql(1); 160 | expect(cutils.ridNumber(9999)).to.be.eql(9999); 161 | expect(cutils.ridNumber(9999, 1)).to.be.eql(1); 162 | expect(cutils.ridNumber(1, 9999)).to.be.eql(9999); 163 | expect(cutils.ridNumber(1, 'xxx')).to.be.eql('xxx'); 164 | 165 | expect(cutils.ridNumber(5602)).to.be.eql(5602); 166 | expect(cutils.ridNumber('5602')).to.be.eql(5602); 167 | expect(cutils.ridNumber('maxMeaValue')).to.be.eql(5602); 168 | expect(cutils.ridNumber('lwm2mServer', 5)).to.be.eql(5); 169 | expect(cutils.ridNumber('lwm2mServer', '5')).to.be.eql(5); 170 | expect(cutils.ridNumber(1, 5)).to.be.eql(5); 171 | expect(cutils.ridNumber(1, '5')).to.be.eql(5); 172 | expect(cutils.ridNumber(1, 'disableTimeout')).to.be.eql(5); 173 | expect(cutils.ridNumber('1', 'disableTimeout')).to.be.eql(5); 174 | }); 175 | 176 | it('#.getPathArray()', function () { 177 | expect(cutils.getPathArray('/x/y/z')).to.be.eql(['x', 'y', 'z']); 178 | expect(cutils.getPathArray('/x/y/z/')).to.be.eql(['x', 'y', 'z']); 179 | expect(cutils.getPathArray('x/y/z/')).to.be.eql(['x', 'y', 'z']); 180 | expect(cutils.getPathArray('x/y/z')).to.be.eql(['x', 'y', 'z']); 181 | }); 182 | 183 | it('#.getPathIdKey()', function () { 184 | expect(cutils.getPathIdKey('/1/2/3')).to.be.eql({ oid: 'lwm2mServer', iid: '2', rid: 'defaultMaxPeriod' }); 185 | expect(cutils.getPathIdKey('/lwm2mServer/2/3')).to.be.eql({ oid: 'lwm2mServer', iid: '2', rid: 'defaultMaxPeriod' }); 186 | expect(cutils.getPathIdKey('/1/2/defaultMaxPeriod')).to.be.eql({ oid: 'lwm2mServer', iid: '2', rid: 'defaultMaxPeriod' }); 187 | expect(cutils.getPathIdKey('/lwm2mServer/2/defaultMaxPeriod')).to.be.eql({ oid: 'lwm2mServer', iid: '2', rid: 'defaultMaxPeriod' }); 188 | }); 189 | 190 | it('#.getNumPath()', function () { 191 | expect(cutils.getNumPath('/1/2/3')).to.be.eql('/1/2/3'); 192 | expect(cutils.getNumPath('/lwm2mServer/2/3')).to.be.eql('/1/2/3'); 193 | expect(cutils.getNumPath('/1/2/defaultMaxPeriod')).to.be.eql('/1/2/3'); 194 | expect(cutils.getNumPath('/lwm2mServer/2/defaultMaxPeriod')).to.be.eql('/1/2/3'); 195 | }); 196 | 197 | it('#.decodeLinkFormat()', function () { 198 | expect(cutils.decodeLinkFormat(';pmin=10;pmax=60,,')).to.be.eql({ path:'/1/2', attrs: { pmin: 10, pmax: 60 }, resrcList: ['/1/2/1', '/1/2/2'] }); 199 | expect(cutils.decodeLinkFormat(';pmin=10;pmax=60;gt=1;lt=100;st=1')).to.be.eql({ path:'/1/2/1', attrs: { pmin: 10, pmax: 60, gt: 1, lt: 100, st: 1 }}); 200 | }); 201 | 202 | it('#.dotPath()', function () { 203 | expect(cutils.dotPath('.x.y.z')).to.be.eql('x.y.z'); 204 | expect(cutils.dotPath('x.y.z.')).to.be.eql('x.y.z'); 205 | expect(cutils.dotPath('/x.y.z.')).to.be.eql('x.y.z'); 206 | expect(cutils.dotPath('/x.y/z.')).to.be.eql('x.y.z'); 207 | expect(cutils.dotPath('/x/y/z')).to.be.eql('x.y.z'); 208 | expect(cutils.dotPath('x/y/z/')).to.be.eql('x.y.z'); 209 | expect(cutils.dotPath('/x.y/z.')).to.be.eql('x.y.z'); 210 | expect(cutils.dotPath('/x.y/z/')).to.be.eql('x.y.z'); 211 | }); 212 | 213 | it('#.createPath()', function () { 214 | expect(cutils.createPath('/', 'x', 'y', 'z')).to.be.eql('x/y/z'); 215 | expect(cutils.createPath('.', 'x', 'y', 'z')).to.be.eql('x.y.z'); 216 | expect(cutils.createPath('', 'x', 'y', 'z')).to.be.eql('xyz'); 217 | expect(cutils.createPath('')).to.be.eql(''); 218 | }); 219 | 220 | it('#.buildPathValuePairs()', function () { 221 | expect(cutils.buildPathValuePairs('/x/y/z', { a: { b: 3 } })).to.be.eql({ 'x.y.z.a.b': 3}); 222 | expect(cutils.buildPathValuePairs('/x/y/z', 3)).to.be.eql({ 'x.y.z': 3}); 223 | expect(cutils.buildPathValuePairs('/x/y/z', 'hello.world')).to.be.eql({ 'x.y.z': 'hello.world'}); 224 | expect(cutils.buildPathValuePairs('/x/y/z', [3, 2, 1])).to.be.eql({ 'x.y.z.0':3, 'x.y.z.1':2, 'x.y.z.2':1 }); 225 | expect(cutils.buildPathValuePairs('/x/y/z', [{ m: 3}, {m: 2}])).to.be.eql({ 'x.y.z.0.m': 3, 'x.y.z.1.m': 2 }); 226 | }); 227 | }); 228 | }); -------------------------------------------------------------------------------- /test/fixture.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | expect = require('chai').expect; 3 | 4 | var _typedArguments = { 5 | 'undefined': undefined, 6 | 'null': null, 7 | 'number': 9527, 8 | 'nan': NaN, 9 | 'string': 'hello', 10 | 'array': [], 11 | 'boolean': true, 12 | 'function': function () {}, 13 | 'object': {} 14 | }; 15 | 16 | var _globalSetTimeout = global.setTimeout; 17 | 18 | function _verifySyncCall(func, arg, type, errorExpected) { 19 | if (errorExpected) 20 | expect(function () { func(arg); }, 'for ' + type + ' argument').to.throw(TypeError); 21 | else 22 | expect(function () { func(arg); }, 'for ' + type + ' argument').not.to.throw(TypeError); 23 | } 24 | 25 | function _verifyAsyncCall(func, arg, type, errorExpected) { 26 | var deferred = Q.defer(); 27 | 28 | func(arg).done(function () { 29 | if (errorExpected) 30 | try { 31 | expect(function (){}, 'for ' + type + ' argument').to.throw(TypeError); 32 | } catch (err) { 33 | deferred.reject(err); 34 | } 35 | else 36 | deferred.resolve(); 37 | }, function (err) { 38 | if ((err instanceof TypeError) && !errorExpected) 39 | try { 40 | expect(function (){ throw err; }, 'for ' + type + ' argument').not.to.throw(TypeError); 41 | } catch (err) { 42 | deferred.reject(err); 43 | } 44 | else 45 | deferred.resolve(); 46 | }); 47 | 48 | return deferred.promise; 49 | } 50 | 51 | function _verifySignature(func, acceptedTypes, verifier) { 52 | acceptedTypes = acceptedTypes || []; 53 | 54 | var results = [], invalids = acceptedTypes.filter(function (type) { 55 | return (typeof type === 'string') && (Object.keys(_typedArguments).indexOf(type) < 0); 56 | }); 57 | if (invalids.length) throw new TypeError('Invalid acceptedTypes: ' + JSON.stringify(invalids)); 58 | 59 | Object.keys(_typedArguments).forEach(function (type) { 60 | results.push(verifier(func, _typedArguments[type], type, acceptedTypes.indexOf(type) < 0)); 61 | }); 62 | 63 | acceptedTypes.filter(function (type) { return (typeof type === 'object') && !Array.isArray(type); }).forEach(function (type) { 64 | results.push(verifier(func, type, 'custom', false)); 65 | }); 66 | 67 | acceptedTypes.filter(function (type) { return Array.isArray(type); }).forEach(function (types) { 68 | types.forEach(function (type) { 69 | results.push(verifier(func, type, type, false)); 70 | }); 71 | }); 72 | 73 | return results; 74 | } 75 | 76 | function _verifySignatureSync(func, acceptedTypes) { 77 | _verifySignature(func, acceptedTypes, _verifySyncCall); 78 | } 79 | 80 | function _verifySignatureAsync(func, acceptedTypes) { 81 | return Q.all(_verifySignature(func, acceptedTypes, _verifyAsyncCall)); 82 | } 83 | 84 | function _fireSetTimeoutCallbackEarlier(counter, delay) { 85 | counter = counter || 1; 86 | delay = delay || 50; 87 | var timerCb, callTimerCb; 88 | 89 | global.setTimeout = function (cb, delay) { 90 | if (!--counter) { 91 | timerCb = cb; 92 | global.setTimeout = _globalSetTimeout; 93 | return _globalSetTimeout(function () {}, delay); 94 | } 95 | else 96 | return _globalSetTimeout(cb, delay); 97 | }; 98 | 99 | callTimerCb = function () { 100 | if (timerCb) 101 | timerCb(); 102 | else 103 | _globalSetTimeout(callTimerCb, delay); 104 | }; 105 | _globalSetTimeout(callTimerCb, delay); 106 | } 107 | 108 | module.exports = { 109 | _verifySignatureSync: _verifySignatureSync, 110 | _verifySignatureAsync: _verifySignatureAsync, 111 | _fireSetTimeoutCallbackEarlier: _fireSetTimeoutCallbackEarlier 112 | }; 113 | -------------------------------------------------------------------------------- /test/nedb-storage.test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | _ = require('busyman'), 4 | expect = require('chai').expect, 5 | chai = require('chai'), 6 | sinon = require('sinon'), 7 | sinonChai = require('sinon-chai'), 8 | Datastore = require('nedb'), 9 | 10 | defaultConfig = require('../lib/config'), 11 | CoapNode = require('../lib/components/coap-node'), 12 | StorageInterface = require('../lib/components/storage-interface'), 13 | NedbStorage = require('../lib/components/nedb-storage.js'), 14 | fixture = require('./fixture'), 15 | _verifySignatureSync = fixture._verifySignatureSync, 16 | _verifySignatureAsync = fixture._verifySignatureAsync, 17 | 18 | baseDir = './test/database_test', 19 | alterDir = baseDir + '/new_dir', 20 | alterPath = alterDir + '/test.db', 21 | storage; 22 | 23 | chai.use(sinonChai); 24 | 25 | var shepherd = { 26 | emit: function () {}, 27 | request: function (req, callback) { 28 | var deferred = Q.defer(); 29 | deferred.resolve({}); 30 | return deferred.promise.nodeify(callback); 31 | }, 32 | _newClientId: function () { return 1; }, 33 | _config: Object.assign({}, defaultConfig) 34 | }; 35 | 36 | var cnode1 = _createNode({ 37 | clientName: 'mock01', 38 | clientId: 1, 39 | locationPath: '1', 40 | lifetime: 86400, 41 | ip: '192.168.1.100', 42 | port: '5685', 43 | mac: 'AA:BB:CC:DD:EE:11', 44 | version: '1.0.0', 45 | heartbeatEnabled: true, 46 | so: { 47 | lwm2mServer: { 48 | 0: { 49 | lifetime: 86400, 50 | defaultMinPeriod: 1, 51 | defaultMaxPeriod: 60 52 | } 53 | }, 54 | connMonitor: { 55 | 0: { 56 | ip: '192.168.1.100' 57 | } 58 | } 59 | }, 60 | objList: { 61 | lwm2mServer: [0], 62 | connMonitor: [0] 63 | } 64 | }); 65 | 66 | var cnode2 = _createNode({ 67 | clientName: 'mock02', 68 | clientId: 2, 69 | locationPath: '2', 70 | lifetime: 85741, 71 | ip: '192.168.1.110', 72 | port: '5686', 73 | mac: 'AA:BB:CC:DD:EE:22', 74 | version: '1.0.0', 75 | heartbeatEnabled: false, 76 | so: { 77 | lwm2mServer: { 78 | 0: { 79 | lifetime: 85741, 80 | defaultMinPeriod: 1, 81 | defaultMaxPeriod: 50 82 | } 83 | } 84 | }, 85 | objList: { 86 | lwm2mServer: [0] 87 | }, 88 | observedList: ['/lwm2mServer/0/defaultMinPeriod'] 89 | }); 90 | 91 | var cnode3 = _createNode({ 92 | clientName: 'mock03', 93 | clientId: 3, 94 | locationPath: '3', 95 | lifetime: 84321, 96 | ip: '192.168.1.120', 97 | port: '5687', 98 | mac: 'AA:BB:CC:DD:EE:33', 99 | version: '1.0.0', 100 | heartbeatEnabled: true, 101 | so: { 102 | lwm2mServer: { 103 | 0: { 104 | lifetime: 84321, 105 | defaultMinPeriod: 1, 106 | defaultMaxPeriod: 40 107 | } 108 | } 109 | }, 110 | objList: { 111 | lwm2mServer: [0] 112 | } 113 | }); 114 | 115 | describe('nedb-storage', function () { 116 | before(function (done) { 117 | storage = new NedbStorage(''); 118 | var dir = path.resolve(baseDir); 119 | if (!fs.existsSync(dir)) 120 | fs.mkdir(dir, function (err) { 121 | expect(err).to.equal(null); 122 | done(); 123 | }); 124 | else 125 | done(); 126 | }); 127 | 128 | describe('Constructor Check', function () { 129 | before(function (done) { 130 | _clearAlterPath(done); 131 | }); 132 | 133 | it('should create an instance of StorageInterface', function () { 134 | expect(storage).to.be.instanceOf(StorageInterface); 135 | }); 136 | 137 | it('should raise an error if dbPath is not a string', function () { 138 | var _createDatabaseStub = sinon.stub(NedbStorage.prototype, '_createDatabase', function () {}); 139 | _verifySignatureSync(function (arg) { new NedbStorage(arg); }, ['string']); 140 | _createDatabaseStub.restore(); 141 | expect(_createDatabaseStub).to.have.been.calledOnce; 142 | }); 143 | 144 | it('should create the directory of dbPath if not exists', function () { 145 | var ensureIndexStub = sinon.stub(Datastore.prototype, 'ensureIndex', function (options, cb) { 146 | cb(null); 147 | }); 148 | 149 | new NedbStorage(alterPath); 150 | 151 | ensureIndexStub.restore(); 152 | expect(fs.existsSync(path.resolve(alterDir))).to.eql(true); 153 | expect(ensureIndexStub).has.been.calledWith({ fieldName: 'clientName', unique: true }); 154 | }); 155 | 156 | it('should create a inMemoryOnly backend nedb when dbPath is empty', function () { 157 | var storage = new NedbStorage(''); 158 | 159 | expect(storage._db.inMemoryOnly).to.eql(true); 160 | }); 161 | 162 | after(function (done) { 163 | setTimeout(function () { 164 | _clearAlterPath(done); 165 | }, 100); 166 | }); 167 | }); 168 | 169 | describe('Signature Check', function () { 170 | it('#.save', function () { 171 | _verifySignatureSync(function (arg) { storage.save(arg); }, [cnode1]); 172 | }); 173 | 174 | it('#.load', function () { 175 | _verifySignatureSync(function (arg) { storage.load(arg); }, [cnode1]); 176 | }); 177 | 178 | it('#.remove', function () { 179 | _verifySignatureSync(function (arg) { storage.remove(arg); }, [cnode1]); 180 | }); 181 | 182 | it('#.updateAttrs', function () { 183 | _verifySignatureSync(function (arg) { storage.updateAttrs(arg, null); }, [cnode1]); 184 | _verifySignatureSync(function (arg) { storage.updateAttrs(cnode1, arg); }, ['null', 'object']); 185 | }); 186 | 187 | it('#.patchSo', function () { 188 | _verifySignatureSync(function (arg) { storage.patchSo(arg, null); }, [cnode1]); 189 | _verifySignatureSync(function (arg) { storage.patchSo(cnode1, arg); }, ['null', 'object']); 190 | }); 191 | }); 192 | 193 | describe('Functional Check', function () { 194 | describe('#.save', function () { 195 | it('should reject when db error occurred', function () { 196 | var updateStub = _createUpdateStub('db error'); 197 | 198 | var promise = storage.save(cnode1); 199 | 200 | return promise.catch(function (err) { 201 | updateStub.restore(); 202 | expect(err).to.be.instanceOf(Error); 203 | expect(err.message).to.eql('db error'); 204 | }); 205 | }); 206 | 207 | it('should save the node', function (done) { 208 | var promise = storage.save(cnode1); 209 | 210 | promise.then(function (data) { 211 | expect(data).to.deep.equal(cnode1.dump()); 212 | storage._db.findOne({ clientName: cnode1.clientName }, { _id: 0 }, function (err, doc) { 213 | expect(err).to.eql(null); 214 | expect(doc).to.deep.equal(cnode1.dump()); 215 | storage._db.count({}, function (err, count) { 216 | expect(err).to.eql(null); 217 | expect(count).to.eql(1); 218 | done(); 219 | }); 220 | }); 221 | }).done(); 222 | }); 223 | 224 | it('should save a new node', function (done) { 225 | var promise = storage.save(cnode2); 226 | 227 | promise.then(function () { 228 | storage._db.findOne({ clientName: cnode2.clientName }, { _id: 0 }, function (err, doc) { 229 | expect(err).to.eql(null); 230 | expect(doc).to.deep.equal(cnode2.dump()); 231 | storage._db.count({}, function (err, count) { 232 | expect(err).to.eql(null); 233 | expect(count).to.eql(2); 234 | done(); 235 | }); 236 | }); 237 | }).done(); 238 | }); 239 | 240 | it('should save another node', function (done) { 241 | var promise = storage.save(cnode3); 242 | 243 | promise.then(function () { 244 | storage._db.findOne({ clientName: cnode3.clientName }, { _id: 0 }, function (err, doc) { 245 | expect(err).to.eql(null); 246 | expect(doc).to.deep.equal(cnode3.dump()); 247 | storage._db.count({}, function (err, count) { 248 | expect(err).to.eql(null); 249 | expect(count).to.eql(3); 250 | done(); 251 | }); 252 | }); 253 | }).done(); 254 | }); 255 | 256 | it('should actually update some value', function (done) { 257 | cnode1.version = '2.0.0'; 258 | cnode1.so.init('connMonitor', 0, { ip: '192.168.1.110' }); 259 | 260 | var promise = storage.save(cnode1); 261 | 262 | promise.then(function () { 263 | storage._db.findOne({ clientName: cnode1.clientName }, { _id: 0 }, function (err, doc) { 264 | expect(err).to.eql(null); 265 | expect(doc).to.deep.equal(cnode1.dump()); 266 | expect(doc.version).to.eql('2.0.0'); 267 | expect(doc.so.connMonitor[0].ip).to.eql('192.168.1.110'); 268 | storage._db.count({}, function (err, count) { 269 | expect(err).to.eql(null); 270 | expect(count).to.eql(3); 271 | done(); 272 | }); 273 | }); 274 | }).done(); 275 | }) 276 | }); 277 | 278 | describe('#.load', function () { 279 | it('should reject when db error occurred', function () { 280 | var findOneStub = sinon.stub(Datastore.prototype, 'findOne', function (query, proj, cb) { cb(new Error('db error')); }); 281 | var cnode = _createNode({ clientName: cnode1.clientName }); 282 | 283 | var promise = storage.load(cnode); 284 | 285 | return promise.catch(function (err) { 286 | findOneStub.restore(); 287 | expect(err).to.be.instanceOf(Error); 288 | expect(err.message).to.eql('db error'); 289 | }); 290 | }); 291 | 292 | it('should reject when node clientName can not be found', function () { 293 | var cnode = _createNode({ clientName: 'mockXX' }); 294 | 295 | var promise = storage.load(cnode); 296 | 297 | return promise.catch(function (err) { 298 | expect(err).to.be.instanceOf(Error); 299 | }); 300 | }); 301 | 302 | it('should load cnode', function () { 303 | var cnode = _createNode({ clientName: cnode1.clientName }); 304 | 305 | var promise = storage.load(cnode); 306 | 307 | return promise.then(function () { 308 | expect(cnode.dump()).to.deep.equal(cnode1.dump()); 309 | }); 310 | }); 311 | }); 312 | 313 | describe('#.loadAll', function () { 314 | it('should reject when db error occurred', function () { 315 | var findStub = sinon.stub(Datastore.prototype, 'find', function (query, proj, cb) { cb(new Error('db error')); }); 316 | 317 | var promise = storage.loadAll(); 318 | 319 | return promise.catch(function (err) { 320 | findStub.restore(); 321 | expect(findStub).to.have.been.called; 322 | expect(err).to.be.instanceOf(Error); 323 | expect(err.message).to.eql('db error'); 324 | }); 325 | }); 326 | 327 | it('should return all node data if everything is ok', function () { 328 | var promise = storage.loadAll(); 329 | 330 | return promise.then(function (attrs) { 331 | attrs.sort(function (a, b) { 332 | if (a.clientName < b.clientName) return -1; 333 | if (a.clientName > b.clientName) return 1; 334 | return 0; 335 | }); 336 | expect(attrs).to.deep.equal([cnode1.dump(), cnode2.dump(), cnode3.dump()]); 337 | }); 338 | }); 339 | }); 340 | 341 | describe('#.remove', function () { 342 | it('should reject when db error occurred', function (done) { 343 | var removeStub = sinon.stub(Datastore.prototype, 'remove', function (query, options, cb) { cb(new Error('db error')); }); 344 | 345 | var promise = storage.remove(cnode3); 346 | 347 | promise.catch(function (err) { 348 | removeStub.restore(); 349 | expect(err).to.be.instanceOf(Error); 350 | expect(err.message).to.eql('db error'); 351 | storage._db.count({}, function (err, count) { 352 | expect(err).to.eql(null); 353 | expect(count).to.eql(3); 354 | done(); 355 | }); 356 | }).done(); 357 | }); 358 | 359 | it('should not reject when node not found', function (done) { 360 | var cnode = _createNode({ clientName: 'clientX' }); 361 | 362 | var promise = storage.remove(cnode); 363 | 364 | promise.then(function (deleted) { 365 | expect(deleted).to.eql(false); 366 | storage._db.count({}, function (err, count) { 367 | expect(err).to.eql(null); 368 | expect(count).to.eql(3); 369 | done(); 370 | }); 371 | }).done(); 372 | }); 373 | 374 | it('should delete a node when it can be found', function (done) { 375 | var promise = storage.remove(cnode3); 376 | 377 | promise.then(function (deleted) { 378 | expect(deleted).to.eql(true); 379 | storage._db.findOne({ clientName: cnode3.clientName }, function (err, doc) { 380 | expect(err).to.eql(null); 381 | expect(doc).to.eql(null); 382 | storage._db.count({}, function (err, count) { 383 | expect(err).to.eql(null); 384 | expect(count).to.eql(2); 385 | done(); 386 | }); 387 | }); 388 | }).done(); 389 | }) 390 | }); 391 | 392 | describe('#.updateAttrs', function () { 393 | it('should reject when db error occurred', function () { 394 | var updateStub = _createUpdateStub('db error'); 395 | 396 | var promise = storage.updateAttrs(cnode2, { ip: '192.168.1.111' }); 397 | 398 | return promise.catch(function (err) { 399 | updateStub.restore(); 400 | expect(err).to.be.instanceOf(Error); 401 | expect(err.message).to.eql('db error'); 402 | }); 403 | }); 404 | 405 | it('should do nothing if diff is null', function () { 406 | var updateStub = _createUpdateStub('should not call me'); 407 | 408 | var promise = storage.updateAttrs(cnode2, null); 409 | 410 | return promise.then(function (diff) { 411 | updateStub.restore(); 412 | expect(updateStub).not.to.been.called; 413 | expect(diff).to.eql(null); 414 | }); 415 | }); 416 | 417 | it('should do nothing if diff is {}}', function () { 418 | var updateStub = _createUpdateStub('should not call me'); 419 | 420 | var promise = storage.updateAttrs(cnode2, {}); 421 | 422 | return promise.then(function (diff) { 423 | updateStub.restore(); 424 | expect(updateStub).not.to.been.called; 425 | expect(diff).to.eql(null); 426 | }); 427 | }); 428 | 429 | it('should reject if clientName is included in diff', function () { 430 | var updateStub = _createUpdateStub('should not call me'); 431 | 432 | var promise = storage.updateAttrs(cnode2, { clientName: 'newClientName' }); 433 | 434 | return promise.catch(function (err) { 435 | updateStub.restore(); 436 | expect(err).to.be.instanceOf(Error); 437 | expect(err.message).to.contains('clientName'); 438 | }); 439 | }); 440 | 441 | it('should do update if everything is ok', function (done) { 442 | var diff = { 443 | ip: '192.168.1.112', 444 | lifetime: 85742, 445 | version: '1.0.2', 446 | objList: { 447 | lwm2mServer: [2, 3], 448 | connMonitor: [0] 449 | }, 450 | observedList: ['/lwm2mServer/0/defaultMaxPeriod'] 451 | }; 452 | var expected = _.merge(cnode2.dump(), diff); 453 | 454 | var promise = storage.updateAttrs(cnode2, diff); 455 | 456 | promise.then(function (arg) { 457 | expect(arg).to.be.equal(diff); 458 | storage._db.findOne({ clientName: cnode2.clientName }, { _id: 0 }, function (err, doc) { 459 | expect(err).to.eql(null); 460 | expect(doc).to.deep.equal(expected); 461 | done(); 462 | }); 463 | }).done(); 464 | }); 465 | }); 466 | 467 | describe('#.patchSo', function () { 468 | it('should reject when db error occurred', function () { 469 | var diff = { 470 | lwm2mServer: { 471 | 0: { 472 | lifetime: 85747, 473 | } 474 | } 475 | }; 476 | var updateStub = _createUpdateStub('db error'); 477 | 478 | var promise = storage.patchSo(cnode2, diff); 479 | 480 | return promise.catch(function (err) { 481 | updateStub.restore(); 482 | expect(err).to.be.instanceOf(Error); 483 | expect(err.message).to.eql('db error'); 484 | }); 485 | }); 486 | 487 | it('should do nothing if diff is null', function () { 488 | var updateStub = _createUpdateStub('should not call me'); 489 | 490 | var promise = storage.patchSo(cnode2, null); 491 | 492 | return promise.then(function (diff) { 493 | updateStub.restore(); 494 | expect(updateStub).not.to.been.called; 495 | expect(diff).to.eql(null); 496 | }); 497 | }); 498 | 499 | it('should do nothing if diff is {}}', function () { 500 | var updateStub = _createUpdateStub('should not call me'); 501 | 502 | var promise = storage.patchSo(cnode2, {}); 503 | 504 | return promise.then(function (diff) { 505 | updateStub.restore(); 506 | expect(updateStub).not.to.been.called; 507 | expect(diff).to.eql(null); 508 | }); 509 | }); 510 | 511 | it('should do update if everything is ok', function (done) { 512 | var diff = { 513 | lwm2mServer: { 514 | 0: { 515 | defaultMaxPeriod: 57 516 | } 517 | }, 518 | connMonitor: { 519 | 0: { 520 | ip: '192.168.1.110' 521 | } 522 | } 523 | }; 524 | var expected = _.merge(cnode2.so.dumpSync(), diff); 525 | 526 | var promise = storage.patchSo(cnode2, diff); 527 | 528 | promise.then(function (arg) { 529 | expect(arg).to.be.equal(diff); 530 | storage._db.findOne({ clientName: cnode2.clientName }, { _id: 0 }, function (err, doc) { 531 | expect(err).to.eql(null); 532 | expect(doc.so).to.deep.equal(expected); 533 | done(); 534 | }); 535 | }).done(); 536 | }); 537 | }); 538 | 539 | describe('#.reset', function () { 540 | it('should reject when db error occurred for remove', function (done) { 541 | var removeStub = sinon.stub(Datastore.prototype, 'remove', function (query, options, cb) { cb(new Error('db error')); }); 542 | 543 | var promise = storage.reset(); 544 | 545 | promise.catch(function (err) { 546 | removeStub.restore(); 547 | expect(removeStub).to.have.been.called; 548 | expect(err).to.be.instanceOf(Error); 549 | expect(err.message).to.eql('db error'); 550 | storage._db.count({}, function (err, count) { 551 | expect(err).to.eql(null); 552 | expect(count).to.eql(2); 553 | done(); 554 | }); 555 | }).done(); 556 | }); 557 | 558 | it('should reject when db error occurred for loadDatabase', function (done) { 559 | var removeStub = sinon.stub(Datastore.prototype, 'remove', function (query, options, cb) { cb(null); }); 560 | var loadDatabaseStub = sinon.stub(Datastore.prototype, 'loadDatabase', function (cb) { cb(new Error('db error')); }); 561 | 562 | var promise = storage.reset(); 563 | 564 | promise.catch(function (err) { 565 | removeStub.restore(); 566 | loadDatabaseStub.restore(); 567 | expect(removeStub).to.have.been.called; 568 | expect(loadDatabaseStub).to.have.been.called; 569 | expect(err).to.be.instanceOf(Error); 570 | expect(err.message).to.eql('db error'); 571 | storage._db.count({}, function (err, count) { 572 | expect(err).to.eql(null); 573 | expect(count).to.eql(2); 574 | done(); 575 | }); 576 | }).done(); 577 | }); 578 | 579 | it('should remove all nodes if everything is ok', function (done) { 580 | var promise = storage.reset(); 581 | 582 | promise.then(function (numRemoved) { 583 | expect(numRemoved).to.eql(2); 584 | storage._db.count({}, function (err, count) { 585 | expect(err).to.eql(null); 586 | expect(count).to.eql(0); 587 | done(); 588 | }); 589 | }).done(); 590 | }); 591 | }); 592 | }); 593 | 594 | after(function (done) { 595 | var dir = path.resolve(baseDir); 596 | if (fs.existsSync(dir)) 597 | fs.rmdir(dir, function (err) { 598 | // just ignore err 599 | done(); 600 | }); 601 | else 602 | done(); 603 | }); 604 | }); 605 | 606 | function _clearAlterPath(done) { 607 | if (fs.existsSync(alterPath)) 608 | fs.unlink(alterPath, function (err) { 609 | expect(err).to.equal(null); 610 | if (fs.existsSync(alterDir)) 611 | fs.rmdir(alterDir, function (err) { 612 | expect(err).to.equal(null); 613 | done(); 614 | }); 615 | else 616 | done(); 617 | }); 618 | else 619 | done(); 620 | } 621 | 622 | function _createNode(attr) { 623 | return new CoapNode(shepherd, attr); 624 | } 625 | 626 | function _createUpdateStub(msg) { 627 | return sinon.stub(Datastore.prototype, 'update', function (query, update, options, cb) { 628 | cb(new Error(msg)); 629 | }); 630 | } 631 | --------------------------------------------------------------------------------