├── .dockerignore ├── .gitignore ├── banner.png ├── Dockerfile ├── .babelrc ├── src ├── logger.js ├── client.js ├── broker.js ├── index.js └── app.js ├── .eslintrc.json ├── config.js ├── LICENSE ├── package.json ├── test └── integration.js └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .config.json 2 | dist 3 | node_modules 4 | *.log 5 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GridPlus/lattice-connect/HEAD/banner.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | WORKDIR /usr/src/app 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | EXPOSE 3000 7 | EXPOSE 1883 8 | CMD ["npm", "run", "start-docker"] -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": "current" 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | ["module-resolver", { 11 | "root": ["./src"] 12 | }], 13 | ["transform-object-rest-spread"] 14 | ] 15 | } -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const pino = require('pino'); 2 | const config = require('../config.js'); 3 | 4 | const opts = { 5 | level: config.LOG_LEVEL || 'error', 6 | }; 7 | const dest = config.LOG_DEST || pino.destination(1); 8 | const logger = pino(opts, dest); 9 | 10 | export default logger; 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 12, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const LOCAL_CONFIG_PATH = './.config.json'; 4 | 5 | module.exports = { 6 | APP_HOST: '0.0.0.0', 7 | APP_PORT: 3000, 8 | LOG_DEST: '/tmp/lattice-connector.log', 9 | LOG_LEVEL: 'error', // trace, debug, info, warn, error 10 | MQTT_CLIENT_ID: 'lattice-connector-endpoint', 11 | MQTT_USERNAME: 'connector', 12 | MQTT_PASSWORD: 'connectorpasswordpleasechangeme', 13 | MQTT_BROKER_PORT: 1883, 14 | TIMEOUT_ITER_MS: 500, 15 | TIMEOUT_TOTAL_MS: 60000, 16 | }; 17 | if (fs.existsSync(LOCAL_CONFIG_PATH)) { 18 | const local = require(LOCAL_CONFIG_PATH); 19 | Object.keys(local).forEach((key) => { 20 | module.exports[key] = local[key]; 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GridPlus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | // Create an internal MQTT client. This is used to communicate with the aedes broker in ./broker.js. 2 | // and is used by the REST API in `./app.js` to converts messages between MQTT and HTTP. 3 | import mqtt from 'mqtt'; 4 | import logger from './logger'; 5 | 6 | const config = require('../config.js'); 7 | 8 | const connectOptions = { 9 | clientId: config.MQTT_CLIENT_ID, 10 | username: config.MQTT_USERNAME, 11 | password: config.MQTT_PASSWORD, 12 | }; 13 | 14 | const brokerURI = `mqtt://${config.APP_HOST}:${config.MQTT_BROKER_PORT}`; 15 | const client = mqtt.connect(brokerURI, connectOptions); 16 | 17 | client.on('connect', () => { 18 | logger.debug(`Connected to MQTT Broker at ${brokerURI}`); 19 | }); 20 | 21 | client.on('error', (error) => { 22 | logger.error('Client failed to connect due to error:', error); 23 | }); 24 | 25 | client.on('close', () => { 26 | logger.trace(`MQTT broker connection closed for ${connectOptions.clientId}`); 27 | }); 28 | 29 | export const pubOptions = { 30 | qos: 1, 31 | retain: false, 32 | dup: false, 33 | }; 34 | 35 | export const subObtions = { 36 | qos: 1, 37 | }; 38 | 39 | export default client; 40 | -------------------------------------------------------------------------------- /src/broker.js: -------------------------------------------------------------------------------- 1 | // Create an aedes MQTT broker as part of this process. Lattices in the field should 2 | // connect to this broker. 3 | import logger from './logger'; 4 | 5 | const aedes = require('aedes'); 6 | const net = require('net'); 7 | 8 | const instance = aedes(); 9 | let connCount = 0; 10 | 11 | instance.on('client', (client) => { 12 | logger.debug(`BROKER (conns=${connCount}): New client (${client.id}) attempting connection.`); 13 | }); 14 | 15 | instance.on('clientReady', (client) => { 16 | connCount += 1; 17 | logger.info(`BROKER (conns=${connCount}): Client (${client.id}) connected.`); 18 | }); 19 | 20 | instance.on('clientDisconnect', (client) => { 21 | connCount -= 1; 22 | logger.info(`BROKER (conns=${connCount}): Client (${client.id}) disconnected.`); 23 | }); 24 | 25 | instance.on('clientError', (client, error) => { 26 | logger.error(`BROKER (conns=${connCount}): Error from client ${client.id}: ${error.message}`); 27 | }); 28 | 29 | instance.on('subscribe', (_subscriptions, client) => { 30 | logger.debug(`BROKER (conns=${connCount}): Client (${client.id}) subscribed to topics: ${JSON.stringify(_subscriptions)}`); 31 | }); 32 | 33 | instance.on('publish', (_packet, client) => { 34 | logger.trace(`BROKER (conns=${connCount}): Client (${client}) published message: ${JSON.stringify(_packet)}`); 35 | }); 36 | 37 | const broker = net.createServer(instance.handle); 38 | export default broker; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lattice-connect", 3 | "version": "0.2.2", 4 | "description": "A small HTTP server + MQTT broker designed to bridge the web with Lattices in the field", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "babel src -d dist", 8 | "lint": "eslint src", 9 | "start": "npm run build && npx pm2 start dist/index.js --name lattice-connect --watch", 10 | "stop": "npx pm2 stop lattice-connect", 11 | "rm": "npx pm2 delete lattice-connect && pkill node", 12 | "logs": "npx pm2 logs lattice-connect", 13 | "test": "mocha --timeout 180000 test/integration.js", 14 | "docker-build": "npm run build && docker build -t lattice-connect:1.0 .", 15 | "docker-run": "docker run -d --name lattice-connect -p 3000:3000 -p 1883:1883 lattice-connect:1.0" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/GridPlus/lattice-connect.git" 20 | }, 21 | "keywords": [ 22 | "Ethereum", 23 | "Bitcoin", 24 | "crypto", 25 | "GridPlus", 26 | "Lattice" 27 | ], 28 | "author": "Alex Miller", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/GridPlus/lattice-connect/issues" 32 | }, 33 | "homepage": "https://github.com/GridPlus/lattice-connect#readme", 34 | "dependencies": { 35 | "aedes": "^0.42.6", 36 | "babel-preset-env": "^1.7.0", 37 | "body-parser": "^1.18.3", 38 | "cors": "^2.8.5", 39 | "express": "^4.16.4", 40 | "mqtt": "^2.18.8", 41 | "pino": "^6.7.0" 42 | }, 43 | "devDependencies": { 44 | "babel-cli": "^6.26.0", 45 | "babel-plugin-module-resolver": "^4.0.0", 46 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 47 | "chai": "^4.2.0", 48 | "eslint": "^7.10.0", 49 | "eslint-config-airbnb-base": "^14.2.0", 50 | "eslint-plugin-import": "^2.22.1", 51 | "gridplus-sdk": "^0.6.1", 52 | "mocha": "^8.1.3", 53 | "pm2": "^4.5.2", 54 | "readline-sync": "^1.4.10" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Lattice messaging service. This service contains three components: 2 | // 1. REST HTTP API (./app.js) - allows external applications to make requests to a target Lattice, 3 | // which typically sits behind a customer's home WiFi firewall. 4 | // 2. MQTT client (./client.js) - an internal MQTT client that is used by the REST API, which cannot 5 | // itself communicate directly with Lattices. This MQTT client can 6 | // take requests and put them into the MQTT pipeline where they will 7 | // reach the Lattice. 8 | // 2. MQTT Broker (./broker.js) - an aedes server which handles connections of both Lattices that 9 | // subscribe to this service and the inernal MQTT client in 10 | // `./client.js`. This broker is the crux of this servce, as it 11 | // allows Lattices to accept external requests from the internet 12 | // using the MQTT pub/sub architecture. 13 | import util from 'util'; 14 | import app from './app'; 15 | import broker from './broker'; 16 | import logger from './logger'; 17 | 18 | const config = require('../config.js'); 19 | const packageJson = require('../package.json'); 20 | 21 | logger.info(`${packageJson.name} version ${packageJson.version} starting`); 22 | 23 | // Error handlers 24 | //----------------------------------- 25 | process.on('uncaughtException', (err) => { 26 | broker.close(() => { logger.info('Closed broker') }); 27 | logger.error('uncaughtException: ', err); 28 | logger.error(err.stack); 29 | throw err; 30 | }); 31 | 32 | process.on('unhandledRejection', (reason, promise) => { 33 | logger.error(`Unhandled Rejection at: ${util.inspect(promise)} reason: ${reason}`); 34 | logger.error(reason); 35 | throw new Error(`Unhandled Rejection at: ${util.inspect(promise)} reason: ${reason}`); 36 | }); 37 | 38 | // 1. Create the MQTT broker (server) 39 | //---------------------------------- 40 | function startBroker() { 41 | logger.info('Starting broker', broker) 42 | broker.listen(config.MQTT_BROKER_PORT, () => { 43 | logger.info('MQTT broker server started on port ', config.MQTT_BROKER_PORT); 44 | }); 45 | } 46 | 47 | if (broker.closed === false) { 48 | broker.close(() => { 49 | logger.info('closed?'); 50 | startBroker(); 51 | }); 52 | } else { 53 | logger.info('Starting now'); 54 | startBroker(); 55 | } 56 | 57 | // 2. Create the REST server 58 | //---------------------------------- 59 | logger.info('app', app); 60 | app.listen(config.APP_PORT, config.APP_HOST, () => { 61 | logger.info(`signing-api-proxy started listening on ${config.APP_PORT}`); 62 | }); 63 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | // Test using this service and the gridplus-sdk to connect to a target Lattice device. 2 | // The Lattice must be configured to point to this host. The easiest way to do so is to run this on a machine 3 | // that shares a LAN with the Lattice device. On the Lattice GCE, you can update your `gridplus.remote_mqtt_address` 4 | // to point to the `en0` address of this machine (`ifconfig en0`). 5 | // 6 | // To change your config on your Lattice (must be a dev lattice), SSH into your GCE and run: 7 | // service gpd stop && service mosquitto stop 8 | // uci set gridplus.remote_mqtt_address=: 9 | // uci commit 10 | // service mosquitto start && service gpd start 11 | const crypto = require('crypto') 12 | const expect = require('chai').expect 13 | const ps = require('child_process') 14 | const SDK = require('gridplus-sdk').Client; 15 | const question = require('readline-sync').question 16 | const config = require('../config.js'); 17 | // Hardcoded key to use with the SDK so that we only have to pair with the Lattice once. 18 | const TEST_KEY = Buffer.from('93b9cacfa4e417bf8513ad8dbbb0bb35d48c4c154959663a9f25cf6508e85f90', 'hex'); 19 | // Global service process 20 | let servicePs = null; 21 | // Global SDK client 22 | let sdkClient = null; 23 | 24 | describe('\x1b[44mIntegration test with gridplus-sdk\x1b[0m', () => { 25 | before(() => { 26 | setupTest() 27 | }) 28 | 29 | it('Should connect to a Lattice', async () => { 30 | const id = question('Please enter the ID of your Lattice: ') 31 | let err = await Promisify(sdkClient, 'connect', id, false) 32 | expect(err).to.equal(null, `Failed to find Lattice: ${err.toString()}`) 33 | // If we are not paired, we need to do that now 34 | if (sdkClient.hasActiveWallet() === false) { 35 | const secret = question('Please enter the pairing secret: '); 36 | err = await Promisify(sdkClient, 'pair', secret, false) 37 | expect(err).to.equal(null, `Failed to pair: ${err.toString()}`) 38 | } 39 | }) 40 | 41 | it('Should get an Ethereum address from the Lattice', async () => { 42 | const opts = { 43 | currency: 'ETH', 44 | startPath: [0x8000002c, 0x8000003c, 0x80000000, 0, 0], // m/44'/60'/0'/0/0 45 | n: 1 46 | } 47 | const err = await Promisify(sdkClient, 'getAddresses', opts) 48 | expect(err).to.equal(null, `Failed to fetch ETH address: ${err.toString()}`) 49 | }) 50 | 51 | it('Should kill service and exit', () => { 52 | kill() 53 | expect(true).to.equal(true) 54 | }) 55 | }) 56 | 57 | async function Promisify(c, f, opts, _reject=true) { 58 | return new Promise((resolve, reject) => { 59 | if (_reject) { 60 | c[f](opts, (err, res) => { 61 | if (err) return reject(err) 62 | else return resolve(res) 63 | }) 64 | } else { 65 | c[f](opts, (err) => resolve(err)) 66 | } 67 | }) 68 | } 69 | 70 | function setupTest() { 71 | // Build the source files 72 | prettyPrint('Building module...', 'yellow') 73 | try { 74 | ps.execSync('npm run build') 75 | } catch (err) { 76 | prettyPrint(`Failed to build module: ${err.toString()}`, 'red', console.error) 77 | process.exit() 78 | } 79 | // Create the process 80 | prettyPrint('Spawning service...', 'yellow') 81 | spawnService(); 82 | // Initialize the SDK 83 | try { 84 | sdkClient = new SDK({ 85 | baseUrl: `http://localhost:${config.APP_PORT}`, 86 | crypto, 87 | name: 'lattice-connector-test', 88 | privKey: TEST_KEY, 89 | }) 90 | } catch (err) { 91 | prettyPrint(`Failed to create SDK client: ${err.toString()}`, 'red', console.error) 92 | kill() 93 | } 94 | prettyPrint('Finished setting up.\n', 'green') 95 | } 96 | 97 | function spawnService() { 98 | servicePs = ps.spawn('node', [`${__dirname}/../dist/index.js`]) 99 | servicePs.on('error', (err) => { 100 | prettyPrint(`Spawned process encountered error: ${err}`, 'red', console.error) 101 | kill() 102 | }) 103 | } 104 | 105 | function kill() { 106 | try { 107 | servicePs.kill('SIGINT') 108 | } catch (err) { 109 | prettyPrint(`Failed to kill service: ${err.toString()}`, 'red', console.error) 110 | } 111 | } 112 | 113 | function prettyPrint(text, color=null, f=console.log) { 114 | let prefix = ''; 115 | switch (color) { 116 | case 'green': 117 | prefix = '\x1b[32m' 118 | break 119 | case 'red': 120 | prefix = '\x1b[31m' 121 | break 122 | case 'yellow': 123 | prefix = '\x1b[33m' 124 | break 125 | } 126 | const term = '\x1b[0m' 127 | f(prefix + text + term) 128 | } 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import crypto from 'crypto'; 3 | import express from 'express'; 4 | import client from './client'; 5 | import logger from './logger'; 6 | 7 | const cors = require('cors'); 8 | const config = require('../config.js'); 9 | 10 | // Create an in-memory cache of responses. These will get cleared once processed. 11 | const responseCache = {}; 12 | 13 | // Start REST server 14 | const app = express(); 15 | app.use(bodyParser.json()); 16 | app.use(cors()); 17 | 18 | //--------------------- 19 | // HELPERS 20 | //--------------------- 21 | 22 | // Parse the request body for a payload 23 | function getPayload(body) { 24 | if (body.data) { 25 | if (body.data.data) { 26 | return Buffer.from(body.data.data); 27 | } 28 | return Buffer.from(body.data); 29 | } 30 | return Buffer.from(body); 31 | } 32 | 33 | // Get the index of data in the internal cache given a Lattice and request id 34 | function getCacheID(latticeId, requestId) { 35 | return `${latticeId}_${requestId}`; 36 | } 37 | 38 | // Subscribe to an internal response topic while we wait for the target Lattice to fill the request. 39 | function subscribeToResponse(responseTopic) { 40 | client.subscribe(responseTopic, client.subOptions, (err, granted) => { 41 | if (err) { 42 | logger.error(`Unable to subscribe to internal topic ${responseTopic}: ${err.toString()}`); 43 | throw new Error(err); 44 | } 45 | if (granted) { 46 | logger.debug(`Subscribed to topic ${responseTopic}`); 47 | } else { 48 | logger.debug(`Failed to subscribe to internal topic ${responseTopic}`); 49 | } 50 | }); 51 | } 52 | 53 | // Unsubscribe from internal response topic when we time out or get a response from the 54 | // target Lattice. 55 | function unsubscribeFromResponse(responseTopic) { 56 | client.unsubscribe(responseTopic, (err) => { 57 | if (err) { 58 | logger.error(`Unable to unsubscribe from topic ${responseTopic}: ${err.toString()}`); 59 | throw new Error(err); 60 | } else { 61 | logger.debug(`Unsubscribed from interal topic ${responseTopic}`); 62 | } 63 | }); 64 | } 65 | 66 | // Subscribe to MQTT broker and await response from Lattice (or timeout) given 67 | // a request ID. 68 | function listenForResponse(res, serial, requestId) { 69 | const cacheID = getCacheID(serial, requestId); 70 | const responseTopic = `from_agent/${serial}/response/${requestId}`; 71 | try { 72 | subscribeToResponse(responseTopic); 73 | } catch (err) { 74 | res.send({ status: 500, message: 'Unable to subscribe to mqtt response topic' }); 75 | return; 76 | } 77 | 78 | // Set a timer to wait for a response to the message sent to the agent 79 | // Activates a subscription during this time that will forward the return message back as 80 | // a response to the original request 81 | const totalTime = config.TIMEOUT_TOTAL_MS; 82 | const iteration = config.TIMEOUT_ITER_MS; 83 | let elapsed = 0; 84 | 85 | const interval = setInterval(() => { 86 | if (responseCache[cacheID] !== undefined) { 87 | // If the response has been recorded in our in-memory cache, we can unsubscribe 88 | // and respond back to the original requester. 89 | let toReturn; 90 | try { 91 | toReturn = responseCache[cacheID].toString('hex'); 92 | res.send({ status: 200, message: toReturn }); 93 | logger.debug(`Successfully responded to request for agent: ${serial} request: ${requestId}`); 94 | } catch (err) { 95 | logger.error(`Could not parse response from agent: ${serial} requestId: ${requestId}, error: ${err}`); 96 | res.send({ status: 500, message: 'Could not parse response from agent' }); 97 | } 98 | // Clear this item from the cache and unsubscribe from the topic. 99 | responseCache[cacheID] = undefined; 100 | unsubscribeFromResponse(responseTopic); 101 | clearInterval(interval); 102 | } else { 103 | // If there is still no response, record the time and return timeout if we have reached 104 | // the timeout threshold. 105 | elapsed += iteration; 106 | if (elapsed >= totalTime) { 107 | res.send({ status: 500, message: `lattice-connector-endpoint timed out after waiting ${Math.ceil(totalTime / 1000)}s` }); 108 | // Clear this item from the cache and unsubscribe from the topic. 109 | responseCache[cacheID] = undefined; 110 | unsubscribeFromResponse(responseTopic); 111 | clearInterval(interval); 112 | } 113 | } 114 | }, iteration); 115 | } 116 | 117 | //--------------------- 118 | // INTERNAL MQTT CLIENT 119 | //--------------------- 120 | 121 | // Create a message handler for our internal MQTT client. This client only subscribes to ephemeral 122 | // request response topics as it waits for the target Lattice(s) to fill requests 123 | client.on('message', (topic, payload) => { 124 | try { 125 | const latticeId = topic.split('/')[1]; 126 | const requestId = topic.split('/')[3]; 127 | responseCache[getCacheID(latticeId, requestId)] = payload; 128 | logger.debug(`Added to internal responseCache (topic=${topic}, latticeId=${latticeId}, requestId=${requestId}): ${payload}`); 129 | } catch (err) { 130 | logger.error(`Failed to add response to internal cache (topic=${topic}): ${err.toString()}`); 131 | } 132 | }); 133 | 134 | //--------------------- 135 | // API 136 | //--------------------- 137 | 138 | // Pass a request body to the device with :latticeId identifier 139 | // @param [latticeId] - Device identifier of Lattice. This should be known to the end user. 140 | // @param [req.body] - Data to be sent to the Lattice. Must be Array-like. May be of form 141 | // [array] or {data: [array]}. 142 | app.post('/:latticeId', (req, res) => { 143 | try { 144 | const payload = getPayload(req.body); 145 | const { latticeId } = req.params; 146 | const requestId = crypto.randomBytes(4).toString('hex'); 147 | const requestTopic = `to_agent/${latticeId}/request/${requestId}`; 148 | client.publish(requestTopic, payload, client.pubOpts, (err) => { 149 | if (err) { 150 | logger.error(`Unable to publish message for ${latticeId}, mqtt possibly disconnecting`); 151 | res.send({ status: 500, message: 'Failed to send message to Lattice' }); 152 | } else { 153 | logger.debug(`published message connect to ${requestTopic}`); 154 | listenForResponse(res, latticeId, requestId); 155 | } 156 | }); 157 | } catch (err) { 158 | res.send({ status: 500, message: err.toString() }); 159 | } 160 | }); 161 | 162 | export default app; 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ⚠️ This has been deprecated. Please use: [lattice-connect-v2](https://github.com/GridPlus/lattice-connect-v2). ⚠️ 4 | 5 | # 👋 Introduction 6 | By default, communication with a [Lattice1](https://gridplus.io/lattice) is routed through GridPlus' centralized cloud infrastructure. Although there is great care that goes into encrypting and securing these communication channels, we at [GridPlus](https://gridplus.io) want your Lattice1 to be 100% yours, so we want to offer `lattice-connect` as an alternative to centralized message routing. 7 | 8 | **If you are an advanced user, you can deploy this module yourself and change your Lattice's config to hook into your own deployed instance.** 9 | 10 | ## 🔗 Related Links 11 | - [📢 Discord](https://twitter.com/gridplus) 12 | - [🐤 Twitter](https://discord.gg/Bt5fVDTJb9) 13 | - [📚 Knowledge Base](https://docs.gridplus.io) 14 |   15 | 16 | # ⌛️ Setup Guide 17 | 18 | ##### Estimated Time (TOTAL): 30–45 minutes 19 | 20 | ##### Overview of steps are: 21 | 22 | 1. ▶️ Installing & Running `lattice-connect`; and, 23 | 2. ☁️ _(OPTIONAL)_ Deploying to the Cloud; and, 24 | 3. 🔌 Configuring your Lattice1 to connect; and, 25 | 4. 🥽 Testing the connection 26 | 27 | ## ▶️ Installing & Running 28 | 29 | ##### Estimated Time: 10 minutes 30 | 31 | This section describes installing the `lattice-connector`, which is a small HTTP server + MQTT broker designed to communicate with Lattice1 hardware wallets over the web. 32 | 33 | 34 | It's possible to run the server: 35 | 36 | - as **a process directly** on a host system (using `node v12`); or, 37 | - through a **Docker** container. 38 | 39 |
40 | 41 | #### 🖥 Start the server with: NPM & PM2 42 | 43 | You can start server with the following steps: 44 | 45 | 1. Clone the repo via `git clone`; 46 | 2. install dependencies via `npm ci`; 47 | 3. start the daemon with `npm run start`. 48 | 49 | Starting the server creates a [pm2](https://pm2.io/) process which will watch for crashes. For more information on pm2, see the [pm2 docs](https://pm2.io/docs/plus/overview/). 50 | 51 | ##### Example: 52 | 53 | ```sh 54 | # Clone the repo 55 | $ git clone https://github.com/GridPlus/lattice-connect.git 56 | $ cd lattice-connect 57 | 58 | # Assumes 'node 12' 59 | $ npm ci && npm run start 60 | 61 | > lattice-connect@0.1.1 start 62 | > npx pm2 start dist/index.js --name lattice-connect --watch 63 | 64 | [PM2] Applying action restartProcessId on app [lattice-connect](ids: [ 0 ]) 65 | [PM2] [lattice-connect](0) ✓ 66 | [PM2] Process successfully started 67 | ⇆ PM2+ activated | Instance Name: laptop.local-5d42 68 | ┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐ 69 | │ id │ name │ mode │ ↺ │ status │ cpu │ memory │ 70 | ├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤ 71 | │ 0 │ lattice-connect │ fork │ 9 │ online │ 0% │ 17.7mb │ 72 | └────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘ 73 | ``` 74 | 75 | #### Stopping the server: 76 | ```sh 77 | $ npm run stop 78 | ``` 79 | 80 | #### Killing the process and remove it from `pm2`: 81 | ```sh 82 | $ npm run rm 83 | ``` 84 | 85 | #### Logging and monitoring crash reports from `pm2`: 86 | ```sh 87 | $ npm run logs 88 | ``` 89 | 90 | > _NOTE: this won't be useful when debugging MQTT connections or other issues internal to the application itself. For that, inspect the logs written to `LOG_DEST` (in `./config.js`). You may also change `LOG_LEVEL` to a lower level for debugging (`trace` will produce the most logs)._ 91 | 92 | To watch logs in real time, start your `pm2` process and run 93 | 94 | ```sh 95 | tail -f 96 | ``` 97 | 98 | #### 📦 Using Docker 99 | 100 | 1. Clone the repo via `git clone`; 101 | 2. build the container via `npm run docker-build`; 102 | 3. start the container with `npm run docker-run`. 103 | 104 | ##### Example: 105 | 106 | ```sh 107 | # Clone the repo 108 | $ git clone https://github.com/GridPlus/lattice-connect.git 109 | $ cd lattice-connect 110 | 111 | # Build the container and start it 112 | $ npm run docker-build && npm run docker-run 113 | 114 | > lattice-connect@0.1.1 docker-run 115 | > docker run -d --name lattice-connect -p 3000:3000 -p 1883:1883 lattice-connect:1.0 116 | 117 | 94915b49dd5cd38242bc0cf8d086b2be4c772687bd707a9b331deca18112854f 118 | ``` 119 | 120 | #### Stopping the container: 121 | ```sh 122 | $ docker stop lattice-connect 123 | ``` 124 | 125 | #### Rebuild source code changes: 126 | ```sh 127 | $ docker stop lattice-connect && \ 128 | docker rm lattice-connect && \ 129 | npm run docker-build 130 | ``` 131 | 132 | #### Logging output from the container 133 | ```sh 134 | $ docker logs -f lattice-container 135 | ``` 136 | 137 | #### 🔍 Configuration 138 | 139 | The config parameters set in `config.js` are referenced when starting the application. These come with defaults and 140 | also look at a local file `.config.json` (if it exists). This local `.config.json` is not tracked in git and it is 141 | where you can define params that are inspected in `config.js`. Any param defined in both `config.js` and `.config.json` will be cast to the valuein `.config.json`. 142 | 143 | ##### Sample `.config.json`: 144 | 145 | ``` 146 | { 147 | "LOG_DEST": "./lattice.log", 148 | "MQTT_PASSWORD": "superdupersecretpassword" 149 | } 150 | ``` 151 | 152 | ## ☁️ Deploying to the Cloud 153 | 154 | ##### Estimated Time: varies 155 | 156 | Either install method described in [Installing & Running](#%EF%B8%8F-installing--running) should work on most cloud hosting providers. **Important:** do be sure to check that your provider allows opening the following ports: 157 | 158 | - HTTP :80 (or, HTTPS :443); 159 | - MQTT :1883 (or, MQTTS :8883). 160 | 161 | ##### Example: AWS EC2 162 | 163 | On AWS, do the following to prepare your AWS instance: 164 | 165 | * **install node.js and npm**: on Ubuntu, you can do this with `sudo apt-get update && sudo apt-get install node.js npm`; 166 | * **update security group firewall settings**: Do this by going to the `Security` tab when you have your EC2 instance selected on the AWS console. Make sure the ports listed in `config.js` are open to `0.0.0.0/0` (by default, these are 3000 for the web server and 1883 for the MQTT broker). They are both `TCP` connections; 167 | * **go to** the [Start the server with: NPM & PM2](#-start-the-server-with-npm--pm2). 168 | 169 | ## 🔌 Configuring your Lattice1 170 | 171 | ##### Estimated Time: 15 minutes 172 | 173 | This section describes how to modify settings on your Lattice1 so it's able to communicate with your `lattice-connect` deployed instance. 174 | 175 | ##### Overview of steps are: 176 | 177 | 1. SSH into your Lattice1; and, 178 | a. stop `gpd` & `mosquitto` via `service stop`; and, 179 | b. review `gpd` settings via `uci show gridplus`; and, 180 | 2. modify settings for the `gpd` & `mosquitto` services: 181 | a. change the MQTT endpoint used in `gpd`; and, 182 | b. _(OPTIONAL)_ disable SSL checks in `mosquitto` (MQTT); and. 183 | 3. restart `gpd` and `mosquitto` services to apply changes. 184 | 185 | #### 1️⃣ SSH into your device 186 | 187 | In order to point your Lattice1 at your own deployed instance of this module, you'll need to SSH into the device and change its configurations manually. 188 | 189 | Your Lattice1's UI displays the necessary `SSH Host` and `SSH Password` parameters. Navigate to them by tapping `Settings -> Advanced -> Device Info`. 190 | 191 | From a remote terminal session, proceed by: 192 | 193 | 1. SSH'ing using `ssh root@.local`; 194 | 2. entering the `SSH Password` displayed on the Lattice1 when prompted. 195 | 196 | ##### Example 197 | ```sh 198 | # SSH command 199 | $ root@.local 200 | 201 | # Input SSH password 202 | root@.local's password: 203 | 204 | 205 | BusyBox v1.28.3 () built-in shell (ash) 206 | 207 | __ __ __ _ ___ 208 | / / ____ _/ /_/ /_(_)_______ < / 209 | / / / __ `/ __/ __/ / ___/ _ \/ / 210 | / /___/ /_/ / /_/ /_/ / /__/ __/ / 211 | /_____/\__,_/\__/\__/_/\___/\___/_/ 212 | ----------------------------------------------------- 213 | Ω-ware: 0.3.2 b228 214 | Gridplus GCE Version: 0.48.12 215 | ----------------------------------------------------- 216 | root@:~# 217 | 218 | ``` 219 | 220 | ##### 1️⃣🅰️ Stopping `gpd` & `mosquitto` services 221 | 222 | Stop these two services in preparation to make your changes: 223 | 224 | ```bash 225 | # Stop `gdp` 226 | $ root@: service gpd stop 227 | 228 | # Stop `mosquitto` 229 | $ root@: service mosquitto stop 230 | ``` 231 | 232 | 233 | ##### 1️⃣🅱️ Review `gpd` settings 234 | 235 | The `gpd` stands for _GridPlus Daemon_ and it has several important functions, among them is connecting to the MQTT pub/sub. 236 | 237 | To view the default settings, run `uci show gridplus`: 238 | 239 | ```bash 240 | $ root@: uci show gridplus 241 | ``` 242 | 243 | > _NOTE: Consider writing down the original values if you want to reset back to GridPlus' default infrastructure._ 244 | 245 | ##### Example: 246 | 247 | ```bash 248 | # Show default 'gpd' configuration 249 | $ root@: uci show gridplus 250 | 251 | # List of settings 252 | gridplus.env=production 253 | gridplus.gpdLogFile=/gpd/gpd.log 254 | gridplus.gceVersion=0.48.12 255 | gridplus.remote_mqtt_address=rabbitmq.gridpl.us:8883 256 | gridplus.releaseCatalogURL=https://release-catalog-api.gridpl.us/update 257 | gridplus.releaseCatalogUser=lattice1 258 | gridplus.releaseCatalogPass= 259 | gridplus.ftla=false 260 | gridplus.personalizationEnabled=true 261 | gridplus.gpdLogLevel=FATAL 262 | gridplus.provisionLatticeAPIURL=https://provision-lattice-api.gridpl.us/provision 263 | gridplus.personalized=true 264 | gridplus.rootPass= 265 | gridplus.deviceID= 266 | gridplus.rabbitmq_password= 267 | ``` 268 | 269 | #### 2️⃣ Modify Settings 270 | You should see a line like the following: 271 | 272 | ```bash 273 | gridplus.remote_mqtt_address=rabbitmq.gridpl.us:8883 274 | ``` 275 | 276 | ##### 2️⃣🅰️ Changing `gpd` settings 277 | 278 | To change this value, use: 279 | 280 | - `uci set gridplus.remote_mqtt_address=[host]:[BROKER_PORT]` 281 | - `uci commit`. 282 | 283 | ##### Example 284 | 285 | ```sh 286 | # Stop 'gpd' & 'mosquitto' 287 | $ root@: service gpd stop 288 | $ root@: service mosquitto stop 289 | 290 | # Point the MQTT connection to the relevant address ('1883' for non-SSL; see next section) 291 | $ root@: uci set gridplus.remote_mqtt_address=10.0.0.1:1883 292 | 293 | # Apply the change 294 | $ root@: uci commit 295 | ``` 296 | 297 | ##### 2️⃣🅱️ (OPTIONAL) Disable SSL checks in `mosquitto` 298 | 299 | If you want to use an insecure connection (i.e. connect to a local IP address, or the default AWS instance host rather than your own secure domain), then: 300 | 301 | - connect to MQTT over port `1883`; and, 302 | - add `bridge_insecure` to the `/etc/init.d/mosquitto`; and, 303 | 304 | See the `man` page for `mosquitto` to review [the full list of configuration options](https://mosquitto.org/man/mosquitto-conf-5.html). 305 | 306 |
307 | 308 | ⚠️ **WARNING**: It's important to consider the risks that disabling SSL has, in that messages you send across the connection will no longer be encrypted. **Proceed cautiously**⚠️ 309 | 310 |
311 | 312 | Open the configuration file using `vim`: 313 | 314 | ```sh 315 | # Open file 316 | $ root@: vim /etc/init.d/mosquitto 317 | ``` 318 | 319 | Once in `vim`: 320 | 321 | - Navigate the cursor **LINE 31**; 322 | - enter `INSERT` mode by pressing the `i` key; 323 | - **insert** a `#` to comment out `bridge_capath /etc/ssl/certs`; 324 | - **insert** `bridge_insecure true` on the next line. 325 | 326 | ##### Example 327 | 328 | ```vim 329 | // BEFORE 330 | 28 ... 331 | 29 connection ${DEVICE_ID} 332 | 30 address ${REMOTE_MQTT_ADDRESS} 333 | 31 bridge_capath /etc/ssl/certs/ 334 | 32 remote_username ${DEVICE_ID} 335 | 33 ... 336 | 337 | // AFTER 338 | 28 ... 339 | 29 address ${REMOTE_MQTT_ADDRESS} 340 | 30 # bridge_capath /etc/ssl/certs/ 341 | 31 bridge_insecure true 342 | 32 remote_username ${DEVICE_ID} 343 | 33 ... 344 | 345 | // Vim command to write file & quit: 346 | // ESC + :x 347 | ``` 348 | 349 | ##### 🔁 Restart services 350 | 351 | After writing the file and closing `vim`: 352 | - restart service via `service mosquitto start`; and, 353 | - restart service via `service gpd start`. 354 | 355 | ##### Example: 356 | 357 | ```sh 358 | # After quitting 'vim' 359 | $ root@: service mosquitto start 360 | $ root@: service gpd start 361 | ``` 362 | 363 | ## 🥽 Testing the connection 364 | 365 | ##### Estimated Time: 5-15 minutes 366 | 367 | This section descbribes how you can test the connection between your Lattice1 and the `lattice-connect` module. 368 | 369 | It's possible to test the connection in multiple ways: 370 | 371 | 1. send a `POST` using `wget` from the Lattice1 to the server; or, 372 | 2. log into the cloud-hosted version of the [Lattice Manager](https://lattice.gridplus.io); or, 373 | 3. connect (or re-connect) your Lattice1 to MetaMask. 374 | 375 |
376 | 377 | #### 💻 Using `wget` from the Lattice1 (and SSH) 378 | 379 | While connected to a remote SSH terminal session: 380 | 381 | 1. get the `deviceID`; and, 382 | 2. use `wget` to send a HTTP `POST` request to the server; and, 383 | 3. if you had accounts connected to MetaMask, you may need to re-pair your device. 384 | 385 | ##### Example 386 | ```sh 387 | # Read the 'deviceID'; 388 | # Your Lattice1 also displays 'Device ID' under 'Settings' 389 | $ root@: uci show gridplus.deviceID 390 | gridplus.deviceID=abc123 391 | 392 | # Send the HTTP 'POST' request 393 | $ root@: wget -O- --post-data='[1,2,3]' \ 394 | --header='Content-Type:application/json' \ 395 | 'http://10.0.0.1:3000/abc123' 396 | 397 | Connecting to 10.0.0.1:3000... connected. 398 | HTTP request sent, awaiting response... 200 OK 399 | Length: 151 [application/json] 400 | Saving to: 'STDOUT' 401 | 402 | ... 403 | 2022-01-01 00:00:00 (1.96 MB/s) - written to stdout [151/151] 404 | ``` 405 | 406 | ##### Testing using a fake `deviceID` 407 | 408 | You can replace a known `deviceID` with a fake one and it will still work for testing purposes, so long as the requst hangs (i. e., you should not immediately get a `Connection refused` error). If you do get `Connection refused` it means your process isn't running on the expected port. 409 | 410 | #### 🌐 Log into the _Lattice Manager_ 411 | 412 |
413 | 414 | ⚠️ **WARNING**: If you're using an insecure connection (see [this earlier section](#2%EF%B8%8F⃣🅱%EF%B8%8F-optional-disable-ssl-checks-in-mosquitto)) you'll need to configure your browser to allow loading insecure content. 415 | 416 | [This article](https://experienceleague.adobe.com/docs/target/using/experiences/vec/troubleshoot-composer/mixed-content.html?lang=en#task_5448763B8DC941FD80F84041AEF0A14D) can guide you through disabling insecure content checks on most browsers. **Proceed cautiously**⚠️ 417 | 418 |
419 | 420 | From an Internet browser, navigate to the _Lattice Manager_: 421 | 422 | 1. go to [https://lattice.gridplus.io](https://lattice.gridplus.io); and, 423 | 2. at the bottom of the page, click _Settings_; and, 424 | 3. in the section titled: _Connection Endpoint:_ 425 | - for HTTPS; `https://[host | ip_address]:[HTTP_PORT]`; or, 426 | - for HTTP; `http://[host | ip_address]:[HTTP_PORT]`; 427 | 4. click _Update and Reload_, 428 | 429 | ##### Example: 430 | ```sh 431 | # Connection Endpoint: 432 | http://10.0.0.1:3000 433 | ``` 434 | 435 | You are now ready to log in: 436 | 437 | 1. enter your `deviceID`; and, 438 | 2. enter your `password`. 439 | 440 | You may be asked to pair (aka, give permssions) to your Lattice1 before completing the login process. 441 | 442 | ##### Forcing your Lattice1 to pair with the _Lattice Manager_ 443 | 444 | If your device was previously paired with the _Lattice Manager_, and you with to re-pair it, then unlock your Lattice1 and tap `Permissions -> Lattice Manager -> Delete`. 445 | 446 | Log out of the _Lattice Manager_ (if necessary), and then follow the steps above to log back in. You will be prompted to re-pair your device during the login process. 447 | 448 | #### 🦊 MetaMask Pairing 449 | 450 | Similarly to the _Lattice Manager_, you may be required to re-pair your device with _MetaMask_. Re-connecting your Lattice1 using _"Connect Hardware Wallet"_ within _MetaMask_ should be your first step. 451 | 452 | It may be that deleting _MetaMask_ from `Permissions` (on your device) is required. Fixing pairing issues is simple, and is how messages get routed from clients to devices. 453 | 454 | For the most comprehensive guide to connecting to the _MetaMask_ extension, review the [Knowledge Base](https://docs.gridplus.io/setup/metamask) article. 455 |   456 | 457 | # 🏛 Appendencies 458 | 459 | 1. Troubleshooting 460 | 2. Integration Tests 461 | 3. Web API 462 | 4. Background 463 | 464 | ## Appendix I: 🛠 Troubleshooting 465 | 466 | Testing may reveal things simply aren't working; please re-read the above documentation be certain you've done everything as described for your situation. 467 | 468 | If you're sure everything was setup properly, consider if: 469 | 470 | - the module's ports (see `config.js`) are set correctly wherever they are changed; or, 471 | - rebuilding the **Docker** container is needed to sync any source file changes; or, 472 | - your Lattice1 is connected to the Internet, and update _WiFi_, if need be. 473 | 474 | #### 🔍 Watching trace logs 475 | 476 | If you want to get more information about what's going on with your app, update `config.js` to use `LOG_LEVEL: 'trace'` and run: 477 | 478 | ``` 479 | npm run stop && npm run start && tail -f 480 | ``` 481 | 482 | (Where `LOG_DEST` is defined in `config.js`). 483 | 484 | If you are trying to get a connection, you will see something like this after running `service mosquitto start && service gpd start`: 485 | 486 | ``` 487 | {"level":10,"time":1612201604724,"pid":45728,"hostname":"ip-172-31-26-163","msg":"BROKER: Client ([object Object]) published message: {\"retain\":true,\"qos\":1,\"topic\":\"$SYS/broker/connection/XXXXXX/state\",\"payload\":{\"type\":\"Buffer\",\"data\":[48]},\"brokerId\":\"20dc7c87-e5a1-4a33-a450-5c50dc5fb5ee\",\"clientId\":\"XXXXXX\"}"} 488 | ``` 489 | 490 | (Where I have replaced my device ID with `XXXXXX`.) 491 | 492 | #### ☢️ When all else fails... 493 | 494 | If you are in a bad state and can't get out, you can always go to your Lattice1 UI and navigate to `Settings -> Advanced -> Reeset Router`. This will restore factory settings for the Linux kernel you have been SSH'ing into. This reset will not delete your wallet, keys, or any secure data. 495 | 496 | ## Appendix II: 🧪 Integration Tests 497 | 498 | This repo includes an integration test script that you can run to ensure your communication endpoint is functioning as expected. 499 | 500 | **Step 1: Deploy locally** 501 | 502 | The test should be run against a local instance. Deploy locally with one of the methods above (`npm start` or using Docker). 503 | 504 | **Step 2: Point your Lattice your local broker** 505 | 506 | Follow the instructions in the previous section to SSH into your Lattice and update `gridplus.remote_mqtt_address`. 507 | 508 | **Step 3: Run test script** 509 | 510 | Once your Lattice is pointed to the desired MQTT broker, it will listen to the correct topics and is ready for testing. You can run the tests with: 511 | 512 | ``` 513 | npm run test 514 | ``` 515 | 516 | This will kick off a few integration tests to validate that we are able to connect to the Lattice and get addresses. If these pass, it means the communication pathway is working as expected. 517 | 518 | ## Appendix III: ℹ️ Web API 519 | 520 | The HTTP webserver hosted from this module only contains one route: 521 | 522 | **POST /:deviceID** 523 | 524 | Contact a Lattice (given its `deviceID`) with a payload. The payload must be a `UInt8Array` or `Buffer` type. On a successful message, a hex string is returned. [`gridplus-sdk`](https://github.com/GridPlus/gridplus-sdk) will parse this data into an appropriate response, so using it is highly recommended. 525 | 526 | **Request data**: 527 | 528 | ``` 529 | { 530 | data: 531 | } 532 | ``` 533 | 534 | **Response**: 535 | 536 | ``` 537 | { 538 | status: // 200 for success, 500 for internal error 539 | message: // Hex string containing response payload (status=200) or error string (status=500) 540 | } 541 | ``` 542 | 543 | ## Appendix IV: 📖 Background 544 | 545 | The [Lattice1](https://gridplus.io/lattice) is a next generation, always-online hardware wallet designed to sit behind a user's home WiFi network router. Because we aren't expecting most users to reconfigure their home router in the event a default firewall might block incoming requests, the Lattice1 is **not** designed to communicate over HTTP. 546 | 547 | The implemented pub/sub model subscribes to specific topics available on a cloud-hosted [MQTT](https://mqtt.org/) broker GridPlus provides by default. Requests from third-party applications are transformed into MQTT messages and sent to this broker, and are then forwarded to a Lattice1 with a matching device ID. 548 | 549 | As previously mentioned, having the choice to deploy the `lattice-connect` server on infrastructure our customers directly control is a priority for us, and a decision GridPlus supports. 550 | 551 | --------------------------------------------------------------------------------