├── js ├── .gitignore ├── README.md ├── package.json ├── index.js ├── lib │ ├── IFlasher.js │ ├── logger.js │ ├── IHandshake.js │ ├── BufferStream.js │ ├── EventPublisher.js │ ├── CryptoStream.js │ ├── ChunkingStream.js │ ├── ICrypto.js │ ├── utilities.js │ ├── Messages.js │ ├── Flasher.js │ └── Handshake.js ├── server │ ├── main.js │ └── DeviceServer.js ├── settings.js ├── clients │ ├── ISparkCore.js │ └── SparkCore.js └── LICENSE.txt ├── doc ├── usage.md └── INSTALL.md ├── .gitignore ├── README.md └── LICENSE.txt /js/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | server/*.pem 3 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Changelog 4 | ========= 5 | 6 | 0.1.5 - cleaning up connection key logging, adding early data patch 7 | 0.1.4 - adding verbose logging option 8 | 0.1.3 - adding OTA size workaround 9 | 0.0.2 - Working alpha version, needs refactor for API wrapper 10 | 0.0.1 - Inital imports and cleanup of base classes -------------------------------------------------------------------------------- /doc/usage.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Keys! 4 | ==================== 5 | 6 | Create your keys 7 | 8 | Keep your private key secret 9 | 10 | Wire up your server public key into your settings / module 11 | 12 | Create a special version of your key with the server DNS name / IP address 13 | Copy your server public key to your core 14 | 15 | Copy your core public key into the server 16 | 17 | 18 | Running the server 19 | ==================== 20 | 21 | Startup a tcp server on the port of your choosing, and startup a SparkCore object with each new socket that's opened 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spark-protocol", 3 | "version": "0.1.6", 4 | "main": "./index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/spark/spark-protocol" 8 | }, 9 | "homepage": "https://github.com/spark/spark-protocol", 10 | "bugs": "https://github.com/spark/spark-protocol/issues", 11 | "author": { 12 | "name": "David Middlecamp", 13 | "email": "david@spark.io", 14 | "url": "https://www.spark.io/" 15 | }, 16 | "license": "LGPL-3.0", 17 | "dependencies": { 18 | "buffer-crc32": "~0.2.3", 19 | "h5.buffers": "~0.1.1", 20 | "h5.coap": "git+https://github.com/morkai/h5.coap.git", 21 | "hogan.js": "*", 22 | "ursa": "*", 23 | "when": "*", 24 | "moment": "*", 25 | "xtend": "*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /doc/INSTALL.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Advanced Install Details: 6 | 7 | 8 | 9 | Installing on Windows 10 | ============ 11 | 12 | 64 bit windows 13 | -------- 14 | 15 | Node.js (64bit): 16 | https://dl.dropboxusercontent.com/u/36134145/Spark-cli/node-v0.10.29-x64.msi 17 | 18 | OpenSSL(64bit): 19 | https://dl.dropboxusercontent.com/u/36134145/Spark-cli/Win64OpenSSL_Light-1_0_1h.exe 20 | 21 | Visual C++ Redistributable (64bit): 22 | https://dl.dropboxusercontent.com/u/36134145/Spark-cli/vcredist_x64.exe 23 | 24 | 25 | 32 bit windows 26 | -------- 27 | 28 | Visual C++ Redistributable (32bit): 29 | https://dl.dropboxusercontent.com/u/36134145/Spark-cli/vcredist_x86.exe 30 | 31 | Node.js (32bit): 32 | https://dl.dropboxusercontent.com/u/36134145/Spark-cli/node-v0.10.29-x86.msi 33 | 34 | OpenSSL(32bit): 35 | https://dl.dropboxusercontent.com/u/36134145/Spark-cli/Win32OpenSSL_Light-1_0_1h.exe -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | exports.DeviceServer = require("./server/DeviceServer.js"); 19 | exports.SparkCore = require("./clients/SparkCore.js"); -------------------------------------------------------------------------------- /js/lib/IFlasher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var extend = require("xtend"); 20 | 21 | var IFlasher = function(options) { }; 22 | IFlasher.prototype = { 23 | client: null, 24 | stage: 0, 25 | 26 | startFlash: function() { }, 27 | _nextStep: function(data) {}, 28 | foo: null 29 | }; 30 | module.exports = IFlasher; -------------------------------------------------------------------------------- /js/lib/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013-2014 Spark Labs, Inc. All rights reserved. - https://www.spark.io/ 3 | * 4 | * This file is part of the Spark-protocol module 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License version 3 8 | * as published by the Free Software Foundation. 9 | * 10 | * Spark-protocol is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Spark-protocol. If not, see . 17 | * 18 | * You can download the source here: https://github.com/spark/spark-protocol 19 | */ 20 | 21 | 22 | module.exports = { 23 | log: function() { 24 | console.log.apply(console, arguments); 25 | }, 26 | error: function() { 27 | console.error.apply(console, arguments); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /js/server/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | // 19 | // Not the best way to deal with errors I'm told, but should be fine on a home server 20 | // 21 | process.on('uncaughtException', function (ex) { 22 | var stack = (ex && ex.stack) ? ex.stack : ""; 23 | logger.error('Caught exception: ' + ex + ' stack: ' + stack); 24 | }); 25 | 26 | 27 | var DeviceServer = require('./DeviceServer.js'); 28 | var server = new DeviceServer(); 29 | server.start(); 30 | 31 | -------------------------------------------------------------------------------- /js/lib/IHandshake.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013-2014 Spark Labs, Inc. All rights reserved. - https://www.spark.io/ 3 | * 4 | * This file is part of the Spark-protocol module 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License version 3 8 | * as published by the Free Software Foundation. 9 | * 10 | * Spark-protocol is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Spark-protocol. If not, see . 17 | * 18 | * You can download the source here: https://github.com/spark/spark-protocol 19 | */ 20 | 21 | 22 | /** 23 | * Interface for the Spark Core Handshake, v1 24 | * @constructor 25 | */ 26 | var Constructor = function () { 27 | 28 | }; 29 | 30 | Constructor.prototype = { 31 | classname: "IHandshake", 32 | socket: null, 33 | 34 | /** 35 | * Tries to establish a secure remote connection using our handshake protocol 36 | * @param client 37 | * @param onSuccess 38 | * @param onFail 39 | */ 40 | handshake: function (client, onSuccess, onFail) { throw new Error("Not yet implemented"); }, 41 | 42 | _: null 43 | }; 44 | module.exports = Constructor; 45 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var path = require('path'); 20 | 21 | module.exports = { 22 | PORT: 5683, 23 | HOST: "localhost", 24 | 25 | 26 | /** 27 | * Your server crypto keys! 28 | */ 29 | serverKeyFile: "default_key.pem", 30 | serverKeyPassFile: null, 31 | serverKeyPassEnvVar: null, 32 | 33 | coreKeysDir: path.join(__dirname, "data"), 34 | 35 | /** 36 | * How high do our counters go before we wrap around to 0? 37 | * (CoAP maxes out at a 16 bit int) 38 | */ 39 | message_counter_max: Math.pow(2, 16), 40 | 41 | /** 42 | * How big can our tokens be in CoAP messages? 43 | */ 44 | message_token_max: 255, 45 | 46 | verboseProtocol: false 47 | }; -------------------------------------------------------------------------------- /js/lib/BufferStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var BufferStream = function (buffer) { this.buf = buffer; }; 20 | BufferStream.prototype = { 21 | idx: 0, 22 | buf: null, 23 | seek: function(idx) { 24 | this.idx = idx; 25 | }, 26 | read: function (size) { 27 | if (!this.buf) { return null; } 28 | 29 | var idx = this.idx, 30 | endIdx = idx + size; 31 | 32 | if (endIdx >= this.buf.length) { 33 | endIdx = this.buf.length; 34 | } 35 | 36 | var result = null; 37 | if ((endIdx - idx) > 0) { 38 | result = this.buf.slice(idx, endIdx); 39 | this.idx = endIdx; 40 | } 41 | return result; 42 | }, 43 | end: function() { 44 | this.buf = null; 45 | } 46 | 47 | }; 48 | module.exports = BufferStream; 49 | -------------------------------------------------------------------------------- /js/clients/ISparkCore.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | /** 20 | * Interface for the Spark Core module 21 | * @constructor 22 | */ 23 | var Constructor = function () { 24 | 25 | }; 26 | 27 | Constructor.prototype = { 28 | classname: "ISparkCore", 29 | socket: null, 30 | 31 | startupProtocol: function () { 32 | this.handshake(this.hello, this.disconnect); 33 | }, 34 | 35 | handshake: function (onSuccess, onFail) { throw new Error("Not yet implemented"); }, 36 | hello: function (onSuccess, onFail) { throw new Error("Not yet implemented"); }, 37 | disconnect: function () { throw new Error("Not yet implemented"); }, 38 | 39 | 40 | /** 41 | * Connect to API 42 | */ 43 | onApiMessage: function(sender, msg) { throw new Error("Not yet implemented"); }, 44 | 45 | /** 46 | * Connect to API 47 | */ 48 | sendApiResponse: function(sender, msg) { throw new Error("Not yet implemented"); }, 49 | 50 | 51 | foo: null 52 | }; 53 | module.exports = Constructor; 54 | -------------------------------------------------------------------------------- /js/lib/EventPublisher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var when = require('when'); 20 | var extend = require('xtend'); 21 | var settings = require("../settings"); 22 | var logger = require('./logger.js'); 23 | var utilities = require("./utilities.js"); 24 | var EventEmitter = require('events').EventEmitter; 25 | 26 | var EventPublisher = function () { 27 | EventEmitter.call(this); 28 | }; 29 | EventPublisher.prototype = { 30 | 31 | publish: function (isPublic, name, userid, data, ttl, published_at, coreid) { 32 | 33 | process.nextTick((function () { 34 | this.emit(name, isPublic, name, userid, data, ttl, published_at, coreid); 35 | this.emit("*all*", isPublic, name, userid, data, ttl, published_at, coreid); 36 | }).bind(this)); 37 | }, 38 | subscribe: function (eventName, obj) { 39 | if (!eventName || (eventName == "")) { 40 | eventName = "*all*"; 41 | } 42 | 43 | var handler = (function (isPublic, name, userid, data, ttl, published_at, coreid) { 44 | var emitName = (isPublic) ? "public" : "private"; 45 | this.emit(emitName, name, data, ttl, published_at, coreid); 46 | }).bind(obj); 47 | obj[eventName + "_handler"] = handler; 48 | 49 | this.on(eventName, handler); 50 | }, 51 | 52 | unsubscribe: function (eventName, obj) { 53 | var handler = obj[eventName + "_handler"]; 54 | if (handler) { 55 | delete obj[eventName + "_handler"]; 56 | this.removeListener(eventName, handler); 57 | } 58 | }, 59 | 60 | 61 | close: function () { 62 | try { 63 | this.removeAllListeners(); 64 | } 65 | catch (ex) { 66 | logger.error("EventPublisher: error thrown during close " + ex); 67 | } 68 | } 69 | }; 70 | EventPublisher.prototype = extend(EventPublisher.prototype, EventEmitter.prototype); 71 | module.exports = EventPublisher; 72 | 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | spark-protocol 2 | ================ 3 | 4 | Node.JS module for hosting direct encrypted CoAP socket connections! Checkout the local [spark-server](https://github.com/spark/spark-server) 5 | 6 |
 7 |                           __      __        __              __
 8 |    _________  ____ ______/ /__   / /___  __/ /_  ___  _____/ /
 9 |   / ___/ __ \/ __ `/ ___/ //_/  / __/ / / / __ \/ _ \/ ___/ / 
10 |  (__  ) /_/ / /_/ / /  / , |   / /_/ /_/ / /_/ /  __(__  )_/  
11 | /____/ .___/\__,_/_/  /_/|_|   \__/\__,_/_.___/\___/____(_)   
12 |     /_/                                                       
13 | 
14 | 15 | 16 | What do I need to know? 17 | ======================== 18 | 19 | This module knows how to talk encrypted CoAP. It's really good at talking with Spark Cores, and any other hardware that uses this protocol. You'll need a server key to use and load onto your devices. You'll also need to grab any public keys for your connected devices and store them somewhere this module can find them. The public server key stored on the device can also store an IP address or DNS name for your server, so make sure you load that onto your server key when copying it to your device. The server will also generate a default key if you don't have one when it starts up. 20 | 21 | What code modules should I start with? 22 | ============================================ 23 | 24 | There's lots of fun stuff here, but in particular you should know about the "SparkCore" ( https://github.com/spark/spark-protocol/blob/master/js/clients/SparkCore.js ) , and "DeviceServer" ( https://github.com/spark/spark-protocol/blob/master/js/server/DeviceServer.js ) modules. The "DeviceServer" module runs a server that creates "SparkCore" objects, which represent your connected devices. 25 | 26 | 27 | How do I start a server in code? 28 | --------------------------- 29 | 30 | ``` 31 | var DeviceServer = require("spark-protocol").DeviceServer; 32 | var server = new DeviceServer({ 33 | coreKeysDir: "/path/to/your/public_device_keys" 34 | }); 35 | global.server = server; 36 | server.start(); 37 | 38 | ``` 39 | 40 | 41 | How do I get my key / ip address on my core? 42 | ================================================ 43 | 44 | 1.) Figure out your IP address, for now lets say it's 192.168.1.10 45 | 46 | 2.) Make sure you have the Spark-CLI (https://github.com/spark/spark-cli) installed 47 | 48 | 3.) Connect your Spark Core to your computer in listening mode (http://docs.spark.io/connect/#appendix-dfu-mode-device-firmware-upgrade) 49 | 50 | 4.) Load the server key and include your IP address / dns address: 51 | 52 | ``` 53 | spark keys server server_public_key.der your_ip_address 54 | spark keys server server_public_key.der 192.168.1.10 55 | ``` 56 | 57 | 5.) That's it! 58 | 59 | 60 | Where's the API / webserver stuff, this is just a TCP server? 61 | =========================================================================== 62 | 63 | Oh, you want the Spark-Server module here: https://github.com/spark/spark-server :) 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /js/lib/CryptoStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var Transform = require('stream').Transform; 20 | var CryptoLib = require("./ICrypto"); 21 | var crypto = require('crypto'); 22 | var logger = require('../lib/logger.js'); 23 | 24 | var CryptoStream = function (options) { 25 | Transform.call(this, options); 26 | this.key = options.key; 27 | this.iv = options.iv; 28 | this.encrypt = !!options.encrypt; 29 | }; 30 | CryptoStream.prototype = Object.create(Transform.prototype, { constructor: { value: CryptoStream }}); 31 | CryptoStream.prototype._transform = function (chunk, encoding, callback) { 32 | try { 33 | 34 | //assuming it comes in full size pieces 35 | var cipher = this.getCipher(callback); 36 | cipher.write(chunk); 37 | cipher.end(); 38 | cipher = null; 39 | 40 | if (!this.encrypt) { 41 | //ASSERT: we just DECRYPTED an incoming message 42 | //THEN: 43 | // update the initialization vector to the first 16 bytes of the encrypted message we just got 44 | this.iv = new Buffer(16); 45 | chunk.copy(this.iv, 0, 0, 16); 46 | 47 | //logger.log('server: GOT ENCRYPTED CHUNK', chunk.toString('hex')); 48 | } 49 | // else { 50 | // console.log('pre-encrypt sending: ' + chunk.toString('hex')); 51 | // } 52 | } 53 | catch (ex) { 54 | logger.error("CryptoStream transform error " + ex); 55 | } 56 | }; 57 | CryptoStream.prototype.getCipher = function (callback) { 58 | var cipher = null; 59 | if (this.encrypt) { 60 | cipher = crypto.createCipheriv('aes-128-cbc', this.key, this.iv); 61 | } 62 | else { 63 | cipher = crypto.createDecipheriv('aes-128-cbc', this.key, this.iv); 64 | } 65 | 66 | var ciphertext = null, 67 | that = this; 68 | 69 | cipher.on('readable', function () { 70 | var chunk = cipher.read(); 71 | 72 | /* 73 | The crypto stream error was coming from the additional null packet before the end of the stream 74 | 75 | IE 76 | 77 | 78 | null 79 | CryptoStream transform error TypeError: Cannot read property 'length' of null 80 | Coap Error: Error: Invalid CoAP version. Expected 1, got: 3 81 | 82 | The if statement solves (I believe) all of the node version dependency ussues 83 | 84 | ( Previously required node 10.X, with this tested and working on node 12.5 ) 85 | 86 | */ 87 | if(chunk){ 88 | if (!ciphertext) { 89 | ciphertext = chunk; 90 | } 91 | else { 92 | ciphertext = Buffer.concat([ciphertext, chunk], ciphertext.length + chunk.length); 93 | } 94 | } 95 | }); 96 | cipher.on('end', function () { 97 | //var action = (that.encrypt) ? "encrypting" : "decrypting"; 98 | //logger.log(action + ' chunk to ', ciphertext.toString('hex')); 99 | 100 | that.push(ciphertext); 101 | 102 | if (that.encrypt) { 103 | //logger.log("ENCRYPTING WITH ", that.iv.toString('hex')); 104 | //get new iv for next time. 105 | that.iv = new Buffer(16); 106 | ciphertext.copy(that.iv, 0, 0, 16); 107 | 108 | //logger.log("ENCRYPTING WITH ", that.iv.toString('hex')); 109 | } 110 | ciphertext = null; 111 | 112 | callback(); 113 | }); 114 | 115 | return cipher; 116 | }; 117 | module.exports = CryptoStream; 118 | 119 | -------------------------------------------------------------------------------- /js/lib/ChunkingStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var Transform = require('stream').Transform; 20 | var logger = require('../lib/logger.js'); 21 | 22 | /** 23 | 24 | Our job here is to accept messages in whole chunks, and put their length in front 25 | as we send them out, and parse them back into those size chunks as we read them in. 26 | 27 | **/ 28 | 29 | var ChunkingUtils = { 30 | MSG_LENGTH_BYTES: 2, 31 | msgLengthBytes: function (msg) { 32 | //that.push(ciphertext.length); 33 | //assuming a maximum encrypted message length of 65K, lets write an unsigned short int before every message, 34 | //so we know how much to read out. 35 | 36 | if (!msg) { 37 | //logger.log('msgLengthBytes - message was empty'); 38 | return null; 39 | } 40 | 41 | var len = msg.length, 42 | lenBuf = new Buffer(ChunkingUtils.MSG_LENGTH_BYTES); 43 | 44 | lenBuf[0] = len >>> 8; 45 | lenBuf[1] = len & 255; 46 | 47 | //logger.log("outgoing message length was " + len); 48 | return lenBuf; 49 | } 50 | }; 51 | 52 | 53 | var ChunkingStream = function (options) { 54 | Transform.call(this, options); 55 | this.outgoing = !!options.outgoing; 56 | this.incomingBuffer = null; 57 | this.incomingIdx = -1; 58 | }; 59 | ChunkingStream.INCOMING_BUFFER_SIZE = 1024; 60 | 61 | ChunkingStream.prototype = Object.create(Transform.prototype, { constructor: { value: ChunkingStream }}); 62 | ChunkingStream.prototype._transform = function (chunk, encoding, callback) { 63 | 64 | if (this.outgoing) { 65 | //we should be passed whole messages here. 66 | //write our length first, then message, then bail. 67 | 68 | this.push( Buffer.concat([ ChunkingUtils.msgLengthBytes(chunk), chunk ])); 69 | process.nextTick(callback); 70 | } 71 | else { 72 | //collect chunks until we hit an expected size, and then trigger a readable 73 | try { 74 | this.process(chunk, callback); 75 | } 76 | catch(ex) { 77 | logger.error("ChunkingStream error!: " + ex); 78 | } 79 | 80 | } 81 | }; 82 | ChunkingStream.prototype.process = function(chunk, callback) { 83 | if (!chunk) { 84 | //process.nextTick(callback); 85 | return; 86 | } 87 | //logger.log("chunk received ", chunk.length, chunk.toString('hex')); 88 | 89 | var isNewMessage = (this.incomingIdx == -1); 90 | var startIdx = 0; 91 | if (isNewMessage) { 92 | this.expectedLength = ((chunk[0] << 8) + chunk[1]); 93 | 94 | //if we don't have a buffer, make one as big as we will need. 95 | this.incomingBuffer = new Buffer(this.expectedLength); 96 | this.incomingIdx = 0; 97 | startIdx = 2; //skip the first two. 98 | //logger.log('hoping for message of length ' + this.expectedLength); 99 | } 100 | 101 | var remainder = null; 102 | var bytesLeft = this.expectedLength - this.incomingIdx; 103 | var endIdx = startIdx + bytesLeft; 104 | if (endIdx > chunk.length) { 105 | endIdx = chunk.length; 106 | } 107 | //startIdx + Math.min(chunk.length - startIdx, bytesLeft); 108 | 109 | if (startIdx < endIdx) { 110 | //logger.log('copying to incoming, starting at ', this.incomingIdx, startIdx, endIdx); 111 | if (this.incomingIdx >= this.incomingBuffer.length) { 112 | logger.log("hmm, shouldn't end up here."); 113 | } 114 | chunk.copy(this.incomingBuffer, this.incomingIdx, startIdx, endIdx); 115 | } 116 | this.incomingIdx += endIdx - startIdx; 117 | 118 | 119 | if (endIdx < chunk.length) { 120 | remainder = new Buffer(chunk.length - endIdx); 121 | chunk.copy(remainder, 0, endIdx, chunk.length); 122 | } 123 | 124 | if (this.incomingIdx == this.expectedLength) { 125 | //logger.log("received msg of length" + this.incomingBuffer.length, this.incomingBuffer.toString('hex')); 126 | this.push(this.incomingBuffer); 127 | this.incomingBuffer = null; 128 | this.incomingIdx = -1; 129 | this.expectedLength = -1; 130 | this.process(remainder, callback); 131 | } 132 | else { 133 | //logger.log('fell through ', this.incomingIdx, ' and ', this.expectedLength, ' remainder ', (remainder) ? remainder.length : 0); 134 | process.nextTick(callback); 135 | callback = null; //yeah, don't call that twice. 136 | } 137 | 138 | if (!remainder && callback) { 139 | process.nextTick(callback); 140 | callback = null; //yeah, don't call that twice. 141 | } 142 | }; 143 | 144 | 145 | module.exports = ChunkingStream; -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright © 2007 Free Software Foundation, Inc. 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 8 | 9 | This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 10 | 11 | 0. Additional Definitions. 12 | As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. 13 | 14 | “The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. 15 | 16 | An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. 17 | 18 | A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. 19 | 20 | The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. 21 | 22 | The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 23 | 24 | 1. Exception to Section 3 of the GNU GPL. 25 | You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 26 | 27 | 2. Conveying Modified Versions. 28 | If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: 29 | 30 | a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or 31 | b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 32 | 3. Object Code Incorporating Material from Library Header Files. 33 | The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: 34 | 35 | a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. 36 | b) Accompany the object code with a copy of the GNU GPL and this license document. 37 | 4. Combined Works. 38 | You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: 39 | 40 | a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. 41 | b) Accompany the Combined Work with a copy of the GNU GPL and this license document. 42 | c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. 43 | d) Do one of the following: 44 | 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 45 | 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. 46 | e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 47 | 5. Combined Libraries. 48 | You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: 49 | 50 | a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. 51 | b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 52 | 6. Revised Versions of the GNU Lesser General Public License. 53 | The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 54 | 55 | Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. 56 | 57 | If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. 58 | -------------------------------------------------------------------------------- /js/LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /js/server/DeviceServer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | var settings = require('../settings.js'); 19 | var CryptoLib = require('../lib/ICrypto.js'); 20 | var SparkCore = require('../clients/SparkCore.js'); 21 | var EventPublisher = require('../lib/EventPublisher.js'); 22 | var utilities = require('../lib/utilities.js'); 23 | var logger = require('../lib/logger.js'); 24 | var crypto = require('crypto'); 25 | var ursa = require('ursa'); 26 | var when = require('when'); 27 | var path = require('path'); 28 | var net = require('net'); 29 | var fs = require('fs'); 30 | 31 | 32 | var DeviceServer = function (options) { 33 | this.options = options; 34 | this.options = options || {}; 35 | settings.coreKeysDir = this.options.coreKeysDir = this.options.coreKeysDir || settings.coreKeysDir; 36 | 37 | this._allCoresByID = {}; 38 | this._attribsByID = {}; 39 | this._allIDs = {}; 40 | 41 | this.init(); 42 | }; 43 | 44 | DeviceServer.prototype = { 45 | _allCoresByID: null, 46 | _attribsByID: null, 47 | _allIDs: null, 48 | 49 | 50 | init: function () { 51 | this.loadCoreData(); 52 | }, 53 | 54 | addCoreKey: function(coreid, public_key) { 55 | try{ 56 | var fullPath = path.join(this.options.coreKeysDir, coreid + ".pub.pem"); 57 | fs.writeFileSync(fullPath, public_key); 58 | return true; 59 | } 60 | catch (ex) { 61 | logger.error("Error saving new core key ", ex); 62 | } 63 | return false; 64 | }, 65 | 66 | 67 | loadCoreData: function () { 68 | var attribsByID = {}; 69 | 70 | if (!fs.existsSync(this.options.coreKeysDir)) { 71 | console.log("core keys directory didn't exist, creating... " + this.options.coreKeysDir); 72 | fs.mkdirSync(this.options.coreKeysDir); 73 | } 74 | 75 | var files = fs.readdirSync(this.options.coreKeysDir); 76 | for (var i = 0; i < files.length; i++) { 77 | var filename = files[i], 78 | fullPath = path.join(this.options.coreKeysDir, filename), 79 | ext = utilities.getFilenameExt(filename), 80 | id = utilities.filenameNoExt(utilities.filenameNoExt(filename)); 81 | 82 | if (ext == ".pem") { 83 | console.log("found " + id); 84 | this._allIDs[id] = true; 85 | 86 | if (!attribsByID[id]) { 87 | var core = {} 88 | core.coreID = id; 89 | attribsByID[id] = core; 90 | } 91 | } 92 | else if (ext == ".json") { 93 | try { 94 | var contents = fs.readFileSync(fullPath); 95 | var core = JSON.parse(contents); 96 | core.coreID = core.coreID || id; 97 | attribsByID[core.coreID ] = core; 98 | 99 | console.log("found " + core.coreID); 100 | this._allIDs[core.coreID ] = true; 101 | } 102 | catch (ex) { 103 | logger.error("Error loading core file " + filename); 104 | } 105 | } 106 | } 107 | 108 | this._attribsByID = attribsByID; 109 | }, 110 | 111 | saveCoreData: function (coreid, attribs) { 112 | try { 113 | //assert basics 114 | attribs = attribs || {}; 115 | // attribs["coreID"] = coreid; 116 | 117 | var jsonStr = JSON.stringify(attribs, null, 2); 118 | if (!jsonStr) { 119 | return false; 120 | } 121 | 122 | var fullPath = path.join(this.options.coreKeysDir, coreid + ".json"); 123 | fs.writeFileSync(fullPath, jsonStr); 124 | return true; 125 | } 126 | catch (ex) { 127 | logger.error("Error saving core data ", ex); 128 | } 129 | return false; 130 | }, 131 | 132 | getCore: function (coreid) { 133 | return this._allCoresByID[coreid]; 134 | }, 135 | getCoreAttributes: function (coreid) { 136 | //assert this exists and is set properly when asked. 137 | this._attribsByID[coreid] = this._attribsByID[coreid] || {}; 138 | //this._attribsByID[coreid]["coreID"] = coreid; 139 | 140 | return this._attribsByID[coreid]; 141 | }, 142 | setCoreAttribute: function (coreid, name, value) { 143 | this._attribsByID[coreid] = this._attribsByID[coreid] || {}; 144 | this._attribsByID[coreid][name] = value; 145 | this.saveCoreData(coreid, this._attribsByID[coreid]); 146 | return true; 147 | }, 148 | getCoreByName: function (name) { 149 | //var cores = this._allCoresByID; 150 | var cores = this._attribsByID; 151 | for (var coreid in cores) { 152 | var attribs = cores[coreid]; 153 | if (attribs && (attribs.name == name)) { 154 | return this._allCoresByID[coreid]; 155 | } 156 | } 157 | return null; 158 | }, 159 | 160 | /** 161 | * return all the cores we know exist 162 | * @returns {null} 163 | */ 164 | getAllCoreIDs: function () { 165 | return this._allIDs; 166 | }, 167 | 168 | /** 169 | * return all the cores that are connected 170 | * @returns {null} 171 | */ 172 | getAllCores: function () { 173 | return this._allCoresByID; 174 | }, 175 | 176 | 177 | //id: core.coreID, 178 | //name: core.name || null, 179 | //last_app: core.last_flashed_app_name || null, 180 | //last_heard: null 181 | 182 | 183 | start: function () { 184 | global.settings = settings; 185 | 186 | // 187 | // Create our basic socket handler 188 | // 189 | 190 | var that = this, 191 | connId = 0, 192 | _cores = {}, 193 | server = net.createServer(function (socket) { 194 | process.nextTick(function () { 195 | try { 196 | var key = "_" + connId++; 197 | logger.log("Connection from: " + socket.remoteAddress + ", connId: " + connId); 198 | 199 | var core = new SparkCore(); 200 | core.socket = socket; 201 | core.startupProtocol(); 202 | core._connection_key = key; 203 | 204 | //TODO: expose to API 205 | 206 | 207 | _cores[key] = core; 208 | core.on('ready', function () { 209 | logger.log("Core online!"); 210 | var coreid = this.getHexCoreID(); 211 | that._allCoresByID[coreid] = core; 212 | that._attribsByID[coreid] = that._attribsByID[coreid] || { 213 | coreID: coreid, 214 | name: null, 215 | ip: this.getRemoteIPAddress(), 216 | product_id: this.spark_product_id, 217 | firmware_version: this.product_firmware_version 218 | }; 219 | }); 220 | core.on('disconnect', function (msg) { 221 | logger.log("Session ended for " + core._connection_key); 222 | delete _cores[key]; 223 | }); 224 | } 225 | catch (ex) { 226 | logger.error("core startup failed " + ex); 227 | } 228 | }); 229 | }); 230 | 231 | global.cores = _cores; 232 | global.publisher = new EventPublisher(); 233 | 234 | server.on('error', function () { 235 | logger.error("something blew up ", arguments); 236 | }); 237 | 238 | 239 | // 240 | // Load the provided key, or generate one 241 | // 242 | if (!fs.existsSync(settings.serverKeyFile)) { 243 | console.warn("Creating NEW server key"); 244 | var keys = ursa.generatePrivateKey(); 245 | 246 | 247 | var extIdx = settings.serverKeyFile.lastIndexOf("."); 248 | var derFilename = settings.serverKeyFile.substring(0, extIdx) + ".der"; 249 | var pubPemFilename = settings.serverKeyFile.substring(0, extIdx) + ".pub.pem"; 250 | 251 | fs.writeFileSync(settings.serverKeyFile, keys.toPrivatePem('binary')); 252 | fs.writeFileSync(pubPemFilename, keys.toPublicPem('binary')); 253 | 254 | //DER FORMATTED KEY for the core hardware 255 | //TODO: fs.writeFileSync(derFilename, keys.toPrivatePem('binary')); 256 | } 257 | 258 | 259 | // 260 | // Load our server key 261 | // 262 | console.info("Loading server key from " + settings.serverKeyFile); 263 | CryptoLib.loadServerKeys( 264 | settings.serverKeyFile, 265 | settings.serverKeyPassFile, 266 | settings.serverKeyPassEnvVar 267 | ); 268 | 269 | // 270 | // Wait for the keys to be ready, then start accepting connections 271 | // 272 | server.listen(settings.PORT, function () { 273 | logger.log("server started", { host: settings.HOST, port: settings.PORT }); 274 | }); 275 | 276 | 277 | } 278 | 279 | }; 280 | module.exports = DeviceServer; -------------------------------------------------------------------------------- /js/lib/ICrypto.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var crypto = require('crypto'); 20 | var ursa = require('ursa'); 21 | var CryptoStream = require("./CryptoStream"); 22 | var when = require('when'); 23 | var fs = require('fs'); 24 | var utilities = require("../lib/utilities.js"); 25 | var logger = require('../lib/logger.js'); 26 | 27 | 28 | /* 29 | install full openssl 30 | http://slproweb.com/products/Win32OpenSSL.html 31 | http://www.microsoft.com/en-us/download/confirmation.aspx?id=15336 32 | */ 33 | 34 | 35 | /** 36 | * Static wrapper for the various Crypto libraries, we should ensure everything is thread safe 37 | * @constructor 38 | */ 39 | var CryptoLib; 40 | module.exports = { 41 | classname: "CryptoLib", 42 | hashtype: 'sha1', 43 | signtype: 'sha256', 44 | 45 | randomBytes: function (size) { 46 | var deferred = when.defer(); 47 | crypto.randomBytes(size, function (ex, buf) { 48 | deferred.resolve(ex, buf); 49 | }); 50 | return deferred; 51 | }, 52 | 53 | 54 | getRandomBytes: function (size, callback) { 55 | crypto.randomBytes(size, callback); 56 | //crypto.randomBytes(256, function(ex, buf) { if (ex) throw ex; logger.log('Have %d bytes of random data: %s', buf.length, buf); }); 57 | }, 58 | 59 | // getRandomUINT32: function () { 60 | // //unsigned 4 byte (32 bit) integers max should be 2^32 61 | // var max = 4294967296 - 1; //Math.pow(2, 32) - 1 62 | // 63 | // //give us a number between 1 and uintmax 64 | // return Math.floor((Math.random() * max) + 1); 65 | // }, 66 | 67 | getRandomUINT16: function () { 68 | //unsigned 2 byte (16 bit) integers max should be 2^16 69 | var max = 65536 - 1; //Math.pow(2, 16) - 1 70 | 71 | //give us a number between 1 and uintmax 72 | return Math.floor((Math.random() * max) + 1); 73 | }, 74 | 75 | // 76 | //var alice = crypto.getDiffieHellman('modp5'); 77 | //var bob = crypto.getDiffieHellman('modp5'); 78 | // 79 | //alice.generateKeys(); 80 | //bob.generateKeys(); 81 | 82 | // init: function () { 83 | // logger.log("generating temporary debug server keys"); 84 | // CryptoLib.serverKeys = ursa.generatePrivateKey(); 85 | // 86 | // logger.log("generating temporary debug core keys"); 87 | // CryptoLib.coreKeys = ursa.generatePrivateKey(); 88 | // }, 89 | 90 | _serverKeys: null, 91 | setServerKeys: function (key) { 92 | if (ursa.isKey(key)) { 93 | logger.log("set server key"); 94 | CryptoLib._serverKeys = key; 95 | } 96 | else { 97 | logger.log("Hey! That's not a key BRO."); 98 | } 99 | }, 100 | getServerKeys: function () { 101 | if (!CryptoLib._serverKeys) { 102 | CryptoLib._serverKeys = ursa.generatePrivateKey(); 103 | } 104 | return CryptoLib._serverKeys; 105 | }, 106 | 107 | // serverKeys: (function () { 108 | // 109 | // 110 | // logger.log("generating temporary debug server keys"); 111 | // return ursa.generatePrivateKey(); 112 | // })(), 113 | 114 | _coreKeys: null, 115 | setCoreKeys: function (key) { 116 | if (ursa.isKey(key)) { 117 | logger.log("set Core key"); 118 | CryptoLib._coreKeys = key; 119 | } 120 | else { 121 | logger.log("Hey! That's not a key BRO."); 122 | } 123 | }, 124 | getCoreKeys: function () { 125 | if (!CryptoLib._coreKeys) { 126 | CryptoLib._coreKeys = ursa.generatePrivateKey(); 127 | } 128 | return CryptoLib._coreKeys; 129 | }, 130 | 131 | 132 | // coreKeys: (function () { 133 | // logger.log("generating temporary debug core keys"); 134 | // return ursa.generatePrivateKey(); 135 | // })(), 136 | 137 | createPublicKey: function (key) { 138 | return ursa.createPublicKey(key); 139 | }, 140 | 141 | encrypt: function (publicKey, data) { 142 | if (!publicKey) { 143 | return null; 144 | } 145 | if (!publicKey) { 146 | logger.error("encrypt: publicKey was null"); 147 | return null; 148 | } 149 | 150 | //logger.log('encrypting ', data.length, data.toString('hex')); 151 | return publicKey.encrypt(data, undefined, undefined, ursa.RSA_PKCS1_PADDING); 152 | }, 153 | decrypt: function (privateKey, data) { 154 | if (!privateKey) { 155 | privateKey = CryptoLib.getServerKeys(); 156 | } 157 | if (!privateKey) { 158 | logger.error("decrypt: privateKey was null"); 159 | return null; 160 | } 161 | if (!ursa.isPrivateKey(privateKey)) { 162 | logger.error("Trying to decrypt with non-private key"); 163 | return null; 164 | } 165 | 166 | //logger.log('decrypting ', data.length, data.toString('hex')); 167 | return privateKey.decrypt(data, undefined, undefined, ursa.RSA_PKCS1_PADDING); 168 | }, 169 | sign: function (privateKey, hash) { 170 | if (!privateKey) { 171 | privateKey = CryptoLib.getServerKeys(); 172 | } 173 | //return privateKey.sign(CryptoLib.signtype, hash); 174 | return privateKey.privateEncrypt(hash); 175 | }, 176 | verify: function (publicKey, hash, signature) { 177 | try { 178 | var plaintext = publicKey.publicDecrypt(signature); 179 | return utilities.bufferCompare(hash, plaintext); 180 | } 181 | catch (ex) { 182 | logger.error("hash verify error: " + ex); 183 | } 184 | return false; 185 | //return CryptoLib.serverKeys.verify(CryptoLib.signtype, hash, signature); 186 | }, 187 | 188 | 189 | /** 190 | * Returns a readable/writeable aes 128 cbc stream for communicating 191 | * with the spark core 192 | * @param sessionKey 193 | * @returns {*} 194 | * @constructor 195 | */ 196 | CreateAESCipher: function (sessionKey) { 197 | 198 | //The first 16 bytes (MSB first) will be the key, the next 16 bytes (MSB first) will be the initialization vector (IV), and the final 8 bytes (MSB first) will be the salt. 199 | var key = new Buffer(16); //just the key... +8); //key plus salt 200 | var iv = new Buffer(16); //initialization vector 201 | 202 | sessionKey.copy(key, 0, 0, 16); //copy the key 203 | //sessionKey.copy(key, 16, 32, 40); //append the 8-byte salt 204 | sessionKey.copy(iv, 0, 16, 32); //copy the iv 205 | 206 | //are we not be doing something with the salt here? 207 | 208 | return crypto.createCipheriv('aes-128-cbc', key, iv); 209 | }, 210 | 211 | 212 | //_transform 213 | 214 | /** 215 | * Returns a readable/writeable aes 128 cbc stream for communicating 216 | * with the spark core 217 | * @param sessionKey 218 | * @returns {*} 219 | * @constructor 220 | */ 221 | CreateAESDecipher: function (sessionKey) { 222 | //The first 16 bytes (MSB first) will be the key, the next 16 bytes (MSB first) will be the initialization vector (IV), and the final 8 bytes (MSB first) will be the salt. 223 | var key = new Buffer(16); //just the key... +8); //key plus salt 224 | var iv = new Buffer(16); //initialization vector 225 | 226 | sessionKey.copy(key, 0, 0, 16); //copy the key 227 | //sessionKey.copy(key, 16, 32, 40); //append the 8-byte salt? 228 | sessionKey.copy(iv, 0, 16, 32); //copy the iv 229 | 230 | //are we not be doing something with the salt here? 231 | 232 | return crypto.createDecipheriv('aes-128-cbc', key, iv); 233 | }, 234 | 235 | 236 | CreateAESCipherStream: function (sessionKey) { 237 | //The first 16 bytes (MSB first) will be the key, the next 16 bytes (MSB first) will be the initialization vector (IV), and the final 8 bytes (MSB first) will be the salt. 238 | var key = new Buffer(16); //just the key... +8); //key plus salt 239 | var iv = new Buffer(16); //initialization vector 240 | 241 | sessionKey.copy(key, 0, 0, 16); //copy the key 242 | sessionKey.copy(iv, 0, 16, 32); //copy the iv 243 | 244 | return new CryptoStream({ 245 | key: key, 246 | iv: iv, 247 | encrypt: true 248 | }); 249 | }, 250 | 251 | /** 252 | * Returns a readable/writeable aes 128 cbc stream for communicating 253 | * with the spark core 254 | * @param sessionKey 255 | * @returns {*} 256 | * @constructor 257 | */ 258 | CreateAESDecipherStream: function (sessionKey) { 259 | //The first 16 bytes (MSB first) will be the key, the next 16 bytes (MSB first) will be the initialization vector (IV), and the final 8 bytes (MSB first) will be the salt. 260 | var key = new Buffer(16); //just the key... +8); //key plus salt 261 | var iv = new Buffer(16); //initialization vector 262 | 263 | sessionKey.copy(key, 0, 0, 16); //copy the key 264 | sessionKey.copy(iv, 0, 16, 32); //copy the iv 265 | 266 | return new CryptoStream({ 267 | key: key, 268 | iv: iv, 269 | encrypt: false 270 | }); 271 | }, 272 | 273 | 274 | createHmacDigest: function (ciphertext, key) { 275 | var hmac = crypto.createHmac('sha1', key); 276 | hmac.update(ciphertext); 277 | return hmac.digest(); 278 | }, 279 | 280 | loadServerPublicKey: function(filename) { 281 | return utilities.promiseDoFile(filename,function (data) { 282 | //CryptoLib.setServerKeys(ursa.createPublicKey(data)); 283 | CryptoLib.setServerKeys(ursa.createKey(data)); 284 | logger.log("server public key is: ", CryptoLib.getServerKeys().toPublicPem('binary')); 285 | return true; 286 | }).promise; 287 | }, 288 | 289 | loadServerKeys: function (filename, passFile, envVar) { 290 | 291 | var password = null; 292 | if (envVar && (envVar != '')) { 293 | password = process.env[envVar]; 294 | 295 | if (!password) { 296 | logger.error("Certificate Password Environment Variable specified but not available"); 297 | } 298 | else { 299 | password = new Buffer(password, 'base64'); 300 | } 301 | } 302 | else if (passFile && (passFile != '') && (fs.existsSync(passFile))) { 303 | password = fs.readFileSync(passFile); 304 | 305 | if (!password) { 306 | logger.error("Certificate Password File specified but was empty"); 307 | } 308 | } 309 | if (!password) { 310 | password = undefined; 311 | } 312 | 313 | //synchronous version 314 | if (!fs.existsSync(filename)) { return false; } 315 | 316 | var data = fs.readFileSync(filename); 317 | var keys = ursa.createPrivateKey(data, password); 318 | 319 | CryptoLib.setServerKeys(keys); 320 | logger.log("server public key is: ", keys.toPublicPem('binary')); 321 | return true; 322 | 323 | 324 | // return utilities.promiseDoFile(filename,function (data) { 325 | // CryptoLib.setServerKeys(ursa.createPrivateKey(data, password)); 326 | // logger.log("server public key is: ", CryptoLib.getServerKeys().toPublicPem('binary')); 327 | // return true; 328 | // }).promise; 329 | }, 330 | 331 | 332 | foo: null 333 | }; 334 | CryptoLib = module.exports; 335 | -------------------------------------------------------------------------------- /js/lib/utilities.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013-2014 Spark Labs, Inc. All rights reserved. - https://www.spark.io/ 3 | * 4 | * This file is part of the Spark-protocol module 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License version 3 8 | * as published by the Free Software Foundation. 9 | * 10 | * Spark-protocol is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Spark-protocol. If not, see . 17 | * 18 | * You can download the source here: https://github.com/spark/spark-protocol 19 | */ 20 | 21 | 22 | var logger = require('./logger.js'); 23 | var when = require('when'); 24 | var extend = require('xtend'); 25 | var fs = require('fs'); 26 | var path = require('path'); 27 | var settings = require('../settings.js'); 28 | var ursa = require('ursa'); 29 | 30 | var utilities; 31 | module.exports = { 32 | 33 | /** 34 | * ensures the function in the provided scope 35 | * @param fn 36 | * @param scope 37 | * @returns {Function} 38 | */ 39 | proxy: function (fn, scope) { 40 | return function () { 41 | try { 42 | return fn.apply(scope, arguments); 43 | } 44 | catch (ex) { 45 | logger.error(ex); 46 | logger.error(ex.stack); 47 | logger.log('error bubbled up ' + ex); 48 | } 49 | }; 50 | }, 51 | 52 | /** 53 | * Surely there is a better way to do this. 54 | * NOTE! This function does NOT short-circuit when an in-equality is detected. This is 55 | * to avoid timing attacks. 56 | * @param left 57 | * @param right 58 | */ 59 | bufferCompare: function (left, right) { 60 | if ((left == null) && (right == null)) { 61 | return true; 62 | } 63 | else if ((left == null) || (right == null)) { 64 | return false; 65 | } 66 | 67 | if (!Buffer.isBuffer(left)) { 68 | left = new Buffer(left); 69 | } 70 | if (!Buffer.isBuffer(right)) { 71 | right = new Buffer(right); 72 | } 73 | 74 | //logger.log('left: ', left.toString('hex'), ' right: ', right.toString('hex')); 75 | 76 | var same = (left.length == right.length), 77 | i = 0, 78 | max = left.length; 79 | 80 | while (i < max) { 81 | same &= (left[i] == right[i]); 82 | i++; 83 | } 84 | 85 | return same; 86 | }, 87 | 88 | /** 89 | * Iterates over the properties of the right object, checking to make 90 | * sure the properties on the left object match. 91 | * @param left 92 | * @param right 93 | */ 94 | leftHasRightFilter: function (left, right) { 95 | if (!left && !right) { 96 | return true; 97 | } 98 | var matches = true; 99 | 100 | for (var prop in right) { 101 | if (!right.hasOwnProperty(prop)) { 102 | continue; 103 | } 104 | matches &= (left[prop] == right[prop]); 105 | } 106 | return matches; 107 | }, 108 | 109 | promiseDoFile: function (filename, callback) { 110 | var deferred = when.defer(); 111 | fs.exists(filename, function (exists) { 112 | if (!exists) { 113 | logger.error("File: " + filename + " doesn't exist."); 114 | deferred.reject(); 115 | } 116 | else { 117 | fs.readFile(filename, function (err, data) { 118 | if (err) { 119 | logger.error("error reading " + filename, err); 120 | deferred.reject(); 121 | } 122 | 123 | if (callback(data)) { 124 | deferred.resolve(); 125 | } 126 | }); 127 | } 128 | }); 129 | return deferred; 130 | }, 131 | 132 | promiseStreamFile: function (filename) { 133 | var deferred = when.defer(); 134 | try { 135 | fs.exists(filename, function (exists) { 136 | if (!exists) { 137 | logger.error("File: " + filename + " doesn't exist."); 138 | deferred.reject(); 139 | } 140 | else { 141 | var readStream = fs.createReadStream(filename); 142 | 143 | //TODO: catch can't read file stuff. 144 | 145 | deferred.resolve(readStream); 146 | } 147 | }); 148 | } 149 | catch (ex) { 150 | logger.error('promiseStreamFile: ' + ex); 151 | deferred.reject("promiseStreamFile said " + ex); 152 | } 153 | return deferred; 154 | }, 155 | 156 | bufferToHexString: function (buf) { 157 | if (!buf || (buf.length <= 0)) { 158 | return null; 159 | } 160 | 161 | var r = []; 162 | for (var i = 0; i < buf.length; i++) { 163 | if (buf[i] < 10) { 164 | r.push('0'); 165 | } 166 | r.push(buf[i].toString(16)); 167 | } 168 | return r.join(''); 169 | }, 170 | 171 | toHexString: function (val) { 172 | return ((val < 10) ? '0' : '') + val.toString(16); 173 | }, 174 | 175 | arrayContains: function (arr, obj) { 176 | if (arr && (arr.length > 0)) { 177 | for (var i = 0; i < arr.length; i++) { 178 | if (arr[i] == obj) { 179 | return true; 180 | } 181 | } 182 | } 183 | return false; 184 | }, 185 | 186 | arrayContainsLower: function (arr, str) { 187 | if (arr && (arr.length > 0)) { 188 | str = str.toLowerCase(); 189 | 190 | for (var i = 0; i < arr.length; i++) { 191 | var key = arr[i]; 192 | if (!key) { 193 | continue; 194 | } 195 | 196 | if (key.toLowerCase() == str) { 197 | return true; 198 | } 199 | } 200 | } 201 | return false; 202 | }, 203 | 204 | /** 205 | * filename should be relative from wherever we're running the require, sorry! 206 | * @param filename 207 | * @returns {*} 208 | */ 209 | tryRequire: function (filename) { 210 | try { 211 | return require(filename); 212 | } 213 | catch (ex) { 214 | logger.error("tryRequire error " + filename, ex); 215 | } 216 | return null; 217 | }, 218 | 219 | tryMixin: function (destObj, newObj) { 220 | try { 221 | return extend(destObj, newObj); 222 | } 223 | catch (ex) { 224 | logger.error("tryMixin error" + ex); 225 | } 226 | return destObj; 227 | }, 228 | 229 | /** 230 | * recursively create a list of all files in a directory and all subdirectories 231 | * @param dir 232 | * @param search 233 | * @returns {Array} 234 | */ 235 | recursiveFindFiles: function (dir, search, excludedDirs) { 236 | excludedDirs = excludedDirs || []; 237 | 238 | var result = []; 239 | var files = fs.readdirSync(dir); 240 | for (var i = 0; i < files.length; i++) { 241 | var fullpath = path.join(dir, files[i]); 242 | var stat = fs.statSync(fullpath); 243 | if (stat.isDirectory() && (!excludedDirs.contains(fullpath))) { 244 | result = result.concat(utilities.recursiveFindFiles(fullpath, search)); 245 | } 246 | else if (!search || (fullpath.indexOf(search) >= 0)) { 247 | result.push(fullpath); 248 | } 249 | } 250 | return result; 251 | }, 252 | 253 | /** 254 | * handle an array of stuff, in order, and don't stop if something fails. 255 | * @param arr 256 | * @param handler 257 | * @returns {promise|*|Function|Promise|when.promise} 258 | */ 259 | promiseDoAllSequentially: function (arr, handler) { 260 | var tmp = when.defer(); 261 | var index = -1; 262 | var results = []; 263 | 264 | var doNext = function () { 265 | try { 266 | index++; 267 | 268 | if (index > arr.length) { 269 | tmp.resolve(results); 270 | } 271 | 272 | var file = arr[index]; 273 | var promise = handler(file); 274 | if (promise) { 275 | when(promise).then(function (result) { 276 | results.push(result); 277 | process.nextTick(doNext); 278 | }, function () { 279 | process.nextTick(doNext); 280 | }); 281 | // when(promise).ensure(function () { 282 | // process.nextTick(doNext); 283 | // }); 284 | } 285 | else { 286 | //logger.log('skipping bad promise'); 287 | process.nextTick(doNext); 288 | } 289 | } 290 | catch (ex) { 291 | logger.error("pdas error: " + ex); 292 | } 293 | }; 294 | 295 | process.nextTick(doNext); 296 | 297 | return tmp.promise; 298 | }, 299 | 300 | endsWith: function(str, sub) { 301 | if (!str || !sub) { 302 | return false; 303 | } 304 | 305 | var idx = str.indexOf(sub); 306 | return (idx == (str.length - sub.length)); 307 | }, 308 | getFilenameExt: function (filename) { 309 | if (!filename || (filename.length === 0)) { 310 | return filename; 311 | } 312 | 313 | var idx = filename.lastIndexOf('.'); 314 | if (idx >= 0) { 315 | return filename.substr(idx); 316 | } 317 | else { 318 | return filename; 319 | } 320 | }, 321 | filenameNoExt: function (filename) { 322 | if (!filename || (filename.length === 0)) { 323 | return filename; 324 | } 325 | 326 | var idx = filename.lastIndexOf('.'); 327 | if (idx >= 0) { 328 | return filename.substr(0, idx); 329 | } 330 | else { 331 | return filename; 332 | } 333 | }, 334 | 335 | get_core_key: function(coreid, callback) { 336 | var keyFile = path.join(global.settings.coreKeysDir || settings.coreKeysDir, coreid + ".pub.pem"); 337 | if (!fs.existsSync(keyFile)) { 338 | logger.log("Expected to find public key for core " + coreid + " at " + keyFile); 339 | callback(null); 340 | } 341 | else { 342 | var keyStr = fs.readFileSync(keyFile).toString(); 343 | var public_key = ursa.createPublicKey(keyStr, 'binary'); 344 | callback(public_key); 345 | } 346 | }, 347 | 348 | save_handshake_key: function(coreid, pem) { 349 | var keyFile = path.join(global.settings.coreKeysDir || settings.coreKeysDir, coreid + "_handshake.pub.pem"); 350 | if (!fs.existsSync(keyFile)) { 351 | 352 | logger.log("I saved a key given during the handshake, (remove the _handshake from the filename to accept this device)", keyFile); 353 | fs.writeFileSync(keyFile, pem); 354 | } 355 | }, 356 | 357 | /** 358 | * base64 encodes raw binary into 359 | * "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHzg9dPG03Kv4NkS3N0xJfU8lT1M+s9HTs75DE1tpwXfU4GkfaLLr04j6jFpMeeggKCgWJsKyIAR9CNlVHC1IUYeejEJQCe6JReTQlq9F6bioK84nc9QsFTpiCIqeTAZE4t6Di5pF8qrUgQvREHrl4Nw0DR7ECODgxc/r5+XFh9wIDAQAB" 360 | * then formats into PEM format: 361 | * 362 | * //-----BEGIN PUBLIC KEY----- 363 | * //MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHzg9dPG03Kv4NkS3N0xJfU8lT 364 | * //1M+s9HTs75DE1tpwXfU4GkfaLLr04j6jFpMeeggKCgWJsKyIAR9CNlVHC1IUYeej 365 | * //EJQCe6JReTQlq9F6bioK84nc9QsFTpiCIqeTAZE4t6Di5pF8qrUgQvREHrl4Nw0D 366 | * //R7ECODgxc/r5+XFh9wIDAQAB 367 | * //-----END PUBLIC KEY----- 368 | * 369 | * @param buf 370 | * @returns {*} 371 | */ 372 | convertDERtoPEM: function(buf) { 373 | if (!buf || (buf.length == 0)) { 374 | return null; 375 | } 376 | var str; 377 | try { 378 | str = buf.toString('base64'); 379 | var lines = [ 380 | "-----BEGIN PUBLIC KEY-----" 381 | ]; 382 | var i = 0; 383 | while (i < str.length) { 384 | var chunk = str.substr(i, 64); 385 | i += chunk.length; 386 | lines.push(chunk); 387 | } 388 | lines.push("-----END PUBLIC KEY-----"); 389 | return lines.join("\n"); 390 | } 391 | catch(ex) { 392 | logger.error("error converting DER to PEM, was: " + str); 393 | } 394 | return null; 395 | }, 396 | 397 | foo: null 398 | }; 399 | utilities = module.exports; 400 | -------------------------------------------------------------------------------- /js/lib/Messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013-2014 Spark Labs, Inc. All rights reserved. - https://www.spark.io/ 3 | * 4 | * This file is part of the Spark-protocol module 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License version 3 8 | * as published by the Free Software Foundation. 9 | * 10 | * Spark-protocol is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Spark-protocol. If not, see . 17 | * 18 | * You can download the source here: https://github.com/spark/spark-protocol 19 | */ 20 | 21 | 22 | var fs = require('fs'); 23 | var settings = require('../settings'); 24 | var Message = require('h5.coap').Message; 25 | var Option = require('h5.coap/lib/Option.js'); 26 | var buffers = require('h5.buffers'); 27 | var hogan = require('hogan.js'); 28 | var logger = require('../lib/logger.js'); 29 | 30 | /** 31 | * Interface for the Spark Core Messages 32 | * @constructor 33 | */ 34 | 35 | var StaticClass; 36 | module.exports = { 37 | classname: "Messages", 38 | _cache: {}, 39 | basePath: settings.message_template_dir, 40 | 41 | 42 | //TODO: ADD 43 | //Describe (core-firmware) 44 | //Header: GET (T=CON, Code=0.01) 45 | //Uri-Path: "d" 46 | // 47 | //Core must respond as soon as possible with piggybacked Description response. 48 | 49 | Spec: { 50 | "Hello": { code: Message.Code.POST, uri: "h", type: Message.Type.NON, Response: "Hello" }, 51 | "KeyChange": { code: Message.Code.PUT, uri: "k", type: Message.Type.CON, Response: "KeyChanged" }, 52 | "UpdateBegin": { code: Message.Code.POST, uri: "u", type: Message.Type.CON, Response: "UpdateReady" }, 53 | "Chunk": { code: Message.Code.POST, uri: "c?{{{crc}}}", type: Message.Type.CON, Response: "ChunkReceived" }, 54 | "ChunkMissed": { code: Message.Code.GET, uri: "c", type: Message.Type.CON, Response: "ChunkMissedAck" }, 55 | 56 | "UpdateDone": { code: Message.Code.PUT, uri: "u", type: Message.Type.CON, Response: null }, 57 | "FunctionCall": { code: Message.Code.POST, uri: "f/{{name}}?{{{args}}}", type: Message.Type.CON, Response: "FunctionReturn" }, 58 | "VariableRequest": { code: Message.Code.GET, uri: "v/{{name}}", type: Message.Type.CON, Response: "VariableValue" }, 59 | 60 | "PrivateEvent": { code: Message.Code.POST, uri: "E/{{event_name}}", type: Message.Type.NON, Response: null }, 61 | "PublicEvent": { code: Message.Code.POST, uri: "e/{{event_name}}", type: Message.Type.NON, Response: null }, 62 | 63 | "Subscribe": { code: Message.Code.GET, uri: "e/{{event_name}}", type: Message.Type.CON, Response: null }, 64 | "Describe": { code: Message.Code.GET, uri: "d", type: Message.Type.CON, Response: "DescribeReturn" }, 65 | "GetTime": { code: Message.Code.GET, uri: "t", type: Message.Type.CON, Response: "GetTimeReturn" }, 66 | "RaiseYourHand": { code: Message.Code.PUT, uri: "s", type: Message.Type.CON, Response: "RaiseYourHandReturn" }, 67 | 68 | 69 | // "PrivateSubscribe": { code: Message.Code.GET, uri: "E/{{event_name}}", type: Message.Type.NON, Response: null }, 70 | 71 | "EventAck": { code: Message.Code.EMPTY, uri: null, type: Message.Type.ACK, Response: null }, 72 | "EventSlowdown": { code: Message.Code.BAD_REQUEST, uri: null, type: Message.Type.ACK, Response: null }, 73 | 74 | "SubscribeAck": { code: Message.Code.EMPTY, uri: null, type: Message.Type.ACK, Response: null }, 75 | "SubscribeFail": { code: Message.Code.BAD_REQUEST, uri: null, type: Message.Type.ACK, Response: null }, 76 | "GetTimeReturn": { code: Message.Code.CONTENT, type: Message.Type.ACK }, 77 | "RaiseYourHandReturn": { code: Message.Code.CHANGED, type: Message.Type.ACK }, 78 | "ChunkMissedAck": { code: Message.Code.EMPTY, type: Message.Type.ACK }, 79 | "DescribeReturn": { code: Message.Code.CHANGED, type: Message.Type.NON }, 80 | "KeyChanged": { code: Message.Code.CHANGED, type: Message.Type.NON }, 81 | "UpdateReady": { code: Message.Code.CHANGED, type: Message.Type.NON }, 82 | "ChunkReceived": { code: Message.Code.CHANGED, type: Message.Type.NON }, 83 | "ChunkReceivedError": { code: Message.Code.BAD_REQUEST, type: Message.Type.NON }, 84 | "FunctionReturn": { code: Message.Code.CHANGED, type: Message.Type.NON }, 85 | "FunctionReturnError": { code: Message.Code.BAD_REQUEST, type: Message.Type.NON }, 86 | "VariableValue": { code: Message.Code.CONTENT, type: Message.Type.ACK }, 87 | "VariableValueError": { code: Message.Code.BAD_REQUEST, type: Message.Type.NON }, 88 | "Ping": { code: Message.Code.EMPTY, type: Message.Type.CON }, 89 | "PingAck": { code: Message.Code.EMPTY, uri: null, type: Message.Type.ACK, Response: null }, 90 | "SocketPing": { code: Message.Code.EMPTY, type: Message.Type.NON } 91 | }, 92 | 93 | /** 94 | * Maps CODE + URL to MessageNames as they appear in "Spec" 95 | */ 96 | Routes: { 97 | // "/u": StaticClass.Spec.UpdateBegin, 98 | //... 99 | }, 100 | 101 | 102 | /** 103 | * does the special URL writing needed directly to the COAP message object, 104 | * since the URI requires non-text values 105 | * 106 | * @param showSignal 107 | * @returns {Function} 108 | */ 109 | raiseYourHandUrlGenerator: function (showSignal) { 110 | return function (msg) { 111 | var b = new Buffer(1); 112 | b.writeUInt8(showSignal ? 1 : 0, 0); 113 | 114 | msg.addOption(new Option(Message.Option.URI_PATH, new Buffer("s"))); 115 | msg.addOption(new Option(Message.Option.URI_QUERY, b)); 116 | return msg; 117 | }; 118 | }, 119 | 120 | 121 | getRouteKey: function (code, path) { 122 | var uri = code + path; 123 | 124 | //find the slash. 125 | var idx = uri.indexOf('/'); 126 | 127 | //this assumes all the messages are one character for now. 128 | //if we wanted to change this, we'd need to find the first non message char, "/" or "?", 129 | //or use the real coap parsing stuff 130 | return uri.substr(0, idx + 2); 131 | }, 132 | 133 | 134 | getRequestType: function (msg) { 135 | var uri = StaticClass.getRouteKey(msg.getCode(), msg.getUriPath()); 136 | return StaticClass.Routes[uri]; 137 | }, 138 | 139 | getResponseType: function (name) { 140 | var spec = StaticClass.Spec[name]; 141 | return (spec) ? spec.Response : null; 142 | }, 143 | 144 | statusIsOkay: function (msg) { 145 | return (msg.getCode() < Message.Code.BAD_REQUEST); 146 | }, 147 | 148 | 149 | // DataTypes: { 150 | //1: BOOLEAN (false=0, true=1) 151 | //2: INTEGER (int32) 152 | //4: OCTET STRING (arbitrary bytes) 153 | //9: REAL (double) 154 | // }, 155 | 156 | _started: false, 157 | init: function () { 158 | if (StaticClass._started) { 159 | return; 160 | } 161 | 162 | logger.log("static class init!"); 163 | 164 | for (var p in StaticClass.Spec) { 165 | var obj = StaticClass.Spec[p]; 166 | 167 | if (obj && obj.uri && (obj.uri.indexOf("{") >= 0)) { 168 | obj.template = hogan.compile(obj.uri); 169 | } 170 | 171 | //GET/u 172 | //PUT/u 173 | //... etc. 174 | if (obj.uri) { 175 | //see what it looks like without params 176 | var uri = (obj.template) ? obj.template.render({}) : obj.uri; 177 | var u = StaticClass.getRouteKey(obj.code, "/" + uri); 178 | 179 | this.Routes[u] = p; 180 | } 181 | 182 | 183 | } 184 | StaticClass._started = true; 185 | }, 186 | 187 | 188 | /** 189 | * 190 | * @param name 191 | * @param id - must be an unsigned 16 bit integer 192 | * @param params 193 | * @param data 194 | * @param token - helps us associate responses w/ requests 195 | * @param onError 196 | * @returns {*} 197 | */ 198 | wrap: function (name, id, params, data, token, onError) { 199 | var spec = StaticClass.Spec[name]; 200 | if (!spec) { 201 | if (onError) { 202 | onError("Unknown Message Type"); 203 | } 204 | return null; 205 | } 206 | 207 | // Setup the Message 208 | var msg = new Message(); 209 | 210 | // Format our url 211 | var uri = spec.uri; 212 | if (params && params._writeCoapUri) { 213 | // for our messages that have nitty gritty urls that require raw bytes and no strings. 214 | msg = params._writeCoapUri(msg); 215 | uri = null; 216 | } 217 | else if (params && spec.template) { 218 | uri = spec.template.render(params); 219 | } 220 | 221 | if (uri) { 222 | msg.setUri(uri); 223 | } 224 | msg.setId(id); 225 | 226 | if (token !== null) { 227 | if (!Buffer.isBuffer(token)) { 228 | var buf = new Buffer(1); 229 | buf[0] = token; 230 | token = buf; 231 | } 232 | msg.setToken(token); 233 | } 234 | msg.setCode(spec.code); 235 | msg.setType(spec.type); 236 | 237 | // Set our payload 238 | if (data) { 239 | msg.setPayload(data); 240 | } 241 | 242 | if (params && params._raw) { 243 | params._raw(msg); 244 | } 245 | 246 | return msg.toBuffer(); 247 | }, 248 | 249 | unwrap: function (data) { 250 | try { 251 | if (data) { 252 | return Message.fromBuffer(data); 253 | } 254 | } 255 | catch (ex) { 256 | logger.error("Coap Error: " + ex); 257 | } 258 | 259 | return null; 260 | }, 261 | 262 | 263 | //http://en.wikipedia.org/wiki/X.690 264 | //=== TYPES: SUBSET OF ASN.1 TAGS === 265 | // 266 | //1: BOOLEAN (false=0, true=1) 267 | //2: INTEGER (int32) 268 | //4: OCTET STRING (arbitrary bytes) 269 | //5: NULL (void for return value only) 270 | //9: REAL (double) 271 | 272 | /** 273 | * Translates the integer variable type enum to user friendly string types 274 | * @param varState 275 | * @returns {*} 276 | * @constructor 277 | */ 278 | TranslateIntTypes: function (varState) { 279 | if (!varState) { 280 | return varState; 281 | } 282 | 283 | for (var varName in varState) { 284 | if (!varState.hasOwnProperty(varName)) { 285 | continue; 286 | } 287 | 288 | var intType = varState[varName]; 289 | if (typeof intType === "number") { 290 | var str = StaticClass.getNameFromTypeInt(intType); 291 | 292 | if (str != null) { 293 | varState[varName] = str; 294 | } 295 | } 296 | } 297 | return varState; 298 | }, 299 | 300 | getNameFromTypeInt: function (typeInt) { 301 | switch (typeInt) { 302 | case 1: 303 | return "bool"; 304 | case 2: 305 | return "int32"; 306 | case 4: 307 | return "string"; 308 | case 5: 309 | return "null"; 310 | case 9: 311 | return "double"; 312 | 313 | default: 314 | logger.error("asked for unknown type: " + typeInt); 315 | return null; 316 | } 317 | }, 318 | 319 | TryFromBinary: function (buf, name) { 320 | var result = null; 321 | try { 322 | result = StaticClass.FromBinary(buf, name); 323 | } 324 | catch (ex) { 325 | 326 | } 327 | return result; 328 | }, 329 | 330 | FromBinary: function (buf, name) { 331 | 332 | //logger.log('converting a ' + name + ' FromBinary input was ' + buf); 333 | 334 | if (!Buffer.isBuffer(buf)) { 335 | buf = new Buffer(buf); 336 | } 337 | 338 | var r = new buffers.BufferReader(buf); 339 | var v; 340 | 341 | switch (name) { 342 | case "bool": 343 | v = (r.shiftByte() != 0); 344 | break; 345 | case "crc": 346 | v = r.shiftUInt32(); 347 | break; 348 | case "uint32": 349 | v = r.shiftUInt32(); 350 | break; 351 | case "uint16": 352 | v = r.shiftUInt16(); 353 | break; 354 | 355 | case "int32": 356 | case "number": 357 | v = r.shiftInt32(); 358 | break; 359 | case "float": 360 | v = r.shiftFloat(); 361 | break; 362 | case "double": 363 | v = r.shiftDouble(true); //doubles on the core are little-endian 364 | break; 365 | case "buffer": 366 | v = buf; 367 | break; 368 | case "string": 369 | default: 370 | v = buf.toString(); 371 | break; 372 | } 373 | //logger.log('FromBinary val is: "', buf.toString('hex'), '" type is ', name, ' converted to ', v); 374 | return v; 375 | }, 376 | 377 | ToBinary: function (val, name, b) { 378 | name = name || (typeof val); 379 | 380 | // if ((name === "number") && (val % 1 != 0)) { 381 | // name = "double"; 382 | // } 383 | 384 | b = b || new buffers.BufferBuilder(); 385 | 386 | switch (name) { 387 | 388 | case "uint32": 389 | case "crc": 390 | b.pushUInt32(val); 391 | break; 392 | case "int32": 393 | b.pushInt32(val); 394 | break; 395 | case "number": 396 | //b.pushInt32(val); 397 | //break; 398 | case "double": 399 | b.pushDouble(val); 400 | break; 401 | case "buffer": 402 | b.pushBuffer(val); 403 | break; 404 | case "string": 405 | default: 406 | b.pushString(val || ""); 407 | break; 408 | } 409 | 410 | //logger.log('converted a ' + name + ' ' + val + ' ToBinary output was ' + b.toBuffer().toString('hex')); 411 | return b.toBuffer(); 412 | }, 413 | 414 | buildArguments: function (obj, args) { 415 | try { 416 | var b = new buffers.BufferBuilder(); 417 | for (var i = 0; i < args.length; i++) { 418 | if (i > 0) { 419 | StaticClass.ToBinary("&", "string", b); 420 | } 421 | 422 | var p = args[i]; 423 | if (!p) { 424 | continue; 425 | } 426 | 427 | var name = p[0] || Object.keys(obj)[0]; //or... just grab the first key. 428 | var type = p[1]; 429 | var val = obj[name]; 430 | 431 | StaticClass.ToBinary(val, type, b); 432 | } 433 | //logger.log('function arguments were ', b.toBuffer().toString('hex')); 434 | return b.toBuffer(); 435 | } 436 | catch (ex) { 437 | logger.error("buildArguments: ", ex); 438 | } 439 | return null; 440 | }, 441 | parseArguments: function (args, desc) { 442 | try { 443 | if (!args || (args.length != desc.length)) { 444 | return null; 445 | } 446 | 447 | var results = []; 448 | for (var i = 0; i < desc.length; i++) { 449 | var p = desc[i]; 450 | if (!p) { 451 | continue; 452 | } 453 | 454 | //desc -> [ [ name, type ], ... ] 455 | var type = p[1]; 456 | var val = (i < args.length) ? args[i] : null; 457 | 458 | results.push( 459 | StaticClass.FromBinary(new Buffer(val, 'binary'), type) 460 | ); 461 | } 462 | 463 | return results; 464 | } 465 | catch (ex) { 466 | logger.error("parseArguments: ", ex); 467 | } 468 | 469 | return null; 470 | }, 471 | 472 | 473 | foo: null 474 | }; 475 | StaticClass = module.exports; 476 | StaticClass.init(); 477 | //module.exports = StaticClass; 478 | -------------------------------------------------------------------------------- /js/lib/Flasher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var when = require("when"); 20 | var extend = require("xtend"); 21 | var IFlasher = require("./IFlasher"); 22 | var messages = require('./Messages.js'); 23 | var logger = require('../lib/logger.js'); 24 | var utilities = require("../lib/utilities.js"); 25 | var BufferStream = require("./BufferStream.js"); 26 | 27 | var buffers = require('h5.buffers'); 28 | var Message = require('h5.coap').Message; 29 | var Option = require('h5.coap/lib/Option.js'); 30 | var crc32 = require('buffer-crc32'); 31 | 32 | 33 | // 34 | //UpdateBegin — sent by Server to initiate an OTA firmware update 35 | //UpdateReady — sent by Core to indicate readiness to receive firmware chunks 36 | //Chunk — sent by Server to send chunks of a firmware binary to Core 37 | //ChunkReceived — sent by Core to respond to each chunk, indicating the CRC of the received chunk data. if Server receives CRC that does not match the chunk just sent, that chunk is sent again 38 | //UpdateDone — sent by Server to indicate all firmware chunks have been sent 39 | // 40 | 41 | var Flasher = function(options) { 42 | this.chunk_size = Flasher.CHUNK_SIZE; 43 | }; 44 | 45 | 46 | Flasher.stages = { PREPARE: 0, BEGIN_UPDATE: 1, SEND_FILE: 2, TEARDOWN: 3, DONE: 4 }; 47 | //Flasher.prototype = Object.create(IFlasher.prototype, { constructor: { value: IFlasher }}); 48 | Flasher.CHUNK_SIZE = 256; 49 | Flasher.MAX_CHUNK_SIZE = 594; 50 | Flasher.MAX_MISSED_CHUNKS = 10; 51 | 52 | Flasher.prototype = extend(IFlasher.prototype, { 53 | client: null, 54 | stage: 0, 55 | _protocolVersion: 0, 56 | _numChunksMissed: 0, 57 | _waitForChunksTimer: null, 58 | 59 | lastCrc: null, 60 | chunk: null, 61 | 62 | // 63 | // OTA tweaks 64 | // 65 | _fastOtaEnabled: false, 66 | _ignoreMissedChunks: false, 67 | 68 | 69 | startFlashFile: function (filename, client, onSuccess, onError) { 70 | this.filename = filename; 71 | this.client = client; 72 | this.onSuccess = onSuccess; 73 | this.onError = onError; 74 | 75 | this.startTime = new Date(); 76 | 77 | if (this.claimConnection()) { 78 | this.stage = 0; 79 | this.nextStep(); 80 | } 81 | }, 82 | startFlashBuffer: function (buffer, client, onSuccess, onError, onStarted) { 83 | this.fileBuffer = buffer; 84 | this.client = client; 85 | this.onSuccess = onSuccess; 86 | this.onError = onError; 87 | this.onStarted = onStarted; 88 | 89 | if (this.claimConnection()) { 90 | this.stage = 0; 91 | this.nextStep(); 92 | } 93 | }, 94 | 95 | setChunkSize: function(size) { 96 | this.chunk_size = size || Flasher.CHUNK_SIZE; 97 | }, 98 | 99 | claimConnection: function() { 100 | //suspend all other messages to the core 101 | if (!this.client.takeOwnership(this)) { 102 | this.failed("Flasher: Unable to take ownership"); 103 | return false; 104 | } 105 | return true; 106 | }, 107 | 108 | nextStep: function (data) { 109 | var that = this; 110 | process.nextTick(function () { that._nextStep(data); }); 111 | }, 112 | 113 | _nextStep: function (data) { 114 | switch (this.stage) { 115 | case Flasher.stages.PREPARE: 116 | this.prepare(); 117 | break; 118 | 119 | case Flasher.stages.BEGIN_UPDATE: 120 | this.begin_update(); 121 | break; 122 | case Flasher.stages.SEND_FILE: 123 | this.send_file(); 124 | break; 125 | 126 | case Flasher.stages.TEARDOWN: 127 | this.teardown(); 128 | break; 129 | 130 | case Flasher.stages.DONE: 131 | break; 132 | 133 | default: 134 | logger.log("Flasher, what stage was this? " + this.stage); 135 | break; 136 | } 137 | }, 138 | 139 | failed: function (msg) { 140 | if (msg) { 141 | logger.error("Flasher failed " + msg); 142 | } 143 | 144 | this.cleanup(); 145 | 146 | if (this.onError) { 147 | this.onError(msg); 148 | } 149 | }, 150 | 151 | 152 | prepare: function () { 153 | 154 | 155 | //make sure we have a file, 156 | // open a stream to our file 157 | 158 | if (this.fileBuffer) { 159 | if (this.fileBuffer.length == 0) { 160 | this.failed("Flasher: this.fileBuffer was empty."); 161 | } 162 | else { 163 | if (!Buffer.isBuffer(this.fileBuffer)) { 164 | this.fileBuffer = new Buffer(this.fileBuffer); 165 | } 166 | 167 | this.fileStream = new BufferStream(this.fileBuffer); 168 | this._chunkIndex = -1; 169 | this.stage++; 170 | this.nextStep(); 171 | } 172 | } 173 | else { 174 | var that = this; 175 | utilities.promiseStreamFile(this.filename) 176 | .promise 177 | .then(function (readStream) { 178 | that.fileStream = readStream; 179 | that._chunkIndex = -1; 180 | that.stage++; 181 | that.nextStep(); 182 | }, that.failed); 183 | } 184 | 185 | this.chunk = null; 186 | this.lastCrc = null; 187 | 188 | //start listening for missed chunks before the update fully begins 189 | this.client.on("msg_chunkmissed", this.onChunkMissed.bind(this)); 190 | }, 191 | teardown: function () { 192 | this.cleanup(); 193 | 194 | //we succeeded, short-circuit the error function so we don't count more errors than appropriate. 195 | this.onError = function(err) { 196 | logger.log("Flasher - already succeeded, not an error: " + err); 197 | }; 198 | 199 | if (this.onSuccess) { 200 | var that = this; 201 | process.nextTick(function () { that.onSuccess(); }); 202 | } 203 | }, 204 | 205 | cleanup: function () { 206 | try { 207 | //resume all other messages to the core 208 | this.client.releaseOwnership(this); 209 | 210 | //release our file handle 211 | if (this.fileStream) { 212 | if (this.fileStream.end) { 213 | this.fileStream.end(); 214 | } 215 | if (this.fileStream.close) { 216 | this.fileStream.close(); 217 | } 218 | 219 | this.fileStream = null; 220 | } 221 | 222 | //release our listeners? 223 | if (this._chunkReceivedHandler) { 224 | this.client.removeListener("ChunkReceived", this._chunkReceivedHandler); 225 | this._chunkReceivedHandler = null; 226 | } 227 | 228 | //cleanup when we're done... 229 | this.clearWatch("UpdateReady"); 230 | this.clearWatch("CompleteTransfer"); 231 | } 232 | catch (ex) { 233 | logger.error("Flasher: error during cleanup " + ex); 234 | } 235 | }, 236 | 237 | 238 | begin_update: function () { 239 | var that = this; 240 | var maxTries = 3; 241 | var resendDelay = 6; //NOTE: this is 6 because it's double the ChunkMissed 3 second delay 242 | 243 | //wait for UpdateReady — sent by Core to indicate readiness to receive firmware chunks 244 | this.client.listenFor("UpdateReady", null, null, function (msg) { 245 | that.clearWatch("UpdateReady"); 246 | 247 | that.client.removeAllListeners("msg_updateabort"); //we got an ok, stop listening for err 248 | 249 | var version = 0; 250 | if (msg && (msg.getPayloadLength() > 0)) { 251 | version = messages.FromBinary(msg.getPayload(), "byte"); 252 | } 253 | that._protocolVersion = version; 254 | 255 | that.stage++; 256 | //that.stage = Flasher.stages.SEND_FILE; //in we ever decide to make this listener re-entrant 257 | 258 | that.nextStep(); 259 | 260 | if (that.onStarted) { 261 | process.nextTick(function () { that.onStarted(); }); 262 | } 263 | }, true); 264 | 265 | this.client.listenFor("UpdateAbort", null, null, function(msg) { 266 | //client didn't like what we had to say. 267 | 268 | that.clearWatch("UpdateReady"); 269 | var failReason = ''; 270 | if (msg && (msg.getPayloadLength() > 0)) { 271 | failReason = messages.FromBinary(msg.getPayload(), "byte"); 272 | } 273 | 274 | that.failed("aborted " + failReason); 275 | }, true); 276 | 277 | 278 | var tryBeginUpdate = function() { 279 | var sentStatus = true; 280 | 281 | if (maxTries > 0) { 282 | that.failWatch("UpdateReady", resendDelay, tryBeginUpdate); 283 | 284 | //(MDM Proposal) Optional payload to enable fast OTA and file placement: 285 | //u8 flags 0x01 - Fast OTA available - when set the server can provide fast OTA transfer 286 | //u16 chunk size Each chunk will be this size apart from the last which may be smaller. 287 | //u32 file size The total size of the file. 288 | //u8 destination Where to store the file 289 | // 0x00 Firmware update 290 | // 0x01 External Flash 291 | // 0x02 User Memory Function 292 | //u32 destination address (0 for firmware update, otherwise the address of external flash or user memory.) 293 | 294 | var flags = 0, //fast ota available 295 | chunkSize = that.chunk_size, 296 | fileSize = that.fileBuffer.length, 297 | destFlag = 0, //TODO: reserved for later 298 | destAddr = 0; //TODO: reserved for later 299 | 300 | if (this._fastOtaEnabled) { 301 | logger.log("fast ota enabled! ", this.getLogInfo()); 302 | flags = 1; 303 | } 304 | 305 | var bb = new buffers.BufferBuilder(); 306 | bb.pushUInt8(flags); 307 | bb.pushUInt16(chunkSize); 308 | bb.pushUInt32(fileSize); 309 | bb.pushUInt8(destFlag); 310 | bb.pushUInt32(destAddr); 311 | 312 | 313 | //UpdateBegin — sent by Server to initiate an OTA firmware update 314 | sentStatus = that.client.sendMessage("UpdateBegin", null, bb.toBuffer(), null, that.failed, that); 315 | maxTries--; 316 | } 317 | else if (maxTries == 0) { 318 | //give us one last LONG wait, for really really slow connections. 319 | that.failWatch("UpdateReady", 90, tryBeginUpdate); 320 | sentStatus = that.client.sendMessage("UpdateBegin", null, null, null, that.failed, that); 321 | maxTries--; 322 | } 323 | else { 324 | that.failed("Failed waiting on UpdateReady - out of retries "); 325 | } 326 | 327 | // did we fail to send out the UpdateBegin message? 328 | if (sentStatus === false) { 329 | that.clearWatch("UpdateReady"); 330 | that.failed("UpdateBegin failed - sendMessage failed"); 331 | } 332 | }; 333 | 334 | 335 | tryBeginUpdate(); 336 | }, 337 | 338 | send_file: function () { 339 | this.chunk = null; 340 | this.lastCrc = null; 341 | 342 | //while iterating over our file... 343 | //Chunk — sent by Server to send chunks of a firmware binary to Core 344 | //ChunkReceived — sent by Core to respond to each chunk, indicating the CRC of the received chunk data. if Server receives CRC that does not match the chunk just sent, that chunk is sent again 345 | 346 | //send when ready: 347 | //UpdateDone — sent by Server to indicate all firmware chunks have been sent 348 | 349 | this.failWatch("CompleteTransfer", 600, this.failed.bind(this)); 350 | 351 | if (this._protocolVersion > 0) { 352 | logger.log("flasher - experimental sendAllChunks!! - ", { coreID: this.client.getHexCoreID() }); 353 | this._sendAllChunks(); 354 | } 355 | else { 356 | this._chunkReceivedHandler = this.onChunkResponse.bind(this); 357 | this.client.listenFor("ChunkReceived", null, null, this._chunkReceivedHandler, false); 358 | 359 | this.failWatch("CompleteTransfer", 600, this.failed.bind(this)); 360 | 361 | //get it started. 362 | this.readNextChunk(); 363 | this.sendChunk(); 364 | } 365 | }, 366 | 367 | readNextChunk: function() { 368 | if (!this.fileStream) { 369 | logger.error("Asked to read a chunk after the update was finished"); 370 | } 371 | 372 | this.chunk = (this.fileStream) ? this.fileStream.read(this.chunk_size) : null; 373 | //workaround for https://github.com/spark/core-firmware/issues/238 374 | if (this.chunk && (this.chunk.length != this.chunk_size)) { 375 | var buf = new Buffer(this.chunk_size); 376 | this.chunk.copy(buf, 0, 0, this.chunk.length); 377 | buf.fill(0, this.chunk.length, this.chunk_size); 378 | this.chunk = buf; 379 | } 380 | this._chunkIndex++; 381 | //end workaround 382 | this.lastCrc = (this.chunk) ? crc32.unsigned(this.chunk) : null; 383 | }, 384 | 385 | sendChunk: function(chunkIndex) { 386 | var includeIndex = (this._protocolVersion > 0); 387 | 388 | if (this.chunk) { 389 | var encodedCrc = messages.ToBinary(this.lastCrc, 'crc'); 390 | // logger.log('sendChunk %s, crc hex is %s ', chunkIndex, encodedCrc.toString('hex'), this.getLogInfo()); 391 | 392 | var writeCoapUri = function(msg) { 393 | msg.addOption(new Option(Message.Option.URI_PATH, new Buffer("c"))); 394 | msg.addOption(new Option(Message.Option.URI_QUERY, encodedCrc)); 395 | if (includeIndex) { 396 | var idxBin = messages.ToBinary(chunkIndex, "uint16"); 397 | msg.addOption(new Option(Message.Option.URI_QUERY, idxBin)); 398 | } 399 | return msg; 400 | }; 401 | 402 | // if (this._gotMissed) { 403 | // console.log("sendChunk %s %s", chunkIndex, this.chunk.toString('hex')); 404 | // } 405 | 406 | this.client.sendMessage("Chunk", { 407 | crc: encodedCrc, 408 | _writeCoapUri: writeCoapUri 409 | }, this.chunk, null, null, this); 410 | } 411 | else { 412 | this.onAllChunksDone(); 413 | } 414 | }, 415 | onChunkResponse: function (msg) { 416 | if (this._protocolVersion > 0) { 417 | // skip normal handling of this during fast ota. 418 | return; 419 | } 420 | 421 | //did the core say the CRCs matched? 422 | if (messages.statusIsOkay(msg)) { 423 | this.readNextChunk(); 424 | } 425 | 426 | if (!this.chunk) { 427 | this.onAllChunksDone(); 428 | } 429 | else { 430 | this.sendChunk(); 431 | } 432 | }, 433 | 434 | _sendAllChunks: function() { 435 | this.readNextChunk(); 436 | while (this.chunk) { 437 | this.sendChunk(this._chunkIndex); 438 | this.readNextChunk(); 439 | } 440 | //this is fast ota, lets let them re-request every single chunk at least once, 441 | //then they'll get an extra ten misses. 442 | this._numChunksMissed = -1 * this._chunkIndex; 443 | 444 | //TODO: wait like 5-6 seconds, and 5-6 seconds after the last chunkmissed? 445 | this.onAllChunksDone(); 446 | }, 447 | 448 | onAllChunksDone: function() { 449 | logger.log('on response, no chunk, transfer done!'); 450 | if (this._chunkReceivedHandler) { 451 | this.client.removeListener("ChunkReceived", this._chunkReceivedHandler); 452 | } 453 | this._chunkReceivedHandler = null; 454 | this.clearWatch("CompleteTransfer"); 455 | 456 | if (!this.client.sendMessage("UpdateDone", null, null, null, null, this)) { 457 | logger.log("Flasher - failed sending updateDone message"); 458 | } 459 | 460 | if (this._protocolVersion > 0) { 461 | this._chunkReceivedHandler = this._waitForMissedChunks.bind(this, true); 462 | this.client.listenFor("ChunkReceived", null, null, this._chunkReceivedHandler, false); 463 | 464 | //fast ota, lets stick around until 10 seconds after the last chunkmissed message 465 | this._waitForMissedChunks(); 466 | } 467 | else { 468 | this.clearWatch("CompleteTransfer"); 469 | this.stage = Flasher.stages.TEARDOWN; 470 | this.nextStep(); 471 | } 472 | }, 473 | 474 | 475 | /** 476 | * delay the teardown until at least like 10 seconds after the last chunkmissed message. 477 | * @private 478 | */ 479 | _waitForMissedChunks: function(wasAck) { 480 | if (this._protocolVersion <= 0) { 481 | //this doesn't apply to normal slow ota 482 | return; 483 | } 484 | 485 | //logger.log("HERE - _waitForMissedChunks wasAck?", wasAck); 486 | 487 | if (this._waitForChunksTimer) { 488 | clearTimeout(this._waitForChunksTimer); 489 | } 490 | 491 | this._waitForChunksTimer = setTimeout(this._waitForMissedChunksDone.bind(this), 60 * 1000); 492 | }, 493 | 494 | /** 495 | * fast ota - done sticking around for missing chunks 496 | * @private 497 | */ 498 | _waitForMissedChunksDone: function() { 499 | if (this._chunkReceivedHandler) { 500 | this.client.removeListener("ChunkReceived", this._chunkReceivedHandler); 501 | } 502 | this._chunkReceivedHandler = null; 503 | 504 | // logger.log("HERE - _waitForMissedChunks done waiting! ", this.getLogInfo()); 505 | 506 | this.clearWatch("CompleteTransfer"); 507 | 508 | this.stage = Flasher.stages.TEARDOWN; 509 | this.nextStep(); 510 | }, 511 | 512 | 513 | getLogInfo: function() { 514 | if (this.client) { 515 | return { coreID: this.client.getHexCoreID(), cache_key: this.client._connection_key }; 516 | } 517 | else { 518 | return { coreID: "unknown" }; 519 | } 520 | }, 521 | 522 | onChunkMissed: function(msg) { 523 | this._waitForMissedChunks(); 524 | //console.log("got chunk missed"); 525 | //this._gotMissed = true; 526 | 527 | this._numChunksMissed++; 528 | if (this._numChunksMissed > Flasher.MAX_MISSED_CHUNKS) { 529 | 530 | logger.error('flasher - chunk missed - core over limit, killing! ', this.getLogInfo()); 531 | this.failed(); 532 | return; 533 | } 534 | 535 | // if we're not doing a fast OTA, and ignore missed is turned on, then ignore this missed chunk. 536 | if (!this._fastOtaEnabled && this._ignoreMissedChunks) { 537 | logger.log("ignoring missed chunk ", this.getLogInfo()); 538 | return; 539 | } 540 | 541 | logger.log('flasher - chunk missed - recovering ', this.getLogInfo()); 542 | 543 | //kosher if I ack before I've read the payload? 544 | this.client.sendReply("ChunkMissedAck", msg.getId(), null, null, null, this); 545 | 546 | //old method 547 | //var idx = messages.FromBinary(msg.getPayload(), "uint16"); 548 | 549 | //the payload should include one or more chunk indexes 550 | var payload = msg.getPayload(); 551 | var r = new buffers.BufferReader(payload); 552 | for(var i = 0; i < payload.length; i += 2) { 553 | try { 554 | var idx = r.shiftUInt16(); 555 | this._resendChunk(idx); 556 | } 557 | catch (ex) { 558 | logger.error("onChunkMissed error reading payload " + ex); 559 | } 560 | } 561 | }, 562 | 563 | _resendChunk: function(idx) { 564 | if (typeof idx == "undefined") { 565 | logger.error("flasher - Got ChunkMissed, index was undefined"); 566 | return; 567 | } 568 | 569 | if (!this.fileStream) { 570 | return this.failed("ChunkMissed, fileStream was empty"); 571 | } 572 | 573 | logger.log("flasher resending chunk " + idx); 574 | 575 | //seek 576 | var offset = idx * this.chunk_size; 577 | this.fileStream.seek(offset); 578 | this._chunkIndex = idx; 579 | 580 | // if (this._protocolVersion > 0) { 581 | // //THIS ASSUMES THIS HAPPENS once the transfer has fully finished. 582 | // //if it happens mid stream, it'll move the filestream, and might disrupt the transfer. 583 | // } 584 | 585 | //re-send 586 | this.readNextChunk(); 587 | this.sendChunk(idx); 588 | }, 589 | 590 | 591 | /** 592 | * Helper for managing a set of named timers and failure callbacks 593 | * @param name 594 | * @param seconds 595 | * @param callback 596 | */ 597 | failWatch: function (name, seconds, callback) { 598 | if (!this._timers) { 599 | this._timers = {}; 600 | } 601 | if (!seconds) { 602 | clearTimeout(this._timers[name]); 603 | delete this._timers[name]; 604 | } 605 | else { 606 | this._timers[name] = setTimeout(function () { 607 | //logger.error("Flasher failWatch failed waiting on " + name); 608 | if (callback) { 609 | callback("failed waiting on " + name); 610 | } 611 | }, seconds * 1000); 612 | } 613 | }, 614 | _timers: null, 615 | clearWatch: function (name) { 616 | this.failWatch(name, 0, null); 617 | }, 618 | 619 | 620 | foo: null 621 | }); 622 | module.exports = Flasher; 623 | -------------------------------------------------------------------------------- /js/lib/Handshake.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var extend = require("xtend"); 20 | var IHandshake = require("./IHandshake"); 21 | var CryptoLib = require("./ICrypto"); 22 | var utilities = require("../lib/utilities.js"); 23 | var ChunkingStream = require("./ChunkingStream"); 24 | var logger = require('../lib/logger.js'); 25 | var buffers = require('h5.buffers'); 26 | var ursa = require('ursa'); 27 | 28 | /* 29 | Handshake protocol v1 30 | 31 | 1.) Socket opens: 32 | 33 | 2.) Server responds with 40 bytes of random data as a nonce. 34 | * Core should read exactly 40 bytes from the socket. 35 | Timeout: 30 seconds. If timeout is reached, Core must close TCP socket and retry the connection. 36 | 37 | * Core appends the 12-byte STM32 Unique ID to the nonce, RSA encrypts the 52-byte message with the Server's public key, 38 | and sends the resulting 256-byte ciphertext to the Server. The Server's public key is stored on the external flash chip at address TBD. 39 | The nonce should be repeated in the same byte order it arrived (FIFO) and the STM32 ID should be appended in the 40 | same byte order as the memory addresses: 0x1FFFF7E8, 0x1FFFF7E9, 0x1FFFF7EA… 0x1FFFF7F2, 0x1FFFF7F3. 41 | 42 | 3.) Server should read exactly 256 bytes from the socket. 43 | Timeout waiting for the encrypted message is 30 seconds. If the timeout is reached, Server must close the connection. 44 | 45 | * Server RSA decrypts the message with its private key. If the decryption fails, Server must close the connection. 46 | * Decrypted message should be 52 bytes, otherwise Server must close the connection. 47 | * The first 40 bytes of the message must match the previously sent nonce, otherwise Server must close the connection. 48 | * Remaining 12 bytes of message represent STM32 ID. Server looks up STM32 ID, retrieving the Core's public RSA key. 49 | * If the public key is not found, Server must close the connection. 50 | 51 | 4.) Server creates secure session key 52 | * Server generates 40 bytes of secure random data to serve as components of a session key for AES-128-CBC encryption. 53 | The first 16 bytes (MSB first) will be the key, the next 16 bytes (MSB first) will be the initialization vector (IV), and the final 8 bytes (MSB first) will be the salt. 54 | Server RSA encrypts this 40-byte message using the Core's public key to create a 128-byte ciphertext. 55 | * Server creates a 20-byte HMAC of the ciphertext using SHA1 and the 40 bytes generated in the previous step as the HMAC key. 56 | * Server signs the HMAC with its RSA private key generating a 256-byte signature. 57 | * Server sends 384 bytes to Core: the ciphertext then the signature. 58 | 59 | 60 | 5.) Release control back to the SparkCore module 61 | 62 | * Core creates a protobufs Hello with counter set to the uint32 represented by the most significant 4 bytes of the IV, encrypts the protobufs Hello with AES, and sends the ciphertext to Server. 63 | * Server reads protobufs Hello from socket, taking note of counter. Each subsequent message received from Core must have the counter incremented by 1. After the max uint32, the next message should set the counter to zero. 64 | 65 | * Server creates protobufs Hello with counter set to a random uint32, encrypts the protobufs Hello with AES, and sends the ciphertext to Core. 66 | * Core reads protobufs Hello from socket, taking note of counter. Each subsequent message received from Server must have the counter incremented by 1. After the max uint32, the next message should set the counter to zero. 67 | */ 68 | 69 | //this._client.write(msg + '\n'); 70 | 71 | 72 | /** 73 | * Interface for the Spark Core module 74 | * @constructor 75 | */ 76 | var Handshake = function () { 77 | 78 | }; 79 | 80 | //statics 81 | Handshake.stages = { SEND_NONCE: 0, READ_COREID: 1, GET_COREKEY: 2, SEND_SESSIONKEY: 3, GET_HELLO: 4, SEND_HELLO: 5, DONE: 6 }; 82 | Handshake.NONCE_BYTES = 40; 83 | Handshake.ID_BYTES = 12; 84 | Handshake.SESSION_BYTES = 40; 85 | 86 | /** 87 | * If we don't finish the handshake in xx seconds, then report a failure 88 | * @type {number} 89 | */ 90 | Handshake.GLOBAL_TIMEOUT = 120; 91 | 92 | 93 | Handshake.prototype = extend(IHandshake.prototype, { 94 | classname: "Handshake", 95 | socket: null, 96 | stage: Handshake.stages.SEND_NONCE, 97 | 98 | _async: true, 99 | nonce: null, 100 | sessionKey: null, 101 | coreID: null, 102 | 103 | //The public RSA key for the given COREID from the datastore 104 | corePublicKey: null, 105 | 106 | useChunkingStream: true, 107 | 108 | 109 | handshake: function (client, onSuccess, onFail) { 110 | this.client = client; 111 | this.socket = client.socket; 112 | this.onSuccess = onSuccess; 113 | this.onFail = onFail; 114 | 115 | this.socket.on('readable', utilities.proxy(this.onSocketData, this)); 116 | 117 | this.nextStep(); 118 | 119 | this.startGlobalTimeout(); 120 | 121 | //grab and cache this before we disconnect 122 | this._ipAddress = this.getRemoteIPAddress(); 123 | }, 124 | 125 | startGlobalTimeout: function() { 126 | if (Handshake.GLOBAL_TIMEOUT <= 0) { 127 | return; 128 | } 129 | 130 | this._globalFailTimer = setTimeout( 131 | function() { this.handshakeFail("Handshake did not complete in " + Handshake.GLOBAL_TIMEOUT + " seconds"); }.bind(this), 132 | Handshake.GLOBAL_TIMEOUT * 1000 133 | ); 134 | }, 135 | 136 | clearGlobalTimeout: function() { 137 | if (this._globalFailTimer) { 138 | clearTimeout(this._globalFailTimer); 139 | } 140 | this._globalFailTimer = null; 141 | }, 142 | 143 | getRemoteIPAddress: function () { 144 | return (this.socket && this.socket.remoteAddress) ? this.socket.remoteAddress.toString() : "unknown"; 145 | }, 146 | 147 | handshakeFail: function (msg) { 148 | //aww 149 | if (this.onFail) { 150 | this.onFail(msg); 151 | } 152 | 153 | var logInfo = { }; 154 | try { 155 | logInfo['ip'] = this._ipAddress; 156 | logInfo['cache_key'] = this.client._connection_key; 157 | logInfo['coreID'] = (this.coreID) ? this.coreID.toString('hex') : null; 158 | } 159 | catch (ex) { } 160 | 161 | logger.error("Handshake failed: ", msg, logInfo); 162 | this.clearGlobalTimeout(); 163 | }, 164 | 165 | routeToClient: function(data) { 166 | var that = this; 167 | process.nextTick(function() { that.client.routeMessage(data); }); 168 | }, 169 | 170 | nextStep: function (data) { 171 | if (this._async) { 172 | var that = this; 173 | process.nextTick(function () { that._nextStep(data); }); 174 | } 175 | else { 176 | this._nextStep(data); 177 | } 178 | }, 179 | 180 | _nextStep: function (data) { 181 | switch (this.stage) { 182 | case Handshake.stages.SEND_NONCE: 183 | //send initial nonce unencrypted 184 | this.send_nonce(); 185 | break; 186 | 187 | case Handshake.stages.READ_COREID: 188 | //reads back our encrypted nonce and the coreid 189 | this.read_coreid(data); 190 | this.client.coreID = this.coreID; 191 | break; 192 | 193 | case Handshake.stages.GET_COREKEY: 194 | //looks up the public rsa key for the given coreID from the datastore 195 | this.get_corekey(); 196 | break; 197 | 198 | case Handshake.stages.SEND_SESSIONKEY: 199 | //creates a session key, encrypts it using the core's public key, and sends it back 200 | this.send_sessionkey(); 201 | break; 202 | 203 | case Handshake.stages.GET_HELLO: 204 | //receive a hello from the client, taking note of the counter 205 | this.get_hello(data); 206 | break; 207 | 208 | case Handshake.stages.SEND_HELLO: 209 | //send a hello to the client, with our new random counter 210 | this.send_hello(); 211 | break; 212 | 213 | case Handshake.stages.DONE: 214 | this.client.sessionKey = this.sessionKey; 215 | 216 | if (this.onSuccess) { 217 | this.onSuccess(this.secureIn, this.secureOut); 218 | } 219 | 220 | this.clearGlobalTimeout(); 221 | this.flushEarlyData(); 222 | this.stage++; 223 | break; 224 | default: 225 | this.routeToClient(data); 226 | break; 227 | } 228 | }, 229 | 230 | _pending: null, 231 | queueEarlyData: function(name, data) { 232 | if (!data) { return; } 233 | if (!this._pending) { this._pending = []; } 234 | this._pending.push(data); 235 | logger.error("recovering from early data! ", { 236 | step: name, 237 | data: (data) ? data.toString('hex') : data, 238 | cache_key: this.client._connection_key 239 | }); 240 | }, 241 | flushEarlyData: function() { 242 | if (this._pending) { 243 | for(var i=0;i (Handshake.NONCE_BYTES + Handshake.ID_BYTES)) { 373 | var coreKey = new Buffer(plaintext.length - 52); 374 | plaintext.copy(coreKey, 0, 52, plaintext.length); 375 | //console.log("got key ", coreKey.toString('hex')); 376 | this.coreProvidedPem = utilities.convertDERtoPEM(coreKey); 377 | } 378 | 379 | //nonces should match 380 | if (!utilities.bufferCompare(vNonce, that.nonce)) { 381 | that.handshakeFail("nonces didn't match"); 382 | return; 383 | } 384 | 385 | //sweet! 386 | that.coreID = vCoreID.toString('hex'); 387 | //logger.log("core reported coreID: " + that.coreID); 388 | 389 | that.stage++; 390 | that.nextStep(); 391 | }, 392 | 393 | 394 | // * Remaining 12 bytes of message represent STM32 ID. Server retrieves the Core's public RSA key. 395 | // * If the public key is not found, Server must close the connection. 396 | get_corekey: function () { 397 | var that = this; 398 | utilities.get_core_key(this.coreID, function (public_key) { 399 | try { 400 | if (!public_key) { 401 | that.handshakeFail("couldn't find key for core: " + this.coreID); 402 | if (that.coreProvidedPem) { 403 | utilities.save_handshake_key(that.coreID, that.coreProvidedPem); 404 | } 405 | 406 | return; 407 | } 408 | 409 | this.corePublicKey = public_key; 410 | 411 | //cool! 412 | this.stage++; 413 | this.nextStep(); 414 | } 415 | catch (ex) { 416 | logger.error("Error handling get_corekey ", ex); 417 | this.handshakeFail("Failed handling find key for core: " + this.coreID); 418 | } 419 | }.bind(this)); 420 | }, 421 | 422 | 423 | // 4.) Server creates secure session key 424 | // * Server generates 40 bytes of secure random data to serve as components of a session key for AES-128-CBC encryption. 425 | // The first 16 bytes (MSB first) will be the key, the next 16 bytes (MSB first) will be the initialization vector (IV), and the final 8 bytes (MSB first) will be the salt. 426 | // Server RSA encrypts this 40-byte message using the Core's public key to create a 128-byte ciphertext. 427 | // * Server creates a 20-byte HMAC of the ciphertext using SHA1 and the 40 bytes generated in the previous step as the HMAC key. 428 | // * Server signs the HMAC with its RSA private key generating a 256-byte signature. 429 | // * Server sends 384 bytes to Core: the ciphertext then the signature. 430 | 431 | //creates a session key, encrypts it using the core's public key, and sends it back 432 | send_sessionkey: function () { 433 | var that = this; 434 | 435 | 436 | CryptoLib.getRandomBytes(Handshake.SESSION_BYTES, function (ex, buf) { 437 | that.sessionKey = buf; 438 | 439 | 440 | //Server RSA encrypts this 40-byte message using the Core's public key to create a 128-byte ciphertext. 441 | var ciphertext = CryptoLib.encrypt(that.corePublicKey, that.sessionKey); 442 | 443 | //Server creates a 20-byte HMAC of the ciphertext using SHA1 and the 40 bytes generated in the previous step as the HMAC key. 444 | var hash = CryptoLib.createHmacDigest(ciphertext, that.sessionKey); 445 | 446 | //Server signs the HMAC with its RSA private key generating a 256-byte signature. 447 | var signedhmac = CryptoLib.sign(null, hash); 448 | 449 | //Server sends ~384 bytes to Core: the ciphertext then the signature. 450 | //logger.log("server: ciphertext was :", ciphertext.toString('hex')); 451 | //console.log("signature block was: " + signedhmac.toString('hex')); 452 | 453 | var msg = Buffer.concat([ciphertext, signedhmac], ciphertext.length + signedhmac.length); 454 | that.socket.write(msg); 455 | //logger.log('Handshake: sent encrypted sessionKey'); 456 | 457 | that.secureIn = CryptoLib.CreateAESDecipherStream(that.sessionKey); 458 | that.secureOut = CryptoLib.CreateAESCipherStream(that.sessionKey); 459 | 460 | 461 | if (that.useChunkingStream) { 462 | 463 | that.chunkingIn = new ChunkingStream({outgoing: false }); 464 | that.chunkingOut = new ChunkingStream({outgoing: true }); 465 | 466 | //what I receive gets broken into message chunks, and goes into the decrypter 467 | that.socket.pipe(that.chunkingIn); 468 | that.chunkingIn.pipe(that.secureIn); 469 | 470 | //what I send goes into the encrypter, and then gets broken into message chunks 471 | that.secureOut.pipe(that.chunkingOut); 472 | that.chunkingOut.pipe(that.socket); 473 | } 474 | else { 475 | that.socket.pipe(that.secureIn); 476 | that.secureOut.pipe(that.socket); 477 | } 478 | 479 | that.secureIn.on('readable', function () { 480 | var chunk = that.secureIn.read(); 481 | if (that.stage > Handshake.stages.DONE) { 482 | that.routeToClient(chunk); 483 | } 484 | else if (that.stage >= Handshake.stages.SEND_HELLO) { 485 | that.queueEarlyData(that.stage, chunk); 486 | } 487 | else { 488 | that.nextStep(chunk); 489 | } 490 | }); 491 | 492 | //a good start 493 | that.stage++; 494 | that.nextStep(); 495 | }); 496 | }, 497 | 498 | /** 499 | * receive a hello from the client, taking note of the counter 500 | */ 501 | get_hello: function (data) { 502 | var that = this; 503 | if (!data) { 504 | if (this._socketTimeout) { 505 | clearTimeout(this._socketTimeout); //just in case 506 | } 507 | 508 | //waiting on data. 509 | //logger.log("server: waiting on hello"); 510 | this._socketTimeout = setTimeout(function () { 511 | that.handshakeFail("get_hello timed out"); 512 | }, 30 * 1000); 513 | 514 | return; 515 | } 516 | clearTimeout(this._socketTimeout); 517 | 518 | var env = this.client.parseMessage(data); 519 | var msg = (env && env.hello) ? env.hello : env; 520 | if (!msg) { 521 | this.handshakeFail("failed to parse hello"); 522 | return; 523 | } 524 | this.client.recvCounter = msg.getId(); 525 | //logger.log("server: got a good hello! Counter was: " + msg.getId()); 526 | 527 | try { 528 | if (msg.getPayload) { 529 | var payload = msg.getPayload(); 530 | if (payload.length > 0) { 531 | var r = new buffers.BufferReader(payload); 532 | this.client.spark_product_id = r.shiftUInt16(); 533 | this.client.product_firmware_version = r.shiftUInt16(); 534 | //logger.log('version of core firmware is ', this.client.spark_product_id, this.client.product_firmware_version); 535 | } 536 | } 537 | else { 538 | logger.log('msg object had no getPayload fn'); 539 | } 540 | } 541 | catch (ex) { 542 | logger.log('error while parsing hello payload ', ex); 543 | } 544 | 545 | // //remind ourselves later that this key worked. 546 | // if (that.corePublicKeyWasUncertain) { 547 | // process.nextTick(function () { 548 | // try { 549 | // //set preferred key for device 550 | // //that.coreFullPublicKeyObject 551 | // } 552 | // catch (ex) { 553 | // logger.error("error marking key as valid " + ex); 554 | // } 555 | // }); 556 | // } 557 | 558 | 559 | this.stage++; 560 | this.nextStep(); 561 | }, 562 | 563 | /** 564 | * send a hello to the client, with our new random counter 565 | */ 566 | send_hello: function () { 567 | //client will set the counter property on the message 568 | //logger.log("server: send hello"); 569 | this.client.secureOut = this.secureOut; 570 | this.client.sendCounter = CryptoLib.getRandomUINT16(); 571 | this.client.sendMessage("Hello", {}, null, null); 572 | 573 | this.stage++; 574 | this.nextStep(); 575 | } 576 | 577 | }); 578 | module.exports = Handshake; 579 | -------------------------------------------------------------------------------- /js/clients/SparkCore.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this program; if not, see . 16 | */ 17 | 18 | 19 | var EventEmitter = require('events').EventEmitter; 20 | var moment = require('moment'); 21 | var extend = require("xtend"); 22 | var when = require("when"); 23 | var fs = require('fs'); 24 | 25 | var Message = require('h5.coap').Message; 26 | 27 | var settings = require("../settings"); 28 | var ISparkCore = require("./ISparkCore"); 29 | var CryptoLib = require("../lib/ICrypto"); 30 | var messages = require("../lib/Messages"); 31 | var Handshake = require("../lib/Handshake"); 32 | var utilities = require("../lib/utilities.js"); 33 | var Flasher = require('../lib/Flasher'); 34 | var logger = require('../lib/logger.js'); 35 | 36 | 37 | 38 | //Hello — sent first by Core then by Server immediately after handshake, never again 39 | //Ignored — sent by either side to respond to a message with a bad counter value. The receiver of an Ignored message can optionally decide to resend a previous message if the indicated bad counter value matches a recently sent message. 40 | 41 | //package flasher 42 | //Chunk — sent by Server to send chunks of a firmware binary to Core 43 | //ChunkReceived — sent by Core to respond to each chunk, indicating the CRC of the received chunk data. if Server receives CRC that does not match the chunk just sent, that chunk is sent again 44 | //UpdateBegin — sent by Server to initiate an OTA firmware update 45 | //UpdateReady — sent by Core to indicate readiness to receive firmware chunks 46 | //UpdateDone — sent by Server to indicate all firmware chunks have been sent 47 | 48 | //FunctionCall — sent by Server to tell Core to call a user-exposed function 49 | //FunctionReturn — sent by Core in response to FunctionCall to indicate return value. void functions will not send this message 50 | //VariableRequest — sent by Server to request the value of a user-exposed variable 51 | //VariableValue — sent by Core in response to VariableRequest to indicate the value 52 | 53 | //Event — sent by Core to initiate a Server Sent Event and optionally an HTTP callback to a 3rd party 54 | //KeyChange — sent by Server to change the AES credentials 55 | 56 | 57 | /** 58 | * Implementation of the Spark Core messaging protocol 59 | * @SparkCore 60 | */ 61 | var SparkCore = function (options) { 62 | if (options) { 63 | this.options = extend(this.options, options); 64 | } 65 | 66 | EventEmitter.call(this); 67 | this._tokens = {}; 68 | }; 69 | 70 | SparkCore.COUNTER_MAX = settings.message_counter_max; 71 | SparkCore.TOKEN_MAX = settings.message_token_max; 72 | 73 | SparkCore.prototype = extend(ISparkCore.prototype, EventEmitter.prototype, { 74 | classname: "SparkCore", 75 | options: { 76 | HandshakeClass: Handshake 77 | }, 78 | 79 | socket: null, 80 | secureIn: null, 81 | secureOut: null, 82 | sendCounter: null, 83 | sendToken: 0, 84 | _tokens: null, 85 | recvCounter: null, 86 | 87 | apiSocket: null, 88 | eventsSocket: null, 89 | 90 | /** 91 | * Our state describing which functions take what arguments 92 | */ 93 | coreFnState: null, 94 | 95 | spark_product_id: null, 96 | product_firmware_version: null, 97 | 98 | /** 99 | * Used to track calls waiting on a description response 100 | */ 101 | _describeDfd: null, 102 | 103 | /** 104 | * configure our socket and start the handshake 105 | */ 106 | startupProtocol: function () { 107 | var that = this; 108 | this.socket.setNoDelay(true); 109 | this.socket.setKeepAlive(true, 15 * 1000); //every 15 second(s) 110 | this.socket.on('error', function (err) { 111 | that.disconnect("socket error " + err); 112 | }); 113 | 114 | this.socket.on('close', function (err) { that.disconnect("socket close " + err); }); 115 | this.socket.on('timeout', function (err) { that.disconnect("socket timeout " + err); }); 116 | 117 | this.handshake(); 118 | }, 119 | 120 | handshake: function () { 121 | var shaker = new this.options.HandshakeClass(); 122 | 123 | //when the handshake is done, we can expect two stream properties, 'secureIn' and 'secureOut' 124 | shaker.handshake(this, 125 | utilities.proxy(this.ready, this), 126 | utilities.proxy(this.disconnect, this) 127 | ); 128 | }, 129 | 130 | ready: function () { 131 | //oh hai! 132 | this._connStartTime = new Date(); 133 | 134 | logger.log("on ready", { 135 | coreID: this.getHexCoreID(), 136 | ip: this.getRemoteIPAddress(), 137 | product_id: this.spark_product_id, 138 | firmware_version: this.product_firmware_version, 139 | cache_key: this._connection_key 140 | }); 141 | 142 | //catch any and all describe responses 143 | this.on('msg_describereturn', this.onDescribeReturn.bind(this)); 144 | this.on(('msg_' + 'PrivateEvent').toLowerCase(), this.onCorePrivateEvent.bind(this)); 145 | this.on(('msg_' + 'PublicEvent').toLowerCase(), this.onCorePublicEvent.bind(this)); 146 | this.on(('msg_' + 'Subscribe').toLowerCase(), this.onCorePublicSubscribe.bind(this)); 147 | this.on(('msg_' + 'GetTime').toLowerCase(), this.onCoreGetTime.bind(this)); 148 | 149 | this.emit("ready"); 150 | }, 151 | 152 | 153 | /** 154 | * TODO: connect to API 155 | * @param sender 156 | * @param response 157 | */ 158 | sendApiResponse: function (sender, response) { 159 | //such boom, wow, very events. 160 | try { 161 | this.emit(sender, sender, response); 162 | } 163 | catch (ex) { 164 | logger.error("Error during response ", ex); 165 | } 166 | }, 167 | 168 | 169 | /** 170 | * Handles messages coming from the API over our message queue service 171 | */ 172 | onApiMessage: function (sender, msg) { 173 | if (!msg) { 174 | logger.log('onApiMessage - no message? got ' + JSON.stringify(arguments), { coreID: this.getHexCoreID() }); 175 | return; 176 | } 177 | var that = this; 178 | 179 | //if we're not the owner, then the socket is busy 180 | var isBusy = (!this._checkOwner(null, function(err) { 181 | logger.error(err + ": " + msg.cmd , { coreID: that.getHexCoreID() }); 182 | })); 183 | if (isBusy) { 184 | this.sendApiResponse(sender, { error: "This core is locked during the flashing process." }); 185 | return; 186 | } 187 | 188 | //TODO: simplify this more? 189 | switch (msg.cmd) { 190 | case "Describe": 191 | 192 | if (isBusy) { 193 | if (settings.logApiMessages) { 194 | logger.log('Describe - flashing', { coreID: that.coreID }); 195 | } 196 | that.sendApiResponse(sender, { 197 | cmd: "DescribeReturn", 198 | name: msg.name, 199 | state: { f: [], v: [] }, 200 | product_id: that.spark_product_id, 201 | firmware_version: that.product_firmware_version 202 | }); 203 | } 204 | else { 205 | when(this.ensureWeHaveIntrospectionData()).then( 206 | function () { 207 | that.sendApiResponse(sender, { 208 | cmd: "DescribeReturn", 209 | name: msg.name, 210 | state: that.coreFnState, 211 | product_id: that.spark_product_id, 212 | firmware_version: that.product_firmware_version 213 | }); 214 | }, 215 | function (msg) { 216 | that.sendApiResponse(sender, { 217 | cmd: "DescribeReturn", 218 | name: msg.name, 219 | err: "Error, no device state" 220 | }); 221 | } 222 | ); 223 | } 224 | 225 | break; 226 | case "GetVar": 227 | if (settings.logApiMessages) { 228 | logger.log('GetVar', { coreID: that.coreID }); 229 | } 230 | this.getVariable(msg.name, msg.type, function (value, buf, err) { 231 | 232 | //don't forget to handle errors! 233 | //if 'error' is set, then don't return the result. 234 | //so we can correctly handle "Variable Not Found" 235 | 236 | that.sendApiResponse(sender, { 237 | cmd: "VarReturn", 238 | name: msg.name, 239 | error: err, 240 | result: value 241 | }); 242 | 243 | }); 244 | break; 245 | case "SetVar": 246 | if (settings.logApiMessages) { 247 | logger.log('SetVar', { coreID: that.coreID }); 248 | } 249 | this.setVariable(msg.name, msg.value, function (resp) { 250 | 251 | //that.sendApiResponse(sender, resp); 252 | 253 | var response = { 254 | cmd: "VarReturn", 255 | name: msg.name, 256 | result: resp.getPayload().toString() 257 | }; 258 | that.sendApiResponse(sender, response); 259 | }); 260 | break; 261 | case "CallFn": 262 | if (settings.logApiMessages) { 263 | logger.log('FunCall', { coreID: that.coreID }); 264 | } 265 | this.callFunction(msg.name, msg.args, function (fnResult) { 266 | var response = { 267 | cmd: "FnReturn", 268 | name: msg.name, 269 | result: fnResult, 270 | error: fnResult.Error 271 | }; 272 | 273 | that.sendApiResponse(sender, response); 274 | }); 275 | break; 276 | case "UFlash": 277 | if (settings.logApiMessages) { 278 | logger.log('FlashCore', { coreID: that.coreID }); 279 | } 280 | 281 | this.flashCore(msg.args.data, sender); 282 | break; 283 | 284 | case "FlashKnown": 285 | if (settings.logApiMessages) { 286 | logger.log('FlashKnown', { coreID: that.coreID, app: msg.app }); 287 | } 288 | 289 | // Responsibility for sanitizing app names lies with API Service 290 | // This includes only allowing apps whose binaries are deployed and thus exist 291 | fs.readFile('known_firmware/' + msg.app + '_' + settings.environment + '.bin', function (err, buf) { 292 | if (err) { 293 | logger.log("Error flashing known firmware", { coreID: that.coreID, err: err }); 294 | that.sendApiResponse(sender, { cmd: "Event", name: "Update", message: "Update failed - " + JSON.stringify(err) }); 295 | return; 296 | } 297 | 298 | that.flashCore(buf, sender); 299 | }); 300 | break; 301 | 302 | case "RaiseHand": 303 | if (isBusy) { 304 | if (settings.logApiMessages) { 305 | logger.log('SignalCore - flashing', { coreID: that.coreID }); 306 | } 307 | that.sendApiResponse(sender, { cmd: "RaiseHandReturn", result: true }); 308 | } 309 | else { 310 | if (settings.logApiMessages) { 311 | logger.log('SignalCore', { coreID: that.coreID }); 312 | } 313 | 314 | var showSignal = (msg.args && msg.args.signal); 315 | this.raiseYourHand(showSignal, function (result) { 316 | that.sendApiResponse(sender, { 317 | cmd: "RaiseHandReturn", 318 | result: result 319 | }); 320 | }); 321 | } 322 | 323 | break; 324 | case "Ping": 325 | if (settings.logApiMessages) { 326 | logger.log('Pinged, replying', { coreID: that.coreID }); 327 | } 328 | 329 | this.sendApiResponse(sender, { cmd: "Pong", online: (this.socket != null), lastPing: this._lastCorePing }); 330 | break; 331 | default: 332 | this.sendApiResponse(sender, {error: "unknown message" }); 333 | } 334 | }, 335 | 336 | /** 337 | * Deals with messages coming from the core over our secure connection 338 | * @param data 339 | */ 340 | routeMessage: function (data) { 341 | var msg = messages.unwrap(data); 342 | if (!msg) { 343 | logger.error("routeMessage got a NULL coap message ", { coreID: this.getHexCoreID() }); 344 | return; 345 | } 346 | 347 | this._lastMessageTime = new Date(); 348 | 349 | //should be adequate 350 | var msgCode = msg.getCode(); 351 | if ((msgCode > Message.Code.EMPTY) && (msgCode <= Message.Code.DELETE)) { 352 | //probably a request 353 | msg._type = messages.getRequestType(msg); 354 | } 355 | 356 | if (!msg._type) { 357 | msg._type = this.getResponseType(msg.getTokenString()); 358 | } 359 | 360 | //console.log("core got message of type " + msg._type + " with token " + msg.getTokenString() + " " + messages.getRequestType(msg)); 361 | 362 | if (msg.isAcknowledgement()) { 363 | if (!msg._type) { 364 | //no type, can't route it. 365 | msg._type = 'PingAck'; 366 | } 367 | this.emit(('msg_' + msg._type).toLowerCase(), msg); 368 | return; 369 | } 370 | 371 | 372 | var nextPeerCounter = ++this.recvCounter; 373 | if (nextPeerCounter > 65535) { 374 | //TODO: clean me up! (I need settings, and maybe belong elsewhere) 375 | this.recvCounter = nextPeerCounter = 0; 376 | } 377 | 378 | if (msg.isEmpty() && msg.isConfirmable()) { 379 | this._lastCorePing = new Date(); 380 | //var delta = (this._lastCorePing - this._connStartTime) / 1000.0; 381 | //logger.log("core ping @ ", delta, " seconds ", { coreID: this.getHexCoreID() }); 382 | this.sendReply("PingAck", msg.getId()); 383 | return; 384 | } 385 | 386 | if (!msg || (msg.getId() != nextPeerCounter)) { 387 | logger.log("got counter ", msg.getId(), " expecting ", nextPeerCounter, { coreID: this.getHexCoreID() }); 388 | 389 | if (msg._type == "Ignored") { 390 | //don't ignore an ignore... 391 | this.disconnect("Got an Ignore"); 392 | return; 393 | } 394 | 395 | //this.sendMessage("Ignored", null, {}, null, null); 396 | this.disconnect("Bad Counter"); 397 | return; 398 | } 399 | 400 | this.emit(('msg_' + msg._type).toLowerCase(), msg); 401 | }, 402 | 403 | sendReply: function (name, id, data, token, onError, requester) { 404 | if (!this._checkOwner(requester, onError, name)) { 405 | return; 406 | } 407 | 408 | //if my reply is an acknowledgement to a confirmable message 409 | //then I need to re-use the message id... 410 | 411 | //set our counter 412 | if (id < 0) { 413 | id = this.getIncrSendCounter(); 414 | } 415 | 416 | 417 | var msg = messages.wrap(name, id, null, data, token, null); 418 | if (!this.secureOut) { 419 | logger.error("SparkCore - sendReply before READY", { coreID: this.getHexCoreID() }); 420 | return; 421 | } 422 | this.secureOut.write(msg, null, null); 423 | //logger.log("Replied with message of type: ", name, " containing ", data); 424 | }, 425 | 426 | 427 | sendMessage: function (name, params, data, onResponse, onError, requester) { 428 | if (!this._checkOwner(requester, onError, name)) { 429 | return false; 430 | } 431 | 432 | //increment our counter 433 | var id = this.getIncrSendCounter(); 434 | 435 | //TODO: messages of type 'NON' don't really need a token // alternatively: "no response type == no token" 436 | var token = this.getNextToken(); 437 | this.useToken(name, token); 438 | 439 | var msg = messages.wrap(name, id, params, data, token, onError); 440 | if (!this.secureOut) { 441 | logger.error("SparkCore - sendMessage before READY", { coreID: this.getHexCoreID() }); 442 | return; 443 | } 444 | this.secureOut.write(msg, null, null); 445 | // logger.log("Sent message of type: ", name, " containing ", data, 446 | // "BYTES: " + msg.toString('hex')); 447 | 448 | return token; 449 | }, 450 | 451 | /** 452 | * Same as 'sendMessage', but sometimes the core can't handle Tokens on certain message types. 453 | * 454 | * Somewhat rare / special case, so this seems like a better option at the moment, should converge these 455 | * back at some point 456 | */ 457 | sendNONTypeMessage: function (name, params, data, onResponse, onError, requester) { 458 | if (!this._checkOwner(requester, onError, name)) { 459 | return; 460 | } 461 | 462 | //increment our counter 463 | var id = this.getIncrSendCounter(); 464 | var msg = messages.wrap(name, id, params, data, null, onError); 465 | if (!this.secureOut) { 466 | logger.error("SparkCore - sendMessage before READY", { coreID: this.getHexCoreID() }); 467 | return; 468 | } 469 | this.secureOut.write(msg, null, null); 470 | //logger.log("Sent message of type: ", name, " containing ", data, 471 | // "BYTES: " + msg.toString('hex')); 472 | 473 | }, 474 | 475 | 476 | parseMessage: function (data) { 477 | //we're assuming data is a serialized CoAP message 478 | return messages.unwrap(data); 479 | }, 480 | 481 | /** 482 | * Adds a listener to our secure message stream 483 | * @param name the message type we're waiting on 484 | * @param uri - a particular function / variable? 485 | * @param token - what message does this go with? (should come from sendMessage) 486 | * @param callback what we should call when we're done 487 | * @param [once] whether or not we should keep the listener after we've had a match 488 | */ 489 | listenFor: function (name, uri, token, callback, once) { 490 | var tokenHex = (token) ? utilities.toHexString(token) : null; 491 | var beVerbose = settings.showVerboseCoreLogs; 492 | 493 | //TODO: failWatch? What kind of timeout do we want here? 494 | 495 | //adds a one time event 496 | var that = this, 497 | evtName = ('msg_' + name).toLowerCase(), 498 | handler = function (msg) { 499 | 500 | if (uri && (msg.getUriPath().indexOf(uri) != 0)) { 501 | if (beVerbose) { 502 | logger.log("uri filter did not match", uri, msg.getUriPath(), { coreID: that.getHexCoreID() }); 503 | } 504 | return; 505 | } 506 | 507 | if (tokenHex && (tokenHex != msg.getTokenString())) { 508 | if (beVerbose) { 509 | logger.log("Tokens did not match ", tokenHex, msg.getTokenString(), { coreID: that.getHexCoreID() }); 510 | } 511 | return; 512 | } 513 | 514 | if (once) { 515 | that.removeListener(evtName, handler); 516 | } 517 | 518 | process.nextTick(function () { 519 | try { 520 | if (beVerbose) { 521 | logger.log('heard ', name, { coreID: that.coreID }); 522 | } 523 | callback(msg); 524 | } 525 | catch (ex) { 526 | logger.error("listenFor - caught error: ", ex, ex.stack, { coreID: that.getHexCoreID() }); 527 | } 528 | }); 529 | }; 530 | 531 | //logger.log('listening for ', evtName); 532 | this.on(evtName, handler); 533 | 534 | return handler; 535 | }, 536 | 537 | /** 538 | * Gets or wraps 539 | * @returns {null} 540 | */ 541 | getIncrSendCounter: function () { 542 | this.sendCounter++; 543 | 544 | if (this.sendCounter >= SparkCore.COUNTER_MAX) { 545 | this.sendCounter = 0; 546 | } 547 | 548 | return this.sendCounter; 549 | }, 550 | 551 | 552 | /** 553 | * increments or wraps our token value, and makes sure it isn't in use 554 | */ 555 | getNextToken: function () { 556 | this.sendToken++; 557 | if (this.sendToken >= SparkCore.TOKEN_MAX) { 558 | this.sendToken = 0; 559 | } 560 | 561 | this.clearToken(this.sendToken); 562 | 563 | return this.sendToken; 564 | }, 565 | 566 | /** 567 | * Associates a particular token with a message we're sending, so we know 568 | * what we're getting back when we get an ACK 569 | * @param name 570 | * @param token 571 | */ 572 | useToken: function (name, token) { 573 | var key = utilities.toHexString(token); 574 | this._tokens[key] = name; 575 | }, 576 | 577 | /** 578 | * Clears the association with a particular token 579 | * @param token 580 | */ 581 | clearToken: function (token) { 582 | var key = utilities.toHexString(token); 583 | 584 | if (this._tokens[key]) { 585 | delete this._tokens[key]; 586 | } 587 | }, 588 | 589 | getResponseType: function (tokenStr) { 590 | var request = this._tokens[tokenStr]; 591 | //logger.log('respType for key ', tokenStr, ' is ', request); 592 | 593 | if (!request) { 594 | return null; 595 | } 596 | return messages.getResponseType(request); 597 | }, 598 | 599 | /** 600 | * Ensures we have introspection data from the core, and then 601 | * requests a variable value to be sent, when received it transforms 602 | * the response into the appropriate type 603 | * @param name 604 | * @param type 605 | * @param callback - expects (value, buf, err) 606 | */ 607 | getVariable: function (name, type, callback) { 608 | var that = this; 609 | var performRequest = function () { 610 | if (!that.HasSparkVariable(name)) { 611 | callback(null, null, "Variable not found"); 612 | return; 613 | } 614 | 615 | var token = this.sendMessage("VariableRequest", { name: name }); 616 | var varTransformer = this.transformVariableGenerator(name, callback); 617 | this.listenFor("VariableValue", null, token, varTransformer, true); 618 | }.bind(this); 619 | 620 | if (this.hasFnState()) { 621 | //slight short-circuit, saves ~5 seconds every 100,000 requests... 622 | performRequest(); 623 | } 624 | else { 625 | when(this.ensureWeHaveIntrospectionData()) 626 | .then( 627 | performRequest, 628 | function (err) { callback(null, null, "Problem requesting variable: " + err); 629 | }); 630 | } 631 | }, 632 | 633 | setVariable: function (name, data, callback) { 634 | 635 | /*TODO: data type! */ 636 | var payload = messages.ToBinary(data); 637 | var token = this.sendMessage("VariableRequest", { name: name }, payload); 638 | 639 | //are we expecting a response? 640 | //watches the messages coming back in, listens for a message of this type with 641 | this.listenFor("VariableValue", null, token, callback, true); 642 | }, 643 | 644 | callFunction: function (name, args, callback) { 645 | var that = this; 646 | when(this.transformArguments(name, args)).then( 647 | function (buf) { 648 | if (settings.showVerboseCoreLogs) { 649 | logger.log('sending function call to the core', { coreID: that.coreID, name: name }); 650 | } 651 | 652 | var writeUrl = function(msg) { 653 | msg.setUri("f/" + name); 654 | if (buf) { 655 | msg.setUriQuery(buf.toString()); 656 | } 657 | return msg; 658 | }; 659 | 660 | var token = that.sendMessage("FunctionCall", { name: name, args: buf, _writeCoapUri: writeUrl }, null); 661 | 662 | //gives us a function that will transform the response, and call the callback with it. 663 | var resultTransformer = that.transformFunctionResultGenerator(name, callback); 664 | 665 | //watches the messages coming back in, listens for a message of this type with 666 | that.listenFor("FunctionReturn", null, token, resultTransformer, true); 667 | }, 668 | function (err) { 669 | callback({Error: "Something went wrong calling this function: " + err}); 670 | } 671 | ); 672 | }, 673 | 674 | /** 675 | * Asks the core to start or stop its "raise your hand" signal 676 | * @param showSignal - whether it should show the signal or not 677 | * @param callback - what to call when we're done or timed out... 678 | */ 679 | raiseYourHand: function (showSignal, callback) { 680 | var timer = setTimeout(function () { callback(false); }, 30 * 1000); 681 | 682 | //TODO: that.stopListeningFor("RaiseYourHandReturn", listenHandler); 683 | //TODO: var listenHandler = this.listenFor("RaiseYourHandReturn", ... ); 684 | 685 | //logger.log("RaiseYourHand: asking core to signal? " + showSignal); 686 | var token = this.sendMessage("RaiseYourHand", { _writeCoapUri: messages.raiseYourHandUrlGenerator(showSignal) }, null); 687 | this.listenFor("RaiseYourHandReturn", null, token, function () { 688 | clearTimeout(timer); 689 | callback(true); 690 | }, true); 691 | 692 | }, 693 | 694 | 695 | flashCore: function (binary, sender) { 696 | var that = this; 697 | 698 | if (!binary || (binary.length == 0)) { 699 | logger.log("flash failed! - file is empty! ", { coreID: this.getHexCoreID() }); 700 | this.sendApiResponse(sender, { cmd: "Event", name: "Update", message: "Update failed - File was too small!" }); 701 | return 702 | } 703 | 704 | if (binary && binary.length > settings.MaxCoreBinaryBytes) { 705 | logger.log("flash failed! - file is too BIG " + binary.length, { coreID: this.getHexCoreID() }); 706 | this.sendApiResponse(sender, { cmd: "Event", name: "Update", message: "Update failed - File was too big!" }); 707 | return; 708 | } 709 | 710 | var flasher = new Flasher(); 711 | flasher.startFlashBuffer(binary, this, 712 | function () { 713 | logger.log("flash core finished! - sending api event", { coreID: that.getHexCoreID() }); 714 | that.sendApiResponse(sender, { cmd: "Event", name: "Update", message: "Update done" }); 715 | }, 716 | function (msg) { 717 | logger.log("flash core failed! - sending api event", { coreID: that.getHexCoreID(), error: msg }); 718 | that.sendApiResponse(sender, { cmd: "Event", name: "Update", message: "Update failed" }); 719 | }, 720 | function () { 721 | logger.log("flash core started! - sending api event", { coreID: that.getHexCoreID() }); 722 | that.sendApiResponse(sender, { cmd: "Event", name: "Update", message: "Update started" }); 723 | }); 724 | }, 725 | 726 | 727 | _checkOwner: function (requester, onError, messageName) { 728 | if (!this._owner || (this._owner == requester)) { 729 | return true; 730 | } 731 | else { 732 | //either call their callback, or log the error 733 | var msg = "this client has an exclusive lock"; 734 | if (onError) { 735 | process.nextTick(function () { 736 | onError(msg); 737 | }); 738 | } 739 | else { 740 | logger.error(msg, { coreID: this.getHexCoreID(), cache_key: this._connection_key, msgName: messageName }); 741 | } 742 | 743 | return false; 744 | } 745 | }, 746 | 747 | takeOwnership: function (obj, onError) { 748 | if (this._owner) { 749 | logger.error("already owned", { coreID: this.getHexCoreID() }); 750 | if (onError) { 751 | onError("Already owned"); 752 | } 753 | return false; 754 | } 755 | else { 756 | //only permit 'obj' to send messages 757 | this._owner = obj; 758 | return true; 759 | } 760 | }, 761 | releaseOwnership: function (obj) { 762 | logger.log('releasing flash ownership ', { coreID: this.getHexCoreID() }); 763 | if (this._owner == obj) { 764 | this._owner = null; 765 | } 766 | else if (this._owner) { 767 | logger.error("cannot releaseOwnership, ", obj, " isn't the current owner ", { coreID: this.getHexCoreID() }); 768 | } 769 | }, 770 | 771 | 772 | /** 773 | * makes sure we have our introspection data, then transforms our object into 774 | * the right coap query string 775 | * @param name 776 | * @param args 777 | * @returns {*} 778 | */ 779 | transformArguments: function (name, args) { 780 | var ready = when.defer(); 781 | var that = this; 782 | 783 | when(this.ensureWeHaveIntrospectionData()).then( 784 | function () { 785 | var buf = that._transformArguments(name, args); 786 | if (buf) { 787 | ready.resolve(buf); 788 | } 789 | else { 790 | //NOTE! The API looks for "Unknown Function" in the error response. 791 | ready.reject("Unknown Function: " + name); 792 | } 793 | }, 794 | function (msg) { 795 | ready.reject(msg); 796 | } 797 | ); 798 | 799 | return ready.promise; 800 | }, 801 | 802 | 803 | transformFunctionResultGenerator: function (name, callback) { 804 | var that = this; 805 | return function (msg) { 806 | that.transformFunctionResult(name, msg, callback); 807 | }; 808 | }, 809 | 810 | /** 811 | * 812 | * @param name 813 | * @param callback -- callback expects (value, buf, err) 814 | * @returns {Function} 815 | */ 816 | transformVariableGenerator: function (name, callback) { 817 | var that = this; 818 | return function (msg) { 819 | that.transformVariableResult(name, msg, callback); 820 | }; 821 | }, 822 | 823 | 824 | /** 825 | * 826 | * @param name 827 | * @param msg 828 | * @param callback-- callback expects (value, buf, err) 829 | * @returns {null} 830 | */ 831 | transformVariableResult: function (name, msg, callback) { 832 | 833 | //grab the variable type, if the core doesn't say, assume it's a "string" 834 | var fnState = (this.coreFnState) ? this.coreFnState.v : null; 835 | var varType = (fnState && fnState[name]) ? fnState[name] : "string"; 836 | 837 | var niceResult = null, data = null; 838 | try { 839 | if (msg && msg.getPayload) { 840 | //leaving raw payload in response message for now, so we don't shock our users. 841 | data = msg.getPayload(); 842 | niceResult = messages.FromBinary(data, varType); 843 | } 844 | } 845 | catch (ex) { 846 | logger.error("transformVariableResult - error transforming response " + ex); 847 | } 848 | 849 | process.nextTick(function () { 850 | try { 851 | callback(niceResult, data); 852 | } 853 | catch (ex) { 854 | logger.error("transformVariableResult - error in callback " + ex); 855 | } 856 | }); 857 | 858 | return null; 859 | }, 860 | 861 | 862 | /** 863 | * Transforms the result from a core function to the correct type. 864 | * @param name 865 | * @param msg 866 | * @param callback 867 | * @returns {null} 868 | */ 869 | transformFunctionResult: function (name, msg, callback) { 870 | var varType = "int32"; //if the core doesn't specify, assume it's a "uint32" 871 | //var fnState = (this.coreFnState) ? this.coreFnState.f : null; 872 | //if (fnState && fnState[name] && fnState[name].returns) { 873 | // varType = fnState[name].returns; 874 | //} 875 | 876 | var niceResult = null; 877 | try { 878 | if (msg && msg.getPayload) { 879 | niceResult = messages.FromBinary(msg.getPayload(), varType); 880 | } 881 | } 882 | catch (ex) { 883 | logger.error("transformFunctionResult - error transforming response " + ex); 884 | } 885 | 886 | process.nextTick(function () { 887 | try { 888 | callback(niceResult); 889 | } 890 | catch (ex) { 891 | logger.error("transformFunctionResult - error in callback " + ex); 892 | } 893 | }); 894 | 895 | return null; 896 | }, 897 | 898 | /** 899 | * transforms our object into a nice coap query string 900 | * @param name 901 | * @param args 902 | * @private 903 | */ 904 | _transformArguments: function (name, args) { 905 | //logger.log('transform args', { coreID: this.getHexCoreID() }); 906 | if (!args) { 907 | return null; 908 | } 909 | 910 | if (!this.hasFnState()) { 911 | logger.error("_transformArguments called without any function state!", { coreID: this.getHexCoreID() }); 912 | return null; 913 | } 914 | 915 | //TODO: lowercase function keys on new state format 916 | name = name.toLowerCase(); 917 | var fn = this.coreFnState[name]; 918 | if (!fn || !fn.args) { 919 | //maybe it's the old protocol? 920 | var f = this.coreFnState.f; 921 | if (f && utilities.arrayContainsLower(f, name)) { 922 | //logger.log("_transformArguments - using old format", { coreID: this.getHexCoreID() }); 923 | //current / simplified function format (one string arg, int return type) 924 | fn = { 925 | returns: "int", 926 | args: [ 927 | [null, "string" ] 928 | ] 929 | }; 930 | } 931 | } 932 | 933 | if (!fn || !fn.args) { 934 | //logger.error("_transformArguments: core doesn't know fn: ", { coreID: this.getHexCoreID(), name: name, state: this.coreFnState }); 935 | return null; 936 | } 937 | 938 | // "HelloWorld": { returns: "string", args: [ {"name": "string"}, {"adjective": "string"} ]} }; 939 | return messages.buildArguments(args, fn.args); 940 | }, 941 | 942 | /** 943 | * Checks our cache to see if we have the function state, otherwise requests it from the core, 944 | * listens for it, and resolves our deferred on success 945 | * @returns {*} 946 | */ 947 | ensureWeHaveIntrospectionData: function () { 948 | if (this.hasFnState()) { 949 | return when.resolve(); 950 | } 951 | 952 | //if we don't have a message pending, send one. 953 | if (!this._describeDfd) { 954 | this.sendMessage("Describe"); 955 | this._describeDfd = when.defer(); 956 | } 957 | 958 | //let everybody else queue up on this promise 959 | return this._describeDfd.promise; 960 | }, 961 | 962 | 963 | /** 964 | * On any describe return back from the core 965 | * @param msg 966 | */ 967 | onDescribeReturn: function(msg) { 968 | //got a description, is it any good? 969 | var loaded = (this.loadFnState(msg.getPayload())); 970 | 971 | if (this._describeDfd) { 972 | if (loaded) { 973 | this._describeDfd.resolve(); 974 | } 975 | else { 976 | this._describeDfd.reject("something went wrong parsing function state") 977 | } 978 | } 979 | //else { //hmm, unsolicited response, that's okay. } 980 | }, 981 | 982 | //------------- 983 | // Core Events / Spark.publish / Spark.subscribe 984 | //------------- 985 | 986 | onCorePrivateEvent: function(msg) { 987 | this.onCoreSentEvent(msg, false); 988 | }, 989 | onCorePublicEvent: function(msg) { 990 | this.onCoreSentEvent(msg, true); 991 | }, 992 | 993 | onCoreSentEvent: function(msg, isPublic) { 994 | if (!msg) { 995 | logger.error("CORE EVENT - msg obj was empty?!"); 996 | return; 997 | } 998 | 999 | //TODO: if the core is publishing messages too fast: 1000 | //this.sendReply("EventSlowdown", msg.getId()); 1001 | 1002 | 1003 | //name: "/E/TestEvent", trim the "/e/" or "/E/" off the start of the uri path 1004 | 1005 | var obj = { 1006 | name: msg.getUriPath().substr(3), 1007 | is_public: isPublic, 1008 | ttl: msg.getMaxAge(), 1009 | data: msg.getPayload().toString(), 1010 | published_by: this.getHexCoreID(), 1011 | published_at: moment().toISOString() 1012 | }; 1013 | 1014 | //snap obj.ttl to the right value. 1015 | obj.ttl = (obj.ttl > 0) ? obj.ttl : 60; 1016 | 1017 | //snap data to not incorrectly default to an empty string. 1018 | if (msg.getPayloadLength() == 0) { 1019 | obj.data = null; 1020 | } 1021 | 1022 | //if the event name starts with spark (upper or lower), then eat it. 1023 | var lowername = obj.name.toLowerCase(); 1024 | if (lowername.indexOf("spark") == 0) { 1025 | //allow some kinds of message through. 1026 | var eat_message = true; 1027 | 1028 | //if we do let these through, make them private. 1029 | isPublic = false; 1030 | 1031 | 1032 | 1033 | //TODO: 1034 | // //if the message is "cc3000-radio-version", save to the core_state collection for this core? 1035 | if (lowername == "spark/cc3000-patch-version") { 1036 | // set_cc3000_version(this.coreID, obj.data); 1037 | // eat_message = false; 1038 | } 1039 | 1040 | if (eat_message) { 1041 | //short-circuit 1042 | this.sendReply("EventAck", msg.getId()); 1043 | return; 1044 | } 1045 | } 1046 | 1047 | 1048 | try { 1049 | if (!global.publisher) { 1050 | return; 1051 | } 1052 | 1053 | if (!global.publisher.publish(isPublic, obj.name, obj.userid, obj.data, obj.ttl, obj.published_at, this.getHexCoreID())) { 1054 | //this core is over its limit, and that message was not sent. 1055 | this.sendReply("EventSlowdown", msg.getId()); 1056 | } 1057 | else { 1058 | this.sendReply("EventAck", msg.getId()); 1059 | } 1060 | } 1061 | catch (ex) { 1062 | logger.error("onCoreSentEvent: failed writing to socket - " + ex); 1063 | } 1064 | }, 1065 | 1066 | /** 1067 | * The core asked us for the time! 1068 | * @param msg 1069 | */ 1070 | onCoreGetTime: function(msg) { 1071 | 1072 | //moment#unix outputs a Unix timestamp (the number of seconds since the Unix Epoch). 1073 | var stamp = moment().utc().unix(); 1074 | var binVal = messages.ToBinary(stamp, "uint32"); 1075 | 1076 | this.sendReply("GetTimeReturn", msg.getId(), binVal, msg.getToken()); 1077 | }, 1078 | 1079 | onCorePublicSubscribe: function(msg) { 1080 | this.onCoreSubscribe(msg, true); 1081 | }, 1082 | onCoreSubscribe: function(msg, isPublic) { 1083 | var name = msg.getUriPath().substr(3); 1084 | 1085 | //var body = resp.getPayload().toString(); 1086 | //logger.log("Got subscribe request from core, path was \"" + name + "\""); 1087 | //uri -> /e/?u --> firehose for all my devices 1088 | //uri -> /e/ (deviceid in body) --> allowed 1089 | //uri -> /e/ --> not allowed (no global firehose for cores, kthxplox) 1090 | //uri -> /e/event_name?u --> all my devices 1091 | //uri -> /e/event_name?u (deviceid) --> deviceid? 1092 | 1093 | if (!name) { 1094 | //no firehose for cores 1095 | this.sendReply("SubscribeFail", msg.getId()); 1096 | return; 1097 | } 1098 | 1099 | var query = msg.getUriQuery(), 1100 | payload = msg.getPayload(), 1101 | myDevices = (query && (query.indexOf("u") >= 0)), 1102 | userid = (myDevices) ? (this.userID || "").toLowerCase() : null, 1103 | deviceID = (payload) ? payload.toString() : null; 1104 | 1105 | //TODO: filter by a particular deviceID 1106 | 1107 | this.sendReply("SubscribeAck", msg.getId()); 1108 | 1109 | //modify our filter on the appropriate socket (create the socket if we haven't yet) to let messages through 1110 | //this.eventsSocket.subscribe(isPublic, name, userid); 1111 | }, 1112 | 1113 | onCorePubHeard: function (name, data, ttl, published_at, coreid) { 1114 | this.sendCoreEvent(true, name, data, ttl, published_at, coreid); 1115 | }, 1116 | onCorePrivHeard: function (name, data, ttl, published_at, coreid) { 1117 | this.sendCoreEvent(false, name, data, ttl, published_at, coreid); 1118 | }, 1119 | 1120 | /** 1121 | * sends a received event down to a core 1122 | * @param isPublic 1123 | * @param name 1124 | * @param data 1125 | * @param ttl 1126 | * @param published_at 1127 | */ 1128 | sendCoreEvent: function (isPublic, name, data, ttl, published_at, coreid) { 1129 | var rawFn = function (msg) { 1130 | try { 1131 | msg.setMaxAge(parseInt((ttl && (ttl >= 0)) ? ttl : 60)); 1132 | if (published_at) { 1133 | msg.setTimestamp(moment(published_at).toDate()); 1134 | } 1135 | } 1136 | catch (ex) { 1137 | logger.error("onCoreHeard - " + ex); 1138 | } 1139 | return msg; 1140 | }; 1141 | 1142 | var msgName = (isPublic) ? "PublicEvent" : "PrivateEvent"; 1143 | var userID = (this.userID || "").toLowerCase() + "/"; 1144 | name = (name) ? name.toString() : name; 1145 | if (name && name.indexOf && (name.indexOf(userID) == 0)) { 1146 | name = name.substring(userID.length); 1147 | } 1148 | 1149 | data = (data) ? data.toString() : data; 1150 | this.sendNONTypeMessage(msgName, { event_name: name, _raw: rawFn }, data); 1151 | }, 1152 | 1153 | // _wifiScan: null, 1154 | // handleFindMe: function (data) { 1155 | // if (!this._wifiScan) { 1156 | // this._wifiScan = []; 1157 | // } 1158 | // 1159 | // if (!data || (data.indexOf("00:00:00:00:00:00") >= 0)) { 1160 | // this.requestLocation(this._wifiScan); 1161 | // this._wifiScan = []; 1162 | // } 1163 | // 1164 | // try { 1165 | // this._wifiScan.push(JSON.parse(data)); 1166 | // } 1167 | // catch(ex) {} 1168 | // }, 1169 | // 1170 | // requestLocation: function (arr) { 1171 | // 1172 | // logger.log("Making geolocation request"); 1173 | // var that = this; 1174 | // request({ 1175 | // uri: "https://location.services.mozilla.com/v1/search?key=0010230303020102030223", 1176 | // method: "POST", 1177 | // body: JSON.stringify({ 1178 | // "wifi": arr 1179 | // }), 1180 | // 'content-type': 'application/json', 1181 | // json: true 1182 | // }, 1183 | // function (error, response, body) { 1184 | // if (error) { 1185 | // logger.log("geolocation Error! ", error); 1186 | // } 1187 | // else { 1188 | // logger.log("geolocation success! ", body); 1189 | // that.sendCoreEvent(false, "Spark/Location", body, 60, new Date(), that.getHexCoreID()); 1190 | // } 1191 | // }); 1192 | // }, 1193 | 1194 | 1195 | hasFnState: function () { 1196 | return !!this.coreFnState; 1197 | }, 1198 | 1199 | HasSparkVariable: function (name) { 1200 | return (this.coreFnState && this.coreFnState.v && this.coreFnState.v[name]); 1201 | }, 1202 | 1203 | HasSparkFunction: function (name) { 1204 | //has state, and... the function is an object, or it's in the function array 1205 | return (this.coreFnState && 1206 | (this.coreFnState[name] || ( this.coreFnState.f && utilities.arrayContainsLower(this.coreFnState.f, name))) 1207 | ); 1208 | }, 1209 | 1210 | /** 1211 | * interprets the introspection message from the core containing 1212 | * argument names / types, and function return types, so we can make it easy to call functions 1213 | * on the core. 1214 | * @param data 1215 | */ 1216 | loadFnState: function (data) { 1217 | var fnState = JSON.parse(data.toString()); 1218 | 1219 | if (fnState && fnState.v) { 1220 | //"v":{"temperature":2} 1221 | fnState.v = messages.TranslateIntTypes(fnState.v); 1222 | } 1223 | 1224 | this.coreFnState = fnState; 1225 | 1226 | //logger.log("got describe return ", this.coreFnState, { coreID: this.getHexCoreID() }); 1227 | 1228 | //an example: 1229 | // this.coreFnState = { 1230 | // "HelloWorld": { 1231 | // returns: "string", 1232 | // args: [ 1233 | // ["name", "string"], 1234 | // ["adjective", "string"] 1235 | // ]} 1236 | // }; 1237 | return true; 1238 | }, 1239 | 1240 | getHexCoreID: function () { 1241 | return (this.coreID) ? this.coreID.toString('hex') : "unknown"; 1242 | }, 1243 | 1244 | getRemoteIPAddress: function () { 1245 | return (this.socket && this.socket.remoteAddress) ? this.socket.remoteAddress.toString() : "unknown"; 1246 | }, 1247 | 1248 | // _idleTimer: null, 1249 | // _lastMessageTime: null, 1250 | // 1251 | // idleChecker: function() { 1252 | // if (!this.socket) { 1253 | // //disconnected 1254 | // return; 1255 | // } 1256 | // 1257 | // clearTimeout(this._idleTimer); 1258 | // this._idleTimer = setTimeout(this.idleChecker.bind(this), 30000); 1259 | // 1260 | // if (!this._lastMessageTime) { 1261 | // this._lastMessageTime = new Date(); 1262 | // } 1263 | // 1264 | // var elapsed = ((new Date()) - this._lastMessageTime) / 1000; 1265 | // if (elapsed > 30) { 1266 | // //we don't expect a response, but by trying to send anything, the socket should blow up if disconnected. 1267 | // logger.log("Socket seems quiet, checking...", { coreID: this.getHexCoreID(), elapsed: elapsed, cache_key: this._connection_key }); 1268 | // this.sendMessage("SocketPing"); 1269 | // this._lastMessageTime = new Date(); //don't check for another 30 seconds. 1270 | // } 1271 | // }, 1272 | 1273 | 1274 | _disconnectCtr: 0, 1275 | disconnect: function (msg) { 1276 | msg = msg || ""; 1277 | this._disconnectCtr++; 1278 | 1279 | if (this._disconnectCtr > 1) { 1280 | //don't multi-disconnect 1281 | return; 1282 | } 1283 | 1284 | try { 1285 | var logInfo = { coreID: this.getHexCoreID(), cache_key: this._connection_key }; 1286 | if (this._connStartTime) { 1287 | var delta = ((new Date()) - this._connStartTime) / 1000.0; 1288 | logInfo['duration'] = delta; 1289 | } 1290 | 1291 | logger.log(this._disconnectCtr + ": Core disconnected: " + msg, logInfo); 1292 | } 1293 | catch (ex) { 1294 | logger.error("Disconnect log error " + ex); 1295 | } 1296 | 1297 | try { 1298 | if (this.socket) { 1299 | this.socket.end(); 1300 | this.socket.destroy(); 1301 | this.socket = null; 1302 | } 1303 | } 1304 | catch (ex) { 1305 | logger.error("Disconnect TCPSocket error: " + ex); 1306 | } 1307 | 1308 | if (this.secureIn) { 1309 | try { 1310 | this.secureIn.end(); 1311 | this.secureIn = null; 1312 | } 1313 | catch(ex) { 1314 | logger.error("Error cleaning up secureIn ", ex); 1315 | } 1316 | } 1317 | if (this.secureOut) { 1318 | try { 1319 | this.secureOut.end(); 1320 | this.secureOut = null; 1321 | } 1322 | catch(ex) { 1323 | logger.error("Error cleaning up secureOut ", ex); 1324 | } 1325 | } 1326 | 1327 | // clearTimeout(this._idleTimer); 1328 | 1329 | this.emit('disconnect', msg); 1330 | 1331 | 1332 | //obv, don't do this before emitting disconnect. 1333 | try { 1334 | this.removeAllListeners(); 1335 | } 1336 | catch(ex) { 1337 | logger.error("Problem removing listeners ", ex); 1338 | } 1339 | } 1340 | 1341 | }); 1342 | module.exports = SparkCore; 1343 | --------------------------------------------------------------------------------