├── .npmignore ├── lib ├── getSystemStatus.js ├── getSecurityImpl.js ├── getUuid.js ├── mdnsServer.js ├── subEvents.js ├── elasticSearch.js ├── sendActivity.js ├── wrapMqttMessage.js ├── getSocketId.js ├── clearCache.js ├── sendConfigActivity.js ├── getPhone.js ├── createReadStream.js ├── cacheDevice.js ├── getYo.js ├── util.js ├── createActivity.js ├── parentConnection.js ├── splunk.js ├── authDevice.js ├── updatePresence.js ├── getParentConnection.js ├── getThrottles.js ├── airbrakeErrors.js ├── subscribe.js ├── redisSplunk.js ├── sendSms.js ├── sendYo.js ├── createSocketEmitter.js ├── setupMqttClient.js ├── getLocalDevices.js ├── bindSocket.js ├── database.js ├── claimDevice.js ├── sendPushNotification.js ├── setupGatewayConfig.js ├── updateFromClient.js ├── logData.js ├── getEvents.js ├── unregister.js ├── whoAmI.js ├── redis.js ├── logEvent.js ├── getDevices.js ├── getData.js ├── updateSocketId.js ├── coapServer.js ├── simpleAuth.js ├── updateDevice.js ├── proxyListener.js ├── register.js ├── coapRouter.js ├── httpServer.js ├── sendMessage.js ├── mqttServer.js ├── setupCoapRoutes.js ├── setupHttpRoutes.js └── socketLogic.js ├── .gitignore ├── app.yml ├── public ├── index.html ├── jsconsole.html └── localjsconsole.html ├── docker ├── supervisor.conf └── config.js.docker ├── app.json ├── newrelic.js ├── Dockerfile ├── .jshintrc ├── LICENSE ├── demo.html ├── .travis.yml ├── package.json ├── config.js.sample ├── config.js ├── server.js └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | !./config.js 2 | -------------------------------------------------------------------------------- /lib/getSystemStatus.js: -------------------------------------------------------------------------------- 1 | module.exports = function(callback) { 2 | var status = {'meshblu':'online'}; 3 | callback(status); 4 | } 5 | -------------------------------------------------------------------------------- /lib/getSecurityImpl.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | 3 | var securityModuleName = config.securityImpl || './../lib/simpleAuth'; 4 | 5 | module.exports = require(securityModuleName); 6 | -------------------------------------------------------------------------------- /lib/getUuid.js: -------------------------------------------------------------------------------- 1 | module.exports = function(socket, callback) { 2 | if(socket.uuid){ 3 | return callback(null, socket.uuid); 4 | }else{ 5 | return callback(new Error('uuid not found for socket' + socket), null); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Npm install files 2 | node_modules 3 | npm-debug.log 4 | 5 | # Config files 6 | config.js 7 | 8 | # Log file 9 | skynet.txt 10 | 11 | # Swap and meta data files 12 | .DS_Store 13 | *.swp 14 | *.swo 15 | 16 | #NEDB files 17 | *.db 18 | -------------------------------------------------------------------------------- /lib/mdnsServer.js: -------------------------------------------------------------------------------- 1 | var mdns = require('mdns'); 2 | 3 | var mdnsServer = function(config) { 4 | return mdns.createAdvertisement( 5 | mdns.tcp('meshblu'), 6 | parseInt(config.port, 10) 7 | ); 8 | } 9 | 10 | module.exports = mdnsServer; 11 | -------------------------------------------------------------------------------- /lib/subEvents.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | 3 | var subEvents = new events.EventEmitter(); 4 | 5 | //theoretically possible for every connected port to simultaenously subscribe to same topic 6 | subEvents.setMaxListeners(65000); 7 | 8 | module.exports = subEvents; 9 | -------------------------------------------------------------------------------- /lib/elasticSearch.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var _ = require('lodash'); 3 | 4 | if(config.elasticSearch && !_.isEmpty(config.elasticSearch.hosts)){ 5 | var elasticsearch = require('elasticsearch'); 6 | var client = new elasticsearch.Client({ 7 | hosts: config.elasticSearch.hosts 8 | }); 9 | module.exports = client; 10 | } 11 | -------------------------------------------------------------------------------- /lib/sendActivity.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var socketEmitter = require('./createSocketEmitter')(); 3 | 4 | 5 | function sendActivity(data){ 6 | //TODO throttle 7 | if(config.broadcastActivity && data && data.ipAddress){ 8 | socketEmitter(config.uuid + '_bc', 'message', data); 9 | } 10 | } 11 | 12 | module.exports = sendActivity; 13 | -------------------------------------------------------------------------------- /app.yml: -------------------------------------------------------------------------------- 1 | name: Meshblu 2 | image: ubuntu-14-04-x64 3 | min_size: 1gb 4 | config: 5 | #cloud-config 6 | users: 7 | - name: meshblu 8 | groups: sudo 9 | shell: /bin/bash 10 | sudo: ['ALL=(ALL) NOPASSWD:ALL'] 11 | packages: 12 | - git 13 | runcmd: 14 | - cd /home/meshblu && git clone git://github.com/octoblu/meshblu.git && cd meshblu && bash build/ubuntu/14.04/provision.sh 15 | -------------------------------------------------------------------------------- /lib/wrapMqttMessage.js: -------------------------------------------------------------------------------- 1 | function wrapMqttMessage(topic, data){ 2 | try{ 3 | if(topic === 'tb'){ 4 | if(typeof data !== 'string'){ 5 | return JSON.stringify(data); 6 | } 7 | return data; 8 | }else{ 9 | return JSON.stringify({topic: topic, data: data}); 10 | } 11 | }catch(ex){ 12 | console.error(ex); 13 | } 14 | } 15 | 16 | module.exports = wrapMqttMessage; 17 | -------------------------------------------------------------------------------- /lib/getSocketId.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | 3 | var devices = require('./database').devices; 4 | 5 | module.exports = function(uuid, callback) { 6 | devices.findOne({ 7 | uuid: uuid 8 | }, function(err, devicedata) { 9 | if(err || !devicedata || devicedata.length < 1) { 10 | callback({}); 11 | } else { 12 | callback(devicedata.socketid); 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/clearCache.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var redis = require('./redis'); 3 | 4 | 5 | function clearCache(name){ 6 | 7 | if(name && config.redis && config.redis.host){ 8 | redis.del(name, function(){ 9 | }); 10 | } 11 | 12 | } 13 | 14 | function noop(){} 15 | 16 | if(config.redis && config.redis.host){ 17 | module.exports = clearCache; 18 | } 19 | else{ 20 | module.exports = noop; 21 | } 22 | -------------------------------------------------------------------------------- /lib/sendConfigActivity.js: -------------------------------------------------------------------------------- 1 | var socketEmitter = require('./createSocketEmitter')(); 2 | var whoAmI = require('./whoAmI'); 3 | 4 | function sendConfigActivity(uuid, emitter){ 5 | whoAmI(uuid, true, function(device) { 6 | if (emitter){ 7 | emitter('config', device, device); 8 | } else { 9 | socketEmitter(uuid, 'config', device); 10 | } 11 | }); 12 | } 13 | 14 | module.exports = sendConfigActivity; 15 | -------------------------------------------------------------------------------- /lib/getPhone.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | 3 | var devices = require('./database').devices; 4 | 5 | module.exports = function(phone, callback) { 6 | devices.findOne({ 7 | phoneNumber: phone 8 | }, function(err, devicedata) { 9 | if(err || !devicedata || devicedata.length < 1) { 10 | callback('phone number not found'); 11 | } else { 12 | callback(null, devicedata); 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/createReadStream.js: -------------------------------------------------------------------------------- 1 | var Readable = require('stream').Readable; 2 | 3 | function noop(){} 4 | 5 | function createReadStream(){ 6 | var rs = new Readable(); 7 | 8 | rs._read = noop; 9 | 10 | rs.pushMsg = function(msg){ 11 | if(typeof msg === 'object'){ 12 | rs.push(JSON.stringify(msg) + ',\n'); 13 | }else{ 14 | rs.push(msg + ',\n'); 15 | } 16 | }; 17 | 18 | return rs; 19 | } 20 | 21 | module.exports = createReadStream; 22 | -------------------------------------------------------------------------------- /lib/cacheDevice.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var redis = require('./redis'); 3 | 4 | var _ = require('lodash'); 5 | 6 | function cacheDevice(device){ 7 | 8 | if(device){ 9 | var cloned = _.clone(device); 10 | redis.set('DEVICE_' + device.uuid, JSON.stringify(cloned),function(){ 11 | }); 12 | } 13 | 14 | } 15 | 16 | function noop(){} 17 | 18 | if(config.redis){ 19 | module.exports = cacheDevice; 20 | } 21 | else{ 22 | module.exports = noop; 23 | } 24 | -------------------------------------------------------------------------------- /lib/getYo.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | 3 | var devices = require('./database').devices; 4 | 5 | module.exports = function(yoUsername, callback) { 6 | var regex = new RegExp(["^",yoUsername,"$"].join(""),"i"); 7 | devices.findOne({ 8 | yoUser: regex 9 | }, function(err, devicedata) { 10 | if(err || !devicedata || devicedata.length < 1) { 11 | callback('Yo user not found'); 12 | } else { 13 | callback(null, devicedata); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var r192 = new RegExp(/^192\.168\./); 2 | var r10 = new RegExp(/^10\./); 3 | 4 | function sameLAN(fromIp, toIp){ 5 | if(!toIp || !fromIp){ 6 | return false; 7 | } 8 | 9 | if(toIp === fromIp){ 10 | return true; 11 | } 12 | else if(r10.test(fromIp) && r10.test(toIp)){ 13 | return true; 14 | } 15 | else if(r192.test(fromIp) && r192.test(toIp)){ 16 | return true; 17 | } 18 | 19 | return false; 20 | 21 | } 22 | 23 | module.exports = { 24 | sameLAN : sameLAN 25 | }; 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | Page Redirection 10 | 11 | 12 | If you are not redirected automatically, follow the link to example 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/jsconsole.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | Page Redirection 10 | 11 | 12 | If you are not redirected automatically, follow the link to example 13 | 14 | 15 | -------------------------------------------------------------------------------- /docker/supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:redis] 2 | command=/usr/bin/redis-server /etc/redis/redis.conf 3 | numprocs=1 4 | autostart=true 5 | autorestart=true 6 | 7 | [program:mongodb] 8 | command=/usr/bin/mongod --config /etc/mongodb.conf 9 | numprocs=1 10 | autostart=true 11 | autorestart=true 12 | 13 | [program:node] 14 | command=/usr/bin/node /var/www/server.js --http --coap 15 | numprocs=1 16 | directory=/var/www/ 17 | stdout_logfile=/dev/fd/1 18 | stdout_logfile_maxbytes=0 19 | redirect_stderr=true 20 | autostart=true 21 | autorestart=true 22 | -------------------------------------------------------------------------------- /lib/createActivity.js: -------------------------------------------------------------------------------- 1 | 2 | function getActivity(topic, ip, device, toDevice, messageData){ 3 | var data = {ipAddress: ip}; 4 | if(topic){ 5 | data.topic = topic; 6 | } 7 | if(device && device.type){ 8 | data.type = device.type; 9 | } 10 | if(toDevice && toDevice.ipAddress){ 11 | data.toIpAddress = toDevice.ipAddress; 12 | } 13 | if(toDevice && toDevice.type){ 14 | data.toType = toDevice.type; 15 | } 16 | if(messageData){ 17 | data.message = messageData; 18 | } 19 | return data; 20 | } 21 | 22 | module.exports = getActivity; 23 | -------------------------------------------------------------------------------- /lib/parentConnection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var skynetClient = require('skynet'); //skynet npm client 4 | 5 | var parentConnection = function(config){ 6 | if(!config.parentConnection.uuid){ 7 | return; 8 | } 9 | 10 | var conn = skynetClient.createConnection(config.parentConnection); 11 | conn.on('notReady', function(data){ 12 | console.log('Failed authenitication to parent cloud', data); 13 | }); 14 | 15 | conn.on('ready', function(data){ 16 | console.log('UUID authenticated for parent cloud connection.', data); 17 | }); 18 | 19 | return conn; 20 | }; 21 | 22 | module.exports = parentConnection; 23 | -------------------------------------------------------------------------------- /lib/splunk.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | 3 | if(config.splunk && config.splunk.indexObj){ 4 | var splunk = require('splunk-sdk'); 5 | var splunkService = new splunk.Service({ 6 | host: config.splunk.host, 7 | port: config.splunk.port, 8 | scheme: config.splunk.protocol, 9 | username: config.splunk.user, 10 | password: config.splunk.password 11 | }); 12 | var myindexes = splunkService.indexes(); 13 | myindexes.fetch(function(err, myindexes) { 14 | if(myindexes){ 15 | config.splunk.indexObj = myindexes.item(config.splunk.index); 16 | } 17 | }); 18 | 19 | module.exports = splunkService; 20 | } 21 | -------------------------------------------------------------------------------- /lib/authDevice.js: -------------------------------------------------------------------------------- 1 | // var devices = require('./database').collection('devices'); 2 | var config = require('./../config'); 3 | var devices = require('./database').devices; 4 | 5 | module.exports = function(uuid, token, callback) { 6 | 7 | if(!uuid && !token){ 8 | return callback({'authenticate': false}); 9 | } 10 | 11 | devices.findOne({ 12 | uuid: uuid, token: token 13 | }, function(err, devicedata) { 14 | if(err || !devicedata || devicedata.length < 1) { 15 | return callback({'authenticate': false}); 16 | } else { 17 | return callback({'authenticate': true, device: devicedata}); 18 | } 19 | }); 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /lib/updatePresence.js: -------------------------------------------------------------------------------- 1 | var bindSocket = require('./bindSocket'); 2 | var config = require('./../config'); 3 | 4 | var devices = require('./database').devices; 5 | 6 | module.exports = function(socket) { 7 | devices.update({ 8 | socketid: socket 9 | }, { 10 | $set: {online: false} 11 | }, function(err, saved) { 12 | if(err || saved === 0 || (saved && !saved.updatedExisting)) { 13 | return; 14 | } else { 15 | //disconnect any bound sockets asynchronously 16 | try{ 17 | bindSocket.disconnect(socket); 18 | } catch(e){ 19 | console.error(e); 20 | } 21 | 22 | return; 23 | } 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /lib/getParentConnection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../config'); 4 | var skynetClient = require('skynet'); //skynet npm client 5 | 6 | var parentConnection = null; 7 | 8 | if(config.parentConnection && config.parentConnection.uuid){ 9 | 10 | parentConnection = skynetClient.createConnection(config.parentConnection); 11 | parentConnection.on('notReady', function(data){ 12 | console.log('Failed authenitication to parent cloud', data); 13 | }); 14 | 15 | parentConnection.on('ready', function(data){ 16 | console.log('UUID authenticated for parent cloud connection.', data); 17 | }); 18 | 19 | } 20 | 21 | module.exports = parentConnection; 22 | -------------------------------------------------------------------------------- /lib/getThrottles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tokenthrottle = require("tokenthrottle"); 4 | var config = require('../config'); 5 | 6 | config.rateLimits = config.rateLimits || {}; 7 | // rate per second 8 | var throttles = { 9 | connection : tokenthrottle({rate: config.rateLimits.connection || 3}), 10 | message : tokenthrottle({rate: config.rateLimits.message || 10}), 11 | data : tokenthrottle({rate: config.rateLimits.data || 10}), 12 | query : tokenthrottle({rate: config.rateLimits.query || 2}), 13 | whoami : tokenthrottle({rate: config.rateLimits.whoami || 10}), 14 | unthrottledIps : config.rateLimits.unthrottledIps || [] 15 | }; 16 | 17 | module.exports = throttles; -------------------------------------------------------------------------------- /lib/airbrakeErrors.js: -------------------------------------------------------------------------------- 1 | var airbrake = require('airbrake').createClient(process.env.AIRBRAKE_KEY); 2 | 3 | var handleExceptions = function() { 4 | var origConsoleError = console.error; 5 | airbrake.log = console.log; 6 | 7 | console.error = function(err) { 8 | if (err instanceof Error) { 9 | origConsoleError(err.message, err.stack); 10 | airbrake.notify(err); 11 | } else { 12 | origConsoleError.apply(this, arguments); 13 | airbrake.notify({error: arguments}); 14 | } 15 | } 16 | 17 | process.on("uncaughtException", function(error) { 18 | console.error(error); 19 | }); 20 | } 21 | 22 | module.exports = { 23 | handleExceptions: handleExceptions 24 | }; 25 | -------------------------------------------------------------------------------- /lib/subscribe.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | var config = require('./../config'); 3 | 4 | var events = require('./database').events; 5 | 6 | module.exports = function(uuid) { 7 | 8 | var newTimestamp = new Date().getTime(); 9 | // var cursor = events.find({ 10 | // $or: [{fromUuid: uuid}, {uuid:uuid}, { devices: {$in: [uuid, "all", "*"]}}], 11 | // timestamp: { $gt : moment(newTimestamp).toISOString() } 12 | // }, {}, {tailable:true, timeout:false}); 13 | 14 | var cursor = events.find({ 15 | $or: [{fromUuid: uuid}, {uuid:uuid}], 16 | timestamp: { $gt : moment(newTimestamp).toISOString() } 17 | }, {}, {tailable:true, timeout:false}); 18 | 19 | return cursor; 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Meshblu", 3 | "description": "Meshblu is an open source machine-to-machine instant messaging network and API. ", 4 | "website": "http://meshblu.org", 5 | "success_url": "/", 6 | "addons": ["mongohq"], 7 | "env": { 8 | "NODE_ENV": "production", 9 | "PORT": "80", 10 | "RATE_LIMITS_MESSAGE" : "10", 11 | "RATE_LIMITS_DATA" : "10", 12 | "RATE_LIMITS_CONNECTION" : "2", 13 | "RATE_LIMITS_QUERY" : "2", 14 | "RATE_LIMITS_WHOAMI" : "10", 15 | "RATE_LIMITS_UNTHROTTLED_IPS" : "" 16 | }, 17 | "repository": "https://github.com/octoblu/meshblu", 18 | "keywords": ["M2M", "IoT", "Communication", "arduino", "skynet", "meshblu"] 19 | } -------------------------------------------------------------------------------- /newrelic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * New Relic agent configuration. 3 | * 4 | * See lib/config.defaults.js in the agent distribution for a more complete 5 | * description of configuration variables and their potential values. 6 | */ 7 | exports.config = { 8 | /** 9 | * Array of application names. 10 | */ 11 | app_name : ['Meshblu'], 12 | /** 13 | * Your New Relic license key. 14 | */ 15 | license_key : 'e70989cafd562b1ba49653bcf0629fea323c8b4d', 16 | logging : { 17 | /** 18 | * Level at which to log. 'trace' is most useful to New Relic when diagnosing 19 | * issues with the agent, 'info' and higher will impose the least overhead on 20 | * production applications. 21 | */ 22 | level : 'info' 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/redisSplunk.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | var config = require('./../config'); 3 | 4 | var redisSplunk; 5 | 6 | var RedisSplunk = function(options){ 7 | var self, client; 8 | 9 | self = this; 10 | client = redis.createClient(options.port, options.host); 11 | client.auth(options.password, console.error); 12 | 13 | self.log = function(data, callback){ 14 | client.rpush('splunk', JSON.stringify(data), callback); 15 | }; 16 | }; 17 | 18 | RedisSplunk.log = function(data, callback){ 19 | if(!config.redis) { 20 | return callback(); 21 | } 22 | 23 | if(!redisSplunk) { 24 | redisSplunk = new RedisSplunk(config.redis); 25 | } 26 | 27 | redisSplunk.log(data, callback); 28 | }; 29 | 30 | module.exports = RedisSplunk; 31 | -------------------------------------------------------------------------------- /lib/sendSms.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var config = require('./../config'); 3 | 4 | var devices = require('./database').devices; 5 | 6 | module.exports = function(uuid, message, callback) { 7 | devices.findOne({ 8 | uuid: uuid 9 | }, function(err, devicedata) { 10 | if(err || !devicedata || devicedata.length < 1) { 11 | callback({}); 12 | } else { 13 | request.post('https://' + devicedata.plivoAuthId + ':' + devicedata.plivoAuthToken + '@api.plivo.com/v1/Account/' + devicedata.plivoAuthId + '/Message/', 14 | {json: {'src': '17144625921', 'dst': devicedata.phoneNumber, 'text': message}} 15 | , function (error, response, body) { 16 | body.uuid = uuid 17 | callback(body); 18 | }); 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/sendYo.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var config = require('./../config'); 3 | 4 | var devices = require('./database').devices; 5 | 6 | module.exports = function(uuid, callback) { 7 | devices.findOne({ 8 | uuid: uuid 9 | }, function(err, devicedata) { 10 | if(err || !devicedata || devicedata.length < 1) { 11 | callback({}); 12 | } else { 13 | request.post('http://api.justyo.co/yo/', 14 | {json: {'api_token': config.yo.token, 'username': devicedata.yoUser}} 15 | , function (error, response, body) { 16 | if (error) { 17 | console.error(error); 18 | callback(); 19 | return; 20 | } 21 | body.uuid = uuid 22 | callback(body); 23 | }); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/createSocketEmitter.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var redis = require('./redis'); 3 | var subEvents = require('./subEvents'); 4 | 5 | var redisIoEmitter; 6 | 7 | if(config.redis && config.redis.host){ 8 | redisIoEmitter = require('socket.io-emitter')(redis.client); 9 | } 10 | 11 | module.exports = function(io, ios){ 12 | if(redisIoEmitter){ 13 | return function(channel, topic, data){ 14 | redisIoEmitter.in(channel).emit(topic, data); 15 | }; 16 | }else if(io){ 17 | return function(channel, topic, data){ 18 | io.sockets.in(channel).emit(topic, data); 19 | if(ios){ 20 | ios.sockets.in(channel).emit(topic, data); 21 | } 22 | //for local http streaming: 23 | subEvents.emit(channel, topic, data); 24 | }; 25 | }else{ 26 | return function(){}; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/setupMqttClient.js: -------------------------------------------------------------------------------- 1 | var mqtt = require('mqtt'); 2 | 3 | function setupMqttClient(skynet, config){ 4 | 5 | var client; 6 | 7 | // Handle MQTT Messages 8 | try{ 9 | 10 | var mqttConfig = config.mqtt || {}; 11 | var mqttsettings = { 12 | keepalive: 1000, // seconds 13 | protocolId: 'MQIsdp', 14 | protocolVersion: 3, 15 | //clientId: 'skynet', 16 | username: 'skynet', 17 | password: process.env.MQTT_PASS || mqttConfig.skynetPass 18 | }; 19 | var mqttPort = process.env.MQTT_PORT || mqttConfig.port || 1833; 20 | var mqttHost = process.env.MQTT_HOST || mqttConfig.host || 'localhost'; 21 | client = mqtt.createClient(mqttPort, mqttHost, mqttsettings); 22 | } catch(e){ 23 | console.error('no mqtt server found', e); 24 | } 25 | 26 | return client; 27 | 28 | }; 29 | 30 | module.exports = setupMqttClient; 31 | -------------------------------------------------------------------------------- /lib/getLocalDevices.js: -------------------------------------------------------------------------------- 1 | var getDevices = require('./getDevices'); 2 | 3 | var r192 = new RegExp(/^192\.168\./); 4 | var r10 = new RegExp(/^10\./); 5 | 6 | module.exports = function(fromDevice, unclaimedOnly, callback){ 7 | if(!fromDevice || !fromDevice.ipAddress){ 8 | callback({error: { 9 | message: "No ipAddress on device", 10 | code: 404 11 | } 12 | }); 13 | } 14 | 15 | var ip = fromDevice.ipAddress; 16 | var query = {}; 17 | 18 | if(r192.test(ip)){ 19 | query.ipAddress = r192; 20 | } 21 | else if(r10.test(ip)){ 22 | query.ipAddress = r10; 23 | } 24 | else{ 25 | query.ipAddress = ip; 26 | } 27 | //TODO 20-bit block 172.16.0.0 - 172.31.255.255 28 | 29 | if(unclaimedOnly){ 30 | query.owner = { $exists: false }; 31 | } 32 | 33 | query.type = { $ne: 'user' }; 34 | 35 | getDevices(fromDevice, query, false, callback); 36 | }; 37 | -------------------------------------------------------------------------------- /docker/config.js.docker: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | databaseUrl: "mongodb://localhost:27017/skynet", 3 | port: 3000, 4 | /*tls: { 5 | sslPort: 443, 6 | cert: "/certs/server.crt", 7 | key: "/certs/server.key" 8 | },*/ 9 | log: true, 10 | /*elasticSearch: { 11 | host: "localhost", 12 | port: "9200" 13 | },*/ 14 | rateLimits: { 15 | message: 10, // 10 transactions per user per second 16 | data: 10, // 10 transactions per user per second 17 | connection: 2, // 2 transactions per IP per second 18 | query: 2, // 2 transactions per user per second 19 | whoami: 10 // 10 transactions per user per second 20 | }, 21 | /*plivo: { 22 | authId: "abc", 23 | authToken: "123" 24 | },*/ 25 | redis: { 26 | host: "localhost", 27 | port: "6379", 28 | password: "localhost" 29 | }, 30 | coap: { 31 | port: 5683, 32 | host: "localhost" 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /lib/bindSocket.js: -------------------------------------------------------------------------------- 1 | var redis = require('./redis'); 2 | var config = require('../config'); 3 | 4 | var TIMEOUT = config.bindTimeout || 3600; 5 | 6 | function connect(client, target, callback){ 7 | if(config.redis){ 8 | redis.setex('SOCKET_BIND_' + client, TIMEOUT, target); 9 | redis.setex('SOCKET_BIND_' + target, TIMEOUT, client, callback); 10 | } 11 | } 12 | 13 | function disconnect(client){ 14 | if(config.redis){ 15 | getTarget(client, function(err, target){ 16 | if(target){ 17 | redis.del('SOCKET_BIND_' + client); 18 | redis.del('SOCKET_BIND_' + target); 19 | } 20 | }); 21 | } 22 | } 23 | 24 | 25 | 26 | function getTarget(client, callback){ 27 | if(config.redis){ 28 | redis.get('SOCKET_BIND_' + client, callback); 29 | } 30 | } 31 | 32 | if(config.redis){ 33 | module.exports = { 34 | connect: connect, 35 | disconnect: disconnect, 36 | getTarget: getTarget 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /lib/database.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var path = require('path'); 3 | 4 | if(config.mongo && config.mongo.databaseUrl){ 5 | 6 | var mongojs = require('mongojs'); 7 | var db = mongojs(config.mongo.databaseUrl); 8 | module.exports = { 9 | devices: db.collection('devices'), 10 | events: db.collection('events'), 11 | data: db.collection('data') 12 | }; 13 | 14 | } else { 15 | 16 | var Datastore = require('nedb'); 17 | var devices = new Datastore({ 18 | filename: path.join(__dirname, '/../devices.db'), 19 | autoload: true } 20 | ); 21 | var events = new Datastore({ 22 | filename: path.join(__dirname, '/../events.db'), 23 | autoload: true } 24 | ); 25 | var data = new Datastore({ 26 | filename: path.join(__dirname, '/../data.db'), 27 | autoload: true } 28 | ); 29 | 30 | module.exports = { 31 | devices: devices, 32 | events: events, 33 | data: data 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /lib/claimDevice.js: -------------------------------------------------------------------------------- 1 | var whoAmI = require('./whoAmI'); 2 | var securityImpl = require('./getSecurityImpl'); 3 | var config = require('./../config'); 4 | 5 | var devices = require('./database').devices; 6 | 7 | module.exports = function(fromDevice, data, callback) { 8 | 9 | if(!fromDevice || !data || !data.uuid){ 10 | callback('invalid from or to device'); 11 | } 12 | else{ 13 | whoAmI(data.uuid, false, function(toDevice){ 14 | if(toDevice.error){ 15 | callback('invalid device to claim'); 16 | } 17 | else{ 18 | if(securityImpl.canUpdate(fromDevice, toDevice, data)){ 19 | var updateFields = {owner: fromDevice.uuid}; 20 | if(data.name){ 21 | updateFields.name = data.name; 22 | } 23 | devices.update({uuid: data.uuid}, {$set: updateFields }, callback); 24 | } 25 | else{ 26 | callback('unauthorized'); 27 | } 28 | } 29 | }); 30 | } 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /lib/sendPushNotification.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var config = require('./../config'); 3 | 4 | module.exports = function(device, message, callback) { 5 | 6 | // remove quotes around message string 7 | message = message.substring(1, message.length -1); 8 | request.post("https://" + config.urbanAirship.key + ":" + config.urbanAirship.secret + "@go.urbanairship.com/api/push/", 9 | { headers: { 10 | "Content-Type": "application/json", 11 | "Accept": "application/vnd.urbanairship+json; version=3;" 12 | }, 13 | json:{ 14 | "audience" : { 15 | "device_token" : device.pushID 16 | }, 17 | "notification" : { 18 | "alert" : message 19 | }, 20 | "device_types" : "all" 21 | } 22 | }, function (error, response, body) { 23 | if (error) { 24 | console.error(error); 25 | callback(); 26 | return; 27 | } 28 | callback(body); 29 | }); 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:12.04 2 | 3 | MAINTAINER Skynet https://skynet.im/ 4 | 5 | RUN apt-get update -y --fix-missing 6 | RUN apt-get install -y python-software-properties 7 | RUN apt-get install -y build-essential 8 | RUN add-apt-repository ppa:chris-lea/node.js 9 | 10 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv 7F0CEB10 11 | RUN echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | tee -a /etc/apt/sources.list.d/10gen.list 12 | RUN apt-get update -y --fix-missing 13 | RUN apt-get -y install redis-server apt-utils supervisor nodejs 14 | RUN apt-get -y install -o apt::architecture=amd64 mongodb-10gen 15 | 16 | RUN sed -i 's/daemonize yes/daemonize no/g' /etc/redis/redis.conf 17 | 18 | ADD . /var/www 19 | RUN cd /var/www && npm install 20 | ADD ./docker/config.js.docker /var/www/config.js 21 | ADD ./docker/supervisor.conf /etc/supervisor/conf.d/supervisor.conf 22 | RUN mkdir /var/log/skynet 23 | 24 | EXPOSE 3000 5683 25 | 26 | CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"] 27 | -------------------------------------------------------------------------------- /lib/setupGatewayConfig.js: -------------------------------------------------------------------------------- 1 | var whoAmI = require('./whoAmI'); 2 | var securityImpl = require('./getSecurityImpl'); 3 | 4 | module.exports = function(clientEmitter){ 5 | 6 | function configAck(fromDevice, data){ 7 | whoAmI(data.uuid, false, function(check){ 8 | if(!check.error && securityImpl.canSend(fromDevice, check)){ 9 | clientEmitter('messageAck', fromDevice, data); 10 | } 11 | }); 12 | } 13 | 14 | function config(fromDevice, data){ 15 | whoAmI(data.uuid, true, function(check){ 16 | if((check.type === 'gateway' || check.type === 'hub') && check.uuid === data.uuid && check.token === data.token){ 17 | data.fromUuid = fromDevice.uuid; 18 | return clientEmitter('config', check, data); 19 | } else { 20 | data.error = { 21 | "message": "Gateway not found", 22 | "code": 404 23 | }; 24 | 25 | return clientEmitter('messageAck', fromDevice, data); 26 | } 27 | }); 28 | } 29 | 30 | return { 31 | config: config, 32 | configAck: configAck 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "angular", 4 | "after", 5 | "afterEach", 6 | "before", 7 | "beforeEach", 8 | "chai", 9 | "describe", 10 | "expect", 11 | "iit", 12 | "it", 13 | "runs", 14 | "sinon", 15 | "spyOn", 16 | "waits", 17 | "waitsFor", 18 | "xit", 19 | "xdescribe", 20 | "define", 21 | "module", 22 | "exports", 23 | "require", 24 | "window", 25 | "process", 26 | "console", 27 | "__dirname", 28 | "Buffer" 29 | ], 30 | 31 | "asi" : false, 32 | "bitwise" : true, 33 | "boss" : false, 34 | "browser" : true, 35 | "curly" : true, 36 | "debug": false, 37 | "devel": false, 38 | "eqeqeq": true, 39 | "evil": false, 40 | "expr": true, 41 | "forin": false, 42 | "immed": true, 43 | "latedef" : false, 44 | "laxbreak": false, 45 | "multistr": true, 46 | "newcap": true, 47 | "noarg": true, 48 | "noempty": false, 49 | "nonew": true, 50 | "onevar": false, 51 | "plusplus": false, 52 | "regexp": false, 53 | "strict": false, 54 | "globalstrict": true, 55 | "sub": false, 56 | "trailing" : true, 57 | "undef": true, 58 | "unused": "vars" 59 | } -------------------------------------------------------------------------------- /lib/updateFromClient.js: -------------------------------------------------------------------------------- 1 | var whoAmI = require('./whoAmI'); 2 | var logEvent = require('./logEvent'); 3 | var securityImpl = require('./getSecurityImpl'); 4 | var updateDevice = require('./updateDevice'); 5 | 6 | function handleUpdate(fromDevice, data, fn){ 7 | fn = fn || function(){}; 8 | 9 | data.uuid = data.uuid || fromDevice.uuid; 10 | 11 | whoAmI(data.uuid, true, function(check){ 12 | 13 | if(check.error){ 14 | return fn(check); 15 | } 16 | 17 | if((data.token && check.token === data.token) || securityImpl.canUpdate(fromDevice, check, data)){ 18 | // if(securityImpl.canUpdate(fromDevice, check, data)){ 19 | updateDevice(data.uuid, data, function(results){ 20 | results.fromUuid = fromDevice.uuid; 21 | results.from = fromDevice; 22 | logEvent(401, results); 23 | 24 | try{ 25 | fn(results); 26 | } catch (e){ 27 | console.error(e); 28 | } 29 | 30 | }); 31 | }else{ 32 | fn({error: {message: 'unauthorized', code: 401} }); 33 | } 34 | }); 35 | 36 | } 37 | 38 | module.exports = handleUpdate; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Octoblu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/logData.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | var config = require('./../config'); 3 | 4 | var data = require('./database').data; 5 | 6 | module.exports = function(params, callback) { 7 | 8 | // Loop through parameters to update device sensor data 9 | var updates = {}; 10 | // updates.timestamp = new Date() 11 | updates.timestamp = moment(data.timestamp).toISOString(); 12 | 13 | for (var param in params) { 14 | var parsed; 15 | try { 16 | parsed = JSON.parse(params[param]); 17 | } catch (e) { 18 | parsed = params[param]; 19 | } 20 | // if(parsed) 21 | updates[param] = parsed.toString(); 22 | } 23 | 24 | delete updates.token; 25 | 26 | data.insert(updates, function(err, saved) { 27 | // if(err || saved === 0) { 28 | if(err) { 29 | var regdata = { 30 | "error": { 31 | "message": "Device not registered", 32 | "code": 500 33 | } 34 | }; 35 | // require('./logEvent')(400, regdata); 36 | callback(regdata); 37 | } else { 38 | updates._id.toString(); 39 | delete updates._id; 40 | 41 | require('./logEvent')(700, updates); 42 | callback(updates); 43 | } 44 | }); 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /public/localjsconsole.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 30 | 31 | 32 |

33 | 34 | Connecting to Meshblu... 35 | 36 |

37 |

38 |

39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/getEvents.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | 3 | var events = require('./database').events; 4 | 5 | module.exports = function(uuid, callback) { 6 | 7 | function processResults(err, eventdata){ 8 | if(err || eventdata.length < 1) { 9 | 10 | var eventResp = { 11 | "error": { 12 | "message": "Events not found", 13 | "code": 404 14 | } 15 | }; 16 | // require('./logEvent')(201, eventResp); 17 | callback(eventResp); 18 | 19 | } else { 20 | 21 | // remove tokens from results object 22 | for (var i=0;i 2 | 3 | 4 | 5 | 6 | 46 | 47 | 48 |

Connecting to Meshblu!

49 |

Subscribing to UUID:

50 |

Listening for sensor data...

51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/unregister.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var socketEmitter = require('./createSocketEmitter')(); 3 | 4 | var devices = require('./database').devices; 5 | var whoAmI = require('./whoAmI'); 6 | var securityImpl = require('./getSecurityImpl'); 7 | 8 | module.exports = function(fromDevice, unregisterUuid, unregisterToken, emitToClient, callback) { 9 | 10 | if(!fromDevice || !unregisterUuid){ 11 | callback('invalid from or to device'); 12 | } 13 | else{ 14 | whoAmI(unregisterUuid, true, function(toDevice){ 15 | if(toDevice.error){ 16 | callback('invalid device to unregister'); 17 | } 18 | else{ 19 | if((unregisterToken && toDevice.token === unregisterToken) || securityImpl.canUpdate(fromDevice, toDevice)){ 20 | if (emitToClient) { 21 | emitToClient('unregistered', toDevice, toDevice); 22 | } else { 23 | socketEmitter(toDevice.uuid, 'unregistered', toDevice); 24 | } 25 | devices.remove({ 26 | uuid: unregisterUuid 27 | }, function(err, devicedata) { 28 | if(err || devicedata === 0) { 29 | callback({ 30 | "message": "Device not found or token not valid", 31 | "code": 404 32 | }); 33 | } else { 34 | callback(null, { 35 | uuid: unregisterUuid 36 | }); 37 | } 38 | }); 39 | } 40 | else{ 41 | callback({message:'unauthorized', code: 401}); 42 | } 43 | } 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | notifications: 5 | flowdock: e7454f5285e7b9ab67009802d5560490 6 | deploy: 7 | - provider: opsworks 8 | access_key_id: AKIAIT4X4NDGM2WVL6VA 9 | secret_access_key: 10 | secure: fqCKEwrQBo2P9G6CiQqU4dv/nwQ64FWv+TRblkvZxCsGe+VKoNYSz57B2AaX0mGvTyS7qt7GHOxcU4T64KYdXpRuYe/Z2yekp7K9mcgGCqYnprwUo7Xh67kstixjKxvs4QzYRWF0tJiaeoXgLoXxu0fO111iP8bCIQc5ThBSF+8= 11 | app-id: e1883763-9db4-432f-900a-628dd84aceaa 12 | on: 13 | branch: master 14 | - provider: opsworks 15 | access_key_id: AKIAIT4X4NDGM2WVL6VA 16 | secret_access_key: 17 | secure: fqCKEwrQBo2P9G6CiQqU4dv/nwQ64FWv+TRblkvZxCsGe+VKoNYSz57B2AaX0mGvTyS7qt7GHOxcU4T64KYdXpRuYe/Z2yekp7K9mcgGCqYnprwUo7Xh67kstixjKxvs4QzYRWF0tJiaeoXgLoXxu0fO111iP8bCIQc5ThBSF+8= 18 | app-id: 7b21bb86-93bb-4a2b-b482-7a8a3ab4a23b 19 | on: 20 | branch: master 21 | - provider: opsworks 22 | access_key_id: AKIAIT4X4NDGM2WVL6VA 23 | secret_access_key: 24 | secure: fqCKEwrQBo2P9G6CiQqU4dv/nwQ64FWv+TRblkvZxCsGe+VKoNYSz57B2AaX0mGvTyS7qt7GHOxcU4T64KYdXpRuYe/Z2yekp7K9mcgGCqYnprwUo7Xh67kstixjKxvs4QzYRWF0tJiaeoXgLoXxu0fO111iP8bCIQc5ThBSF+8= 25 | app-id: 5fcc76e4-b0cc-41d3-b794-8a8b4ff0ed9e 26 | on: 27 | branch: staging 28 | - provider: opsworks 29 | access_key_id: AKIAIT4X4NDGM2WVL6VA 30 | secret_access_key: 31 | secure: fqCKEwrQBo2P9G6CiQqU4dv/nwQ64FWv+TRblkvZxCsGe+VKoNYSz57B2AaX0mGvTyS7qt7GHOxcU4T64KYdXpRuYe/Z2yekp7K9mcgGCqYnprwUo7Xh67kstixjKxvs4QzYRWF0tJiaeoXgLoXxu0fO111iP8bCIQc5ThBSF+8= 32 | app-id: 3110aaa3-dd25-49d1-be43-6c0ac5c0a8ad 33 | on: 34 | branch: staging 35 | -------------------------------------------------------------------------------- /lib/whoAmI.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var devices = require('./database').devices; 3 | var redis = require('./redis'); 4 | var cacheDevice = require('./cacheDevice'); 5 | 6 | 7 | function genError(uuid){ 8 | return { 9 | error: { 10 | uuid: uuid, 11 | message: 'Device not found', 12 | code: 404 13 | } 14 | }; 15 | } 16 | 17 | 18 | function dbGet(uuid, callback){ 19 | devices.findOne({ 20 | uuid: uuid 21 | }, function(merr, mdata) { 22 | if(merr || !mdata) { 23 | callback(genError(uuid)); 24 | }else{ 25 | callback(null, mdata); 26 | } 27 | }); 28 | } 29 | 30 | function getDevice(uuid, callback){ 31 | if(config.redis && config.redis.host){ 32 | redis.get('DEVICE_' + uuid, function(rerr, rdata){ 33 | if(!rdata){ 34 | dbGet(uuid, function(merr, mdata){ 35 | if(!merr){ 36 | cacheDevice(mdata); 37 | } 38 | callback(merr, mdata); 39 | //store to redis in background 40 | 41 | }); 42 | }else{ 43 | callback(null, JSON.parse(rdata)); 44 | } 45 | }); 46 | }else{ 47 | dbGet(uuid, callback); 48 | } 49 | } 50 | 51 | module.exports = function(uuid, owner, callback) { 52 | try{ 53 | getDevice(uuid, function(err, devicedata){ 54 | if(err){ 55 | callback(err); 56 | }else{ 57 | 58 | if(!owner){ 59 | delete devicedata.token; 60 | } 61 | 62 | delete devicedata._id; 63 | delete devicedata.timestamp; 64 | callback(devicedata); 65 | } 66 | }); 67 | }catch(exp){ 68 | console.error(exp); 69 | callback({error: exp}); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meshblu-server", 3 | "description": "communications platform and api for iot", 4 | "version": "1.0.0", 5 | "author": "Octoblu, Inc.", 6 | "engines": { 7 | "node": ">=0.6" 8 | }, 9 | "dependencies": { 10 | "JSONStream": "~0.9.0", 11 | "coap": "~0.8.0", 12 | "commander": "^2.3.0", 13 | "elasticsearch": "~2.4.0", 14 | "lodash": "~2.4.1", 15 | "moment": "~2.8.1", 16 | "mongojs": "~0.13.1", 17 | "mosca": "~0.25.1", 18 | "mqtt": "~0.3.13", 19 | "msgpack-js": "^0.3.0", 20 | "nedb": "~0.10.11", 21 | "newrelic": "^1.13.0", 22 | "node-static": "~0.7.6", 23 | "node-uuid": "~1.4.1", 24 | "qs": "~0.6.6", 25 | "redis": "~0.10.3", 26 | "request": "~2.40.0", 27 | "restify": "~2.8.3", 28 | "skynet": "1.5.0", 29 | "socket.io": "1.0.6", 30 | "socket.io-emitter": "^0.2.0", 31 | "socket.io-redis": "^0.1.3", 32 | "splunk-sdk": "latest", 33 | "tokenthrottle": "~1.1.0", 34 | "when": "~3.4.4", 35 | "airbrake": "^0.3.8", 36 | "geoip-lite": "^1.1.3", 37 | "mdns": "^2.2.0" 38 | }, 39 | "optionalDependencies": { 40 | "airbrake": "^0.3.8", 41 | "geoip-lite": "^1.1.3", 42 | "mdns": "^2.2.0" 43 | }, 44 | "subdomain": "skynet", 45 | "domains": [ 46 | "skynet.im" 47 | ], 48 | "scripts": { 49 | "start": "node server.js --http --mqtt" 50 | }, 51 | "main": "server.js", 52 | "devDependencies": {}, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/octoblu/meshblu.git" 56 | }, 57 | "license": "MIT", 58 | "bugs": { 59 | "url": "https://github.com/octoblu/meshblu/issues" 60 | }, 61 | "homepage": "https://github.com/octoblu/meshblu" 62 | } 63 | -------------------------------------------------------------------------------- /lib/redis.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | var redisStore = require('socket.io-redis'); 3 | var subEvents = require('./subEvents'); 4 | var msgpack = require('msgpack-js'); 5 | var config = require('../config'); 6 | var _ = require('lodash'); 7 | 8 | 9 | 10 | function createClient(options){ 11 | 12 | var client = redis.createClient(config.redis.port, config.redis.host, options); 13 | 14 | client.auth(config.redis.password, function(err){ 15 | if(err){ 16 | throw err; 17 | } 18 | }); 19 | 20 | return client; 21 | } 22 | 23 | function createIoStore(){ 24 | 25 | if(config.redis && config.redis.host){ 26 | var pubClient = createClient({ detect_buffers:true }); 27 | var subClient = createClient({ detect_buffers:true }); 28 | 29 | //additional message handler for non-socket.io clients 30 | subClient.on('pmessage', function(pattern, channel, msg){ 31 | try{ 32 | msg = msgpack.decode(msg); 33 | _.forEach(msg[1].rooms, function(room){ 34 | subEvents.emit(room, msg[0].data[0], msg[0].data[1]); 35 | }); 36 | }catch(exp){ 37 | console.error('unable to handle pmessage', exp); 38 | } 39 | 40 | }); 41 | 42 | var store = redisStore({ 43 | host: config.redis.host, 44 | port: config.redis.port, 45 | pubClient: pubClient, 46 | subClient: subClient 47 | }); 48 | 49 | store.pubClient = pubClient; 50 | store.subClient = subClient; 51 | 52 | return store; 53 | } 54 | } 55 | 56 | if(config.redis && config.redis.host){ 57 | var client = createClient({parser: "javascript"}); 58 | module.exports = { 59 | createIoStore: createIoStore, 60 | get: client.get.bind(client), 61 | set: client.set.bind(client), 62 | del: client.del.bind(client), 63 | setex: client.setex.bind(client), 64 | client: client 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /lib/logEvent.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var moment = require('moment'); 3 | var config = require('./../config'); 4 | var client = require('./elasticSearch'); 5 | var splunkService = require('./splunk'); 6 | var RedisSplunk = require('./redisSplunk'); 7 | 8 | var events = require('./database').events; 9 | 10 | module.exports = function(eventCode, data) { 11 | 12 | // add timestamp if one isn't passed 13 | if (data && !data.hasOwnProperty("timestamp")){ 14 | var newTimestamp = new Date().getTime(); 15 | data.timestamp = newTimestamp; 16 | } 17 | // moment().toISOString() 18 | data.timestamp = moment(data.timestamp).toISOString() 19 | 20 | if(eventCode === undefined){ 21 | eventCode = 0; 22 | } 23 | data.eventCode = eventCode; 24 | var uuid = eventCode; //Set the Default to EventCode just in case. 25 | if (data && data.hasOwnProperty("uuid")){ // if the data has a uuid (and it always should) 26 | uuid = data.uuid; // set the uuid to the actual uuid. 27 | } 28 | 29 | if(config.log){ 30 | fs.appendFile('./skynet.txt', JSON.stringify(data) + '\r\n', function (err) { 31 | if (err) { 32 | console.error(err); 33 | } 34 | }); 35 | 36 | if(config.elasticSearch && config.elasticSearch.hosts){ 37 | if(data._id){ 38 | try{ 39 | data._id.toString(); 40 | delete data._id; 41 | } catch(e){ 42 | console.error(e); 43 | } 44 | } 45 | 46 | var msg = { 47 | index: "skynet_trans_log", 48 | type: uuid, 49 | timestamp: data.timestamp, 50 | body: data 51 | }; 52 | client.index(msg, function (error, response) { 53 | }); 54 | } 55 | 56 | // Replaced by Splunk Forwarding Agent 57 | // RedisSplunk.log(data, function(err){ 58 | // if(err){ 59 | // console.error(err); 60 | // } 61 | // }); 62 | } 63 | 64 | events.insert(data, function(err, saved) { 65 | if(err || saved.length < 1) { 66 | console.error(err); 67 | } 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /config.js.sample: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 3000, 3 | //tls: { 4 | // sslPort: 443, 5 | // cert: "/certs/server.crt", 6 | // key: "/certs/server.key" 7 | //}, 8 | log: true, 9 | // MongoDB is optional. Comment this section out if not desired. 10 | mongo: { 11 | databaseUrl: "mongodb://user:pass@adam.mongohq.com:1337/skynet" 12 | }, 13 | // REDIS is optional. It's used for scaling session and sockets horizontally. Comment this section out if not desired. 14 | redis: { 15 | host: "xyz.redistogo.com", 16 | port: "1234", 17 | password: "abcdef" 18 | }, 19 | // ElasticSearch is optional. It's used to analyze data. Comment this section out if not desired. 20 | elasticSearch: { 21 | host: "localhost", 22 | port: "9200" 23 | }, 24 | // this skynet cloud's uuid 25 | uuid: 'xxxx-my-cloud's-uuid-xxxx', 26 | token: 'xxx --- my token ---- xxxx', 27 | broadcastActivity: false, 28 | // if you want to resolve message up to another skynet server: 29 | parentConnection: { 30 | uuid: 'xxxx-my-uuid-on-parent-server-xxxx', 31 | token: 'xxx-my-token-on-parent-server-xxxx', 32 | server: 'skynet.im', 33 | port: 80 34 | }, 35 | rateLimits: { 36 | message: 10, // 10 transactions per user per second 37 | data: 10, // 10 transactions per user per second 38 | connection: 2, // 2 transactions per IP per second 39 | query: 2, // 2 transactions per user per second 40 | whoami: 10, // 10 transactions per user per second 41 | unthrottledIps: ["54.186.134.252"] // allow unlimited transactions from these IP addresses 42 | }, 43 | plivo: { 44 | authId: "abc", 45 | authToken: "123" 46 | }, 47 | urbanAirship: { 48 | key: "abc", 49 | secret: "123" 50 | }, 51 | coap: { 52 | port: 5683, 53 | host: "localhost" 54 | }, 55 | //these settings are for the mqtt server, and skynet mqtt client 56 | mqtt: { 57 | databaseUrl: "mongodb://user:pass@adam.mongohq.com:1337/skynet", 58 | port: 1883, 59 | skynetPass: "Very big random password 34lkj23orfjvi3-94ufpvuha4wuef-a09v4ji0rhgouj" 60 | }, 61 | yo: { 62 | token: "your yo token from http://yoapi.justyo.co/" 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /lib/getDevices.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var securityImpl = require('./getSecurityImpl'); 3 | 4 | var devices = require('./database').devices; 5 | 6 | module.exports = function(fromDevice, query, owner, callback) { 7 | 8 | function processResults(err, devicedata){ 9 | if(err || devicedata.length < 1) { 10 | 11 | devicedata = { 12 | "error": { 13 | "message": "Devices not found", 14 | "code": 404 15 | } 16 | }; 17 | require('./logEvent')(403, devicedata); 18 | callback(devicedata); 19 | 20 | 21 | } else { 22 | 23 | var deviceResults = []; 24 | 25 | devicedata.forEach(function(device){ 26 | if(securityImpl.canView(fromDevice, device)){ 27 | deviceResults.push(device); 28 | 29 | //TODO maybe configurable secure props? 30 | if(!owner && fromDevice.uuid != device.uuid){ 31 | delete device.token; 32 | delete device.socketid; 33 | delete device._id; 34 | delete device.sendWhitelist; 35 | delete device.sendBlacklist; 36 | delete device.viewWhitelist; 37 | delete device.viewBlacklist; 38 | delete device.owner; 39 | } 40 | 41 | } 42 | }); 43 | 44 | require('./logEvent')(403, {"devices": deviceResults}); 45 | callback({"devices": deviceResults}); 46 | } 47 | 48 | } 49 | 50 | // query = JSON.parse(query); 51 | var fetch = {}; 52 | // Loop through parameters to update device 53 | for (var param in query) { 54 | fetch[param] = query[param]; 55 | if (query[param] === 'null' || query[param] === ''){ 56 | fetch[param] = { "$exists" : false }; 57 | } 58 | 59 | } 60 | if (query.online){ 61 | fetch.online = query.online === "true"; 62 | } 63 | delete fetch.token; 64 | //sorts newest devices on top 65 | if(config.mongo && config.mongo.databaseUrl){ 66 | devices.find(fetch).sort({ _id: -1 }, function(err, devicedata) { 67 | processResults(err, devicedata); 68 | }); 69 | } else { 70 | devices.find(fetch).sort({ timestamp: -1 }).exec(function(err, devicedata) { 71 | processResults(err, devicedata); 72 | }); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /lib/getData.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | var config = require('./../config'); 3 | 4 | var data = require('./database').data; 5 | var events = require('./database').events; 6 | 7 | module.exports = function(req, callback) { 8 | 9 | function processResults(err, eventdata){ 10 | if(err || eventdata.length < 1) { 11 | 12 | var eventResp = { 13 | "error": { 14 | "message": "Data not found", 15 | "code": 404 16 | } 17 | }; 18 | // require('./logEvent')(201, eventResp); 19 | callback(eventResp); 20 | 21 | } else { 22 | 23 | // remove tokens from results object 24 | for (var i=0;i', 'Set the HTTP port (defaults to 3000)') 17 | // .option('--https-port ', 'Set the HTTP port (defaults to null)') 18 | // .option('--mqtt-port ', 'Set the MQTT port (defaults to 1883)') 19 | // .option('--coap-port ', 'Set the CoAP port (defaults to 5683)') 20 | .option('--coap', 'Enable CoAP server (defaults to false)') 21 | .option('--http', 'Enable HTTP server (defaults to false)') 22 | .option('--https', 'Enable HTTPS server (defaults to false)') 23 | .option('--mdns', 'Enable Multicast DNS (defaults to false)') 24 | .option('--mqtt', 'Enable MQTT server (defaults to false)') 25 | .option('--parent', 'Enable Parent connection (defaults to false)') 26 | .parse(process.argv); 27 | 28 | // Defaults 29 | program.environment = program.environment || process.env.NODE_ENV || 'development'; 30 | // program.coapPort = program.coapPort || 5683; 31 | // program.httpPort = program.httpPort || 3000; 32 | // program.httpsPort = program.httpsPort || 4000; 33 | // program.mqttPort = program.mqttPort || 1883; 34 | program.coap = program.coap || false; 35 | program.http = program.http || false; 36 | program.https = program.https || false; 37 | program.mdns = program.mdns || false; 38 | program.mqtt = program.mqtt || false; 39 | program.parent = program.parent || false; 40 | 41 | console.log(""); 42 | console.log("MM MM hh bb lll "); 43 | console.log("MMM MMM eee sss hh bb lll uu uu "); 44 | console.log("MM MM MM ee e s hhhhhh bbbbbb lll uu uu "); 45 | console.log("MM MM eeeee sss hh hh bb bb lll uu uu "); 46 | console.log("MM MM eeeee s hh hh bbbbbb lll uuuu u "); 47 | console.log(" sss "); 48 | console.log('\Meshblu (formerly skynet.im) %s environment loaded... ', program.environment); 49 | console.log(""); 50 | 51 | if (process.env.AIRBRAKE_KEY) { 52 | var airbrakeErrors = require("./lib/airbrakeErrors"); 53 | airbrakeErrors.handleExceptions() 54 | } else { 55 | process.on("uncaughtException", function(error) { 56 | console.error(error.message, error.stack); 57 | }); 58 | } 59 | 60 | if (program.parent) { 61 | process.stdout.write('Starting Parent connection...'); 62 | parentConnection = require('./lib/parentConnection')(config); 63 | console.log(' done.'); 64 | } 65 | 66 | if (program.coap) { 67 | process.stdout.write('Starting CoAP...'); 68 | var coapServer = require('./lib/coapServer')(config, parentConnection); 69 | console.log(' done.'); 70 | } 71 | 72 | if (program.http || program.https) { 73 | process.stdout.write('Starting HTTP/HTTPS...'); 74 | var httpServer = require('./lib/httpServer')(config, parentConnection); 75 | console.log(' done.'); 76 | } 77 | 78 | if (program.mdns) { 79 | process.stdout.write('Starting mDNS...'); 80 | var mdnsServer = require('./lib/mdnsServer')(config); 81 | mdnsServer.start(); 82 | console.log(' done.'); 83 | } 84 | 85 | if (program.mqtt) { 86 | process.stdout.write('Starting MQTT...'); 87 | var mqttServer = require('./lib/mqttServer')(config, parentConnection); 88 | console.log(' done.'); 89 | } 90 | -------------------------------------------------------------------------------- /lib/proxyListener.js: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/daguej/node-proxywrap 2 | function connectionListener(socket) { 3 | var options = {}; 4 | var self = this, realEmit = socket.emit, history = [], protocolError = false; 5 | 6 | // override the socket's event emitter so we can process data (and discard the PROXY protocol header) before the underlying Server gets it 7 | socket.emit = function(event, data) { 8 | history.push(Array.prototype.slice.call(arguments)); 9 | if (event == 'readable') { 10 | onReadable(); 11 | } 12 | } 13 | 14 | function restore() { 15 | // restore normal socket functionality, and fire any events that were emitted while we had control of emit() 16 | socket.emit = realEmit; 17 | for (var i = 0; i < history.length; i++) { 18 | realEmit.apply(socket, history[i]); 19 | if (history[i][0] == 'end' && socket.onend) socket.onend(); 20 | } 21 | history = null; 22 | } 23 | 24 | socket.on('readable', onReadable); 25 | 26 | var header = '', buf = new Buffer(0); 27 | function onReadable() { 28 | var chunk; 29 | while (null != (chunk = socket.read())) { 30 | buf = Buffer.concat([buf, chunk]); 31 | header += chunk.toString('ascii'); 32 | 33 | // if the first 5 bytes aren't PROXY, something's not right. 34 | if (header.length >= 5 && header.substr(0, 5) != 'PROXY') { 35 | protocolError = true; 36 | if (options.strict) { 37 | return socket.destroy('PROXY protocol error'); 38 | } 39 | } 40 | 41 | var crlf = header.indexOf('\r'); 42 | if (crlf > 0 || protocolError) { 43 | socket.removeListener('readable', onReadable); 44 | header = header.substr(0, crlf); 45 | 46 | var hlen = header.length; 47 | header = header.split(' '); 48 | 49 | if (!protocolError) { 50 | Object.defineProperty(socket, 'remoteAddress', { 51 | enumerable: false, 52 | configurable: true, 53 | get: function() { 54 | return header[2]; 55 | } 56 | }); 57 | Object.defineProperty(socket, 'remotePort', { 58 | enumerable: false, 59 | configurable: true, 60 | get: function() { 61 | return parseInt(header[4], 10); 62 | } 63 | }); 64 | } 65 | 66 | // unshifting will fire the readable event 67 | socket.emit = realEmit; 68 | socket.unshift(buf.slice(protocolError ? 0 : crlf+2)); 69 | 70 | self.emit('proxiedConnection', socket); 71 | 72 | restore(); 73 | 74 | if (socket.ondata) { 75 | var data = socket.read(); 76 | if (data) socket.ondata(data, 0, data.length); 77 | } 78 | 79 | break; 80 | 81 | } 82 | else if (header.length > 107) return socket.destroy('PROXY protocol error'); // PROXY header too long 83 | } 84 | } 85 | } 86 | 87 | function resetListeners(socket) { 88 | var cl = socket.listeners('connection'); 89 | socket.removeAllListeners('connection'); 90 | socket.addListener('connection', connectionListener); 91 | 92 | // add the old connection listeners to a custom event, which we'll fire after processing the PROXY header 93 | for (var i = 0; i < cl.length; i++) { 94 | socket.addListener('proxiedConnection', cl[i]); 95 | } 96 | 97 | } 98 | 99 | module.exports = { 100 | connectionListener : connectionListener, 101 | resetListeners : resetListeners 102 | } 103 | -------------------------------------------------------------------------------- /lib/register.js: -------------------------------------------------------------------------------- 1 | var uuid = require('node-uuid'); 2 | var moment = require('moment'); 3 | var config = require('./../config'); 4 | 5 | var devices = require('./database').devices; 6 | 7 | module.exports = function(params, callback) { 8 | 9 | function pad(width, string, padding) { 10 | return (width <= string.length) ? string : pad(width, padding + string, padding) 11 | } 12 | 13 | var rand = function() { 14 | return Math.random().toString(36).substr(2); // remove `0.` 15 | }; 16 | 17 | var generateToken = function() { 18 | return pad(32, rand() + rand(), '0'); // to make it longer 19 | }; 20 | 21 | var newUuid = uuid.v1(); 22 | var newTimestamp = new Date().getTime(); 23 | 24 | var updates = {}; 25 | 26 | // Loop through parameters to update device 27 | for (var param in params) { 28 | var parsed; 29 | if(typeof params[param] === 'object'){ 30 | try{ 31 | parsed = JSON.parse(params[param]); 32 | } catch (e){ 33 | parsed = params[param]; 34 | } 35 | } else { 36 | parsed = params[param]; 37 | } 38 | updates[param] = parsed; 39 | } 40 | 41 | 42 | var deviceChannel; 43 | if (params.channel){ 44 | deviceChannel = params.channel; 45 | } else { 46 | deviceChannel = 'main'; 47 | } 48 | 49 | var devicePresence; 50 | if(typeof params.online === 'string'){ 51 | if (params.online == "true"){ 52 | devicePresence = true; 53 | } else if(params.online == "false"){ 54 | devicePresence = false; 55 | } 56 | } else if (typeof params.online === 'boolean'){ 57 | devicePresence = params.online; 58 | } else { 59 | devicePresence = false; 60 | } 61 | 62 | var deviceIpAddress; 63 | var deviceGeo; 64 | if (params.ipAddress){ 65 | deviceIpAddress = params.ipAddress; 66 | try { 67 | var geoip = require('geoip-lite'); 68 | deviceGeo = geoip.lookup(params.ipAddress); 69 | } catch(geoip){ 70 | deviceGeo = null; 71 | } 72 | } else { 73 | deviceIpAddress = ""; 74 | deviceGeo = null; 75 | } 76 | 77 | var token; 78 | if (params.token){ 79 | token = params.token; 80 | } else { 81 | token = generateToken(); 82 | } 83 | 84 | var myUuid; 85 | if (params.uuid){ 86 | devices.findOne({ 87 | uuid: params.uuid 88 | }, function(err, devicedata) { 89 | if(err || !devicedata || devicedata.length < 1) { 90 | myUuid = params.uuid; 91 | writeUuid(); 92 | } else { 93 | // myUuid = uuid.v1(); 94 | // writeUuid(); 95 | 96 | // Return an error now if UUID is already registered rather than auto generate a new uuid 97 | var regdata = { 98 | "error": { 99 | "message": "Device UUID already registered", 100 | "code": 500 101 | } 102 | }; 103 | callback(regdata); 104 | 105 | } 106 | }); 107 | } else { 108 | myUuid = uuid.v1(); 109 | writeUuid(); 110 | } 111 | 112 | function writeUuid() { 113 | updates.uuid = myUuid; 114 | updates.timestamp = moment(newTimestamp).toISOString(); 115 | updates.token = token; 116 | updates.channel = deviceChannel; 117 | updates.online = devicePresence; 118 | updates.ipAddress = deviceIpAddress; 119 | updates.geo = deviceGeo; 120 | updates.socketid = params.socketid; 121 | 122 | devices.insert(updates, function(err, saved) { 123 | 124 | if(err) { 125 | var regdata = { 126 | "error": { 127 | "message": "Device not registered", 128 | "code": 500 129 | } 130 | }; 131 | callback(regdata); 132 | } else { 133 | updates._id.toString(); 134 | delete updates._id; 135 | callback(updates); 136 | } 137 | }); 138 | } 139 | 140 | }; 141 | -------------------------------------------------------------------------------- /lib/coapRouter.js: -------------------------------------------------------------------------------- 1 | var qs = require('qs'), 2 | parse = require('url').parse, 3 | _ = require('lodash'); 4 | 5 | var coapRouter = { 6 | routes : [], 7 | compilePattern : function (pattern) { 8 | if(pattern instanceof RegExp) return pattern; 9 | 10 | var fragments = pattern.split(/\//); 11 | return fragments.reduce( 12 | function (route, fragment, index, fragments) { 13 | if(fragment.match(/^\:/)) { 14 | route.tokens.push(fragment.replace(/^\:/, '')); 15 | route.fragments.push('([a-zA-Z0-9-_~\\.%@]+)'); 16 | } else { 17 | route.fragments.push(fragment); 18 | } 19 | 20 | if(index === (fragments.length - 1)) route.regexp = route.compileRegExp(); 21 | return route; 22 | }, 23 | { 24 | fragments : [], 25 | tokens : [], 26 | compileRegExp : function () { return new RegExp(this.fragments.join("\\/") + "$"); }, 27 | regexp : '' 28 | } 29 | ); 30 | }, 31 | add : function (method, pattern, callback) { 32 | var route = this.compilePattern(pattern); 33 | 34 | route.method = method; 35 | route.pattern = route.regexp; 36 | route.callback = callback; 37 | 38 | this.routes.push(route); 39 | return this; 40 | }, 41 | "get" : function (pattern, callback) { return this.add('get', pattern, callback); }, 42 | "post" : function (pattern, callback) { return this.add('post', pattern, callback); }, 43 | "put" : function (pattern, callback) { return this.add('put', pattern, callback); }, 44 | "delete" : function (pattern, callback) { return this.add('delete', pattern, callback); }, 45 | find : function (method, url) { 46 | var routes = this.routes.filter(function (route) { 47 | return (route.method === method && url.match(route.pattern)); 48 | }); 49 | return routes[0]; 50 | }, 51 | parseValues : function (url, route) { 52 | var tokenLength = route.tokens.length + 1; 53 | var matches = url.match(route.pattern); 54 | var values = matches.slice(1, tokenLength); 55 | return route.tokens.reduce(function (finalValues, token, index, tokens) { 56 | finalValues[token] = values[index]; 57 | return finalValues; 58 | }, {}); 59 | }, 60 | attachQuery : function (request) { 61 | var query = request.options.filter(function (option) { return option.name === 'Uri-Query'; }); 62 | if(query) { 63 | query = query.reduce(function (options, option, index, query) { 64 | return _.extend(options, qs.parse(option.value.toString())); 65 | }, {}); 66 | } 67 | 68 | var params = request.payload; 69 | 70 | if(params && params.length > 0) { 71 | try { 72 | params = JSON.parse(params.toString()); 73 | } catch (e) { 74 | if(e instanceof SyntaxError) { 75 | params = params.toString().replace(/^\/.*\?/g, ''); 76 | params = qs.parse(params); 77 | } 78 | } 79 | } else { params = {}; } 80 | 81 | params = _.extend(params, query); 82 | query = params; 83 | 84 | request.query = query; 85 | request.params = params; 86 | 87 | return request; 88 | }, 89 | attachJson : function (response) { 90 | response.json = function (data) { 91 | response.end(JSON.stringify(data)); 92 | }; 93 | return response; 94 | }, 95 | process : function (request, response) { 96 | coapRouter.attachQuery(request); 97 | coapRouter.attachJson(response); 98 | 99 | var method = request.method ? request.method.toLowerCase() : 'get', 100 | url = parse(request.url).pathname; 101 | 102 | var route = coapRouter.find(method, url); 103 | 104 | if(route) { 105 | var callback = route.callback, 106 | values = coapRouter.parseValues(url, route); 107 | 108 | _.extend(request.params, values); 109 | 110 | callback.apply(callback, [request, response]); 111 | } else { 112 | response.statusCode = 404; 113 | response.json({error: {message: "We could not find that resource", code: 404}}); 114 | } 115 | } 116 | }; 117 | 118 | module.exports = coapRouter; 119 | -------------------------------------------------------------------------------- /lib/httpServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var restify = require('restify'); 4 | var socketio = require('socket.io'); 5 | var proxyListener = require('./proxyListener'); 6 | var setupRestfulRoutes = require('./setupHttpRoutes'); 7 | var setupMqttClient = require('./setupMqttClient'); 8 | var socketLogic = require('./socketLogic'); 9 | var sendMessageCreator = require('./sendMessage'); 10 | var wrapMqttMessage = require('./wrapMqttMessage'); 11 | var createSocketEmitter = require('./createSocketEmitter'); 12 | var sendActivity = require('./sendActivity'); 13 | var sendConfigActivity = require('./sendConfigActivity'); 14 | var throttles = require('./getThrottles'); 15 | var fs = require('fs'); 16 | var setupGatewayConfig = require('./setupGatewayConfig'); 17 | var parentConnection = require('./getParentConnection'); 18 | var skynetClient = require("skynet"); 19 | 20 | var httpServer = function(config, parentConnection) { 21 | var useHTTPS = config.tls && config.tls.cert; 22 | 23 | // Instantiate our two servers (http & https) 24 | var server = restify.createServer(); 25 | server.pre(restify.pre.sanitizePath()); 26 | 27 | if(useHTTPS){ 28 | 29 | // Setup some https server options 30 | var https_options = { 31 | certificate: fs.readFileSync(config.tls.cert), 32 | key: fs.readFileSync(config.tls.key) 33 | }; 34 | 35 | var https_server = restify.createServer(https_options); 36 | https_server.pre(restify.pre.sanitizePath()); 37 | } 38 | 39 | if (config.useProxyProtocol) { 40 | proxyListener.resetListeners(server.server); 41 | if(useHTTPS){ 42 | proxyListener.resetListeners(https_server.server); 43 | } 44 | } 45 | 46 | // Setup websockets 47 | var io, ios, redisStore; 48 | io = socketio(server.server); 49 | if(config.redis && config.redis.host){ 50 | var redis = require('./redis'); 51 | redisStore = redis.createIoStore(); 52 | io.adapter(redisStore); 53 | } 54 | 55 | if(useHTTPS){ 56 | ios = socketio(https_server.server); 57 | if(config.redis && config.redis.host){ 58 | ios.adapter(redisStore); 59 | } 60 | } 61 | 62 | restify.CORS.ALLOW_HEADERS.push('skynet_auth_uuid'); 63 | restify.CORS.ALLOW_HEADERS.push('skynet_auth_token'); 64 | restify.CORS.ALLOW_HEADERS.push('meshblu_auth_uuid'); 65 | restify.CORS.ALLOW_HEADERS.push('meshblu_auth_token'); 66 | restify.CORS.ALLOW_HEADERS.push('accept'); 67 | restify.CORS.ALLOW_HEADERS.push('sid'); 68 | restify.CORS.ALLOW_HEADERS.push('lang'); 69 | restify.CORS.ALLOW_HEADERS.push('origin'); 70 | restify.CORS.ALLOW_HEADERS.push('withcredentials'); 71 | restify.CORS.ALLOW_HEADERS.push('x-requested-with'); 72 | 73 | // http params 74 | server.use(restify.queryParser()); 75 | server.use(restify.bodyParser()); 76 | server.use(restify.CORS({ headers: [ 'skynet_auth_uuid', 'skynet_auth_token', 'meshblu_auth_uuid', 'meshblu_auth_token' ], origins: ['*:*'] })); 77 | server.use(restify.fullResponse()); 78 | 79 | // https params 80 | if (useHTTPS) { 81 | https_server.use(restify.queryParser()); 82 | https_server.use(restify.bodyParser()); 83 | https_server.use(restify.CORS({ headers: [ 'skynet_auth_uuid', 'skynet_auth_token', 'meshblu_auth_uuid', 'meshblu_auth_token' ], origins: ['*:*'] })); 84 | https_server.use(restify.fullResponse()); 85 | } 86 | 87 | var socketEmitter = createSocketEmitter(io, ios); 88 | 89 | function mqttEmitter(uuid, wrappedData, options){ 90 | if(mqttclient){ 91 | mqttclient.publish(uuid, wrappedData, options); 92 | } 93 | } 94 | 95 | var sendMessage = sendMessageCreator(socketEmitter, mqttEmitter, parentConnection); 96 | if(parentConnection){ 97 | parentConnection.on('message', function(data, fn){ 98 | if(data){ 99 | var devices = data.devices; 100 | if (!_.isArray(devices)) { 101 | devices = [devices]; 102 | } 103 | _.each(devices, function(device) { 104 | if(device !== config.parentConnection.uuid){ 105 | sendMessage({uuid: data.fromUuid}, data, fn); 106 | } 107 | }); 108 | } 109 | }); 110 | } 111 | 112 | 113 | function emitToClient(topic, device, msg){ 114 | if(device.protocol === "mqtt"){ 115 | // MQTT handler 116 | mqttEmitter(device.uuid, wrapMqttMessage(topic, msg), {qos:msg.qos || 0}); 117 | } 118 | else{ 119 | socketEmitter(device.uuid, topic, msg); 120 | } 121 | } 122 | 123 | var skynet = { 124 | sendMessage: sendMessage, 125 | gateway : setupGatewayConfig(emitToClient), 126 | sendActivity: sendActivity, 127 | sendConfigActivity: sendConfigActivity, 128 | throttles: throttles, 129 | io: io, 130 | ios: ios, 131 | emitToClient: emitToClient 132 | }; 133 | 134 | function checkConnection(socket, secure){ 135 | var ip = socket.handshake.headers["x-forwarded-for"] || socket.request.connection.remoteAddress; 136 | 137 | if(_.contains(throttles.unthrottledIps, ip)){ 138 | socket.throttled = false; 139 | socketLogic(socket, secure, skynet); 140 | }else{ 141 | socket.throttled = true; 142 | throttles.connection.rateLimit(ip, function (err, limited) { 143 | if(limited){ 144 | socket.emit('notReady',{error: 'rate limit exceeded ' + ip}); 145 | socket.disconnect(); 146 | }else{ 147 | socketLogic(socket, secure, skynet); 148 | } 149 | }); 150 | } 151 | } 152 | 153 | io.on('connection', function (socket) { 154 | checkConnection(socket, false); 155 | }); 156 | 157 | if(useHTTPS){ 158 | ios.on('connection', function (socket) { 159 | checkConnection(socket, true); 160 | }); 161 | } 162 | 163 | var mqttclient = setupMqttClient(skynet, config); 164 | 165 | // Now, setup both servers in one step 166 | setupRestfulRoutes(server, skynet); 167 | 168 | if(useHTTPS){ 169 | setupRestfulRoutes(https_server, skynet); 170 | } 171 | 172 | var serverPort = process.env.PORT || config.port; 173 | server.listen(serverPort, function() { 174 | console.log('HTTP listening at %s', server.url); 175 | }); 176 | 177 | if(useHTTPS){ 178 | https_server.listen(process.env.SSLPORT || config.tls.sslPort, function() { 179 | console.log('HTTPS listening at %s', https_server.url); 180 | }); 181 | } 182 | }; 183 | 184 | module.exports = httpServer; 185 | -------------------------------------------------------------------------------- /lib/sendMessage.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var whoAmI = require('./whoAmI'); 3 | var logEvent = require('./logEvent'); 4 | var sendYo = require('./sendYo'); 5 | var sendSms = require('./sendSms'); 6 | var securityImpl = require('./getSecurityImpl'); 7 | var sendActivity = require('./sendActivity'); 8 | var createActivity = require('./createActivity'); 9 | var wrapMqttMessage = require('./wrapMqttMessage'); 10 | var sendPushNotification = require('./sendPushNotification'); 11 | 12 | var DEFAULT_QOS = 0; 13 | 14 | function publishActivity(topic, fromDevice, toDevice, data){ 15 | if(fromDevice && fromDevice.ipAddress){ 16 | sendActivity(createActivity(topic, fromDevice.ipAddress, fromDevice, toDevice, data)); 17 | } 18 | } 19 | 20 | function cloneMessage(msg, device, fromUuid){ 21 | if(typeof msg === 'object'){ 22 | var clonedMsg = _.clone(msg); 23 | clonedMsg.devices = device; //strip other devices from message 24 | delete clonedMsg.protocol; 25 | delete clonedMsg.api; 26 | clonedMsg.fromUuid = msg.fromUuid; // add from device object to message for logging 27 | return clonedMsg; 28 | } 29 | 30 | return msg; 31 | } 32 | 33 | 34 | function createMessageSender(socketEmitter, mqttEmitter, parentConnection){ 35 | 36 | function forwardMessage(message){ 37 | if(parentConnection && message){ 38 | try{ 39 | message.originUuid = message.fromUuid; 40 | delete message.fromUuid; 41 | parentConnection.message(message); 42 | }catch(ex){ 43 | console.error('error forwarding message', ex); 44 | } 45 | } 46 | } 47 | 48 | function broadcastMessage(data, topic, fromUuid, fromDevice){ 49 | var devices = data.devices; 50 | 51 | if(!isBroadcast(devices, topic)){ return; } 52 | 53 | //broadcasting should never require responses 54 | delete data.ack; 55 | 56 | var broadcastType = topic === 'tb' ? '_tb' : '_bc'; 57 | socketEmitter(fromUuid + broadcastType, topic, data); 58 | mqttEmitter(fromUuid + broadcastType, wrapMqttMessage(topic, data), {qos: data.qos || DEFAULT_QOS}); 59 | publishActivity(topic, fromDevice, null, data); 60 | var logMsg = _.clone(data); 61 | logMsg.from = _.clone(fromDevice); 62 | if (logMsg.from) { 63 | delete logMsg.from.token; 64 | } 65 | logEvent(300, logMsg); 66 | }; 67 | 68 | var sendMessage = function(device, data, topic, fromUuid, fromDevice, toDevices){ 69 | if (!device) { 70 | return; 71 | } 72 | if( isBroadcast([device], topic) ) { 73 | return; 74 | } 75 | var toDeviceProp = device; 76 | if (device.length > 35){ 77 | 78 | var deviceArray = device.split('/'); 79 | if(deviceArray.length > 1){ 80 | device = deviceArray.shift(); 81 | toDeviceProp = deviceArray.join('/'); 82 | } 83 | 84 | //check devices are valid 85 | whoAmI(device, false, function(check){ 86 | var clonedMsg = cloneMessage(data, toDeviceProp, fromUuid); 87 | if(topic === 'tb'){ 88 | delete clonedMsg.devices; 89 | } 90 | 91 | if(!check.error){ 92 | if(securityImpl.canSend(fromDevice, check)){ 93 | 94 | publishActivity(topic, fromDevice, check, data); 95 | 96 | // //to phone, but not from same phone 97 | if(check.phoneNumber && check.type === "outboundSMS"){ 98 | // SMS handler 99 | sendSms(device, JSON.stringify(clonedMsg.payload), function(sms){ 100 | var logMsg = _.clone(clonedMsg); 101 | logMsg.toUuid = check.uuid; 102 | logMsg.to = check; 103 | logEvent(302, logMsg); 104 | }); 105 | }else if(check.yoUser && check.type === "yo"){ 106 | // Yo handler 107 | sendYo(device, function(yo){ 108 | var logMsg = _.clone(clonedMsg); 109 | logMsg.toUuid = check.uuid; 110 | logMsg.to = check; 111 | logEvent(304, logMsg); 112 | }); 113 | }else if(check.type === 'octobluMobile'){ 114 | // Push notification handler 115 | sendPushNotification(check, JSON.stringify(clonedMsg.payload), function(push){ 116 | var logMsg = _.clone(clonedMsg); 117 | logMsg.toUuid = check.uuid; 118 | logMsg.to = check; 119 | logEvent(305, logMsg); 120 | }); 121 | } 122 | 123 | var emitMsg = clonedMsg; 124 | 125 | // Added to preserve to devices in message 126 | emitMsg.devices = toDevices; 127 | 128 | if(check.payloadOnly){ 129 | emitMsg = clonedMsg.payload; 130 | } 131 | 132 | if(check.protocol === 'mqtt'){ 133 | mqttEmitter(check.uuid, wrapMqttMessage(topic, emitMsg), {qos: data.qos || DEFAULT_QOS}); 134 | } 135 | else{ 136 | socketEmitter(check.uuid, topic, emitMsg); 137 | } 138 | }else{ 139 | clonedMsg.UNAUTHORIZED=true; //for logging 140 | } 141 | 142 | }else{ 143 | forwardMessage(clonedMsg); 144 | } 145 | 146 | var logMsg = _.clone(clonedMsg); 147 | logMsg.toUuid = check.uuid; 148 | logMsg.to = check; 149 | logEvent(300, logMsg); 150 | }); 151 | } 152 | }; 153 | 154 | var isBroadcast = function(devices, topic){ 155 | return _.contains(devices, '*') || _.contains(devices, 'all') || (topic === 'tb' && _.isEmpty(devices)); 156 | }; 157 | 158 | return function(fromDevice, data, topic){ 159 | topic = topic || 'message'; 160 | var fromUuid; 161 | if(fromDevice){ 162 | fromUuid = fromDevice.uuid; 163 | } 164 | 165 | if(fromUuid){ 166 | data.fromUuid = fromUuid; 167 | } 168 | 169 | if(data.token){ 170 | //never forward token to another client 171 | delete data.token; 172 | } 173 | 174 | broadcastMessage(data, topic, fromUuid, fromDevice); 175 | 176 | if(!data.devices) { 177 | return; 178 | } 179 | 180 | var devices = data.devices; 181 | if( typeof devices === 'string' ) { 182 | devices = [ devices ]; 183 | } 184 | 185 | //cant ack to multiple devices 186 | if(devices.length > 1){ 187 | delete data.ack; 188 | } 189 | 190 | devices.forEach( function(device) { 191 | sendMessage(device, data, topic, fromUuid, fromDevice, data.devices); 192 | }); 193 | }; 194 | } 195 | 196 | module.exports = createMessageSender; 197 | 198 | -------------------------------------------------------------------------------- /lib/mqttServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var mosca = require('mosca'); 4 | var whoAmI = require('./whoAmI'); 5 | var logData = require('./logData'); 6 | var updateSocketId = require('./updateSocketId'); 7 | var sendMessageCreator = require('./sendMessage'); 8 | var wrapMqttMessage = require('./wrapMqttMessage'); 9 | var securityImpl = require('./getSecurityImpl'); 10 | var updateFromClient = require('./updateFromClient'); 11 | var proxyListener = require('./proxyListener'); 12 | 13 | var mqttServer = function(config, parentConnection){ 14 | var server, io; 15 | 16 | if(config.redis && config.redis.host){ 17 | var redis = require('./redis'); 18 | io = require('socket.io-emitter')(redis.client); 19 | } 20 | 21 | var dataLogger = { 22 | level: 'debug' 23 | }; 24 | 25 | var settings = { 26 | port: config.mqtt.port || 1883, 27 | logger: dataLogger, 28 | stats: config.mqtt.stats || false 29 | }; 30 | 31 | 32 | config.mqtt = config.mqtt || {}; 33 | 34 | 35 | if(config.redis && config.redis.host){ 36 | var ascoltatore = { 37 | type: 'redis', 38 | redis: require('redis'), 39 | port: config.redis.port || 6379, 40 | return_buffers: true, // to handle binary payloads 41 | host: config.redis.host || "localhost" 42 | }; 43 | settings.backend = ascoltatore; 44 | settings.persistence= { 45 | factory: mosca.persistence.Redis, 46 | host: ascoltatore.host, 47 | port: ascoltatore.port 48 | }; 49 | 50 | }else if(config.mqtt.databaseUrl){ 51 | settings.backend = { 52 | type: 'mongo', 53 | url: config.mqtt.databaseUrl, 54 | pubsubCollection: 'mqtt', 55 | mongo: {} 56 | }; 57 | }else{ 58 | settings.backend = {}; 59 | } 60 | 61 | var skynetTopics = ['message', 62 | 'messageAck', 63 | 'update', 64 | 'data', 65 | 'config', 66 | 'gatewayConfig', 67 | 'whoami', 68 | 'tb', 69 | 'directText']; 70 | 71 | function endsWith(str, suffix) { 72 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 73 | } 74 | 75 | 76 | function socketEmitter(uuid, topic, data){ 77 | if(io){ 78 | io.in(uuid).emit(topic, data); 79 | } 80 | } 81 | 82 | function mqttEmitter(uuid, wrappedData, options){ 83 | options = options || {}; 84 | var message = { 85 | topic: uuid, 86 | payload: wrappedData, // or a Buffer 87 | qos: options.qos || 0, // 0, 1, or 2 88 | retain: false // or true 89 | }; 90 | 91 | server.publish(message, function() { 92 | }); 93 | 94 | } 95 | 96 | function emitToClient(topic, device, msg){ 97 | if(device.protocol === "mqtt"){ 98 | // MQTT handler 99 | mqttEmitter(device.uuid, wrapMqttMessage(topic, msg), {qos: msg.qos || 0}); 100 | } 101 | else{ 102 | socketEmitter(device.uuid, topic, msg); 103 | } 104 | 105 | } 106 | 107 | var sendMessage = sendMessageCreator(socketEmitter, mqttEmitter, parentConnection); 108 | if(parentConnection){ 109 | parentConnection.on('message', function(data, fn){ 110 | if(data){ 111 | var devices = data.devices; 112 | if (!_.isArray(devices)) { 113 | devices = [devices]; 114 | } 115 | _.each(devices, function(device) { 116 | if(device !== config.parentConnection.uuid){ 117 | sendMessage({uuid: data.fromUuid}, data, fn); 118 | } 119 | }); 120 | } 121 | }); 122 | } 123 | 124 | function clientAck(fromDevice, data){ 125 | if(fromDevice && data && data.ack){ 126 | whoAmI(data.devices, false, function(check){ 127 | if(!check.error && securityImpl.canSend(fromDevice, check)){ 128 | emitToClient('messageAck', check, data); 129 | } 130 | }); 131 | } 132 | } 133 | 134 | function serverAck(fromDevice, ack, resp){ 135 | if(fromDevice && ack && resp){ 136 | var msg = { 137 | ack: ack, 138 | payload: resp, 139 | qos: resp.qos 140 | }; 141 | mqttEmitter(fromDevice.uuid, wrapMqttMessage('messageAck', msg), {qos: msg.qos || 0}); 142 | } 143 | } 144 | 145 | 146 | // Accepts the connection if the username and password are valid 147 | function authenticate(client, username, password, callback) { 148 | if(username && username.toString() === 'skynet' && password){ 149 | if(password && password.toString() === config.mqtt.skynetPass){ 150 | client.skynetDevice = { 151 | uuid: 'skynet', 152 | }; 153 | callback(null, true); 154 | }else{ 155 | callback('unauthorized'); 156 | } 157 | }else if(username && password){ 158 | var data = { 159 | uuid: username.toString(), 160 | token: password.toString(), 161 | socketid: username.toString(), 162 | ipAddress: client.connection.stream.remoteAddress, 163 | protocol: 'mqtt', 164 | online: true 165 | }; 166 | 167 | updateSocketId(data, function(auth){ 168 | if (auth.device){ 169 | client.skynetDevice = auth.device; 170 | callback(null, true); 171 | } else { 172 | callback('unauthorized'); 173 | } 174 | }); 175 | }else{ 176 | callback('unauthorized'); 177 | } 178 | } 179 | 180 | // In this case the client authorized as alice can publish to /users/alice taking 181 | // the username from the topic and verifing it is the same of the authorized user 182 | function authorizePublish(client, topic, payload, callback) { 183 | 184 | function reject(reason){ 185 | callback('unauthorized'); 186 | } 187 | 188 | //TODO refactor this mess 189 | if(client.skynetDevice){ 190 | if(client.skynetDevice.uuid === 'skynet'){ 191 | callback(null, true); 192 | }else if(_.contains(skynetTopics, topic)){ 193 | try{ 194 | var payloadObj = payload.toString(); 195 | try{ 196 | payloadObj = JSON.parse(payload.toString()); 197 | payloadObj.fromUuid = client.skynetDevice.uuid; 198 | callback(null, new Buffer(JSON.stringify(payloadObj))); 199 | }catch(exp){ 200 | callback(null, true); 201 | } 202 | 203 | }catch(err){ 204 | reject(err); 205 | } 206 | }else{ 207 | reject('invalid topic'); 208 | } 209 | }else{ 210 | reject('no skynet device'); 211 | } 212 | } 213 | 214 | // In this case the client authorized as alice can subscribe to /users/alice taking 215 | // the username from the topic and verifing it is the same of the authorized user 216 | function authorizeSubscribe(client, topic, callback) { 217 | 218 | if(endsWith(topic, '_bc') || endsWith(topic, '_tb') || 219 | (client.skynetDevice && 220 | ((client.skynetDevice.uuid === 'skynet') || (client.skynetDevice.uuid === topic)))){ 221 | callback(null, true); 222 | }else{ 223 | callback('unauthorized'); 224 | } 225 | } 226 | 227 | // fired when the mqtt server is ready 228 | function setup() { 229 | if (config.useProxyProtocol) { 230 | _.each(server.servers, function(server){ 231 | proxyListener.resetListeners(server); 232 | }) 233 | } 234 | 235 | server.authenticate = authenticate; 236 | server.authorizePublish = authorizePublish; 237 | server.authorizeSubscribe = authorizeSubscribe; 238 | console.log('MQTT listening at mqtt://0.0.0.0:' + settings.port); 239 | } 240 | 241 | // // fired when a message is published 242 | 243 | server = new mosca.Server(settings); 244 | 245 | server.on('ready', setup); 246 | 247 | server.servers.forEach(function(s){ 248 | s.on('error', console.error); 249 | }) 250 | 251 | server.on('published', function(packet, client) { 252 | try{ 253 | var msg, ack; 254 | if('message' === packet.topic){ 255 | sendMessage(client.skynetDevice, JSON.parse(packet.payload.toString())); 256 | } 257 | else if('tb' === packet.topic){ 258 | sendMessage(client.skynetDevice, {payload: packet.payload.toString(), devices: ['*']}, 'tb'); 259 | } 260 | else if('directText' === packet.topic){ 261 | sendMessage(client.skynetDevice, JSON.parse(packet.payload.toString()), 'tb'); 262 | } 263 | else if('messageAck' === packet.topic){ 264 | clientAck(client.skynetDevice, JSON.parse(packet.payload.toString())); 265 | } 266 | else if('update' === packet.topic){ 267 | msg = JSON.parse(packet.payload.toString()); 268 | if(msg.ack){ 269 | ack = msg.ack; 270 | delete msg.ack; 271 | updateFromClient(client.skynetDevice, msg, function(resp){ 272 | serverAck(client.skynetDevice, ack, resp); 273 | whoAmI(client.skynetDevice.uuid, true, function(data){ 274 | emitToClient('config', client.skynetDevice, data); 275 | }) 276 | }); 277 | } 278 | } 279 | else if('whoami' === packet.topic){ 280 | msg = JSON.parse(packet.payload.toString()); 281 | if(msg.ack){ 282 | ack = msg.ack; 283 | delete msg.ack; 284 | whoAmI(client.skynetDevice.uuid, true, function(resp){ 285 | serverAck(client.skynetDevice, ack, resp); 286 | }); 287 | } 288 | } 289 | else if('data' === packet.topic){ 290 | msg = JSON.parse(packet.payload.toString()); 291 | delete msg.token; 292 | msg.uuid = client.skynetDevice.uuid; 293 | 294 | logData(msg, function(results){ 295 | // Send messsage regarding data update 296 | var message = {}; 297 | message.payload = msg; 298 | // message.devices = data.uuid; 299 | message.devices = "*"; 300 | 301 | sendMessage(client.skynetDevice, message); 302 | }); 303 | } 304 | }catch(ex){ 305 | console.error(ex); 306 | } 307 | }); 308 | }; 309 | 310 | module.exports = mqttServer; 311 | -------------------------------------------------------------------------------- /lib/setupCoapRoutes.js: -------------------------------------------------------------------------------- 1 | var JSONStream = require('JSONStream'); 2 | 3 | var whoAmI = require('./whoAmI'); 4 | var getData = require('./getData'); 5 | var logData = require('./logData'); 6 | var logEvent = require('./logEvent'); 7 | var register = require('./register'); 8 | var getEvents = require('./getEvents'); 9 | var subEvents = require('./subEvents'); 10 | var getDevices = require('./getDevices'); 11 | var authDevice = require('./authDevice'); 12 | var unregister = require('./unregister'); 13 | var securityImpl = require('./getSecurityImpl'); 14 | var createActivity = require('./createActivity'); 15 | var getSystemStatus = require('./getSystemStatus'); 16 | var updateFromClient = require('./updateFromClient'); 17 | var createReadStream = require('./createReadStream'); 18 | var _ = require('lodash'); 19 | 20 | function getActivity(topic, req, device, toDevice){ 21 | var ip = req.rsinfo.address; 22 | return createActivity(topic, ip, device, toDevice); 23 | } 24 | 25 | //psuedo middleware 26 | function authorizeRequest(req, res, callback){ 27 | var uuid = _.find(req.options, {name:'98'}); 28 | var token = _.find(req.options, {name:'99'}); 29 | if (uuid && uuid.value) { 30 | uuid = uuid.value.toString(); 31 | } 32 | if (token && token.value) { 33 | token = token.value.toString(); 34 | } 35 | authDevice(uuid, token, function (auth) { 36 | if (auth.authenticate) { 37 | callback(auth.device); 38 | }else{ 39 | res.statusCode = 401; 40 | res.json({error: 'unauthorized'}); 41 | } 42 | }); 43 | } 44 | 45 | function errorResponse(error, res){ 46 | if(error.code){ 47 | res.statusCode = error.code; 48 | res.json(error); 49 | }else{ 50 | res.statusCode = 400; 51 | res.json(400, error); 52 | } 53 | } 54 | 55 | function streamMessages(req, res, topic){ 56 | var rs = createReadStream(); 57 | var subHandler = function(topic, msg){ 58 | rs.pushMsg(JSON.stringify(msg)); 59 | }; 60 | 61 | subEvents.on(topic, subHandler); 62 | rs.pipe(res); 63 | 64 | //functionas a heartbeat. 65 | //If client stops responding, we can assume disconnected and cleanup 66 | var interval = setInterval(function() { 67 | res.write(''); 68 | }, 10000); 69 | 70 | res.once('finish', function(err) { 71 | clearInterval(interval); 72 | subEvents.removeListener(topic, subHandler); 73 | }); 74 | 75 | } 76 | 77 | function subscribeBroadcast(req, res, type, skynet){ 78 | authorizeRequest(req, res, function(fromDevice){ 79 | skynet.sendActivity(getActivity(type, req, fromDevice)); 80 | var uuid = req.params.uuid; 81 | logEvent(204, {fromUuid: fromDevice, uuid: uuid}); 82 | if(uuid && uuid.length > 30){ 83 | //no token provided, attempt to only listen for public broadcasts FROM this uuid 84 | whoAmI(req.params.uuid, false, function(results){ 85 | if(results.error){ 86 | errorResponse(results.error, res); 87 | }else{ 88 | if(securityImpl.canRead(fromDevice, results)){ 89 | streamMessages(req, res, uuid + '_bc'); 90 | }else{ 91 | errorResponse({error: "unauthorized access"}, res); 92 | } 93 | } 94 | }); 95 | } 96 | }); 97 | } 98 | 99 | 100 | function setupCoapRoutes(coapRouter, skynet){ 101 | 102 | // coap get coap://localhost/status 103 | coapRouter.get('/status', function (req, res) { 104 | skynet.sendActivity(getActivity('status', req)); 105 | 106 | getSystemStatus(function (data) { 107 | if(data.error) { 108 | res.statusCode = data.error.code; 109 | res.json(data.error); 110 | } else { 111 | res.statusCode = 200; 112 | res.json(data); 113 | } 114 | }); 115 | }); 116 | 117 | 118 | // coap get coap://localhost/ipaddress 119 | coapRouter.get('/ipaddress', function (req, res) { 120 | skynet.sendActivity(getActivity('ipaddress', req)); 121 | res.json({ipAddress: req.rsinfo.address}); 122 | }); 123 | 124 | coapRouter.get('/devices', function (req, res) { 125 | authorizeRequest(req, res, function(fromDevice){ 126 | skynet.sendActivity(getActivity('devices', req, fromDevice)); 127 | 128 | getDevices(fromDevice, req.query, false, function(data){ 129 | if(data.error){ 130 | errorResponse(data.error, res); 131 | }else{ 132 | res.json(data); 133 | } 134 | }); 135 | }); 136 | }); 137 | 138 | coapRouter.post('/devices', function (req, res) { 139 | skynet.sendActivity(getActivity('devices', req)); 140 | 141 | req.params.ipAddress = req.rsinfo.address; 142 | register(req.params, function (data) { 143 | if(data.error) { 144 | res.statusCode = data.error.code; 145 | res.json(data.error); 146 | } else { 147 | res.json(data); 148 | } 149 | 150 | }); 151 | }); 152 | 153 | // coap get coap://localhost/devices/a1634681-cb10-11e3-8fa5-2726ddcf5e29 154 | coapRouter.get('/devices/:uuid', function (req, res) { 155 | authorizeRequest(req, res, function(fromDevice){ 156 | skynet.sendActivity(getActivity('devices', req, fromDevice)); 157 | getDevices(fromDevice, {uuid: req.params.uuid}, false, function(data){ 158 | if(data.error){ 159 | errorResponse(data.error, res); 160 | }else{ 161 | res.json(data); 162 | } 163 | }); 164 | }); 165 | 166 | 167 | }); 168 | 169 | 170 | coapRouter.put('/devices/:uuid', function (req, res) { 171 | authorizeRequest(req, res, function(fromDevice){ 172 | skynet.sendActivity(getActivity('devices', req, fromDevice)); 173 | updateFromClient(fromDevice, req.params, function(result){ 174 | if(result.error){ 175 | errorResponse(result.error, res); 176 | }else{ 177 | skynet.sendConfigActivity(req.params.uuid || fromDevice.uuid, skynet.emitToClient); 178 | res.json(result); 179 | } 180 | }); 181 | }); 182 | }); 183 | 184 | coapRouter.delete('/devices/:uuid', function (req, res) { 185 | authorizeRequest(req, res, function(fromDevice){ 186 | skynet.sendActivity(getActivity('unregister', req, fromDevice)); 187 | unregister(fromDevice, req.params.uuid, req.params.token, skynet.emitToClient, function(err, data){ 188 | if(err){ 189 | errorResponse(err, res); 190 | } else { 191 | res.json(data); 192 | } 193 | }); 194 | }); 195 | 196 | }); 197 | 198 | 199 | coapRouter.get('/mydevices', function (req, res) { 200 | authorizeRequest(req, res, function(fromDevice){ 201 | skynet.sendActivity(getActivity('mydevices', req, fromDevice)); 202 | getDevices(fromDevice, {owner: fromDevice.uuid}, true, function(data){ 203 | if(data.error){ 204 | errorResponse(data.error, res); 205 | } else { 206 | res.json(data); 207 | } 208 | }); 209 | }); 210 | }); 211 | 212 | coapRouter.post('/gatewayConfig', function(req, res){ 213 | authorizeRequest(req, res, function(fromDevice){ 214 | skynet.sendActivity(getActivity('gatewayConfig', req, fromDevice)); 215 | var body; 216 | try { 217 | body = JSON.parse(req.body); 218 | } catch(err) { 219 | console.error('error parsing', err, req.body); 220 | body = {}; 221 | } 222 | 223 | skynet.gatewayConfig(body, function(result){ 224 | if(result && result.error){ 225 | errorResponse(result.error, res); 226 | }else{ 227 | res.json(result); 228 | } 229 | }); 230 | 231 | logEvent(300, body); 232 | }); 233 | }); 234 | 235 | // coap get coap://localhost/events/196798f1-b5d8-11e3-8c93-45a0c0308eaa -p "token=00cpk8akrmz8semisbebhe0358livn29" 236 | coapRouter.get('/events/:uuid', function (req, res) { 237 | authorizeRequest(req, res, function(fromDevice){ 238 | skynet.sendActivity(getActivity('events', req, fromDevice)); 239 | logEvent(201, {fromUuid: fromDevice.uuid, from: fromDevice, uuid: req.params.uuid}); 240 | getEvents(fromDevice.uuid, function(data){ 241 | if(data.error){ 242 | errorResponse(data.error, res); 243 | } else { 244 | res.json(data); 245 | } 246 | }); 247 | }); 248 | 249 | 250 | }); 251 | 252 | 253 | // coap post coap://localhost/data/196798f1-b5d8-11e3-8c93-45a0c0308eaa -p "token=00cpk8akrmz8semisbebhe0358livn29&temperature=43" 254 | coapRouter.post('/data/:uuid', function(req, res){ 255 | authorizeRequest(req, res, function(fromDevice){ 256 | skynet.sendActivity(getActivity('data', req, fromDevice)); 257 | delete req.params.token; 258 | 259 | req.params.ipAddress = getIP(req); 260 | logData(req.params, function(data){ 261 | if(data.error){ 262 | errorResponse(data.error, res); 263 | } else { 264 | 265 | // Send messsage regarding data update 266 | var message = {}; 267 | message.payload = req.params; 268 | // message.devices = req.params.uuid; 269 | message.devices = "*"; 270 | skynet.sendMessage(fromDevice, message); 271 | res.json(data); 272 | } 273 | }); 274 | }); 275 | }); 276 | 277 | // coap get coap://localhost/data/196798f1-b5d8-11e3-8c93-45a0c0308eaa -p "token=00cpk8akrmz8semisbebhe0358livn29&limit=1" 278 | coapRouter.get('/data/:uuid', function(req, res){ 279 | if(req.query.stream){ 280 | subscribeBroadcast(req, res, 'data', skynet); 281 | } 282 | else{ 283 | authorizeRequest(req, res, function(fromDevice){ 284 | skynet.sendActivity(getActivity('data',req, fromDevice)); 285 | getData(req, function(data){ 286 | if(data.error){ 287 | errorResponse(data.error, res); 288 | } else { 289 | res.json(data); 290 | } 291 | }); 292 | }); 293 | } 294 | }); 295 | 296 | // coap post coap://localhost/messages -p "devices=a1634681-cb10-11e3-8fa5-2726ddcf5e29&payload=test" 297 | coapRouter.post('/messages', function (req, res, next) { 298 | authorizeRequest(req, res, function(fromDevice){ 299 | skynet.sendActivity(getActivity('messages', req, fromDevice)); 300 | var body; 301 | try { 302 | body = JSON.parse(req.params); 303 | } catch(err) { 304 | body = req.params; 305 | } 306 | if (!body.devices){ 307 | try { 308 | body = JSON.parse(req.params); 309 | } catch(err) { 310 | body = req.params; 311 | } 312 | } 313 | var devices = body.devices; 314 | var message = {}; 315 | message.payload = body.payload; 316 | message.devices = body.devices; 317 | message.subdevice = body.subdevice; 318 | message.topic = body.topic; 319 | 320 | skynet.sendMessage(fromDevice, message); 321 | res.json({devices:devices, subdevice: body.subdevice, payload: body.payload}); 322 | 323 | logEvent(300, message); 324 | }); 325 | }); 326 | 327 | coapRouter.get('/subscribe', function (req, res) { 328 | authorizeRequest(req, res, function(fromDevice){ 329 | skynet.sendActivity(getActivity('subscribe', req, fromDevice)); 330 | logEvent(204, {fromUuid: fromDevice.uuid, from: fromDevice, uuid: req.params.uuid}); 331 | streamMessages(req, res, fromDevice.uuid); 332 | }); 333 | }); 334 | 335 | coapRouter.get('/subscribe/:uuid', function (req, res) { 336 | subscribeBroadcast(req, res, 'subscribe', skynet); 337 | }); 338 | } 339 | 340 | module.exports = setupCoapRoutes; 341 | -------------------------------------------------------------------------------- /lib/setupHttpRoutes.js: -------------------------------------------------------------------------------- 1 | var nstatic = require('node-static'); 2 | 3 | var config = require('../config'); 4 | 5 | var getYo = require('./getYo'); 6 | var whoAmI = require('./whoAmI'); 7 | var getData = require('./getData'); 8 | var logData = require('./logData'); 9 | var logEvent = require('./logEvent'); 10 | var getPhone = require('./getPhone'); 11 | var register = require('./register'); 12 | var getEvents = require('./getEvents'); 13 | var subEvents = require('./subEvents'); 14 | var getDevices = require('./getDevices'); 15 | var authDevice = require('./authDevice'); 16 | var unregister = require('./unregister'); 17 | var claimDevice = require('./claimDevice'); 18 | var securityImpl = require('./getSecurityImpl'); 19 | var createActivity = require('./createActivity'); 20 | var getLocalDevices = require('./getLocalDevices'); 21 | var getSystemStatus = require('./getSystemStatus'); 22 | var updateFromClient = require('./updateFromClient'); 23 | var createReadStream = require('./createReadStream'); 24 | 25 | function getIP(req){ 26 | return req.headers["x-forwarded-for"] || req.connection.remoteAddress; 27 | } 28 | 29 | function getActivity(topic, req, device, toDevice){ 30 | return createActivity(topic, getIP(req), device, toDevice); 31 | } 32 | 33 | //psuedo middleware 34 | function authorizeRequest(req, res, callback){ 35 | 36 | if(req.header('skynet_auth_uuid') && req.header('skynet_auth_token')){ 37 | var authUuid = req.header('skynet_auth_uuid'); 38 | var authToken = req.header('skynet_auth_token'); 39 | } else { 40 | var authUuid = req.header('meshblu_auth_uuid'); 41 | var authToken = req.header('meshblu_auth_token'); 42 | } 43 | authDevice(authUuid, authToken, function (auth) { 44 | if (auth.authenticate) { 45 | callback(auth.device); 46 | }else{ 47 | res.json(401, {error: 'unauthorized'}); 48 | } 49 | }); 50 | } 51 | 52 | function errorResponse(error, res){ 53 | if(error.code){ 54 | res.json(error.code, error); 55 | }else{ 56 | res.json(400, error); 57 | } 58 | } 59 | 60 | function streamMessages(req, res, topic){ 61 | res.setHeader('Connection','keep-alive'); 62 | var rs = createReadStream(); 63 | 64 | var subHandler = function(topic, msg){ 65 | rs.pushMsg(msg); 66 | }; 67 | 68 | subEvents.on(topic, subHandler); 69 | 70 | rs.pipe(res); 71 | 72 | res.once('close', function(a){ 73 | subEvents.removeListener(topic, subHandler); 74 | }); 75 | } 76 | 77 | function subscribeBroadcast(req, res, type, skynet){ 78 | authorizeRequest(req, res, function(fromDevice){ 79 | skynet.sendActivity(getActivity(type,req, fromDevice)); 80 | var uuid = req.params.uuid; 81 | logEvent(204, {fromUuid: fromDevice.uuid, from: fromDevice, uuid: uuid}); 82 | if(uuid && uuid.length > 30){ 83 | //no token provided, attempt to only listen for public broadcasts FROM this uuid 84 | whoAmI(req.params.uuid, false, function(results){ 85 | if(results.error){ 86 | errorResponse(results.error, res); 87 | }else{ 88 | if(securityImpl.canRead(fromDevice, results)){ 89 | streamMessages(req, res, uuid + '_bc'); 90 | }else{ 91 | errorResponse({error: "unauthorized access"}, res); 92 | } 93 | } 94 | }); 95 | } 96 | }); 97 | } 98 | 99 | function setupHttpRoutes(server, skynet){ 100 | 101 | // curl http://localhost:3000/status 102 | server.get('/status', function(req, res){ 103 | skynet.sendActivity(getActivity('status',req)); 104 | res.setHeader('Access-Control-Allow-Origin','*'); 105 | getSystemStatus(function(data){ 106 | if(data.error){ 107 | errorResponse(data.error, res); 108 | } else { 109 | res.json(data); 110 | } 111 | 112 | }); 113 | }); 114 | 115 | // curl http://localhost:3000/ipaddress 116 | server.get('/ipaddress', function(req, res){ 117 | skynet.sendActivity(getActivity('ipaddress',req)); 118 | res.setHeader('Access-Control-Allow-Origin','*'); 119 | res.json({ipAddress: getIP(req)}); 120 | }); 121 | 122 | // curl http://localhost:3000/devices 123 | // curl http://localhost:3000/devices?key=123 124 | // curl http://localhost:3000/devices?online=true 125 | // server.get('/devices/:uuid', function(req, res){ 126 | server.get('/devices', function(req, res){ 127 | authorizeRequest(req, res, function(fromDevice){ 128 | skynet.sendActivity(getActivity('devices',req, fromDevice)); 129 | getDevices(fromDevice, req.query, false, function(data){ 130 | if(data.error){ 131 | errorResponse(data.error, res); 132 | }else{ 133 | res.json(data); 134 | } 135 | }); 136 | }); 137 | }); 138 | 139 | server.get('/devices/:uuid', function(req, res){ 140 | authorizeRequest(req, res, function(fromDevice){ 141 | skynet.sendActivity(getActivity('devices',req, fromDevice)); 142 | getDevices(fromDevice, {uuid: req.params.uuid}, false, function(data){ 143 | if(data.error){ 144 | errorResponse(data.error, res); 145 | }else{ 146 | res.json(data); 147 | } 148 | }); 149 | }); 150 | }); 151 | 152 | server.get('/localdevices', function(req, res){ 153 | authorizeRequest(req, res, function(fromDevice){ 154 | skynet.sendActivity(getActivity('localdevices',req, fromDevice)); 155 | if(req.query.overrideIp && config.skynet_override_token && req.header('Skynet_override_token') == config.skynet_override_token){ 156 | fromDevice.ipAddress = req.query.overrideIp; 157 | }else{ 158 | fromDevice.ipAddress = getIP(req); 159 | } 160 | 161 | getLocalDevices(fromDevice, false, function(data){ 162 | if(data.error){ 163 | errorResponse(data.error, res); 164 | }else{ 165 | res.json(data); 166 | } 167 | }); 168 | }); 169 | 170 | }); 171 | 172 | server.get('/unclaimeddevices', function(req, res){ 173 | authorizeRequest(req, res, function(fromDevice){ 174 | skynet.sendActivity(getActivity('localdevices',req, fromDevice)); 175 | if(req.query.overrideIp && config.skynet_override_token && req.header('Skynet_override_token') == config.skynet_override_token){ 176 | fromDevice.ipAddress = req.query.overrideIp; 177 | }else{ 178 | fromDevice.ipAddress = getIP(req); 179 | } 180 | 181 | getLocalDevices(fromDevice, true, function(data){ 182 | if(data.error){ 183 | errorResponse(data.error, res); 184 | }else{ 185 | res.json(data); 186 | } 187 | }); 188 | }); 189 | 190 | }); 191 | 192 | server.put('/claimdevice/:uuid', function(req, res){ 193 | authorizeRequest(req, res, function(fromDevice){ 194 | skynet.sendActivity(getActivity('claimdevice',req, fromDevice)); 195 | //TODO: Any server-to-server overriding should be done with ouath 196 | if(req.query.overrideIp && config.skynet_override_token && req.header('Skynet_override_token') == config.skynet_override_token){ 197 | fromDevice.ipAddress = req.query.overrideIp; 198 | }else{ 199 | fromDevice.ipAddress = getIP(req); 200 | } 201 | 202 | claimDevice(fromDevice, req.params, function(error, data){ 203 | if(error){ 204 | errorResponse(error, res); 205 | }else{ 206 | res.json(data); 207 | } 208 | }); 209 | }); 210 | }); 211 | 212 | 213 | // curl http://localhost:3000/gateway/01404680-2539-11e3-b45a-d3519872df26 214 | server.get('/gateway/:uuid', function(req, res){ 215 | skynet.sendActivity(getActivity('gateway',req)); 216 | // res.setHeader('Access-Control-Allow-Origin','*'); 217 | whoAmI(req.params.uuid, false, function(data){ 218 | if(data.error){ 219 | res.writeHead(302, { 220 | 'location': 'http://skynet.im' 221 | }); 222 | } else { 223 | res.writeHead(302, { 224 | 'location': 'http://' + data.localhost + ":" + data.port 225 | }); 226 | } 227 | res.end(); 228 | 229 | }); 230 | }); 231 | 232 | 233 | // curl -X POST -d "name=arduino&description=this+is+a+test" http://localhost:3000/devices 234 | server.post('/devices', function(req, res){ 235 | skynet.sendActivity(getActivity('devices',req)); 236 | res.setHeader('Access-Control-Allow-Origin','*'); 237 | req.params.ipAddress = getIP(req); 238 | register(req.params, function(data){ 239 | if(data.error){ 240 | errorResponse(data.error, res); 241 | } else { 242 | res.json(data); 243 | } 244 | }); 245 | }); 246 | 247 | // curl -X POST -d "name=arduino&description=this+is+a+test" http://localhost:3000/devices/micro 248 | server.post('/devices/micro', function(req, res){ 249 | skynet.sendActivity(getActivity('devices/micro',req)); 250 | //dont bother with CORS, this isnt for brosers 251 | //res.setHeader('Access-Control-Allow-Origin','*'); 252 | req.params.ipAddress = getIP(req); 253 | register(req.params, function(data){ 254 | if(data.error){ 255 | errorResponse('', res); 256 | res.writeHead(400, {}); 257 | res.write(''); 258 | res.end(); 259 | } else { 260 | var body = data.uuid + '\n' + data.token; 261 | res.writeHead(200, {}); 262 | res.write(body); 263 | res.end(); 264 | } 265 | }); 266 | }); 267 | 268 | // curl -X PUT -d "token=123&online=true&temp=hello&temp2=world" http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26 269 | // curl -X PUT -d "token=123&online=true&temp=hello&temp2=null" http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26 270 | // curl -X PUT -d "token=123&online=true&temp=hello&temp2=" http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26 271 | // curl -X PUT -d "token=123&myArray=[1,2,3]" http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26 272 | // curl -X PUT -d "token=123&myArray=4&action=push" http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26 273 | server.put('/devices/:uuid', function(req, res){ 274 | authorizeRequest(req, res, function(fromDevice){ 275 | skynet.sendActivity(getActivity('devices',req, fromDevice)); 276 | updateFromClient(fromDevice, req.params, function(result){ 277 | if(result.error){ 278 | errorResponse(result.error, res); 279 | }else{ 280 | skynet.sendConfigActivity(req.params.uuid || fromDevice.uuid, skynet.emitToClient); 281 | res.json(result); 282 | } 283 | }); 284 | }); 285 | }); 286 | 287 | // curl -X DELETE -d "token=123" http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26 288 | server.del('/devices/:uuid', function(req, res){ 289 | authorizeRequest(req, res, function(fromDevice){ 290 | skynet.sendActivity(getActivity('unregister',req, fromDevice)); 291 | unregister(fromDevice, req.params.uuid, req.params.token, skynet.emitToClient, function(err, data){ 292 | if(err){ 293 | errorResponse(err, res); 294 | } else { 295 | res.json(data); 296 | } 297 | }); 298 | }); 299 | 300 | }); 301 | 302 | // Returns all devices owned by authenticated user 303 | // curl -X GET http://localhost:3000/mydevices/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc?token=qirqglm6yb1vpldixflopnux4phtcsor 304 | server.get('/mydevices', function(req, res){ 305 | authorizeRequest(req, res, function(fromDevice){ 306 | skynet.sendActivity(getActivity('mydevices',req, fromDevice)); 307 | getDevices(fromDevice, {owner: fromDevice.uuid}, true, function(data){ 308 | if(data.error){ 309 | errorResponse(data.error, res); 310 | } else { 311 | res.json(data); 312 | } 313 | }); 314 | }); 315 | }); 316 | 317 | 318 | // curl -X GET http://localhost:3000/events/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc?token=qirqglm6yb1vpldixflopnux4phtcsor 319 | server.get('/events/:uuid', function(req, res){ 320 | res.send(401); 321 | // authorizeRequest(req, res, function(fromDevice){ 322 | // skynet.sendActivity(getActivity('events',req, fromDevice)); 323 | // logEvent(201, {fromUuid: fromDevice.uuid, from: fromDevice, uuid: req.params.uuid}); 324 | // getEvents(fromDevice.uuid, function(data){ 325 | // if(data.error){ 326 | // errorResponse(data.error, res); 327 | // } else { 328 | // res.json(data); 329 | // } 330 | // }); 331 | // }); 332 | }); 333 | 334 | // curl -X GET http://localhost:3000/subscribe/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc 335 | server.get('/subscribe/:uuid', function(req, res){ 336 | subscribeBroadcast(req, res, 'subscribe', skynet); 337 | }); 338 | 339 | // curl -X GET http://localhost:3000/subscribe 340 | server.get('/subscribe', function(req, res){ 341 | authorizeRequest(req, res, function(fromDevice){ 342 | skynet.sendActivity(getActivity('subscribe',req, fromDevice)); 343 | //var uuid = req.params.uuid; 344 | logEvent(204, {fromUuid: fromDevice.uuid, from: fromDevice}); 345 | streamMessages(req, res, fromDevice.uuid ); 346 | }); 347 | }); 348 | 349 | // curl -X GET http://localhost:3000/authenticate/81246e80-29fd-11e3-9468-e5f892df566b?token=5ypy4rurayktke29ypbi30kcw5ovfgvi 350 | server.get('/authenticate/:uuid', function(req, res){ 351 | skynet.sendActivity(getActivity('authenticate',req)); 352 | res.setHeader('Access-Control-Allow-Origin','*'); 353 | authDevice(req.params.uuid, req.query.token, function(auth){ 354 | if (auth.authenticate){ 355 | res.json({uuid:req.params.uuid, authentication: true}); 356 | } else { 357 | var regdata = { 358 | "error": { 359 | "message": "Device not found or token not valid", 360 | "code": 404 361 | } 362 | }; 363 | res.json(regdata.error.code, {uuid:req.params.uuid, authentication: false}); 364 | 365 | } 366 | }); 367 | }); 368 | 369 | 370 | // curl -X POST -d '{"devices": "all", "payload": {"yellow":"off"}}' http://localhost:3000/messages 371 | // curl -X POST -d '{"devices": ["ad698900-2546-11e3-87fb-c560cb0ca47b","2f3113d0-2796-11e3-95ef-e3081976e170"], "payload": {"yellow":"off"}}' http://localhost:3000/messages 372 | // curl -X POST -d '{"devices": "ad698900-2546-11e3-87fb-c560cb0ca47b", "payload": {"yellow":"off"}}' http://localhost:3000/messages 373 | server.post('/messages', function(req, res, next){ 374 | 375 | authorizeRequest(req, res, function(fromDevice){ 376 | //skynet.sendActivity(getActivity('messages',req)); 377 | var body; 378 | try { 379 | body = JSON.parse(req.body); 380 | } catch(err) { 381 | body = req.body; 382 | } 383 | if (!body.devices){ 384 | try { 385 | body = JSON.parse(req.params); 386 | } catch(err) { 387 | body = req.params; 388 | } 389 | } 390 | var devices = body.devices; 391 | var message = {}; 392 | message.devices = body.devices; 393 | message.subdevice = body.subdevice; 394 | message.topic = body.topic; 395 | message.payload = body.payload; 396 | 397 | skynet.sendMessage(fromDevice, message); 398 | res.json({devices:devices, subdevice: body.subdevice, payload: body.payload}); 399 | 400 | logEvent(300, message); 401 | }); 402 | 403 | }); 404 | 405 | // curl -X POST -d '{"uuid": "ad698900-2546-11e3-87fb-c560cb0ca47b", "token": "g6jmsla14j2fyldi7hqijbylwmrysyv5", "method": "getSubdevices"' http://localhost:3000/gatewayConfig 406 | server.post('/gatewayconfig', function(req, res, next){ 407 | authorizeRequest(req, res, function(fromDevice){ 408 | skynet.sendActivity(getActivity('gatewayconfig',req, fromDevice)); 409 | var body; 410 | try { 411 | body = JSON.parse(req.body); 412 | } catch(err) { 413 | console.error('error parsing', err, req.body); 414 | body = {}; 415 | } 416 | 417 | skynet.gatewayConfig(body, function(result){ 418 | if(result && result.error){ 419 | errorResponse(result.error, res); 420 | }else{ 421 | res.json(result); 422 | } 423 | }); 424 | 425 | logEvent(300, body); 426 | }); 427 | }); 428 | 429 | // Inbound SMS 430 | // curl -X GET -d "token=123" http://localhost:3000/inboundsms 431 | server.get('/inboundsms', function(req, res){ 432 | skynet.sendActivity(getActivity('inboundsms',req)); 433 | 434 | res.setHeader('Access-Control-Allow-Origin','*'); 435 | // { To: '17144625921', 436 | // Type: 'sms', 437 | // MessageUUID: 'f1f3cc84-8770-11e3-9f8a-842b2b455655', 438 | // From: '14803813574', 439 | // Text: 'Test' } 440 | var data; 441 | try{ 442 | data = JSON.parse(req.params); 443 | } catch(e){ 444 | data = req.params; 445 | } 446 | var toPhone = data.To; 447 | var fromPhone = data.From; 448 | var message = data.Text; 449 | 450 | getPhone(toPhone, function(err, phoneDevice){ 451 | var eventData = {devices: phoneDevice, payload: message}; 452 | if(err){ 453 | err.code = 404; 454 | eventData.error = err; 455 | } 456 | else{ 457 | var msg = { 458 | devices: "*", 459 | payload: message, 460 | api: 'message', 461 | fromUuid: phoneDevice.uuid, 462 | eventCode: 300, 463 | fromPhone: fromPhone, 464 | sms: true 465 | }; 466 | 467 | skynet.sendMessage(phoneDevice, msg); 468 | } 469 | 470 | logEvent(301, eventData); 471 | if(eventData.error){ 472 | errorResponse(eventData.error, res); 473 | } else { 474 | res.json(eventData); 475 | } 476 | 477 | }); 478 | }); 479 | 480 | // Inbound Yo 481 | // curl -X GET "http://localhost:3000/inboundyo?username=christoffe" 482 | server.get('/inboundyo', function(req, res){ 483 | skynet.sendActivity(getActivity('inboundyo',req)); 484 | 485 | res.setHeader('Access-Control-Allow-Origin','*'); 486 | var yoUsername = req.params.username; 487 | 488 | getYo(yoUsername, function(err, yoDevice){ 489 | var eventData = {devices: yoDevice, payload: "yo"}; 490 | if(err){ 491 | err.code = 404; 492 | eventData.error = err; 493 | } 494 | else{ 495 | var msg = { 496 | devices: "*", 497 | payload: "yo", 498 | api: 'message', 499 | fromUuid: yoDevice.uuid, 500 | eventCode: 300, 501 | fromPhone: yoUsername, 502 | yo: true 503 | }; 504 | 505 | skynet.sendMessage(yoDevice, msg); 506 | } 507 | 508 | logEvent(303, eventData); 509 | if(eventData.error){ 510 | errorResponse(eventData.error, res); 511 | } else { 512 | res.json(eventData); 513 | } 514 | 515 | }); 516 | }); 517 | 518 | 519 | // curl -X POST -d "token=123&temperature=78" http://localhost:3000/data/ad698900-2546-11e3-87fb-c560cb0ca47b 520 | server.post('/data/:uuid', function(req, res){ 521 | authorizeRequest(req, res, function(fromDevice){ 522 | skynet.sendActivity(getActivity('data',req, fromDevice)); 523 | delete req.params.token; 524 | 525 | req.params.ipAddress = getIP(req); 526 | logData(req.params, function(data){ 527 | if(data.error){ 528 | errorResponse(data.error, res); 529 | } else { 530 | 531 | // Send messsage regarding data update 532 | var message = {}; 533 | message.payload = req.params; 534 | // message.devices = req.params.uuid; 535 | message.devices = "*"; 536 | 537 | skynet.sendMessage(fromDevice, message); 538 | 539 | res.json(data); 540 | } 541 | }); 542 | }); 543 | 544 | }); 545 | 546 | // curl -X GET http://localhost:3000/data/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc?token=qirqglm6yb1vpldixflopnux4phtcsor 547 | server.get('/data/:uuid', function(req, res){ 548 | if(req.query.stream){ 549 | subscribeBroadcast(req, res, 'data', skynet); 550 | } 551 | else{ 552 | authorizeRequest(req, res, function(fromDevice){ 553 | skynet.sendActivity(getActivity('data',req, fromDevice)); 554 | getData(req, function(data){ 555 | if(data.error){ 556 | errorResponse(data.error, res); 557 | } else { 558 | res.json(data); 559 | } 560 | }); 561 | }); 562 | } 563 | 564 | }); 565 | 566 | 567 | // Serve static website 568 | var file = new nstatic.Server('./public'); 569 | server.get('/demo/:uuid', function(req, res, next) { 570 | skynet.sendActivity(getActivity('demo',req)); 571 | file.serveFile('/demo.html', 200, {}, req, res); 572 | }); 573 | 574 | server.get('/localjsconsole', function(req, res, next) { 575 | skynet.sendActivity(getActivity('localjsconsole',req)); 576 | file.serveFile('/localjsconsole.html', 200, {}, req, res); 577 | }); 578 | 579 | server.get('/', function(req, res, next) { 580 | skynet.sendActivity(getActivity('website',req)); 581 | file.serveFile('/index.html', 200, {}, req, res); 582 | }); 583 | 584 | server.get(/^\/.*/, function(req, res, next) { 585 | file.serve(req, res, next); 586 | }); 587 | 588 | return server; 589 | } 590 | 591 | module.exports = setupHttpRoutes; 592 | -------------------------------------------------------------------------------- /lib/socketLogic.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var whoAmI = require('./whoAmI'); 3 | var config = require('../config'); 4 | var getData = require('./getData'); 5 | var logData = require('./logData'); 6 | var logEvent = require('./logEvent'); 7 | var register = require('./register'); 8 | var getEvents = require('./getEvents'); 9 | var getDevices = require('./getDevices'); 10 | var authDevice = require('./authDevice'); 11 | var unregister = require('./unregister'); 12 | var claimDevice = require('./claimDevice'); 13 | var securityImpl = require('./getSecurityImpl'); 14 | var createActivity = require('./createActivity'); 15 | var updateSocketId = require('./updateSocketId'); 16 | var updatePresence = require('./updatePresence'); 17 | var getLocalDevices = require('./getLocalDevices'); 18 | var getSystemStatus = require('./getSystemStatus'); 19 | var updateFromClient = require('./updateFromClient'); 20 | 21 | function getActivity(topic, socket, device, toDevice){ 22 | return createActivity(topic, socket.ipAddress, device, toDevice); 23 | } 24 | 25 | function getDevice(socket, callback) { 26 | if(socket.skynetDevice){ 27 | whoAmI(socket.skynetDevice.uuid, true, function(device) { 28 | return callback(null, device); 29 | }); 30 | }else{ 31 | return callback(new Error('skynetDevice not found for socket' + socket), null); 32 | } 33 | } 34 | 35 | function socketLogic (socket, secure, skynet){ 36 | var ipAddress = socket.handshake.headers["x-forwarded-for"] || socket.request.connection.remoteAddress; 37 | socket.ipAddress = ipAddress; 38 | logEvent(100, {"socketid": socket.id, "protocol": "websocket"}); 39 | 40 | socket.emit('identify', { socketid: socket.id }); 41 | socket.on('identity', function (data) { 42 | data = _.extend({}, data, { 43 | socketid: socket.id, 44 | ipAddress: ipAddress, 45 | secure: secure, 46 | online: true 47 | }); 48 | if(!data.protocol){ 49 | data.protocol = "websocket"; 50 | } 51 | updateSocketId(data, function(auth){ 52 | 53 | if (auth.status == 201){ 54 | 55 | socket.skynetDevice = auth.device; 56 | 57 | socket.emit('ready', {"api": "connect", "status": auth.status, "socketid": socket.id, "uuid": auth.device.uuid, "token": auth.device.token}); 58 | // Have device join its uuid room name so that others can subscribe to it 59 | 60 | //Announce presence online 61 | var message = { 62 | devices: '*', 63 | topic: 'device-status', 64 | payload: {online: true} 65 | }; 66 | skynet.sendMessage(auth.device, message); 67 | 68 | //make sure not in there already: 69 | try{ 70 | socket.leave(auth.device.uuid); 71 | }catch(lexp){ 72 | console.log('error leaving room', lexp); 73 | } 74 | socket.join(auth.device.uuid); 75 | 76 | } else { 77 | socket.emit('notReady', {"api": "connect", "status": auth.status, "uuid": data.uuid}); 78 | } 79 | 80 | whoAmI(data.uuid, false, function(results){ 81 | data.auth = auth; 82 | data.fromUuid = results.uuid; 83 | data.from = results; 84 | logEvent(101, data); 85 | }); 86 | }); 87 | }); 88 | 89 | socket.on('disconnect', function (data) { 90 | updatePresence(socket.id); 91 | // Emit API request from device to room for subscribers 92 | getDevice(socket, function(err, device){ 93 | //Announce presence offline 94 | var message = { 95 | devices: '*', 96 | topic: 'device-status', 97 | payload: {online: false} 98 | }; 99 | skynet.sendMessage(device, message); 100 | 101 | logEvent(102, {api: "disconnect", socketid: socket.id, device: device}); 102 | }); 103 | }); 104 | 105 | socket.on('subscribeText', function(data, fn) { 106 | if(!data){ return; } 107 | 108 | getDevice(socket, function(err, device){ 109 | skynet.sendActivity(getActivity('subscribeText', socket, device)); 110 | if(err){ return; } 111 | 112 | if(data.uuid && data.uuid.length > 30){ 113 | //no token provided, attempt to only listen for public broadcasts FROM this uuid 114 | whoAmI(data.uuid, false, function(results){ 115 | if(results.error){ 116 | fn(results); 117 | }else{ 118 | if(securityImpl.canRead(device, results)){ 119 | socket.join(data.uuid + "_tb"); 120 | fn({"api": "subscribe", "socketid": socket.id, "toUuid": data.uuid, "result": true}); 121 | }else{ 122 | fn({error: "unauthorized access"}); 123 | } 124 | } 125 | 126 | data.toUuid = results.uuid; 127 | data.to = results; 128 | logEvent(204, data); 129 | }); 130 | } 131 | }); 132 | }); 133 | 134 | // Is this API still needed with MQTT? 135 | socket.on('subscribe', function(data, fn) { 136 | if(!data){ return; } 137 | if(!fn){ fn = function(){}; } 138 | 139 | getDevice(socket, function(err, device){ 140 | skynet.sendActivity(getActivity('subscribe', socket, device)); 141 | if(err){ return; } 142 | 143 | if(data.uuid && data.uuid.length > 30 && !data.token){ 144 | //no token provided, attempt to only listen for public broadcasts FROM this uuid 145 | whoAmI(data.uuid, false, function(results){ 146 | if(results.error){ 147 | fn(results); 148 | }else{ 149 | 150 | if(securityImpl.canRead(device, results)){ 151 | // if you are the owner, allow subscribe without token 152 | if (results.owner === device.uuid) { 153 | socket.join(data.uuid); 154 | } 155 | socket.join(data.uuid + "_bc"); 156 | fn({"api": "subscribe", "socketid": socket.id, "toUuid": data.uuid, "result": true}); 157 | }else{ 158 | fn({error: "unauthorized access"}); 159 | } 160 | 161 | } 162 | 163 | data.toUuid = results.uuid; 164 | data.to = results; 165 | logEvent(204, data); 166 | }); 167 | }else{ 168 | //token provided, attempt to listen to any broadcast FOR this uuid 169 | authDevice(data.uuid, data.token, function(auth){ 170 | if (auth.authenticate){ 171 | socket.join(data.uuid); 172 | socket.join(data.uuid + "_bc"); //shouldnt be here? 173 | 174 | // Emit API request from device to room for subscribers 175 | var results = {"api": "subscribe", "socketid": socket.id, "fromUuid": device.uuid, "toUuid": data.uuid, "result": true}; 176 | 177 | data.auth = auth; 178 | data.fromUuid = device.uuid; 179 | data.from = device; 180 | data.toUuid = auth.device.uuid; 181 | data.to = auth.device; 182 | logEvent(204, data); 183 | 184 | try{ 185 | fn(results); 186 | } catch (e){ 187 | console.error(e); 188 | } 189 | 190 | } else { 191 | var results = {"api": "subscribe", "uuid": data.uuid, "result": false}; 192 | // socket.broadcast.to(uuid).emit('message', results); 193 | 194 | logEvent(204, results); 195 | 196 | try{ 197 | fn(results); 198 | 199 | } catch (e){ 200 | console.error(e); 201 | } 202 | } 203 | }); 204 | } 205 | }); 206 | }); 207 | 208 | socket.on('unsubscribeText', function(data, fn) { 209 | skynet.sendActivity(getActivity('unsubscribeText', socket)); 210 | try{ 211 | socket.leave(data.uuid + "_tb"); 212 | if(fn){ 213 | fn({"api": "unsubscribeText", "uuid": data.uuid}); 214 | } 215 | } catch (e){ 216 | console.error(e); 217 | } 218 | }); 219 | 220 | socket.on('unsubscribe', function(data, fn) { 221 | try{ 222 | socket.leave(data.uuid); 223 | socket.leave(data.uuid + "_bc"); 224 | if(fn){ 225 | fn({"api": "unsubscribe", "uuid": data.uuid}); 226 | } 227 | getDevice(socket, function(err, device){ 228 | skynet.sendActivity(getActivity('unsubscribe', socket, device)); 229 | if(err){ return; } 230 | data.fromUuid = device.uuid; 231 | data.from = device; 232 | logEvent(205, data); 233 | }); 234 | } catch (e){ 235 | console.error(e); 236 | } 237 | }); 238 | 239 | // APIs 240 | socket.on('status', function (fn) { 241 | 242 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 243 | if(socket.throttled && limited){ 244 | console.log('status throttled', socket.id); 245 | }else{ 246 | 247 | // Emit API request from device to room for subscribers 248 | getDevice(socket, function(err, device){ 249 | skynet.sendActivity(getActivity('status', socket, device)); 250 | if(err){ return; } 251 | // socket.broadcast.to(uuid).emit('message', {"api": "status"}); 252 | 253 | getSystemStatus(function(results){ 254 | 255 | results.fromUuid = device.uuid; 256 | results.from = device; 257 | logEvent(200, results); 258 | try{ 259 | fn(results); 260 | } catch (e){ 261 | console.error(e); 262 | } 263 | }); 264 | }); 265 | } 266 | }); 267 | }); 268 | 269 | 270 | socket.on('device', function (data, fn) { 271 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 272 | if(socket.throttled && limited){ 273 | console.log('query throttled', socket.id); 274 | }else{ 275 | 276 | if(!data || (typeof data != 'object')){ 277 | data = {}; 278 | } 279 | 280 | getDevice(socket, function(err, device){ 281 | skynet.sendActivity(getActivity('device', socket, device)); 282 | if(err){ return; } 283 | var reqData = data; 284 | getDevices(device, data, false, function(results){ 285 | var msg = { 286 | fromUuid : device.uuid, 287 | from : device, 288 | device: _.first(results.devices) 289 | }; 290 | logEvent(403, msg); 291 | 292 | try{ 293 | fn(msg); 294 | } catch (e){ 295 | console.error(e); 296 | } 297 | }); 298 | }); 299 | } 300 | }); 301 | }); 302 | 303 | socket.on('devices', function (data, fn) { 304 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 305 | if(socket.throttled && limited){ 306 | console.log('query throttled', socket.id); 307 | }else{ 308 | 309 | if(!data || (typeof data != 'object')){ 310 | data = {}; 311 | } 312 | 313 | getDevice(socket, function(err, device){ 314 | skynet.sendActivity(getActivity('devices', socket, device)); 315 | if(err){ return; } 316 | var reqData = data; 317 | getDevices(device, data, false, function(results){ 318 | results.fromUuid = device.uuid; 319 | results.from = device; 320 | logEvent(403, results); 321 | 322 | try{ 323 | fn(results); 324 | } catch (e){ 325 | console.error(e); 326 | } 327 | }); 328 | }); 329 | } 330 | }); 331 | }); 332 | 333 | socket.on('mydevices', function (data, fn) { 334 | 335 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 336 | if(socket.throttled && limited){ 337 | console.log('query throttled', socket.id); 338 | }else{ 339 | getDevice(socket, function(err, device){ 340 | skynet.sendActivity(getActivity('mydevices', socket, device)); 341 | if(err){ return; } 342 | getDevices(device, {owner: device.uuid}, true, function(results){ 343 | try{ 344 | results.fromUuid = device.uuid; 345 | results.from = device; 346 | logEvent(403, results); 347 | fn(results); 348 | } catch (e){ 349 | console.error(e); 350 | } 351 | }); 352 | }); 353 | } 354 | }); 355 | }); 356 | 357 | socket.on('localdevices', function (data, fn) { 358 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 359 | if(socket.throttled && limited){ 360 | console.log('query throttled', socket.id); 361 | }else{ 362 | 363 | if(!data || (typeof data != 'object')){ 364 | data = {}; 365 | } 366 | // Emit API request from device to room for subscribers 367 | getDevice(socket, function(err, device){ 368 | skynet.sendActivity(getActivity('localdevices', socket, device)); 369 | if(err){ return; } 370 | getLocalDevices(device, false, function(results){ 371 | results.fromUuid = device.uuid; 372 | results.from = device; 373 | logEvent(403, results); 374 | 375 | try{ 376 | fn(results); 377 | } catch (e){ 378 | console.error(e); 379 | } 380 | }); 381 | }); 382 | } 383 | }); 384 | }); 385 | 386 | socket.on('unclaimeddevices', function (data, fn) { 387 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 388 | if(socket.throttled && limited){ 389 | console.log('query throttled', socket.id); 390 | }else{ 391 | 392 | if(!data || (typeof data != 'object')){ 393 | data = {}; 394 | } 395 | // Emit API request from device to room for subscribers 396 | getDevice(socket, function(err, device){ 397 | skynet.sendActivity(getActivity('localdevices', socket, device)); 398 | if(err){ return; } 399 | getLocalDevices(device, true, function(results){ 400 | results.fromUuid = device.uuid; 401 | results.from = device; 402 | logEvent(403, results); 403 | 404 | try{ 405 | fn(results); 406 | } catch (e){ 407 | console.error(e); 408 | } 409 | }); 410 | }); 411 | } 412 | }); 413 | }); 414 | 415 | socket.on('claimdevice', function (data, fn) { 416 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 417 | if(socket.throttled && limited){ 418 | console.log('query throttled', socket.id); 419 | }else{ 420 | 421 | if(!data || (typeof data != 'object')){ 422 | data = {}; 423 | } 424 | // Emit API request from device to room for subscribers 425 | getDevice(socket, function(err, device){ 426 | skynet.sendActivity(getActivity('claimdevice', socket, device)); 427 | if(err){ return; } 428 | claimDevice(device, data, function(err, results){ 429 | logEvent(403, {error: err, results: results, fromUuid: device.uuid, from: device}); 430 | 431 | try{ 432 | fn({error: err, results: results}); 433 | } catch (e){ 434 | console.error(e); 435 | } 436 | }); 437 | }); 438 | } 439 | }); 440 | }); 441 | 442 | socket.on('whoami', function (data, fn) { 443 | 444 | skynet.throttles.whoami.rateLimit(socket.id, function (err, limited) { 445 | if(socket.throttled && limited){ 446 | console.log('whoami throttled', socket.id); 447 | }else{ 448 | getDevice(socket, function(err, device){ 449 | skynet.sendActivity(getActivity('whoami', socket, device)); 450 | if(err){ return; } 451 | try{ 452 | fn(device); 453 | } catch (e){ 454 | console.error(e); 455 | } 456 | }); 457 | 458 | } 459 | }); 460 | 461 | }); 462 | 463 | 464 | socket.on('register', function (data, fn) { 465 | skynet.sendActivity(getActivity('register', socket)); 466 | 467 | if(!data){ 468 | data = {}; 469 | } 470 | data.socketid = socket.id; 471 | data.ipAddress = ipAddress; 472 | 473 | register(data, function(results){ 474 | whoAmI(data.uuid, false, function(check){ 475 | results.fromUuid = check.uuid; 476 | results.from = check; 477 | logEvent(400, results); 478 | }); 479 | 480 | try{ 481 | fn(results); 482 | } catch (e){ 483 | console.error(e); 484 | } 485 | }); 486 | }); 487 | 488 | socket.on('update', function (data, fn) { 489 | if(!data){ 490 | data = {}; 491 | } 492 | // Emit API request from device to room for subscribers 493 | getDevice(socket, function(err, fromDevice){ 494 | skynet.sendActivity(getActivity('update', socket, fromDevice)); 495 | if(err){ return; } 496 | 497 | updateFromClient(fromDevice, data, function(regData) { 498 | try { 499 | skynet.sendConfigActivity(data.uuid, skynet.emitToClient); 500 | if (fn) { 501 | fn(regData); 502 | } 503 | } catch(error) { 504 | console.error(error); 505 | } 506 | }); 507 | }); 508 | }); 509 | 510 | socket.on('unregister', function (data, fn) { 511 | if(!data){ 512 | data = {}; 513 | } 514 | // Emit API request from device to room for subscribers 515 | getDevice(socket, function(err, fromDevice){ 516 | if(err){ return; } 517 | var reqData = data; 518 | skynet.sendActivity(getActivity('unregister', socket, fromDevice)); 519 | unregister(fromDevice, data.uuid, data.token, skynet.emitToClient, function(results){ 520 | if(results == null || results == undefined){ 521 | results = {}; 522 | } 523 | results.fromUuid = fromDevice.uuid; 524 | results.from = fromDevice; 525 | logEvent(402, results); 526 | 527 | try{ 528 | fn(results); 529 | } catch (e){ 530 | console.error(e); 531 | } 532 | }); 533 | }); 534 | }); 535 | 536 | socket.on('events', function(data, fn) { 537 | 538 | authDevice(data.uuid, data.token, function(auth){ 539 | 540 | // Emit API request from device to room for subscribers 541 | getDevice(socket, function(err, device){ 542 | skynet.sendActivity(getActivity('events', socket, device)); 543 | if(err){ return; } 544 | var reqData = data; 545 | reqData.api = "events"; 546 | 547 | if (auth.authenticate){ 548 | getEvents(data.uuid, function(results){ 549 | try{ 550 | fn(results); 551 | } catch (e){ 552 | console.error(e); 553 | } 554 | }); 555 | 556 | } else { 557 | var results = {"api": "events", "result": false}; 558 | 559 | try{ 560 | fn(results); 561 | } catch (e){ 562 | console.error(e); 563 | } 564 | } 565 | }); 566 | }); 567 | }); 568 | 569 | socket.on('authenticate', function(data, fn) { 570 | skynet.sendActivity(getActivity('authenticate', socket)); 571 | 572 | authDevice(data.uuid, data.token, function(auth){ 573 | var results; 574 | if (auth.authenticate){ 575 | results = {"uuid": data.uuid, "authentication": true}; 576 | 577 | data = _.extend({}, data, { 578 | socketid: socket.id, 579 | ipAddress: ipAddress, 580 | secure: secure, 581 | online: true 582 | }); 583 | if(!data.protocol){ 584 | data.protocol = "websocket"; 585 | } 586 | updateSocketId(data, function(auth) { 587 | 588 | socket.emit('ready', {"api": "connect", "status": 201, "socketid": socket.id, "uuid": data.uuid}); 589 | socket.join(data.uuid); 590 | 591 | try{ 592 | fn(results); 593 | } catch (e){ 594 | console.error(e); 595 | } 596 | }); 597 | 598 | } else { 599 | results = {"uuid": data.uuid, "authentication": false}; 600 | try{ 601 | fn(results); 602 | } catch (e){ 603 | console.error(e); 604 | } 605 | 606 | } 607 | 608 | // require('./lib/whoAmI')(data.uuid, false, function(check){ 609 | whoAmI(data.uuid, false, function(check){ 610 | // check._id.toString(); 611 | // delete check._id; 612 | results.toUuid = check.uuid; 613 | results.to = check; 614 | logEvent(102, results); 615 | }); 616 | }); 617 | }); 618 | 619 | socket.on('data', function (messageX, fn) { 620 | skynet.throttles.data.rateLimit(socket.id, function (err, limited) { 621 | if(socket.throttled && limited){ 622 | console.log('data throttled', socket.id); 623 | }else{ 624 | var data = messageX; 625 | 626 | getDevice(socket, function(err, fromDevice){ 627 | //skynet.sendActivity(getActivity('data', socket, fromDevice)); 628 | if(err){ return; } 629 | 630 | if (data) { 631 | delete data.token; 632 | } 633 | 634 | logData(data, function(results){ 635 | // Send messsage regarding data update 636 | var message = {}; 637 | message.payload = data; 638 | // message.devices = data.uuid; 639 | message.devices = "*"; 640 | 641 | skynet.sendMessage(fromDevice, message); 642 | 643 | try{ 644 | if (fn) { 645 | fn(results); 646 | } 647 | } catch (e){ 648 | console.error(e); 649 | } 650 | }); 651 | }); 652 | } 653 | }); 654 | }); 655 | 656 | socket.on('getdata', function (data, fn) { 657 | skynet.throttles.query.rateLimit(socket.id, function (err, limited) { 658 | if(socket.throttled && limited){ 659 | console.log('query throttled', socket.id); 660 | }else{ 661 | skynet.sendActivity(getActivity('getdata', socket)); 662 | authDevice(data.uuid, data.token, function(auth){ 663 | 664 | if (auth.authenticate){ 665 | if(!data || (typeof data != 'object')){ 666 | data = {}; 667 | } 668 | data.params = {}; 669 | data.query = {}; 670 | 671 | data.params.uuid = data.uuid; 672 | data.query.start = data.start; // time to start from 673 | data.query.finish = data.finish; // time to end 674 | data.query.limit = data.limit; // 0 bypasses the limit 675 | 676 | getData(data, function(results){ 677 | // if(err){ return; } 678 | 679 | results.fromUuid = socket.skynetDevice.uuid; 680 | 681 | try{ 682 | fn(results); 683 | } catch (e){ 684 | console.error(e); 685 | } 686 | }); 687 | 688 | } else { 689 | var results = {"api": "getdata", "result": false}; 690 | 691 | try{ 692 | fn(results); 693 | } catch (e){ 694 | console.error(e); 695 | } 696 | } 697 | }); 698 | } 699 | }); 700 | }); 701 | 702 | 703 | socket.on('gatewayConfig', function(data) { 704 | getDevice(socket, function(err, device){ 705 | skynet.sendActivity(getActivity('gatewayConfig', socket, device)); 706 | if(err){ return; } 707 | skynet.gateway.config(device, data); 708 | }); 709 | }); 710 | 711 | socket.on('gatewayConfigAck', function (data) { 712 | getDevice(socket, function(err, device){ 713 | skynet.sendActivity(getActivity('gatewayConfigAck', socket, device)); 714 | if(err){ return; } 715 | skynet.gateway.configAck(device, data); 716 | }); 717 | }); 718 | 719 | socket.on('messageAck', function (data) { 720 | getDevice(socket, function(err, fromDevice){ 721 | skynet.sendActivity(getActivity('messageAck', socket, fromDevice)); 722 | if(fromDevice){ 723 | whoAmI(data.devices, false, function(check){ 724 | data.fromUuid = fromDevice.uuid; 725 | if(!check.error && securityImpl.canSend(fromDevice, check)){ 726 | skynet.emitToClient('messageAck', check, data); 727 | } 728 | }); 729 | } 730 | }); 731 | }); 732 | 733 | 734 | socket.on('tb', function (messageX) { 735 | skynet.throttles.message.rateLimit(socket.id, function (err, limited) { 736 | var message = messageX; 737 | 738 | if (socket.throttled && limited) { 739 | // TODO: Emit rate limit exceeded message 740 | console.log("Rate limit exceeded for socket:", socket.id); 741 | console.log("message", message); 742 | } else { 743 | if(!message){ 744 | return; 745 | }else{ 746 | message = message.toString(); 747 | 748 | // Broadcast to room for pubsub 749 | getDevice(socket, function(err, fromDevice){ 750 | //skynet.sendActivity(getActivity('tb', socket, fromDevice)); 751 | if(fromDevice){ 752 | skynet.sendMessage(fromDevice, {payload: message}, 'tb'); 753 | } 754 | }); 755 | } 756 | } 757 | }); 758 | }); 759 | 760 | socket.on('message', function (messageX) { 761 | 762 | // socket.limiter.removeTokens(1, function(err, remainingRequests) { 763 | skynet.throttles.message.rateLimit(socket.id, function (err, limited) { 764 | var message = messageX; 765 | 766 | if (socket.throttled && limited) { 767 | // TODO: Emit rate limit exceeded message 768 | console.log("Rate limit exceeded for socket:", socket.id); 769 | console.log("message", message); 770 | } else { 771 | if(typeof message !== 'object'){ 772 | return; 773 | }else{ 774 | // Broadcast to room for pubsub 775 | getDevice(socket, function(err, fromDevice){ 776 | //skynet.sendActivity(getActivity('message', socket, fromDevice)); 777 | if(fromDevice){ 778 | message.api = "message"; 779 | skynet.sendMessage(fromDevice, message); 780 | } 781 | }); 782 | } 783 | } 784 | }); 785 | }); 786 | 787 | socket.on('directText', function (messageX) { 788 | skynet.throttles.message.rateLimit(socket.id, function (err, limited) { 789 | var message = messageX; 790 | 791 | if (socket.throttled && limited) { 792 | // TODO: Emit rate limit exceeded message 793 | console.log("Rate limit exceeded for socket:", socket.id); 794 | console.log("message", message); 795 | } else { 796 | getDevice(socket, function(err, fromDevice){ 797 | if(fromDevice){ 798 | skynet.sendMessage(fromDevice, message, 'tb'); 799 | } 800 | }); 801 | } 802 | }); 803 | }); 804 | } 805 | 806 | module.exports = socketLogic; 807 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ``` 2 | MM MM hh bb lll 3 | MMM MMM eee sss hh bb lll uu uu 4 | MM MM MM ee e s hhhhhh bbbbbb lll uu uu 5 | MM MM eeeee sss hh hh bb bb lll uu uu 6 | MM MM eeeee s hh hh bbbbbb lll uuuu u 7 | sss 8 | ``` 9 | meshblu.octoblu.com 10 | 11 | OPEN HTTP, WebSocket, MQTT, & CoAP COMMUNICATIONS NETWORK & API FOR THE INTERNET OF THINGS (IoT)! 12 | 13 | Visit [developer.octoblu.com](http://developer.octoblu.com) for up-to-the-latest documentation and screencasts. 14 | 15 | ====== 16 | 17 | [![Build Status](https://travis-ci.org/octoblu/meshblu.svg?branch=master)](https://travis-ci.org/octoblu/meshblu) 18 | 19 | Introduction 20 | ------------ 21 | 22 | Meshblu is an open source machine-to-machine instant messaging network and API. Our API is available on HTTP REST, realtime Web Sockets via RPC (remote procedure calls), [MQTT](http://mqtt.org), and [CoAP](http://en.wikipedia.org/wiki/Constrained_Application_Protocol). We seamlessly bridge all of these protocols. For instance, an MQTT device can communicate with any CoAP or HTTP or WebSocket connected device on Meshblu. 23 | 24 | Meshblu auto-assigns 36 character UUIDs and secret tokens to each registered device connected to the network. These device "credentials" are used to authenticate with Meshblu and maintain your device's JSON description in the device directory. 25 | 26 | Meshblu allows you to discover/query devices such as drones, hue light bulbs, weemos, insteons, raspberry pis, arduinos, server nodes, etc. that meet your criteria and send IM messages to 1 or all devices. 27 | 28 | You can also subscribe to messages being sent to/from devices and their sensor activities. 29 | 30 | Meshblu offers a Node.JS NPM module called [Meshblu](https://www.npmjs.org/package/meshblu) and a [meshblu.js](http://meshblu.octoblu.com/#javascript) file for simplifying Node.JS and mobile/client-side connectivity to Meshblu. 31 | 32 | 33 | Press 34 | ----- 35 | [AllSeen Alliance](https://allseenalliance.org/announcement/allseen-alliance-reaches-50-members-expands-smart-home-connected-car-and-security-focus) - Allseen Alliance reaches 50 members; expands smart home, connected car and security focus 36 | 37 | [Sys-Con](http://iot.sys-con.com/node/3125944) - Exclusive Octoblu Interview @ThingsExpo Silicon Valley 38 | 39 | [Onalytica](http://www.onalytica.com/blog/posts/the-internet-of-things-top-100-organizations) - The Internet of Things - Top 100 Organizations 40 | 41 | [GigaOm](http://gigaom.com/2014/07/21/octoblu-launches-to-make-skynet-internet-of-things-tools-professional-grade/) - Octoblu launches to make Skynet internet of things tools professional grade 42 | 43 | [VentureBeat](http://venturebeat.com/2014/07/22/internet-of-things-startup-octoblu-designs-a-platform-that-translates-protocols/) - Internet of things startup Octoblu designs a platform that translates protocols 44 | 45 | [Forbes](http://www.forbes.com/sites/benkepes/2014/07/22/octoblu-rolls-out-its-internet-of-things-thing/) - Octoblu Peels Back The Covers On Its Internet Of Things Platform 46 | 47 | [GigaOm](http://gigaom.com/2014/02/04/podcast-meet-skynet-an-open-source-im-platform-for-the-internet-of-things/) - Listen to Stacey Higginbotham from GigaOm interview Chris Matthieu, the founder of SkyNet, about our capabilities, uses, and future direction. 48 | 49 | [Wired](http://www.wired.com/wiredenterprise/2014/02/skynet/) - ‘Yes, I’m trying to build SkyNet from Terminator.’ 50 | 51 | [Wired](http://www.wired.com/2014/05/iot-report/) - Why Tech’s Best Minds Are Very Worried About the Internet of Things 52 | 53 | [LeapMotion](https://labs.leapmotion.com/46/) - Developer newsletter covers flying drones connected to SkyNet with LeapMotion sensor! 54 | 55 | [The New Stack](http://thenewstack.io/a-messaging-network-for-drones-called-skynet/) - Drones Get A Messaging Network Aptly Called SkyNet 56 | 57 | Roadmap 58 | ------- 59 | 60 | * Phase 1 - Build a network and realtime API for enabling machine-to-machine communications. 61 | * Phase 2 - Connect all of the thingz. 62 | * Phase 3 - Become self-aware! 63 | 64 | Installing/Running Meshblu private cloud 65 | ---------- 66 | 67 | Clone the git repository, then: 68 | 69 | ```bash 70 | $ npm install 71 | $ cp config.js.sample config.js 72 | ``` 73 | 74 | [Meshblu](http://meshblu.octoblu.com) uses Mongo, Redis, ElasticSearch, and Splunk; however, we have made this infrastructure optional for you. Meshblu falls back to file system and memory storage if these services are not configured allowing you to deploy a private Meshblu cloud to a Raspberry Pi or other mini-computer! 75 | 76 | If you want to include these services for a scalable infrastructure, you can make the following changes to your `config.js` file. 77 | 78 | Modify `config.js` with your MongoDB connection string. If you have MongoDB running locally use: 79 | 80 | ``` 81 | mongo: { 82 | databaseUrl: mongodb://localhost:27017/skynet 83 | }, 84 | ``` 85 | 86 | You can also modify `config.js` with your Redis connection information. If you have Redis running locally use: 87 | 88 | ``` 89 | redis: { 90 | host: "127.0.0.1", 91 | port: "6379", 92 | password: "abcdef" 93 | }, 94 | ``` 95 | 96 | You can also modify `config.js` with your ElasticSearch connection information. If you have ES running locally use: 97 | 98 | ``` 99 | elasticSearch: { 100 | host: "localhost", 101 | port: "9200" 102 | }, 103 | ``` 104 | 105 | If you would like to connect your private Meshblu cloud to [meshblu.octoblu.com](http://meshblu.octoblu.com) or another private Meshblu cloud, register a UUID on the parent cloud (i.e. meshblu.octoblu.com) using the POST /Devices REST API and then add the returned UUID and token to the following section to your private cloud's config.js file: 106 | 107 | ``` 108 | parentConnection: { 109 | uuid: 'xxxx-my-uuid-on-parent-server-xxxx', 110 | token: 'xxx --- my token ---- xxxx', 111 | server: 'meshblu.octoblu.com', 112 | port: 80 113 | }, 114 | ``` 115 | 116 | Start the Meshblu server running HTTP and WebSocket protocols use: 117 | 118 | ```bash 119 | $ node server.js --http 120 | ``` 121 | 122 | You may also run something like [forever](https://www.npmjs.org/package/forever) to keep it up and running: 123 | 124 | ```bash 125 | $ forever start server.js --http 126 | ``` 127 | 128 | MQTT Broker 129 | ----------- 130 | 131 | MQTT is an optional Meshblu protocol. If you would like to run our MQTT broker with your private Meshblu cloud, open another console tab and run: 132 | 133 | ```bash 134 | $ node server.js --http --mqtt 135 | ``` 136 | 137 | or using forever 138 | 139 | ```bash 140 | $ forever start server.js --http --mqtt 141 | ``` 142 | 143 | Note: Our MQTT Broker defaults to using Mongo; however, you can run it in memory by removing the databaseUrl from the config.js. 144 | 145 | ``` 146 | mqtt: { 147 | port: 1883, 148 | skynetPass: "Very big random password 34lkj23orfjvi3-94ufpvuha4wuef-a09v4ji0rhgouj" 149 | } 150 | ``` 151 | 152 | CoAP 153 | ---- 154 | 155 | CoAP is an optional Meshblu protocol. If you would like to run our CoAP protocol with your private Meshblu cloud, open another console tab and run: 156 | 157 | ```bash 158 | $ node server.js --http --coap 159 | ``` 160 | 161 | or using forever 162 | 163 | ```bash 164 | $ forever start server.js --http --coap 165 | ``` 166 | 167 | Note: Our CoAP protocol defaults to using Mongo; however, you can run it in memory by removing the databaseUrl from the config.js. 168 | 169 | ``` 170 | coap: { 171 | port: 5683, 172 | host: "localhost" 173 | } 174 | ``` 175 | 176 | Heroku 177 | ------ 178 | 179 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 180 | 181 | Use the button above to deploy to the [Heroku](http://heroku.com/) cloud for Free! Starts out with a basic implementation of Meshblu with only MongoDB. The app.json / config.js can be modified to allow for more protocols and storage systems. 182 | 183 | [Heroku](http://heroku.com/) allows you scale with a touch of a button. 184 | 185 | 186 | DigitalOcean 187 | ------------ 188 | 189 | [![Install on DigitalOcean](http://installer.71m.us/button.svg)](http://installer.71m.us/install?url=https://github.com/octoblu/meshblu) 190 | 191 | Use the button above to deploy to the [DigitalOcean](https://www.digitalocean.com/) cloud for as little as $5/month! The app.json / config.js can be modified to allow for more protocols and storage systems. 192 | 193 | 194 | Docker 195 | ------ 196 | 197 | The default Dockerfile will run Meshblu, MongoDB and Redis in a single container to make quick experiments easier. 198 | 199 | You'll need docker installed, then to build the Meshblu image: 200 | 201 | From the directory where the Dockerfile resides run. 202 | 203 | ``` 204 | $ docker build -t=skynet . 205 | ``` 206 | 207 | To run a fully self contained instance using the source bundled in the container. 208 | 209 | ``` 210 | $ docker run -i -t -p 3000 skynet 211 | ``` 212 | 213 | This will run Meshblu (formerly skynet) and expose port 3000 from the container on a random host port that you can find by running docker ps. 214 | 215 | If you want to do development and run without rebuilding the image you can bind mount your source directory including node_modules onto the container. This example also binds a directory to hold the log of stdout & stderr from the Meshblu node process. 216 | 217 | ``` 218 | $ docker run -d -p 3000 --name=skynet_dev -v /path/to/your/skynet:/var/www -v /path/to/your/logs:/var/log/skynet skynet 219 | ``` 220 | 221 | If you change the code restarting the container is as easy as: 222 | 223 | ``` 224 | $ docker restart skynet_dev 225 | ``` 226 | 227 | Nodeblu Developer Toolkit 228 | -------------------------------- 229 | Play with [Meshblu](http://meshblu.octoblu.com) IoT platform in Chrome! [Nodeblu](https://chrome.google.com/webstore/detail/nodeblu/aanmmiaepnlibdlobmbhmfemjioahilm) helps you experiment with the [Octoblu](http://octoblu.com) and [Meshblu](http://meshblu.octoblu.com) Internet of Things platforms by dragging, dropping, and wiring up various nodes connected to Meshblu! 230 | 231 | Nodeblu is Octoblu's fork of the popular [NodeRED](https://github.com/node-red/node-red) application from IBM. Since our app is deployed as a Chrome extension, we decided to add extra local features such as speech recognition and text-to-speech, a WebRTC webcam, HTML5 notifications, access to ChromeDB, a gamepad controller, and access to local and remote Arduino and Spark devices via our [Microblu](https://github.com/octoblu/microblu_mqtt) OS. 232 | 233 |

234 | 235 | 236 | 237 |

238 | 239 | Gateblu 240 | ------- 241 | 242 | We have an open source Octoblu Gateway also available on [GitHub](https://github.com/octoblu/gateblu). Our Gateway allows you to connect devices with or *without* IP addresses to Meshblu and communicate bi-directionally! The Gateway uses WebSockets to connect to Meshblu so it can traverse NAT firewalls and send/receive messages to/from Meshblu. 243 | 244 | Gateblu has an extensible [plugin](https://github.com/octoblu/gateblu/blob/master/plugins.md) architecture allowing you to create plugins for devices that we have not had a chance to connect yet. You can search [NPMJS.org](https://www.npmjs.org/search?q=skynet-plugin) for a list of active Gateblu plugins. 245 | 246 | Microblu Operating System 247 | ----------------------- 248 | 249 | Meshblu includes a micro-controller operating system that is compatible with [Arduino](http://www.arduino.cc/), [Spark](https://www.spark.io/), and [Pinoccio](https://pinocc.io/)! The OS is available on [GitHub](https://github.com/octoblu/microblu_mqtt) and comes with firmata built-in. 250 | 251 | On power-on, the Microblu OS connects to Meshblu, obtains a UUID, and awaits your instructions! You can message the micro-controller to turn on/off pins and servos as well as request streaming analog sensor data from various pins. 252 | 253 | HTTP(S) REST API 254 | ---------------- 255 | 256 | Most of our API endpoints require authentication credentials (UUID and secret token) passed in the HTTP headers as meshblu_auth_uuid and meshblu_auth_token respectively. These credentials are generated by registering a device or user with Meshblu via the POST /Devices API (see below). If you would like to associate additional Meshblu devices with the UUID and Token that you created (as a user), you can add an "owner" property to your other devices with the user's UUID as its value; otherwise, you can use the device's UUID and token in the headers to control the device itself. 257 | 258 | We support the following device permissions: View/Discover, Send Messages, Read Messages (subscribe) and Update. These permissions are manageable by adding UUIDs to whitelists and blacklists arrays with the following names: viewWhitelist, viewBlacklist, sendWhitelist, sendBlacklist, readWhitelist, readBlacklist, updateWhitelist, updateBlacklist. Note: If your UUID is the same as the "owner" UUID, these permissions are not enforced (you are the owner). 259 | 260 | GET /status 261 | 262 | Returns the current status of the Meshblu network 263 | 264 | ``` 265 | curl http://localhost:3000/status 266 | 267 | => {"skynet":"online","timestamp":1380480777750,"eventCode":200} 268 | ``` 269 | 270 | GET /devices 271 | 272 | Returns an array of all devices available to you on Meshblu. Notice you can query against custom properties i.e. all drones or light switches and online/offline etc. 273 | 274 | ``` 275 | curl "http://localhost:3000/devices" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 276 | 277 | curl "http://localhost:3000/devices?key=123" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 278 | 279 | curl "http://localhost:3000/devices?online=true" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 280 | 281 | curl "http://localhost:3000/devices?key=123&online=true" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 282 | 283 | => ["ad698900-2546-11e3-87fb-c560cb0ca47b","2f3113d0-2796-11e3-95ef-e3081976e170","9c62a150-29f6-11e3-89e7-c741cd5bd6dd","f828ef20-29f7-11e3-9604-b360d462c699","d896f9f0-29fb-11e3-a27c-614201ddde6e"] 284 | ``` 285 | 286 | GET /devices/uuid 287 | 288 | Returns all information on a given device by its UUID 289 | 290 | ``` 291 | curl "http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 292 | 293 | => {"_id":"5241d9140345450000000001","channel":"main","deviceDescription":"this is a test","deviceName":"hackboard","key":"123","online":true,"socketid":"pG5UAhaZa_xXlvrItvTd","timestamp":1380340661522,"uuid":"ad698900-2546-11e3-87fb-c560cb0ca47b"}b 294 | ``` 295 | 296 | POST /devices 297 | 298 | Registers a device on the Meshblu network. You can add as many properties to the device object as desired. Meshblu returns a device UUID and token which needs to be used with future updates to the device object 299 | 300 | Note: You can pass in a token parameter to overide skynet issuing you one 301 | 302 | ``` 303 | curl -X POST -d "name=arduino&description=this+is+a+test" "http://localhost:3000/devices" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 304 | 305 | curl -X POST -d "name=arduino&token=123" "http://localhost:3000/devices" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 306 | 307 | => {"name":"arduino","description":"this is a test","uuid":"8220cff0-2939-11e3-88cd-0b8e5fdfd7d4","timestamp":1380481272431,"token":"1yw0nfc54okcsor2tfqqsuvnrcf2yb9","online":false,"_id":"524878f8cc12f0877f000003"} 308 | ``` 309 | 310 | PUT /devices/uuid 311 | 312 | Updates a device object. Token is required for security. 313 | 314 | ``` 315 | curl -X PUT -d "token=123&online=true" "http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 316 | 317 | => {"uuid":"8220cff0-2939-11e3-88cd-0b8e5fdfd7d4","timestamp":1380481439002,"online":true} 318 | ``` 319 | 320 | DELETE /devices/uuid 321 | 322 | Unregisters a device on the Meshblu network. Token is required for security. 323 | 324 | ``` 325 | curl -X DELETE -d "token=123" "http://localhost:3000/devices/01404680-2539-11e3-b45a-d3519872df26" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 326 | 327 | => {"uuid":"8220cff0-2939-11e3-88cd-0b8e5fdfd7d4","timestamp":1380481567799} 328 | ``` 329 | 330 | GET /localdevices 331 | 332 | Returns a list of unclaimed devices that are on the same network as the requesting resource. 333 | 334 | ``` 335 | curl -X GET http://meshblu.octoblu.com/localdevices --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 336 | 337 | => {"devices":[{"autoRegister":true,"online":false,"timestamp":"2014-08-05T20:38:31.139Z","ipAddress":"184.98.43.115","protocol":"websocket","secure":false,"uuid":"76537331-1ce0-11e4-861d-89322229e557","channel":"main"},{"autoRegister":true,"online":true,"timestamp":"2014-08-05T16:50:52.492Z","ipAddress":"184.98.43.115","protocol":"websocket","secure":false,"uuid":"a92350c1-1cc0-11e4-861d-89322229e557","channel":"main"}]} 338 | ``` 339 | 340 | GET /claimdevice/:uuid 341 | 342 | Adds the meshblu_auth_uuid as the owner of this device UUID allowing a user or device to claim ownership of another device. 343 | 344 | ``` 345 | curl -X PUT http://meshblu.octoblu.com/claimdevice/{:uuid} --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 346 | 347 | => {"updatedExisting":true,"n":1,"connectionId":232,"err":null,"ok":1} 348 | ``` 349 | 350 | GET /mydevices 351 | 352 | Returns all information (including tokens) of all devices or nodes belonging to a user's UUID (identified as "owner") 353 | 354 | ``` 355 | curl -X GET "http://meshblu.octoblu.com/mydevices" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 356 | 357 | => {"devices":[{"owner":"0d1234a0-1234-11e3-b09c-1234e847b2cc","name":"SMS","phoneNumber":"16025551234","uuid":"1c1234e1-xxxx-11e3-1234-671234c01234","timestamp":1390861609070,"token":"1234eg1234zz1tt1234w0op12346bt9","channel":"main","online":false,"_id":"52e6d1234980420c4a0001db"}}]} 358 | ``` 359 | 360 | POST /messages 361 | 362 | Sends a JSON message to all devices or an array of devices or a specific device on the Meshblu network. 363 | 364 | ``` 365 | curl -X POST -d '{"devices": "all", "message": {"yellow":"off"}}' "http://localhost:3000/messages" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 366 | 367 | curl -X POST -d '{"devices": ["ad698900-2546-11e3-87fb-c560cb0ca47b","2f3113d0-2796-11e3-95ef-e3081976e170"], "message": {"yellow":"off"}}' "http://localhost:3000/messages" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 368 | 369 | curl -X POST -d '{"devices": "ad698900-2546-11e3-87fb-c560cb0ca47b", "message": {"yellow":"off"}}' "http://localhost:3000/messages" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 370 | 371 | => {"devices":"ad698900-2546-11e3-87fb-c560cb0ca47b","message":{"yellow":"off"},"timestamp":1380930482043,"eventCode":300} 372 | ``` 373 | 374 | Note: If your Meshblu cloud is connected to meshblu.octoblu.com or other private Meshblu clouds, you can send messages across Meshblu clouds by chaining UUIDs together separated by slashes (/) where the first UUID is the target cloud and the second UUID is the device on that cloud. 375 | 376 | ``` 377 | curl -X POST -d '{"devices": "ad698900-2546-11e3-87fb-c560cb0ca47b/2f3113d0-2796-11e3-95ef-e3081976e170", "message": {"yellow":"off"}}' "http://localhost:3000/messages" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 378 | ``` 379 | 380 | GET /events/uuid?token=token 381 | 382 | Returns last 10 events related to a specific device or node 383 | 384 | ``` 385 | curl -X GET "http://meshblu.octoblu.com/events/ad698900-2546-11e3-87fb-c560cb0ca47b?token=123" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 386 | 387 | => {"events":[{"uuid":"0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc","socketid":"lnHHS06ijWUXEzb01ZRy","timestamp":1382632438785,"eventCode":101,"_id":"52694bf6ad11379eec00003f"},{"uuid":"0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc","socketid":"BuwnWQ_oLmpk5R3m1ZRv","timestamp":1382561240563,"eventCode":101,"_id":"526835d8ad11379eec000017"}]} 388 | ``` 389 | 390 | GET /subscribe 391 | 392 | This is a streaming API that returns device/node mesages as they are sent and received. Notice the comma at the end of the response. Meshblu doesn't close the stream. 393 | 394 | ``` 395 | curl -X GET "http://meshblu.octoblu.com/subscribe" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 396 | 397 | => [{"devices":"0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc","message":{"red":"on"},"timestamp":1388768270795,"eventCode":300,"_id":"52c6ec0e4f67671e44000001"},{"devices":"0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc","message":{"red":"on"},"timestamp":1388768277473,"eventCode":300,"_id":"52c6ec154f67671e44000002"}, 398 | ``` 399 | 400 | GET /subscribe/uuid 401 | 402 | This is a streaming API that returns device/node broadcast mesages as they are sent. Notice the comma at the end of the response. Meshblu doesn't close the stream. 403 | 404 | ``` 405 | curl -X GET "http://meshblu.octoblu.com/subscribe/ad698900-2546-11e3-87fb-c560cb0ca47b" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 406 | 407 | => [{"fromUuid":"ad698900-2546-11e3-87fb-c560cb0ca47b","devices":"*",message":{"red":"on"},"timestamp":1388768270795,"eventCode":300,"_id":"52c6ec0e4f67671e44000001"},{"fromUuid":"ad698900-2546-11e3-87fb-c560cb0ca47b","devices":"*",,"message":{"red":"on"},"timestamp":1388768277473,"eventCode":300,"_id":"52c6ec154f67671e44000002"}, 408 | ``` 409 | 410 | GET /authenticate/uuid?token=token 411 | 412 | Returns UUID and authticate: true or false based on the validity of uuid/token credentials 413 | 414 | ``` 415 | curl -X GET "http://meshblu.octoblu.com/authenticate/81246e80-29fd-11e3-9468-e5f892df566b?token=5ypy4rurayktke29ypbi30kcw5ovfgvi" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 416 | 417 | => {"uuid":"81246e80-29fd-11e3-9468-e5f892df566b","authentication":true} OR {"uuid":"81246e80-29fd-11e3-9468-e5f892df566b","authentication":false} 418 | ``` 419 | 420 | GET /ipaddress 421 | 422 | Returns the public IP address of the request. This is useful when working with the Octoblu Gateway behind a firewall. 423 | 424 | ``` 425 | curl -X GET http://meshblu.octoblu.com/ipaddress 426 | 427 | => {"ipAddress":"70.171.149.205"} 428 | ``` 429 | 430 | POST /data/uuid 431 | 432 | Stores your device's sensor data to Meshblu 433 | 434 | ``` 435 | curl -X POST -d "token=123&temperature=78" "http://meshblu.octoblu.com/data/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 436 | 437 | => {"timestamp":"2014-03-25T16:38:48.148Z","uuid":"0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc","temperature":"30","ipAddress":"127.0.0.1","eventCode":700,"_id":"5331b118512c974805000002"} 438 | ``` 439 | 440 | GET /data/uuid 441 | 442 | Retrieves your device's sensor data to Meshblu 443 | 444 | ``` 445 | curl -X GET "http://localhost:3000/data/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc?token=123" --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}" 446 | 447 | => {"data":[{"timestamp":"2014-03-25T16:38:48.148Z","uuid":"0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc","temperature":"30","ipAddress":"127.0.0.1","id":"5331b118512c974805000001"},{"timestamp":"2014-03-23T18:57:16.093Z","uuid":"0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc","temperature":"78","ipAddress":"127.0.0.1","id":"532f2e8c9c23809e93000001"}]} 448 | ``` 449 | 450 | CoAP API 451 | -------- 452 | 453 | Our CoAP API works exactly like our REST API. You can use [Matteo Collina's](https://twitter.com/matteocollina) [CoAP CLI](https://www.npmjs.org/package/coap-cli) for testing CoAP REST API calls. Here are a few examples: 454 | 455 | coap get coap://coap.octoblu.com/status 456 | 457 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/devices?type=drone 458 | 459 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/devices/ad698900-2546-11e3-87fb-c560cb0ca47b 460 | 461 | coap post -p "type=drone&color=black" -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/devices 462 | 463 | coap put -p "token=123&color=blue&online=true" -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/devices/ad698900-2546-11e3-87fb-c560cb0ca47b 464 | 465 | coap delete -p "token=123" -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/devices/ad698900-2546-11e3-87fb-c560cb0ca47b 466 | 467 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/mydevices/0d1234a0-1234-11e3-b09c-1234e847b2cc?token=1234glm6y1234ldix1234nux41234sor 468 | 469 | coap post -p '{"devices": "*", "payload": {"yellow":"off"}}' -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/messages 470 | 471 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/events/ad698900-2546-11e3-87fb-c560cb0ca47b?token=123 472 | 473 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/subscribe -o 474 | 475 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/subscribe/ad698900-2546-11e3-87fb-c560cb0ca47b -o 476 | 477 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/ipaddress 478 | 479 | coap post -p "token=123&temperature=78" -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/data/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc 480 | 481 | coap get -H "meshblu_auth_uuid={:UUID}&meshblu_auth_token={:TOKEN}" coap://coap.octoblu.com/data/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc?token=123 482 | 483 | 484 | WEBSOCKET API 485 | ------------- 486 | 487 | Request and receive system status 488 | 489 | ```js 490 | socket.emit('status', function (data) { 491 | console.log(data); 492 | }); 493 | ``` 494 | 495 | Request and receive an array of devices matching a specific criteria 496 | 497 | ```js 498 | socket.emit('devices', {"key":"123"}, function (data) { 499 | console.log(data); 500 | }); 501 | ``` 502 | 503 | Request and receive information about a specific device 504 | 505 | ```js 506 | socket.emit('whoami', {"uuid":"ad698900-2546-11e3-87fb-c560cb0ca47b"}, function (data) { 507 | console.log(data); 508 | }); 509 | ``` 510 | 511 | Request and receive a device registration 512 | 513 | ```js 514 | socket.emit('register', {"key":"123"}, function (data) { 515 | console.log(data); 516 | }); 517 | ``` 518 | 519 | Request and receive a device update 520 | 521 | ```js 522 | socket.emit('update', {"uuid":"ad698900-2546-11e3-87fb-c560cb0ca47b", "token": "zh4p7as90pt1q0k98fzvwmc9rmjkyb9", "key":"777"}, function (data) { 523 | console.log(data); 524 | }); 525 | ``` 526 | 527 | Request and receive a device unregistration 528 | 529 | ```js 530 | socket.emit('unregister', {"uuid":"b5535950-29fd-11e3-9113-0bd381f0b5ef", "token": "2ls40jx80s9bpgb9w2g0vi2li72v5cdi"}, function (data) { 531 | console.log(data); 532 | }); 533 | ``` 534 | 535 | Store sensor data for a device uuid 536 | 537 | ```js 538 | socket.emit('data', {"uuid":"b5535950-29fd-11e3-9113-0bd381f0b5ef", "token": "2ls40jx80s9bpgb9w2g0vi2li72v5cdi", "temperature": 55}, function (data) { 539 | console.log(data); 540 | }); 541 | 542 | ``` 543 | 544 | Request and receive an array of sensor data matching a specific criteria 545 | 546 | ```js 547 | socket.emit('getdata', {"uuid":"b5535950-29fd-11e3-9113-0bd381f0b5ef", "token": "2ls40jx80s9bpgb9w2g0vi2li72v5cdi", "limit": 1}, function (data) { 548 | console.log(data); 549 | }); 550 | ``` 551 | 552 | 553 | Request and receive a message broadcast 554 | 555 | ```js 556 | // sending message to all devices 557 | socket.emit('message', {"devices": "all", "message": {"yellow":"on"}}); 558 | 559 | // sending message to a specific devices 560 | socket.emit('message', {"devices": "b5535950-29fd-11e3-9113-0bd381f0b5ef", "message": {"yellow":"on"}}); 561 | 562 | // sending message to an array of devices 563 | socket.emit('message', {"devices": ["b5535950-29fd-11e3-9113-0bd381f0b5ef", "ad698900-2546-11e3-87fb-c560cb0ca47b"], "message": {"yellow":"on"}}); 564 | ``` 565 | 566 | Websocket API commands include: status, register, unregister, update, whoami, devices, subscribe, unsubscribe, authenticate, and message. You can send a message to a specific UUID or an array of UUIDs or all nodes on SkyNet. 567 | 568 | MQTT API 569 | -------- 570 | 571 | Our MQTT API works similar to our WebSocket API. In fact, we have a [skynet-mqtt](https://www.npmjs.org/package/skynet-mqtt) NPM module for to simplify client-side MQTT connects with SkyNet. 572 | 573 | ```bash 574 | $ npm install skynet-mqtt 575 | ``` 576 | 577 | Here are a few examples: 578 | 579 | 580 | ```javascript 581 | var skynet = require('skynet-mqtt'); 582 | 583 | var conn = skynet.createConnection({ 584 | "uuid": "xxxxxxxxxxxx-My-UUID-xxxxxxxxxxxxxx", 585 | "token": "xxxxxxx-My-Token-xxxxxxxxx", 586 | "qos": 0, // MQTT Quality of Service (0=no confirmation, 1=confirmation, 2=N/A) 587 | "host": "localhost", // optional - defaults to meshblu.octoblu.com 588 | "port": 1883 // optional - defaults to 1883 589 | }); 590 | 591 | conn.on('ready', function(){ 592 | 593 | console.log('UUID AUTHENTICATED!'); 594 | 595 | //Listen for messages 596 | conn.on('message', function(message){ 597 | console.log('message received', message); 598 | }); 599 | 600 | 601 | // Send a message to another device 602 | conn.message({ 603 | "devices": "xxxxxxx-some-other-uuid-xxxxxxxxx", 604 | "payload": { 605 | "skynet":"online" 606 | } 607 | }); 608 | 609 | 610 | // Broadcast a message to any subscribers to your uuid 611 | conn.message({ 612 | "devices": "*", 613 | "payload": { 614 | "hello":"skynet" 615 | } 616 | }); 617 | 618 | 619 | // Subscribe to broadcasts from another device 620 | conn.subscribe('xxxxxxx-some-other-uuid-xxxxxxxxx'); 621 | 622 | 623 | // Log sensor data to skynet 624 | conn.data({temperature: 75, windspeed: 10}); 625 | 626 | }); 627 | 628 | ``` 629 | 630 | Event Codes 631 | ----------- 632 | 633 | If `log: true` in config.js, all transactions are logged to skynet.txt. Here are the event codes associated with Meshblu transactions. 634 | 635 | * 100 = Web socket connected 636 | * 101 = Web socket identification 637 | * 102 = Authenticate 638 | * 200 = System status API call 639 | * 201 = Get events 640 | * 204 = Subscribe 641 | * 205 = Unsubscribe 642 | * 300 = Incoming message 643 | * 301 = Incoming SMS message 644 | * 302 = Outgoing SMS message 645 | * 303 = Incoming Yo message 646 | * 304 = Outgoing Yo message 647 | * 305 = Outgoing Push Notification message 648 | * 400 = Register device 649 | * 401 = Update device 650 | * 402 = Delete device 651 | * 403 = Query devices 652 | * 500 = WhoAmI 653 | * 600 = Gateway Config API call 654 | * 700 = Write sensor data 655 | * 701 = Read sensor data 656 | 657 | FOLLOW US! 658 | ---------- 659 | 660 | * [Twitter/Octoblu](http://twitter.com/octoblu) 661 | * [Facebook/Octoblu](http://facebook.com/octoblu) 662 | * [Google Plus](https://plus.google.com/communities/106179367424841209509) 663 | 664 | 665 | LICENSE 666 | ------- 667 | 668 | (MIT License) 669 | 670 | Copyright (c) 2014 Octoblu 671 | 672 | Permission is hereby granted, free of charge, to any person obtaining 673 | a copy of this software and associated documentation files (the 674 | "Software"), to deal in the Software without restriction, including 675 | without limitation the rights to use, copy, modify, merge, publish, 676 | distribute, sublicense, and/or sell copies of the Software, and to 677 | permit persons to whom the Software is furnished to do so, subject to 678 | the following conditions: 679 | 680 | The above copyright notice and this permission notice shall be 681 | included in all copies or substantial portions of the Software. 682 | 683 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 684 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 685 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 686 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 687 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 688 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 689 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 690 | --------------------------------------------------------------------------------