├── 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 |
--------------------------------------------------------------------------------