├── .gitignore ├── CONTRIBUTING.md ├── DEPLOYMENT.md ├── Dockerfile ├── LICENSE.md ├── __tests__ ├── config.js ├── config.ts ├── index.js ├── index.ts ├── logger.js ├── logger.ts ├── on_ready.js ├── on_ready.ts └── security │ ├── authenticate.js │ ├── authenticate.ts │ ├── authorize_publish.js │ ├── authorize_publish.ts │ ├── authorize_subscribe.js │ ├── authorize_subscribe.ts │ ├── can_use_topic.js │ ├── can_use_topic.ts │ ├── verify_token.js │ └── verify_token.ts ├── app ├── authentication │ ├── authenticate.js │ ├── authenticate.ts │ ├── authenticate_no.js │ ├── authenticate_no.ts │ ├── authenticate_ok.js │ └── authenticate_ok.ts ├── authorization │ ├── authorize_publish.js │ ├── authorize_publish.ts │ ├── authorize_subscribe.js │ ├── authorize_subscribe.ts │ ├── can_use_topic.js │ └── can_use_topic.ts ├── config.js ├── config.ts ├── index.js ├── index.ts ├── logger.js ├── logger.ts ├── on_ready.js ├── on_ready.ts ├── public │ └── index.html └── security │ ├── verify_token.js │ └── verify_token.ts ├── devops_spellbook.sh ├── letsencrypt_renewal.sh ├── mosca.d.ts ├── package.json ├── public └── index.html ├── readme.md ├── support ├── fetch_real_token.js └── fetch_real_token.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | pids 5 | *.pid 6 | *.seed 7 | lib-cov 8 | coverage 9 | .grunt 10 | .lock-wscript 11 | build/Release 12 | node_modules 13 | jspm_packages 14 | .npm 15 | .node_repl_history 16 | .vscode 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | To get started, sign the Contributor License Agreement. 2 | -------------------------------------------------------------------------------- /DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # How to Deploy 2 | 3 | 0. Get a nice, fresh VM with Ubuntu 16. We recomend the one that comes pre-installed Docker on Digital Ocean. 4 | 0. [Install Docker if you didn't use Digital Ocean's docker image.](https://docs.docker.com/engine/installation/linux/ubuntulinux/). DigitalOcean offers an Ubuntu image that comes pre-installed with Docker. 5 | 0. `sudo docker build -t mqtt https://github.com/FarmBot/mqtt-gateway.git` 6 | 0. Run this: 7 | 8 | ```shell 9 | sudo docker run -d \ 10 | -e WEB_API_URL=http://YOUR_API_URL_HERE \ 11 | -p 3002:3002 \ 12 | -p 8883:8883 \ 13 | -p 1883:1883 \ 14 | -p 80:3002 \ 15 | -p 443:443 \ 16 | -v /etc/letsencrypt/:/etc/letsencrypt/ \ 17 | --restart=always mqtt 18 | ``` 19 | 20 | Add this to command above if you use SSL: 21 | 22 | ``` 23 | -e SSL_DOMAIN=YOUR_MQTT_SERVER_HOSTNAME_HERE 24 | ``` 25 | 26 | The server is now running. 27 | 28 | # Renewing SSL Certs with Let's Encrypt 29 | 30 | **NOTE:** I have made a script, `letsencrypt_renewal.sh` to help. 31 | 32 | **Step 1** 33 | 34 | SSH into the runing docker container (`docker exec -i -t CONTAINER_ID_HERE /bin/bash`) 35 | 36 | **Step 2** 37 | 38 | Run `letsencrypt renew` within 90 day. There is a `--force` flag if you care to use it. 39 | 40 | **Step 3** 41 | 42 | Kill the container. `docker kill CONTAINER_NAME`. 43 | Re-run the container, this time with two extra ENV vars: 44 | 45 | ```shell 46 | 47 | sudo docker run -d \ 48 | -e WEB_API_URL=http://YOUR_API_URL_HERE \ 49 | -e SSL_DOMAIN=YOUR_MQTT_URL_HERE \ 50 | -e SSL_EMAIL=you@domain.com \ 51 | -p 3002:3002 \ 52 | -p 8883:8883 \ 53 | -p 1883:1883 \ 54 | -p 80:3002 \ 55 | -p 443:443 \ 56 | -v /etc/letsencrypt/:/etc/letsencrypt/ \ 57 | --restart=always mqtt 58 | ``` 59 | 60 | # Adding SSL to New Setups with Let's Encrypt 61 | 62 | **STEP 1:** 63 | 64 | SSH into the runing docker container (`docker exec -i -t CONTAINER_ID_HERE /bin/bash`) 65 | 66 | **STEP 2:** 67 | 68 | From inside the container, run: 69 | 70 | ```shell 71 | 72 | letsencrypt certonly --webroot \ 73 | -w /app/public \ 74 | -d SSL_DOMAIN_HERE \ 75 | --text \ 76 | --non-interactive \ 77 | --agree-tos \ 78 | --email SSL_EMAIL_HERE 79 | 80 | ``` 81 | 82 | **Step 3:** 83 | 84 | Exit from the shell session (`exit`) and set the `SSL_DOMAIN`. 85 | 86 | You can accomplish this by running the same command during setup (see top of document), but this time add an additional flag to `docker run`: 87 | ``` 88 | sudo docker run -d \ 89 | -e WEB_API_URL=http://YOUR_API_URL_HERE \ 90 | -e SSL_DOMAIN=YOUR-MQTT-DOMAIN-HERE \ 91 | -p 3002:3002 \ 92 | -p 8883:8883 \ 93 | -p 1883:1883 \ 94 | -p 80:3002 \ 95 | -p 443:443 \ 96 | -v /etc/letsencrypt/:/etc/letsencrypt/ \ 97 | --restart=always mqtt 98 | ``` 99 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | MAINTAINER rick@farmbot.io 3 | 4 | EXPOSE 1883 5 | EXPOSE 8883 6 | EXPOSE 80 7 | EXPOSE 443 8 | EXPOSE 3002 9 | 10 | RUN apt-get update 11 | RUN apt-get install -y curl 12 | 13 | # Install node: 14 | RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - 15 | RUN apt-get install -y nodejs 16 | RUN apt-get install -y letsencrypt 17 | 18 | # Install our app: 19 | COPY . /app 20 | WORKDIR /app 21 | RUN npm install 22 | 23 | CMD ["npm", "start"] 24 | # sudo docker run -d -p 3002:3002 -p 1883:1883 --restart=always mqtt -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 FarmBot Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | require("jest"); 4 | describe("foo", function () { 5 | it("bars", function () { 6 | expect(true).toBe((true)); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /__tests__/config.ts: -------------------------------------------------------------------------------- 1 | import * as all from "../app/config"; 2 | import "jest"; 3 | 4 | describe("foo", () => { 5 | it("bars", () => { 6 | expect(true).toBe((true)); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | describe("foo", function () { 4 | it("bars", function () { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import * as all from "../app/index"; 2 | 3 | describe("foo", () => { 4 | it("bars", () => { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | describe("foo", function () { 4 | it("bars", function () { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/logger.ts: -------------------------------------------------------------------------------- 1 | import * as all from "../app/logger"; 2 | 3 | describe("foo", () => { 4 | it("bars", () => { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/on_ready.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | describe("foo", function () { 4 | it("bars", function () { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/on_ready.ts: -------------------------------------------------------------------------------- 1 | import * as all from "../app/on_ready"; 2 | 3 | describe("foo", () => { 4 | it("bars", () => { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/security/authenticate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var verify_token_1 = require("../../app/security/verify_token"); 4 | var fetch_real_token_1 = require("../../support/fetch_real_token"); 5 | var EMAIL = "admin@admin.com"; 6 | var PASSWORD = "password123"; 7 | var validJWT; 8 | var invalidJWT = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE0NTg4MTk1MzYsImp0aSI6ImE0MWQxMGU1LTk3NDUtNDEzOS1hYmJlLWQ5MjgzY2M2MGRjMiIsImlzcyI6ImZhcm1ib3Qtd2ViLWFwcCIsImV4cCI6MTQ1OTE2NTEzNiwiYWxnIjoiUlMyNTYifQ.reuRxMr_WMgu9prisSjGBuIuKRQw9Tmc5U_kWJyzFm0'; 9 | describe("token verification", function () { 10 | beforeAll(function (done) { 11 | fetch_real_token_1.fetchRealJWT() 12 | .then(function (token) { 13 | validJWT = token; 14 | done(); 15 | }); 16 | }); 17 | it("knows when you're lying", function (done) { 18 | function assertions(error) { 19 | expect(error.message).toBeDefined(); 20 | done(); 21 | } 22 | verify_token_1.verifyToken(invalidJWT).then(assertions, assertions); 23 | }); 24 | it("knows when you're telling the truth", function (done) { 25 | function assertions(data) { 26 | expect(data.sub).toEqual('admin@admin.com'); 27 | expect(data.iss).toEqual('//localhost:3000'); 28 | done(); 29 | } 30 | function failure(error) { 31 | fail("Failed to validate JWT. Error is above."); 32 | done(); 33 | } 34 | verify_token_1.verifyToken(validJWT).then(assertions, failure); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/security/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { authenticate as auth } from "../../app/authentication/authenticate"; 2 | import { verifyToken as verify } from '../../app/security/verify_token'; 3 | import { fetchRealJWT } from "../../support/fetch_real_token"; 4 | 5 | var EMAIL = "admin@admin.com"; 6 | var PASSWORD = "password123"; 7 | var validJWT; 8 | var invalidJWT = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE0NTg4MTk1MzYsImp0aSI6ImE0MWQxMGU1LTk3NDUtNDEzOS1hYmJlLWQ5MjgzY2M2MGRjMiIsImlzcyI6ImZhcm1ib3Qtd2ViLWFwcCIsImV4cCI6MTQ1OTE2NTEzNiwiYWxnIjoiUlMyNTYifQ.reuRxMr_WMgu9prisSjGBuIuKRQw9Tmc5U_kWJyzFm0'; 9 | 10 | describe("token verification", function () { 11 | beforeAll(function (done) { 12 | fetchRealJWT() 13 | .then(function (token) { 14 | validJWT = token; 15 | done(); 16 | }) 17 | }) 18 | it("knows when you're lying", function (done) { 19 | function assertions(error) { 20 | expect(error.message).toBeDefined(); 21 | done(); 22 | } 23 | verify(invalidJWT).then(assertions, assertions) 24 | }); 25 | 26 | it("knows when you're telling the truth", function (done) { 27 | function assertions(data) { 28 | expect(data.sub).toEqual('admin@admin.com'); 29 | expect(data.iss).toEqual('//localhost:3000'); 30 | done(); 31 | } 32 | 33 | function failure(error) { 34 | fail("Failed to validate JWT. Error is above."); 35 | done(); 36 | } 37 | verify(validJWT).then(assertions, failure) 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/security/authorize_publish.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | describe("foo", function () { 4 | it("bars", function () { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/security/authorize_publish.ts: -------------------------------------------------------------------------------- 1 | import * as all from "../../app/authorization/authorize_publish"; 2 | 3 | describe("foo", () => { 4 | it("bars", () => { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/security/authorize_subscribe.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | describe("foo", function () { 4 | it("bars", function () { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/security/authorize_subscribe.ts: -------------------------------------------------------------------------------- 1 | import * as all from "../../app/authorization/authorize_subscribe"; 2 | 3 | describe("foo", () => { 4 | it("bars", () => { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/security/can_use_topic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | describe("foo", function () { 4 | it("bars", function () { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/security/can_use_topic.ts: -------------------------------------------------------------------------------- 1 | import * as all from "../../app/authorization/can_use_topic"; 2 | 3 | describe("foo", () => { 4 | it("bars", () => { 5 | expect(true).toBe((true)); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/security/verify_token.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var verify_token_1 = require("../../app/security/verify_token"); 4 | var fetch_real_token_1 = require("../../support/fetch_real_token"); 5 | var EMAIL = "admin@admin.com"; 6 | var PASSWORD = "password123"; 7 | var validJWT; 8 | var invalidJWT = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE0NTg4MTk1MzYsImp0aSI6ImE0MWQxMGU1LTk3NDUtNDEzOS1hYmJlLWQ5MjgzY2M2MGRjMiIsImlzcyI6ImZhcm1ib3Qtd2ViLWFwcCIsImV4cCI6MTQ1OTE2NTEzNiwiYWxnIjoiUlMyNTYifQ.reuRxMr_WMgu9prisSjGBuIuKRQw9Tmc5U_kWJyzFm0'; 9 | describe("token verification", function () { 10 | beforeAll(function (done) { 11 | fetch_real_token_1.fetchRealJWT() 12 | .then(function (token) { 13 | validJWT = token; 14 | done(); 15 | }); 16 | }); 17 | it("knows when you're lying", function (done) { 18 | function assertions(error) { 19 | expect(error.message).toBeDefined(); 20 | done(); 21 | } 22 | verify_token_1.verifyToken(invalidJWT).then(assertions, assertions); 23 | }); 24 | it("knows when you're telling the truth", function (done) { 25 | function assertions(data) { 26 | expect(data.sub).toEqual('admin@admin.com'); 27 | expect(data.iss).toEqual('//localhost:3000'); 28 | done(); 29 | } 30 | function failure(error) { 31 | fail("Failed to validate JWT. Error is above."); 32 | done(); 33 | } 34 | verify_token_1.verifyToken(validJWT).then(assertions, failure); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/security/verify_token.ts: -------------------------------------------------------------------------------- 1 | import { authenticate as auth } from "../../app/authentication/authenticate"; 2 | import { verifyToken as verify } from '../../app/security/verify_token'; 3 | import { fetchRealJWT } from "../../support/fetch_real_token"; 4 | 5 | var EMAIL = "admin@admin.com"; 6 | var PASSWORD = "password123"; 7 | var validJWT; 8 | var invalidJWT = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE0NTg4MTk1MzYsImp0aSI6ImE0MWQxMGU1LTk3NDUtNDEzOS1hYmJlLWQ5MjgzY2M2MGRjMiIsImlzcyI6ImZhcm1ib3Qtd2ViLWFwcCIsImV4cCI6MTQ1OTE2NTEzNiwiYWxnIjoiUlMyNTYifQ.reuRxMr_WMgu9prisSjGBuIuKRQw9Tmc5U_kWJyzFm0'; 9 | 10 | describe("token verification", function () { 11 | beforeAll(function (done) { 12 | fetchRealJWT() 13 | .then(function (token) { 14 | validJWT = token; 15 | done(); 16 | }) 17 | }) 18 | it("knows when you're lying", function (done) { 19 | function assertions(error) { 20 | expect(error.message).toBeDefined(); 21 | done(); 22 | } 23 | verify(invalidJWT).then(assertions, assertions) 24 | }); 25 | 26 | it("knows when you're telling the truth", function (done) { 27 | function assertions(data) { 28 | expect(data.sub).toEqual('admin@admin.com'); 29 | expect(data.iss).toEqual('//localhost:3000'); 30 | done(); 31 | } 32 | 33 | function failure(error) { 34 | fail("Failed to validate JWT. Error is above."); 35 | done(); 36 | } 37 | verify(validJWT).then(assertions, failure) 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /app/authentication/authenticate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | // test@test.com password123 4 | var verify_token_1 = require("../security/verify_token"); 5 | var logger_1 = require("../logger"); 6 | var authenticate_ok_1 = require("./authenticate_ok"); 7 | var authenticate_no_1 = require("./authenticate_no"); 8 | /** Determine if user is authorized to use the server. */ 9 | function authenticate(client, username, password, callback) { 10 | logger_1.log("AUTH START"); 11 | var ok = authenticate_ok_1.authenticateOk(client, callback, username); 12 | var no = authenticate_no_1.authenticateNo(client, callback, username); 13 | if (username && password) { 14 | verify_token_1.verifyToken(password.toString()).then(ok, no); 15 | } 16 | else { 17 | no(new Error("username and password required")); 18 | } 19 | } 20 | exports.authenticate = authenticate; 21 | -------------------------------------------------------------------------------- /app/authentication/authenticate.ts: -------------------------------------------------------------------------------- 1 | // test@test.com password123 2 | import { verifyToken } from "../security/verify_token"; 3 | import { log } from "../logger"; 4 | import { authenticateOk } from "./authenticate_ok"; 5 | import { authenticateNo } from "./authenticate_no"; 6 | type MaybeString = string | undefined; 7 | 8 | /** Determine if user is authorized to use the server. */ 9 | export function authenticate(client, username: MaybeString, password: MaybeString, callback) { 10 | log("AUTH START"); 11 | let ok = authenticateOk(client, callback, username); 12 | let no = authenticateNo(client, callback, username); 13 | if (username && password) { 14 | verifyToken(password.toString()).then(ok, no); 15 | } else { 16 | no(new Error("username and password required")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/authentication/authenticate_no.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var logger_1 = require("../logger"); 4 | /** Creates a function that is triggered when a JWT is invalid. */ 5 | function authenticateNo(client, callback, username) { 6 | return function (error) { 7 | logger_1.log("AUTH FAIL " + username); 8 | logger_1.log(error.message); 9 | logger_1.log(error); 10 | client.authError = error; 11 | callback(null, false); 12 | }; 13 | } 14 | exports.authenticateNo = authenticateNo; 15 | -------------------------------------------------------------------------------- /app/authentication/authenticate_no.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../logger"; 2 | /** Creates a function that is triggered when a JWT is invalid. */ 3 | export function authenticateNo(client, callback, username) { 4 | return function (error) { 5 | log("AUTH FAIL " + username); 6 | log(error.message); 7 | log(error); 8 | client.authError = error; 9 | callback(null, false); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /app/authentication/authenticate_ok.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var logger_1 = require("../logger"); 4 | /** Creates a function that is triggered when a JWT is invalid. */ 5 | function authenticateOk(client, callback, username) { 6 | return function (permissions) { 7 | logger_1.log("AUTH OK " + username); 8 | client.permissions = permissions; 9 | callback(null, true); 10 | }; 11 | } 12 | exports.authenticateOk = authenticateOk; 13 | -------------------------------------------------------------------------------- /app/authentication/authenticate_ok.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../logger"; 2 | /** Creates a function that is triggered when a JWT is invalid. */ 3 | export function authenticateOk(client, callback, username) { 4 | return function (permissions) { 5 | log("AUTH OK " + username); 6 | client.permissions = permissions; 7 | callback(null, true); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /app/authorization/authorize_publish.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var can_use_topic_1 = require("./can_use_topic"); 4 | /** Determines if a user is allowed to publish to a particular topic. */ 5 | function authorizePublish(client, topic, payload, callback) { 6 | callback(null, can_use_topic_1.canUseTopic(client, topic)); 7 | } 8 | exports.authorizePublish = authorizePublish; 9 | -------------------------------------------------------------------------------- /app/authorization/authorize_publish.ts: -------------------------------------------------------------------------------- 1 | import { canUseTopic } from "./can_use_topic"; 2 | 3 | /** Determines if a user is allowed to publish to a particular topic. */ 4 | export function authorizePublish(client, topic, payload, callback) { 5 | callback(null, canUseTopic(client, topic)); 6 | } 7 | -------------------------------------------------------------------------------- /app/authorization/authorize_subscribe.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var can_use_topic_1 = require("./can_use_topic"); 4 | /** Determines if a user is allowed to listen to a particular topic. */ 5 | function authorizeSubscribe(client, topic, callback) { 6 | callback(null, can_use_topic_1.canUseTopic(client, topic)); 7 | } 8 | exports.authorizeSubscribe = authorizeSubscribe; 9 | -------------------------------------------------------------------------------- /app/authorization/authorize_subscribe.ts: -------------------------------------------------------------------------------- 1 | import { canUseTopic } from "./can_use_topic"; 2 | 3 | /** Determines if a user is allowed to listen to a particular topic. */ 4 | export function authorizeSubscribe(client, topic, callback) { 5 | callback(null, canUseTopic(client, topic)); 6 | } 7 | -------------------------------------------------------------------------------- /app/authorization/can_use_topic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var logger_1 = require("../logger"); 4 | /** If a user has a bot of id XYZ, then they may access any topic 5 | * following pattern bot/XYZ/# */ 6 | function canUseTopic(client, topic) { 7 | var hasBot = topic && 8 | client && 9 | client.permissions && 10 | client.permissions.bot; 11 | if (!hasBot) { 12 | logger_1.log("Tried to access topic " + 13 | (topic || "???") + 14 | " but no bot/topic provided."); 15 | return false; 16 | } 17 | ; 18 | var botID = client.permissions.bot; 19 | var allowedTopic = "bot/" + botID + "/"; 20 | return topic.startsWith(allowedTopic); 21 | } 22 | exports.canUseTopic = canUseTopic; 23 | -------------------------------------------------------------------------------- /app/authorization/can_use_topic.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../logger"; 2 | 3 | /** If a user has a bot of id XYZ, then they may access any topic 4 | * following pattern bot/XYZ/# */ 5 | export function canUseTopic(client, topic) { 6 | let hasBot = topic && 7 | client && 8 | client.permissions && 9 | client.permissions.bot; 10 | if (!hasBot) { 11 | log("Tried to access topic " + 12 | (topic || "???") + 13 | " but no bot/topic provided."); 14 | return false; 15 | }; 16 | let botID = client.permissions.bot; 17 | let allowedTopic = "bot/" + botID + "/"; 18 | return topic.startsWith(allowedTopic); 19 | } 20 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var logger_1 = require("./logger"); 4 | exports.webAppUrl = process.env.WEB_API_URL || "http://localhost:3000"; 5 | logger_1.log("Using " + exports.webAppUrl + " as API URL"); 6 | function generateConfig(sslDomain) { 7 | if (sslDomain === void 0) { sslDomain = ""; } 8 | var SSL_DIR = "/etc/letsencrypt/live/" + sslDomain + "/"; 9 | var config = { 10 | allowNonSecure: true, 11 | port: 1883, 12 | http: { 13 | port: 3002, 14 | bundle: true, 15 | static: "./public" 16 | }, 17 | https: { 18 | port: 443 19 | }, 20 | secure: { 21 | port: 8883, 22 | keyPath: SSL_DIR + "privkey.pem", 23 | certPath: SSL_DIR + "cert.pem" 24 | } 25 | }; 26 | // Remove SSL features if SSL_DOMAIN 27 | // was not set. 28 | if (!sslDomain) { 29 | delete config.https; 30 | delete config.secure; 31 | } 32 | return config; 33 | } 34 | exports.generateConfig = generateConfig; 35 | -------------------------------------------------------------------------------- /app/config.ts: -------------------------------------------------------------------------------- 1 | import { log } from "./logger"; 2 | import { ServerOpts } from "mosca"; 3 | 4 | export let webAppUrl = process.env.WEB_API_URL || "http://localhost:3000"; 5 | log(`Using ${webAppUrl} as API URL`); 6 | 7 | export function generateConfig(sslDomain = "") { 8 | const SSL_DIR = `/etc/letsencrypt/live/${sslDomain}/`; 9 | 10 | let config: ServerOpts = { 11 | allowNonSecure: true, 12 | port: 1883, 13 | http: { // for teh websockets 14 | port: 3002, 15 | bundle: true, 16 | static: "./public" 17 | }, 18 | https: { 19 | port: 443 20 | }, 21 | secure: { 22 | port: 8883, 23 | keyPath: SSL_DIR + "privkey.pem", 24 | certPath: SSL_DIR + "cert.pem" 25 | } 26 | }; 27 | 28 | // Remove SSL features if SSL_DOMAIN 29 | // was not set. 30 | if (!sslDomain) { 31 | delete config.https; 32 | delete config.secure; 33 | } 34 | 35 | return config; 36 | } 37 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var mosca_1 = require("mosca"); 4 | var config_1 = require("./config"); 5 | var on_ready_1 = require("./on_ready"); 6 | var server = new mosca_1.Server(config_1.generateConfig(process.env.SSL_DOMAIN)); 7 | server.on("ready", on_ready_1.onReady(server)); 8 | -------------------------------------------------------------------------------- /app/index.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "mosca"; 2 | import { generateConfig } from "./config"; 3 | import { onReady } from "./on_ready"; 4 | 5 | let server = new Server(generateConfig(process.env.SSL_DOMAIN)); 6 | 7 | server.on("ready", onReady(server)); 8 | -------------------------------------------------------------------------------- /app/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function log(args) { 4 | if (!process.env.DISABLE_LOGS) { 5 | console.log.apply(this, arguments); 6 | } 7 | } 8 | exports.log = log; 9 | -------------------------------------------------------------------------------- /app/logger.ts: -------------------------------------------------------------------------------- 1 | export function log(args: any) { 2 | if (!process.env.DISABLE_LOGS) { 3 | console.log.apply(this, arguments); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/on_ready.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var authenticate_1 = require("./authentication/authenticate"); 4 | var authorize_publish_1 = require("./authorization/authorize_publish"); 5 | var authorize_subscribe_1 = require("./authorization/authorize_subscribe"); 6 | var logger_1 = require("./logger"); 7 | exports.onReady = function (server) { return function () { 8 | logger_1.log("Server online"); 9 | server.on("clientConnected", function () { return logger_1.log("clientConnected"); }); 10 | server.on("clientDisconnecting", function () { return logger_1.log("clientDisconnecting"); }); 11 | server.on("clientDisconnected", function () { return logger_1.log("clientDisconnected"); }); 12 | server.on("published", function () { 13 | logger_1.log("\n=== Incoming Message ==="); 14 | var output; 15 | try { 16 | console.log(arguments[0].topic); 17 | var s = String.fromCharCode.apply(null, new Uint16Array(arguments[0].payload)); 18 | output = JSON.stringify(JSON.parse(s), null, 2); 19 | } 20 | catch (error) { 21 | output = arguments[0].payload; 22 | } 23 | finally { 24 | console.log(output); 25 | } 26 | }); 27 | server.on("subscribed", function () { return logger_1.log("subscribed"); }); 28 | server.on("unsubscribed", function () { return logger_1.log("unsubscribed"); }); 29 | server.on("error", function () { return logger_1.log("error"); }); 30 | server.authenticate = authenticate_1.authenticate; 31 | server.authorizePublish = authorize_publish_1.authorizePublish; 32 | server.authorizeSubscribe = authorize_subscribe_1.authorizeSubscribe; 33 | }; }; 34 | -------------------------------------------------------------------------------- /app/on_ready.ts: -------------------------------------------------------------------------------- 1 | import { authenticate } from "./authentication/authenticate"; 2 | import { authorizePublish } from "./authorization/authorize_publish"; 3 | import { authorizeSubscribe } from "./authorization/authorize_subscribe"; 4 | import { log } from "./logger"; 5 | 6 | export let onReady = (server) => () => { 7 | log("Server online"); 8 | server.on("clientConnected", () => log("clientConnected")); 9 | server.on("clientDisconnecting", () => log("clientDisconnecting")); 10 | server.on("clientDisconnected", () => log("clientDisconnected")); 11 | server.on("published", function () { 12 | log("\n=== Incoming Message ==="); 13 | let output: string; 14 | try { 15 | console.log(arguments[0].topic); 16 | let s = String.fromCharCode.apply(null, new Uint16Array(arguments[0].payload)); 17 | output = JSON.stringify(JSON.parse(s), null, 2); 18 | } catch (error) { 19 | output = arguments[0].payload; 20 | } finally { 21 | console.log(output); 22 | } 23 | }); 24 | server.on("subscribed", () => log("subscribed")); 25 | server.on("unsubscribed", () => log("unsubscribed")); 26 | server.on("error", () => log("error")); 27 | server.authenticate = authenticate; 28 | server.authorizePublish = authorizePublish; 29 | server.authorizeSubscribe = authorizeSubscribe; 30 | }; 31 | -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | MQTT Broker is running. 10 |

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/security/verify_token.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var config_1 = require("../config"); 4 | var axios_1 = require("axios"); 5 | var jwt = require("jsonwebtoken"); 6 | var logger_1 = require("../logger"); 7 | var url = config_1.webAppUrl + "/api/public_key"; 8 | function keyOk(resp) { 9 | logger_1.log("Downloaded certificate from " + url); 10 | return new Buffer(resp.data, "utf8"); 11 | } 12 | function no(error) { 13 | logger_1.log("Unable to download certificate from " + url); 14 | logger_1.log("Is the FarmBot API running?"); 15 | process.exit(); 16 | } 17 | var getCertificate = axios_1.default.get(url).then(keyOk, no); 18 | function verifyToken(token) { 19 | function no(error) { 20 | logger_1.log("Unable to verify token " + url); 21 | } 22 | function ok(cert) { 23 | logger_1.log("Did fetch certificate. Will verify token with certificate."); 24 | return jwt.verify(token, cert, { algorithms: ["RS256"] }); 25 | } 26 | logger_1.log("Will fetch certificate..."); 27 | return getCertificate.then(ok, no); 28 | } 29 | exports.verifyToken = verifyToken; 30 | ; 31 | -------------------------------------------------------------------------------- /app/security/verify_token.ts: -------------------------------------------------------------------------------- 1 | import { webAppUrl } from "../config"; 2 | import Axios from "axios"; 3 | import * as jwt from "jsonwebtoken"; 4 | import { log } from "../logger"; 5 | 6 | let url = webAppUrl + "/api/public_key"; 7 | 8 | function keyOk(resp) { 9 | log("Downloaded certificate from " + url); 10 | return new Buffer(resp.data, "utf8"); 11 | } 12 | 13 | function no(error) { 14 | log("Unable to download certificate from " + url); 15 | log("Is the FarmBot API running?"); 16 | process.exit(); 17 | } 18 | 19 | let getCertificate = Axios.get(url).then(keyOk, no); 20 | 21 | export function verifyToken(token) { 22 | function no(error) { 23 | log("Unable to verify token " + url); 24 | } 25 | 26 | function ok(cert) { 27 | log("Did fetch certificate. Will verify token with certificate."); 28 | return jwt.verify(token, cert, { algorithms: ["RS256"] }); 29 | } 30 | log("Will fetch certificate..."); 31 | return getCertificate.then(ok, no); 32 | }; 33 | -------------------------------------------------------------------------------- /devops_spellbook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exit 3 | # 4 | # DONT FORGET!!! 5 | # MOST OF THESE NEED TO RUN INSIDE THE __CONTAINER__ and __NOT__ 6 | # THE HOST MACHINE. 7 | # 8 | # 9 | # SEE ACTIVE LET'S ENCRYPT KEYS: 10 | ls /etc/letsencrypt/live/$SSL_DOMAIN/ 11 | 12 | # HOW TO GET LET'S ENCRYPT RUNNING: 13 | # You should only need to run this once per provisioning 14 | # STEP 1: 15 | # SSH into runing container and execute: 16 | # docker exec -i -t loving_heisenberg /bin/bash 17 | # STEP 2: 18 | # Run this from the shell: 19 | letsencrypt certonly --webroot \ 20 | -w /app/public \ 21 | -d $SSL_DOMAIN \ 22 | --text \ 23 | --non-interactive \ 24 | --agree-tos \ 25 | --email $SSL_EMAIL 26 | 27 | # HOW TO BUILD THE IMAGE: 28 | # Using local repo: 29 | sudo docker build -t mqtt . 30 | # Using Official Repo: 31 | sudo docker build -t mqtt https://github.com/FarmBot/mqtt-gateway.git 32 | 33 | # HOW TO RUN THE IMAGE: 34 | sudo docker run -d \ 35 | -e WEB_API_URL=http://YOUR_API_URL_HERE \ 36 | -e SSL_DOMAIN=YOUR_MQTT_URL_HERE \ 37 | -e SSL_EMAIL=you@domain.com \ 38 | -p 3002:3002 \ 39 | -p 8883:8883 \ 40 | -p 1883:1883 \ 41 | -p 80:3002 \ 42 | -p 443:443 \ 43 | -v /etc/letsencrypt/:/etc/letsencrypt/ \ 44 | --restart=always mqtt 45 | 46 | # HOW TO RENEW CERTS: 47 | # See `letsencrypt_renewal.sh` 48 | -------------------------------------------------------------------------------- /letsencrypt_renewal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | letsencrypt renew 4 | # Restart node: 5 | pkill -9 node 6 | -------------------------------------------------------------------------------- /mosca.d.ts: -------------------------------------------------------------------------------- 1 | declare module "mosca" { 2 | interface ServerOpts { 3 | allowNonSecure?: boolean, 4 | port?: number, 5 | logger?: { 6 | level: string 7 | }, 8 | https?: { 9 | port?: number, 10 | }, 11 | http?: { 12 | port?: number, 13 | bundle?: boolean, 14 | static?: string 15 | }, 16 | secure?: { 17 | port: number; 18 | keyPath: string; 19 | certPath: string; 20 | } 21 | } 22 | 23 | export class Server extends NodeJS.EventEmitter { 24 | constructor(opts: ServerOpts); 25 | public toString: () => string; 26 | public subscribe: (topic, callback, done) => any; 27 | public publish: (packet, client, callback) => any; 28 | public authenticate: (client, username, password, callback) => any; 29 | public authorizePublish: any; 30 | public authorizeSubscribe: any; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "farmbot-mqtt-gateway", 3 | "version": "1.1.0", 4 | "description": "MQTT gateway for Farmbot", 5 | "main": "app/index.js", 6 | "scripts": { 7 | "start": "nodemon ./app/index.js", 8 | "test": "jest --coverage --no-cache", 9 | "dev": "WEB_API_URL=http://127.0.0.1:3000 npm start" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/farmbot/mqtt-gateway.git" 14 | }, 15 | "author": "Rick Carlino, Farmbot.io", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/farmbot/mqtt-gateway/issues" 19 | }, 20 | "homepage": "https://github.com/farmbot/mqtt-gateway#readme", 21 | "dependencies": { 22 | "@types/axios": "^0.9.34", 23 | "@types/jsonwebtoken": "^7.1.33", 24 | "@types/mqtt": "0.0.32", 25 | "@types/node": "0.0.2", 26 | "axios": "^0.15.2", 27 | "jsonwebtoken": "^7.1.9", 28 | "mosca": "^2.2.0", 29 | "mqtt": "^1.7.4" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^16.0.1", 33 | "jest": "^17.0.3", 34 | "nodemon": "^1.11.0", 35 | "ts-jest": "^17.0.3", 36 | "ts-node": "^1.7.0", 37 | "typescript": "^2.0.10" 38 | }, 39 | "jest": { 40 | "coverageReporters": [ 41 | "html" 42 | ], 43 | "rootDir": "./", 44 | "transform": { 45 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 46 | }, 47 | "testResultsProcessor": "/node_modules/ts-jest/coverageprocessor.js", 48 | "testRegex": "/__tests__/.*\\.(ts|tsx|js)$", 49 | "testEnvironment": "node", 50 | "moduleFileExtensions": [ 51 | "ts", 52 | "tsx", 53 | "js" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | MQTT Broker is running. 10 |

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # PLEASE READ 2 | 3 | You might not need to install this software. This software is used by the FarmBot team and advanced users who run their own FarmBot servers. We highly recommend that you use our publicly hosted server at [my.farmbot.io](http://my.farmbot.io), which eliminates the need for server setup. 4 | 5 | # How It Works 6 | 7 | Farmbot uses [MQTT](https://en.wikipedia.org/wiki/MQTT) for realtime events. On the device side, this is handled over a TCP connection to the MQTT broker. On the browser, this is performed over a Websocket connection. 8 | 9 | ## Our MQTT Implementation 10 | 11 | * Log in to the broker using the same email as on the web app. A JSON Web Token from the API can be used as a password. 12 | * Messages are sent using [Celery Script](https://github.com/RickCarlino/farmbot-js/blob/master/src/corpus.ts), which is a domain-specific JSON format used to send messages (and sequences) to FarmBot. If you would like documentation or specifics, please let me know via an issue. CeleryScript transmission is usually handled by [FarmBotJS](https://github.com/FarmBot/farmbot-js), a wrapper library that eliminates the need to write Celery Script directly. 13 | 14 | ## Available MQTT Topics 15 | * `bot/device_{ BOT_ID }/from_clients`: Commands originated from browsers and clients. 16 | * `bot/device_{ BOT_ID }/from_device`: This is where the bot publishes messages. 17 | * `bot/${uuid}/status`: Everytime bot state changes (Eg: a pin is flipped, movement, etc.) a JSON representation of the bot status is sent. 18 | * `bot/${uuid}/logs`: General log messages. The same ones seen on the nav bar of the FarmBot Web App. 19 | 20 | Subscribing to `bot/{ BOT_UUID }/*` via 3rd party MQTT client (Such as [MQTT FX](http://www.mqttfx.org/)) is useful for debugging and monitoring. 21 | 22 | # Installation 23 | 24 | 1. git clone THIS_REPO 25 | 2. cd THIS_REPO 26 | 3. npm install 27 | 4. Setup and run the [Web API](https://github.com/FarmBot/Farmbot-Web-API) locally. We recommend running it on `http://localhost:3000` 28 | 5. `WEB_API_URL=http://localhost:3000 npm start`. See note below*. 29 | 6. Websocket MQTT is now available via `ws://localhost:3002` and `wss://localhost:443`. Raw MQTT (TCP connections) are available via `mqtt://localhost:1883`. 30 | 31 | \* The assumption is that you are running a [Web API](https://github.com/FarmBot/Farmbot-Web-API) instance on `localhost:3000`. If you are using a different API server, please change `WEB_API_URL` accordingly. 32 | 33 | # ENV var reference 34 | 35 | The MQTT broker uses ENV vars as the main means of configuration. These must be set properly for the app to work. 36 | 37 | * `WEB_API_URL`: URL to your [FarmBot API](https://github.com/FarmBot/Farmbot-Web-API). For instance, if you were running the API locally, you would set this value to `localhost:3000`. 38 | * `SSL_DOMAIN`: **Optional.**. Do not set if you do not plan on using [Let's Encrypt](https://letsencrypt.org/). This is the domain that Let's Encrypt will verify ownership of. 39 | * `SSL_EMAIL`: **Optional.**. Email for correspondence related to [Let's Encrypt](https://letsencrypt.org/). 40 | 41 | # Running on Local (for development) 42 | 43 | * Start the [FarmBot API](https://github.com/FarmBot/Farmbot-Web-API) on your local machine on default port. 44 | * Run `npm run dev` 45 | 46 | # Running the Test Suite 47 | 48 | `npm test` 49 | 50 | # Provisioning a Production Server 51 | 52 | See `DEPLOYMENT.md`. 53 | 54 | # Want to Help? 55 | 56 | See [TODO items in the codebase](https://github.com/FarmBot/mqtt-gateway/search?q=TODO&utf8=%E2%9C%93) or ask how you can help via an issue. 57 | -------------------------------------------------------------------------------- /support/fetch_real_token.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var axios_1 = require("axios"); 4 | var p = axios_1.default.post("http://localhost:3000/api/tokens", { 5 | user: { 6 | email: "admin@admin.com", 7 | password: "password123" 8 | } 9 | }); 10 | function fetchRealJWT() { 11 | function ok(resp) { 12 | return resp.data.token.encoded; 13 | } 14 | function no() { 15 | console.log("\n We test the MQTT server against a real running API instance on localhost:3000.\n Something went wrong while testing.\n Usually, this means you are not running a server.\n "); 16 | } 17 | return p.then(ok, no); 18 | } 19 | exports.fetchRealJWT = fetchRealJWT; 20 | -------------------------------------------------------------------------------- /support/fetch_real_token.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | var p = axios.post("http://localhost:3000/api/tokens", { 4 | user: { 5 | email: "admin@admin.com", 6 | password: "password123" 7 | } 8 | }); 9 | 10 | export function fetchRealJWT() { 11 | function ok(resp) { 12 | return resp.data.token.encoded; 13 | } 14 | function no() { 15 | console.log(` 16 | We test the MQTT server against a real running API instance on localhost:3000. 17 | Something went wrong while testing. 18 | Usually, this means you are not running a server. 19 | `); 20 | } 21 | return p.then(ok, no); 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2017", 5 | "es2015.promise" 6 | ], 7 | "module": "commonjs", 8 | "target": "es5", 9 | "noImplicitAny": false, 10 | "sourceMap": false, 11 | "types": [ 12 | "node" 13 | ] 14 | }, 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-line-length": [ 4 | true, 5 | 100 6 | ], 7 | "no-inferrable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "indent": [ 14 | true, 15 | "spaces" 16 | ], 17 | "no-switch-case-fall-through": true, 18 | "eofline": true, 19 | "no-string-literal": true, 20 | "no-eval": true, 21 | "no-duplicate-variable": true, 22 | "no-arg": true, 23 | "no-internal-module": true, 24 | "no-trailing-whitespace": true, 25 | "no-bitwise": true, 26 | "no-var-requires": true, 27 | "no-shadowed-variable": true, 28 | "no-unused-expression": true, 29 | "no-unused-variable": true, 30 | "one-line": [ 31 | true, 32 | "check-catch", 33 | "check-else", 34 | "check-open-brace", 35 | "check-whitespace" 36 | ], 37 | "quotemark": [ 38 | true, 39 | "double", 40 | "avoid-escape" 41 | ], 42 | "semicolon": [ 43 | true, 44 | "always" 45 | ], 46 | "typedef-whitespace": [ 47 | true, 48 | { 49 | "call-signature": "nospace", 50 | "index-signature": "nospace", 51 | "parameter": "nospace", 52 | "property-declaration": "nospace", 53 | "variable-declaration": "nospace" 54 | } 55 | ], 56 | "curly": true, 57 | "whitespace": [ 58 | true, 59 | "check-branch", 60 | "check-decl", 61 | "check-operator", 62 | "check-separator", 63 | "check-type" 64 | ], 65 | "no-any": true 66 | } 67 | } --------------------------------------------------------------------------------