diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..f5ee264
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+conduit
diff --git a/.env b/.env
new file mode 100644
index 0000000..d5482c1
--- /dev/null
+++ b/.env
@@ -0,0 +1,6 @@
+POSTGRES_PASSWORD=postgres123test
+PORT=8000
+DBConnString=host=db port=5432 user=postgres sslmode=disable dbname=postgres password=postgres123test
+CertKey=fullchain.pem
+PrivKey=privkey.pem
+SigningKey=mytestsigningkey
diff --git a/.gitignore b/.gitignore
index 9e7a26f..a25e285 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,11 @@
.pioenvs
.clang_complete
.gcc-flags.json
-server/secret.js
*.swp
-go-server/src/routes/secret.go
-go-server/src/mqtt/secret.go
+vendor/
+go-starter
+.idea/
+logs.txt
+postgres_data
+conduit
+
diff --git a/.travis.yml b/.travis.yml
index acf421f..7b45c99 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
language: go
-go:
- - 1.7.x
+go:
+ - "1.10.x"
- master
addons:
apt:
@@ -8,23 +8,12 @@ addons:
- nodejs
- cmake
before_install:
- - mkdir ./server/secrets
- - echo -e "package secrets\n\nconst SubSecret=\"aaa\"\nconst SECRET=\"aaaa\"\nconst DB_DIAL_URL=\"mongodb://localhost\"" > ./server/secrets/secret.go
- nvm install v6.10.3
- nvm use v6.10.3
- npm cache clean -f
-install:
- - go get ./server
- - go get "github.com/stretchr/testify/assert"
- - cd ./web-client
- - npm -v
- - npm install
- - cd ..
-script:
- - go test -v ./server/tests
- - cd server
- - go build
- - cd ..
- - cd ./web-client
- - npm run test
- - cd ..
+install:
+ - go get -u github.com/golang/dep/cmd/dep
+script:
+ - make
+ - make test
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e972737
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,7 @@
+from golang:1.9
+WORKDIR /go/src/github.com/suyashkumar/conduit
+RUN go get -u github.com/golang/dep/cmd/dep
+COPY . .
+RUN make
+CMD ./conduit
+
diff --git a/Gopkg.lock b/Gopkg.lock
new file mode 100644
index 0000000..70616b8
--- /dev/null
+++ b/Gopkg.lock
@@ -0,0 +1,111 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+ name = "github.com/dgrijalva/jwt-go"
+ packages = ["."]
+ revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
+ version = "v3.2.0"
+
+[[projects]]
+ name = "github.com/gorilla/websocket"
+ packages = ["."]
+ revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
+ version = "v1.2.0"
+
+[[projects]]
+ name = "github.com/jinzhu/gorm"
+ packages = [
+ ".",
+ "dialects/postgres"
+ ]
+ revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166"
+ version = "v1.9.1"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/jinzhu/inflection"
+ packages = ["."]
+ revision = "04140366298a54a039076d798123ffa108fff46c"
+
+[[projects]]
+ name = "github.com/julienschmidt/httprouter"
+ packages = ["."]
+ revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669"
+ version = "v1.1"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/lib/pq"
+ packages = [
+ ".",
+ "hstore",
+ "oid"
+ ]
+ revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
+
+[[projects]]
+ name = "github.com/rifflock/lfshook"
+ packages = ["."]
+ revision = "bf539943797a1f34c1f502d07de419b5238ae6c6"
+ version = "v2.3"
+
+[[projects]]
+ name = "github.com/rs/cors"
+ packages = ["."]
+ revision = "feef513b9575b32f84bafa580aad89b011259019"
+ version = "v1.3.0"
+
+[[projects]]
+ name = "github.com/satori/go.uuid"
+ packages = ["."]
+ revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
+ version = "v1.2.0"
+
+[[projects]]
+ name = "github.com/sirupsen/logrus"
+ packages = ["."]
+ revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
+ version = "v1.0.5"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/suyashkumar/auth"
+ packages = ["."]
+ revision = "46e30814f7bce8a70598f487d79367daab4e8ae8"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/suyashkumar/golang-socketio"
+ packages = [
+ ".",
+ "protocol",
+ "transport"
+ ]
+ revision = "d319f78bb742e7375eef96a20a99bcdfff0c922d"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/crypto"
+ packages = [
+ "bcrypt",
+ "blowfish",
+ "ssh/terminal"
+ ]
+ revision = "e73bf333ef8920dbb52ad18d4bd38ad9d9bc76d7"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/sys"
+ packages = [
+ "unix",
+ "windows"
+ ]
+ revision = "79b0c6888797020a994db17c8510466c72fe75d9"
+
+[solve-meta]
+ analyzer-name = "dep"
+ analyzer-version = 1
+ inputs-digest = "6ef743250b7d45a2b507ffc1bd263d4b7854eebcccbfd74414a3f5f15614bdfd"
+ solver-name = "gps-cdcl"
+ solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
new file mode 100644
index 0000000..e57a2f5
--- /dev/null
+++ b/Gopkg.toml
@@ -0,0 +1,46 @@
+# Gopkg.toml example
+#
+# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
+# for detailed Gopkg.toml documentation.
+#
+# required = ["github.com/user/thing/cmd/thing"]
+# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
+#
+# [[constraint]]
+# name = "github.com/user/project"
+# version = "1.0.0"
+#
+# [[constraint]]
+# name = "github.com/user/project2"
+# branch = "dev"
+# source = "github.com/myfork/project2"
+#
+# [[override]]
+# name = "github.com/x/y"
+# version = "2.4.0"
+#
+# [prune]
+# non-go = false
+# go-tests = true
+# unused-packages = true
+
+
+[[constraint]]
+ name = "github.com/jinzhu/gorm"
+ version = "1.9.1"
+
+[[constraint]]
+ name = "github.com/julienschmidt/httprouter"
+ version = "1.1.0"
+
+[[constraint]]
+ name = "github.com/rifflock/lfshook"
+ version = "2.3.0"
+
+[[constraint]]
+ name = "github.com/sirupsen/logrus"
+ version = "1.0.4"
+
+[prune]
+ go-tests = true
+ unused-packages = true
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c2faedf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,23 @@
+BINARY = conduit
+
+.PHONY: build
+build:
+ dep ensure
+ make test
+ go build -o ${BINARY}
+
+.PHONY: test
+test:
+ go test ./...
+
+.PHONY: run
+run:
+ make build
+ ./${BINARY}
+
+.PHONY: release
+release:
+ glide install
+ GOOS=linux GOARCH=amd64 go build -o build/${BINARY}-linux-amd64 .;
+ GOOS=darwin GOARCH=amd64 go build -o build/${BINARY}-darwin-amd64 .;
+ GOOS=windows GOARCH=amd64 go build -o build/${BINARY}-windows-amd64.exe .;
diff --git a/README.md b/README.md
index c2871ea..9e86c7b 100644
--- a/README.md
+++ b/README.md
@@ -1,69 +1,118 @@
# conduit
-:eyes:NOTE: conduit is going through a ground-up re-write on the [conduit-v2-master](https://github.com/suyashkumar/conduit/tree/conduit-v2-master) branch! :eyes:
+:eyes:Conduit V2 was just realeased! :eyes:
[Conduit featured on Hackaday!](http://hackaday.com/2017/01/17/servo-controlled-iot-light-switches/)
-Conduit allows you to quickly build cloud-connected IoT devices that you can communicate with and control from anywhere in the world. Conduit provides a RESTful API that allows you to remotely call functions (e.g. `lightsOn()`) on your ESP8266/Arduino device from the cloud, even if it's behind a LAN and doesn't have a public IP address. You can do all this simply by dropping in a few lines of code into your firmware:
+Conduit is an entirely open-source web service that allows you to quickly and easily call functions on your [ESP8266 IoT devices](https://www.amazon.com/HiLetgo-Version-NodeMCU-Internet-Development/dp/B010O1G1ES/ref=sr_1_3?ie=UTF8&qid=1483953570&sr=8-3&keywords=nodemcu+esp8266) from anywhere in the world (even if those devices are behind private networks).
+You can do all this simply by dropping in a few lines of code into your firmware and then issuing RESTful API requests to the conduit web service to call your firmware functions. Skip ahead to a [full minimal example](README.md/#bink-an-led-from-the-cloud-full-example) if you're ready to get started right away!
+
+## Conduit Components
+* [Conduit backend web service (here)](https://github.com/suyashkumar/conduit)
+* [Conduit firmware library](https://github.com/suyashkumar/conduit-firmware-library)
+* [Conduit frontend](https://github.com/suyashkumar/conduit-frontend)
+
+## Conduit API
+A central conduit API server is already deployed at https://api.conduit.suyash.io (should be used for all API routes) with a user-friendly front-end deployed at https://conduit.suyash.io.
+
+| Method | Route | Sample Request | Notes |
+|--------|----------------|------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
+| POST | /api/login | ``` { "email": "test@test.com", "password": "test" } ``` | Authenticate with Conduit, get issued a JWT |
+| POST | /api/call | ```{"token": "JWT token from login", "device_name": "myDeviceName", "function_name": "ledToggle", "wait_for_device_response": "true"}``` | Call a function (ledToggle) on one of your ESP8266 devices (named "myDeviceName" here)! |
+| POST | /api/user_info | ```{"token": "JWT token from login"}``` | This returns information about your user account, including your account secret which you must include in your firmware. |
+
+
+## Sample Application using Conduit
+[smart-lights](https://github.com/suyashkumar/smart-lights) is a sample project that uses v1 of this this library to switch lights from the cloud. It currently uses v1 of this library and should be updated to v2 shortly.
+
+![](https://github.com/suyashkumar/smart-lights/blob/master/img/lightswitch.gif)
+
+## Minimal Example
+Below is a minimal example of firmware code needed to get started with Conduit to blink an LED. See the next section for a complete example.
```C
#include
-#include
+#include // get from suyashkumar/conduit-firmware-library or platformio
#define LED D0
-Conduit conduit("my-device-name", "conduit.suyash.io", "api-key-here"); // init Conduit
+const char* ssid = ""; // wifi ssid
+const char* password = ""; // wifi password
+const char* device_name = ""; // you pick this! IDentifier for your device
+const char* server_url = "api.conduit.suyash.io"; // location of the API server
+const char* account_secret = ""; // register and call /api/user_info to get this
-// Turns on a LED
-int ledOn() {
- digitalWrite(LED, HIGH);
- conduit.publishMessage("LED ON");
+Conduit conduit(device_name, server_url, account_secret);
+int ledStatus = 0;
+
+int ledToggle(RequestParams *rq){
+ digitalWrite(LED, (ledStatus) ? HIGH : LOW); // LED is on when LOW
+ ledStatus = (ledStatus) ? 0 : 1;
+ Serial.println(rq->request_uuid);
+ conduit.sendResponse(rq, (ledStatus) ? "ON":"OFF");
}
-void setup(void) {
- pinMode(LED, OUTPUT); // Set LED pin to output
- digitalWrite(LED, LOW); // Start out with LED off
+void setup(void){
+ Serial.begin(115200); // Start serial
+ digitalWrite(LED, HIGH);
- conduit.startWIFI("ssid", "password"); // Config/start wifi
+ conduit.startWIFI(ssid, password); // Config/start wifi
conduit.init();
- conduit.addHandler("ledOn", &ledOn); // Registers ledOn function to be callable remotely
+ conduit.addHandler("ledToggle", &ledToggle);
+
}
-void loop(void) {
+void loop(void){
conduit.handle();
}
```
-...and just like that, you can now call `ledOn` on this device by making a `GET https://conduit.suyash.io/api/send/my-device-name/ledOn` from anywhere in the world! You will also have to supply a valid auth token by including an `x-access-token` header to ensure your call is authorized, and have an associated valid Conduit account.
+and now you can call `ledToggle` on that device from anywhere in the world by using the following APIs:
-Conduit also provides a streamlined interface for recieving and making available arbitrary data produced from your devices in real time.
+### Call RESTful APIs to trigger ESP8266 Functions:
+1) Get your login token by authenticating
+POST https://api.conduit.suyash.io/api/login:
+ ```sh
+ curl 'https://api.conduit.suyash.io/api/login' \
+ -H 'content-type: application/json;charset=UTF-8' \
+ --data-binary '{"email":"YOUR_EMAIL", "password":"YOUR_PASSWORD"}' --compressed
+ ```
+
+2) POST https://api.conduit.suyash.io/api/call:
+
+ ```sh
+ curl 'https://api.conduit.suyash.io/api/call' \
+ -H 'content-type: application/json;charset=UTF-8' \
+ --data-binary '{"token":"YOUR_LOGIN_TOKEN","device_name":"YOUR_DEVICE_NAME","function_name":"ledToggle","wait_for_device_response":true}' --compressed
+ ```
-Conduit is **entirely open source** (the firmware, backend web service, and frontend), allowing you to deploy your own instance of Conduit behind protected networks (like hospitals) or to audit the Conduit code. Conduit currently works with the [low-cost ESP8266 WiFi microcontroller](https://www.amazon.com/HiLetgo-Version-NodeMCU-Internet-Development/dp/B010O1G1ES/ref=sr_1_3?ie=UTF8&qid=1483953570&sr=8-3&keywords=nodemcu+esp8266) or Arduino like microcontroller, but there is no reason why it can't also work on other systems.
Conduit is currently in active development, so please feel free to contact me with comments/questions and submit pull requests!
-### Bink an LED from the Cloud
-Controlling an LED on the ESP8266 from the Cloud takes less than 5 minutes with Conduit. Please make sure you've installed the relevant drivers ([here](https://www.silabs.com/products/mcu/Pages/USBtoUARTBridgeVCPDrivers.aspx) if you're using the nodemcu ESP8266 chip linked above) and installed the [platformio](http://docs.platformio.org/en/latest/installation.html) build system (simply `brew install platformio` if you're on a mac).
+## Bink an LED from the Cloud (full example).
+Controlling an LED on the ESP8266 from the Cloud takes less than 5 minutes with Conduit.
+
+Please make sure you've installed the relevant drivers ([here](https://www.silabs.com/products/mcu/Pages/USBtoUARTBridgeVCPDrivers.aspx) if you're using the nodemcu ESP8266 chip linked above) and installed the [platformio](http://docs.platformio.org/en/latest/installation.html) build system (simply `brew install platformio` if you're on a mac).
1. Create a conduit account at https://conduit.suyash.io/#/login
-2. Retreive your API key from the Account view at https://conduit.suyash.io/#/account
-3. Clone this repo and change into the conduit directory.
+2. Retreive your account secret from the Account view at https://conduit.suyash.io/#/account
+3. Clone the conduit firmware repo and change into the `examples/basic_functionality` directory.
```sh
- git clone https://github.com/suyashkumar/conduit.git
- cd conduit
+ git clone https://github.com/suyashkumar/conduit-firmware-library.git
+ cd examples/basic_functionality
```
-4. Navigate into the firmware directory (`cd firmware`) and open `src/main.ino`. Fill in the following lines (API key comes from step 2):
+4. Open `src/main.ino`. Fill in the following lines (account secret comes from step 2):
```C
- // Fill out the below Github folks:
- const char* ssid = "mywifi";
- const char* password = "";
- const char* deviceName = "suyash";
- const char* apiKey = "api-key-here";
+const char* ssid = ""; // wifi ssid
+const char* password = ""; // wifi password
+const char* device_name = ""; // you pick this! IDentifier for your device
+const char* server_url = "api.conduit.suyash.io"; // location of the API server
+const char* account_secret = ""; // register and call /api/user_info to get this
```
-5. Build the project using platformio. You should [install platformio](http://docs.platformio.org/en/latest/installation.html#python-package-manager) (if you haven't already) to build this properly. Ensure you're in the firmware directory (`conduit/firmware`) and run:
+5. Build the project using platformio. You should [install platformio](http://docs.platformio.org/en/latest/installation.html#python-package-manager) (if you haven't already) to build this properly. Ensure you're in the root directory of the example (not `src`) and run:
```sh
platformio run
@@ -72,16 +121,12 @@ Controlling an LED on the ESP8266 from the Cloud takes less than 5 minutes with
```sh
platformio run --target upload
```
- NOTE: to properly upload to an ESP8266 chip, you must have installed the ESP8266 drivers on your system already.
+ NOTE: to properly upload to an ESP8266 chip, you must have installed the [ESP8266 drivers](https://www.silabs.com/products/mcu/Pages/USBtoUARTBridgeVCPDrivers.aspx) on your system already.
-6. You should be set! You can now go to the conduit interact view (https://conduit.suyash.io/#/interact) and type in your device name (that you chose in step 4) and `ledToggle` as the function and hit "Go!" to see your LED on your device toggle! Note that because we're using the built-in LED the on/off statuses are reversed (LED is on when D0 is low), but with your own LED things should be normal!
-7. There's a lot more to explore--you can publish persisted data to conduit (to be retrieved later via API) and build your own applications around conduit using the secure JSON web token based API.
+6. You should be set! You can now go to the conduit interact view (https://conduit.suyash.io/#/interact) and type in your device name (that you chose in step 4) and `ledToggle` as the function and hit "Execute!" to see your LED on your device toggle! You can also issue RESTful API requests to conduit to trigger functions on your device from any app that you build as mentioned [here](README.md#call-restful-apis-to-trigger-esp8266-functions)
-### Sample Project
-[smart-lights](https://github.com/suyashkumar/smart-lights) is a sample project that uses this library to switch lights from the cloud.
-![](https://github.com/suyashkumar/smart-lights/blob/master/img/lightswitch.gif)
-### License
-Copyright (c) 2017 Suyash Kumar
+## License
+Copyright (c) 2018 Suyash Kumar
See [conduit/LICENSE.txt](https://github.com/suyashkumar/conduit/blob/master/LICENSE.txt) for license text (CC Attribution-NonCommercial 3.0)
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..187024a
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,17 @@
+package config
+
+import "os"
+
+func Get(key string) string {
+ if value, ok := os.LookupEnv(key); ok {
+ return value
+ }
+
+ defValue, ok := defaults[key]
+ if !ok {
+ return ""
+ }
+
+ return defValue
+}
+
diff --git a/config/defaults.go b/config/defaults.go
new file mode 100644
index 0000000..36cf606
--- /dev/null
+++ b/config/defaults.go
@@ -0,0 +1,17 @@
+package config
+
+const LogFile = "LogFile"
+const DBConnString = "DBConnString"
+const Port = "Port"
+const CertKey = "CertKey"
+const PrivKey = "PrivKey"
+const UseSSL = "UseSSL"
+const SigningKey = "SigningKey"
+
+var defaults = map[string]string{
+ LogFile: "logs.txt",
+ DBConnString: "host=localhost port=5432 user=postgres sslmode=disable dbname=postgres password=postgres123test",
+ Port: "8000",
+ UseSSL: "false",
+ SigningKey: "mytestsigningkey",
+}
diff --git a/db/db.go b/db/db.go
new file mode 100644
index 0000000..e6c903d
--- /dev/null
+++ b/db/db.go
@@ -0,0 +1,98 @@
+package db
+
+import (
+ "errors"
+
+ "github.com/jinzhu/gorm"
+ _ "github.com/jinzhu/gorm/dialects/postgres"
+ uuid "github.com/satori/go.uuid"
+ "github.com/suyashkumar/auth"
+ "github.com/suyashkumar/conduit/entities"
+)
+
+const DefaultMaxIdleConns = 5
+
+var ErrorNoConnectionString = errors.New("A connection string must be specified on the first call to Get")
+
+// Handler abstracts away common persistence operations needed for this package
+type Handler interface {
+ // GetUser gets a user from the database that matches constraints on the input user
+ GetUser(u auth.User) (auth.User, error)
+ // UpsertUser updates a user (if input user UUID matches one in the db) or inserts a user
+ UpsertUser(u auth.User) error
+ // GetAccountSecret gets a user's device secret
+ GetAccountSecret(uuid uuid.UUID) (entities.AccountSecret, error)
+ // InsertAccountSecret updates or inserts a device secret for the User
+ InsertAccountSecret(uuid uuid.UUID, ds entities.AccountSecret) error
+ // GetDB returns the Handler's underlying *gorm.DB
+ GetDB() *gorm.DB
+}
+
+type handler struct {
+ db *gorm.DB
+ authDBHandler auth.DatabaseHandler
+}
+
+// NewHandler initializes and returns a new Handler
+func NewHandler(dbConnection string) (Handler, error) {
+ db, err := getDB(dbConnection)
+ if err != nil {
+ return nil, err
+ }
+ // AutoMigrate relevant schemas
+ db.AutoMigrate(&entities.AccountSecret{})
+ ah, err := auth.NewDatabaseHandlerFromGORM(db)
+ if err != nil {
+ return nil, err
+ }
+ return &handler{
+ db: db,
+ authDBHandler: ah,
+ }, nil
+}
+
+func (d *handler) GetUser(u auth.User) (auth.User, error) {
+ return d.authDBHandler.GetUser(u)
+}
+
+func (d *handler) UpsertUser(u auth.User) error {
+ return d.authDBHandler.UpsertUser(u)
+}
+
+func (d *handler) GetAccountSecret(uuid uuid.UUID) (entities.AccountSecret, error) {
+ var foundDeviceSecret entities.AccountSecret
+ // this could return multiple, but convention right now is one secret per user. May change in future
+ err := d.db.Where(entities.AccountSecret{UserUUID: uuid}).Order("created_at desc").First(&foundDeviceSecret).Error
+ if err != nil {
+ return foundDeviceSecret, err
+ }
+ return foundDeviceSecret, nil
+}
+
+func (d *handler) InsertAccountSecret(uuid uuid.UUID, secret entities.AccountSecret) error {
+ err := d.db.Create(&secret).Error
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (d *handler) GetDB() *gorm.DB {
+ return d.db
+}
+
+func getDB(dbConnection string) (*gorm.DB, error) {
+ if dbConnection == "" {
+ return nil, ErrorNoConnectionString
+ }
+
+ d, err := gorm.Open("postgres", dbConnection)
+ if err != nil {
+ return nil, err
+ }
+
+ d.DB().SetMaxIdleConns(DefaultMaxIdleConns)
+
+ return d, nil
+
+}
diff --git a/device/callbacks.go b/device/callbacks.go
new file mode 100644
index 0000000..b3c717e
--- /dev/null
+++ b/device/callbacks.go
@@ -0,0 +1,26 @@
+package device
+
+import (
+ "github.com/sirupsen/logrus"
+ "github.com/suyashkumar/golang-socketio"
+)
+
+const OK_MSG = "OK"
+
+func onHello(c *gosocketio.Channel) string {
+ logrus.Println("Something successfully handled")
+ c.Emit("hello", "Hello emit")
+ return "OK"
+}
+
+func onConnection(c *gosocketio.Channel) {
+ logrus.Printf("New Connection (SID: %s)", c.Id())
+ c.Emit("id_message", c.Id())
+}
+
+func onAPIKeyReceive(c *gosocketio.Channel, msg string) string {
+ logrus.Infof("Received an API key message from %s: %s", c.Id(), msg)
+ //TODO: Validate msg, consider receiving as JSON based on firmware
+ c.Join(msg)
+ return OK_MSG
+}
diff --git a/device/device.go b/device/device.go
new file mode 100644
index 0000000..b158bf3
--- /dev/null
+++ b/device/device.go
@@ -0,0 +1,73 @@
+package device
+
+import (
+ "net/http"
+
+ "fmt"
+
+ "github.com/satori/go.uuid"
+ "github.com/sirupsen/logrus"
+ "github.com/suyashkumar/golang-socketio"
+ "github.com/suyashkumar/golang-socketio/transport"
+)
+
+type Handler interface {
+ // Call issues an RPC to the device specified. Call returns a channel along which a device
+ // response to this RPC may be communicated.
+ Call(deviceName, accountSecret, functionName string, wait bool) chan string
+ On(deviceName, deviceID, eventName string, callback func(deviceName, eventName, body string))
+ GetHTTPHandler() http.Handler
+}
+
+type handler struct {
+ server *gosocketio.Server
+}
+
+var globalDeviceHandler *handler
+
+func NewHandler() Handler {
+ s := gosocketio.NewServer(transport.GetDefaultWebsocketTransport())
+
+ // Attach socket event handlers
+ s.On("hello", onHello)
+ s.On(gosocketio.OnConnection, onConnection)
+ s.On("api_key", onAPIKeyReceive)
+
+ globalDeviceHandler = &handler{
+ server: s,
+ }
+ return globalDeviceHandler
+}
+
+func getRoomName(deviceName, accountSecret string) string {
+ return fmt.Sprintf("%s_%s", accountSecret, deviceName)
+}
+
+func (h *handler) Call(deviceName, accountSecret, functionName string, wait bool) chan string {
+ reqUUID := uuid.NewV4().String()
+ message := fmt.Sprintf("%s,%s", functionName, reqUUID)
+
+ var c chan string
+
+ if wait {
+ // Listen for device response
+ c = make(chan string)
+ logrus.WithField("request_uuid", reqUUID).Info("Setting up event listener")
+ h.server.On(reqUUID, func(ch *gosocketio.Channel, msg string) string {
+ logrus.WithField("request_uuid", reqUUID).Info("Response returned")
+ c <- msg
+ return "OK"
+ })
+ }
+
+ h.server.BroadcastTo(getRoomName(deviceName, accountSecret), "server_directives", message)
+ return c
+}
+
+func (h *handler) On(deviceName, deviceID, eventName string, callback func(deviceName, eventName, body string)) {
+
+}
+
+func (h *handler) GetHTTPHandler() http.Handler {
+ return h.server
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a4120a8
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,25 @@
+version: '2'
+services:
+ server:
+ build: .
+ ports:
+ - ${PORT}:8000
+ environment:
+ - POSTGRES_PASSWORD
+ - DBConnString
+ - UseSSL
+ - CertKey
+ - PrivKey
+ - SigningKey
+ command: ["./wait-for-it.sh", "db:5432", "--", "./conduit"]
+ db:
+ image: postgres
+ environment:
+ - POSTGRES_PASSWORD
+ - POSTGRES_USER=postgres
+ - POSTGRES_DB=postgres
+ volumes:
+ - $PWD/postgres_data:/var/lib/postgresql/data
+ ports:
+ - 5432:5432
+
diff --git a/entities/account_secret.go b/entities/account_secret.go
new file mode 100644
index 0000000..4f99cbb
--- /dev/null
+++ b/entities/account_secret.go
@@ -0,0 +1,14 @@
+package entities
+
+import (
+ "time"
+
+ uuid "github.com/satori/go.uuid"
+)
+
+type AccountSecret struct {
+ UUID uuid.UUID `sql:"type:uuid;" gorm:"primary_key"`
+ UserUUID uuid.UUID `sql:"type:uuid;" gorm:"index:idx_user_uuid"`
+ Secret string
+ CreatedAt time.Time // TODO: create desc index on this
+}
diff --git a/entities/requests.go b/entities/requests.go
new file mode 100644
index 0000000..e2264af
--- /dev/null
+++ b/entities/requests.go
@@ -0,0 +1,22 @@
+package entities
+
+type RegisterRequest struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+}
+
+type LoginRequest struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+}
+
+type CallRequest struct {
+ Token string `json:"token"`
+ DeviceName string `json:"device_name"`
+ FunctionName string `json:"function_name"`
+ WaitForDeviceResponse bool `json:"wait_for_device_response"`
+}
+
+type UserInfoRequest struct {
+ Token string `json:"token"`
+}
diff --git a/entities/responses.go b/entities/responses.go
new file mode 100644
index 0000000..916af85
--- /dev/null
+++ b/entities/responses.go
@@ -0,0 +1,22 @@
+package entities
+
+type GenericResponse struct {
+ Message string `json:"message"`
+}
+
+type ErrorResponse struct {
+ Code int64 `json:"code"`
+ Error string `json:"error"`
+}
+
+type LoginResponse struct {
+ Token string `json:"token"`
+}
+
+type SendResponse struct {
+ Response string `json:"response"`
+}
+
+type UserInfoResponse struct {
+ AccountSecret string `json:"account_secret"`
+}
diff --git a/firmware/.gitignore b/firmware/.gitignore
deleted file mode 100644
index 92f53ff..0000000
--- a/firmware/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.pioenvs
-.clang_complete
-.gcc-flags.json
-.piolibdeps/
\ No newline at end of file
diff --git a/firmware/README.md b/firmware/README.md
deleted file mode 100644
index 93482b7..0000000
--- a/firmware/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# conduit/firmware
-
-This directory contains a basic example of using the [conduit firmware library](https://github.com/suyashkumar/conduit-firmware-library) on a ESP8266 microcontroller.
-
----
-
-The conduit firmware library source code can be found at [suyashkumar/conduit-firmware-library](https://github.com/suyashkumar/conduit-firmware-library) and is installable in Arduino/ESP8266 projects as a [platformio dependency](http://platformio.org/lib/show/1184/conduit/).
-
-To install in your own project simply:
-
-```sh
- platformio lib install "conduit"
-```
diff --git a/firmware/lib/readme.txt b/firmware/lib/readme.txt
deleted file mode 100644
index 607a92a..0000000
--- a/firmware/lib/readme.txt
+++ /dev/null
@@ -1,38 +0,0 @@
-
-This directory is intended for the project specific (private) libraries.
-PlatformIO will compile them to static libraries and link to executable file.
-
-The source code of each library should be placed in separate directory, like
-"lib/private_lib/[here are source files]".
-
-For example, see how can be organized `Foo` and `Bar` libraries:
-
-|--lib
-| |--Bar
-| | |--docs
-| | |--examples
-| | |--src
-| | |- Bar.c
-| | |- Bar.h
-| |--Foo
-| | |- Foo.c
-| | |- Foo.h
-| |- readme.txt --> THIS FILE
-|- platformio.ini
-|--src
- |- main.c
-
-Then in `src/main.c` you should use:
-
-#include
-#include
-
-// rest H/C/CPP code
-
-PlatformIO will find your libraries automatically, configure preprocessor's
-include paths and build them.
-
-See additional options for PlatformIO Library Dependency Finder `lib_*`:
-
-http://docs.platformio.org/en/latest/projectconf.html#lib-install
-
diff --git a/firmware/platformio.ini b/firmware/platformio.ini
deleted file mode 100644
index cfc5c18..0000000
--- a/firmware/platformio.ini
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# Project Configuration File
-#
-# A detailed documentation with the EXAMPLES is located here:
-# http://docs.platformio.org/en/latest/projectconf.html
-#
-
-# A sign `#` at the beginning of the line indicates a comment
-# Comment lines are ignored.
-
-# Simple and base environment
-# [env:mybaseenv]
-# platform = %INSTALLED_PLATFORM_NAME_HERE%
-# framework =
-# board =
-#
-# Automatic targets - enable auto-uploading
-# targets = upload
-
-[env:nodemcu]
-platform = espressif
-framework = arduino
-board = nodemcu
-lib_deps = conduit
diff --git a/firmware/src/main.ino b/firmware/src/main.ino
deleted file mode 100644
index 2562e59..0000000
--- a/firmware/src/main.ino
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
-server.ino
-Example for my library that handles ESP8266 communication with a server (even on private
-networks). Consumers of this library can simply write functions and have them
-be fired whenver the server fires a given event directed at this device. There is
-a 1-1 mapping of event to function. For example the "led" event may fire the
-ledToggle function on the device. The communication needed to get that event to the
-device and decide what funciton to all is abstracted away entirely by this library.
-
-@author: Suyash Kumar
-*/
-#include
-#include // You can include secret wifi info in a seperate file
-#include
-
-#define LED D0
-#define LED_ON 1
-#define LED_OFF 0
-
-// Fill out the below Github folks:
-const char* ssid = "mywifi";
-const char* password = "";
-const char* deviceName = "suyash_1";
-const char* apiKey = "your-api-key-here";
-const char* serverUrl = "conduit.suyash.io";
-
-Conduit conduit(deviceName, serverUrl, apiKey); // init Conduit
-int ledStatus = LED_OFF;
-
-// Toggles an LED attached on the LED pin!
-int ledToggle(){
- digitalWrite(LED, (ledStatus) ? LOW : HIGH);
- ledStatus = (ledStatus) ? LED_OFF : LED_ON;
- Serial.println("Toggled");
- conduit.publishMessage((ledStatus) ? "LED ON" : "LED OFF"); // if using built-in LED on D0, will be the REVERSE
-}
-
-// Publishes a message response to the server
-// when this function is called
-int publishMessage(){
- conduit.publishMessage("hey there");
-}
-
-// When this function is called
-// sends data to the "testing" datastream
-// to be persisted in a database on the server
-// sends a "Done" response when done
-int publishSomeData(){
- conduit.publishData("10", "testing");
- conduit.publishMessage("Done");
-}
-
-void setup(void){
- Serial.begin(115200); // Start serial
- pinMode(LED, OUTPUT); // Set LED pin to output
- digitalWrite(LED, LOW);
-
- conduit.startWIFI(ssid, password); // Config/start wifi
- conduit.init();
-
- // Conduit bindings allow you to use the
- // function name to call the associated function
- // using the conduit API
- conduit.addHandler("ledToggle", &ledToggle);
- conduit.addHandler("hello", &publishMessage);
- conduit.addHandler("publishSomeData", &publishSomeData);
-
-}
-
-void loop(void){
- conduit.handle();
-}
diff --git a/handlers/api_handlers.go b/handlers/api_handlers.go
new file mode 100644
index 0000000..2a391ab
--- /dev/null
+++ b/handlers/api_handlers.go
@@ -0,0 +1,189 @@
+package handlers
+
+import (
+ "net/http"
+
+ "encoding/json"
+
+ "time"
+
+ "github.com/julienschmidt/httprouter"
+ "github.com/satori/go.uuid"
+ "github.com/sirupsen/logrus"
+ "github.com/suyashkumar/auth"
+ "github.com/suyashkumar/conduit/db"
+ "github.com/suyashkumar/conduit/device"
+ "github.com/suyashkumar/conduit/entities"
+ sec "github.com/suyashkumar/conduit/secret"
+)
+
+// Register allows a new user to create an account with Conduit
+func Register(w http.ResponseWriter, r *http.Request, ps httprouter.Params, d device.Handler, db db.Handler, a auth.Authenticator) {
+ req := entities.RegisterRequest{}
+ err := json.NewDecoder(r.Body).Decode(&req)
+ // TODO: req validation
+ if err != nil {
+ logrus.WithError(err).Error("Could not parse RegisterRequest")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Could not parse RegisterRequest"}, 400)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (RegisterRequest)")
+ }
+ return
+ }
+
+ // Create new user:
+ u := auth.User{
+ Email: req.Email,
+ MaxPermissionLevel: auth.PERMISSIONS_USER,
+ }
+ a.Register(&u, req.Password)
+
+ // Create and add user's initial device secret
+ logrus.Info(u.UUID)
+ err = db.InsertAccountSecret(u.UUID, entities.AccountSecret{
+ UUID: uuid.NewV4(),
+ UserUUID: u.UUID,
+ Secret: sec.GetRandString(10),
+ })
+
+ if err != nil {
+ logrus.WithError(err).WithField("user_uuid", u.UUID).Error("Error upserting device secret")
+ }
+
+ sendOK(w)
+}
+
+// Login allows the user to authenticate with conduit and get a freshly minted JWT
+func Login(w http.ResponseWriter, r *http.Request, ps httprouter.Params, d device.Handler, db db.Handler, a auth.Authenticator) {
+ req := entities.LoginRequest{}
+ err := json.NewDecoder(r.Body).Decode(&req)
+ // TODO: req validation
+ if err != nil {
+ logrus.WithError(err).Error("Could not parse LoginRequest")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Could not parse LoginRequest"}, 400)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (LoginRequest)")
+ }
+ return
+ }
+
+ // Get user if exists
+ user, err := db.GetUser(auth.User{Email: req.Email})
+ if err != nil {
+ logrus.WithError(err).Error("Trouble fetching user")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Trouble fetching user"}, 400)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (Login)")
+ }
+ return
+ }
+
+ // Get user's AccountSecret to embed into Token
+ secret, err := db.GetAccountSecret(user.UUID)
+ if err != nil {
+ logrus.WithError(err).WithField("user_uuid", user.UUID).Error("Issue fetching device secret")
+ }
+
+ // Get Token for user
+ token, err := a.GetToken(req.Email, req.Password, &auth.GetTokenOpts{
+ RequestedPermissions: auth.PERMISSIONS_USER,
+ Data: auth.TokenData{ACCOUNT_SECRET_KEY: secret.Secret},
+ })
+
+ if err != nil {
+ logrus.WithError(err).Error("Error getting token for user")
+ }
+
+ res := entities.LoginResponse{Token: token}
+ sendJSON(w, res, 200)
+}
+
+// Call allows a user to issue an RPC to one of their devices and optionally get a response from the device
+func Call(w http.ResponseWriter, r *http.Request, ps httprouter.Params, d device.Handler, db db.Handler, a auth.Authenticator) {
+ req := entities.CallRequest{}
+ err := json.NewDecoder(r.Body).Decode(&req)
+ if err != nil {
+ logrus.WithError(err).Error("Could not parse CallRequest")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Could not parse CallRequest"}, 400)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (CallRequest)")
+ }
+ return
+ }
+
+ // Authenticate User
+ claims, err := a.Validate(req.Token)
+ if err == auth.ErrorValidatingToken {
+ logrus.WithField("token", req.Token).Info("Error validating token")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Error validating token"}, 401)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (CallRequest)")
+ }
+ return
+ }
+ if err != nil {
+ logrus.WithError(err).Error("Unknown error validating token")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Error validating token"}, 500)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (CallRequest)")
+ }
+ return
+ }
+
+ c := d.Call(req.DeviceName, claims.Data[ACCOUNT_SECRET_KEY], req.FunctionName, req.WaitForDeviceResponse)
+
+ if req.WaitForDeviceResponse {
+ select {
+ case res := <-c:
+ logrus.WithField("response", res).Info("Device responded")
+ r := entities.SendResponse{
+ Response: res,
+ }
+ sendJSON(w, r, 200)
+ case <-time.After(3 * time.Second):
+ logrus.Warn("Timed out waiting for device response")
+ e := entities.ErrorResponse{
+ Error: "Timed out while waiting for the device to respond",
+ }
+ sendJSON(w, e, 500)
+ }
+ } else {
+ sendOK(w)
+ }
+}
+
+// UserInfo returns information to the user about their account, including their current account secret
+func UserInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params, d device.Handler, db db.Handler, a auth.Authenticator) {
+ req := entities.UserInfoRequest{}
+ err := json.NewDecoder(r.Body).Decode(&req)
+ if err != nil {
+ logrus.WithError(err).Error("Could not parse CallRequest")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Could not parse CallRequest"}, 400)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (CallRequest)")
+ }
+ return
+ }
+
+ // Authenticate User. TODO: factor out
+ claims, err := a.Validate(req.Token)
+ if err == auth.ErrorValidatingToken {
+ logrus.WithField("token", req.Token).Info("Error validating token")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Error validating token"}, 401)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response (UserInfoRequest)")
+ }
+ return
+ }
+ if err != nil {
+ logrus.WithError(err).Error("Unknown error validating token")
+ err := sendJSON(w, entities.ErrorResponse{Error: "Error validating token"}, 500)
+ if err != nil {
+ logrus.WithError(err).Error("!!!! Could not send error JSON response")
+ }
+ return
+ }
+
+ sendJSON(w, entities.UserInfoResponse{AccountSecret: claims.Data[ACCOUNT_SECRET_KEY]}, 200)
+
+}
diff --git a/handlers/auth_helpers.go b/handlers/auth_helpers.go
new file mode 100644
index 0000000..5ac8282
--- /dev/null
+++ b/handlers/auth_helpers.go
@@ -0,0 +1 @@
+package handlers
diff --git a/handlers/constants.go b/handlers/constants.go
new file mode 100644
index 0000000..9a4ac77
--- /dev/null
+++ b/handlers/constants.go
@@ -0,0 +1,3 @@
+package handlers
+
+const ACCOUNT_SECRET_KEY = "accountSecret"
diff --git a/handlers/index.go b/handlers/index.go
new file mode 100644
index 0000000..54cce54
--- /dev/null
+++ b/handlers/index.go
@@ -0,0 +1,11 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+func Index(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ http.ServeFile(w, r, "public/index.html")
+}
diff --git a/handlers/send_helpers.go b/handlers/send_helpers.go
new file mode 100644
index 0000000..49656d1
--- /dev/null
+++ b/handlers/send_helpers.go
@@ -0,0 +1,24 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/suyashkumar/conduit/entities"
+)
+
+func sendOK(w http.ResponseWriter) {
+ sendJSON(w, entities.GenericResponse{Message: "OK"}, 200)
+}
+
+func sendJSON(w http.ResponseWriter, v interface{}, statusCode int) error {
+ resBytes, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(statusCode)
+ fmt.Fprintf(w, string(resBytes))
+ return nil
+}
diff --git a/log/log.go b/log/log.go
new file mode 100644
index 0000000..6343c80
--- /dev/null
+++ b/log/log.go
@@ -0,0 +1,22 @@
+package log
+
+import (
+ "github.com/rifflock/lfshook"
+ "github.com/sirupsen/logrus"
+ "github.com/suyashkumar/conduit/config"
+)
+
+// Configure logging for this project
+func Configure() {
+ f := config.Get(config.LogFile)
+ h := lfshook.NewHook(lfshook.PathMap{
+ logrus.InfoLevel: f,
+ logrus.WarnLevel: f,
+ logrus.ErrorLevel: f,
+ logrus.DebugLevel: f,
+ logrus.FatalLevel: f,
+ logrus.PanicLevel: f,
+ }, &logrus.JSONFormatter{})
+
+ logrus.AddHook(h)
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..d4ee13f
--- /dev/null
+++ b/main.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/rs/cors"
+ "github.com/sirupsen/logrus"
+ "github.com/suyashkumar/auth"
+ "github.com/suyashkumar/conduit/config"
+ db2 "github.com/suyashkumar/conduit/db"
+ "github.com/suyashkumar/conduit/device"
+ "github.com/suyashkumar/conduit/log"
+ "github.com/suyashkumar/conduit/routes"
+)
+
+func main() {
+ log.Configure()
+
+ d := device.NewHandler()
+ db, err := db2.NewHandler(config.Get(config.DBConnString))
+ if err != nil {
+ logrus.WithError(err).WithField("DBConnString", config.Get(config.DBConnString)).Fatal("Could not connect to DB")
+ }
+ a, err := auth.NewAuthenticatorFromGORM(db.GetDB(), []byte(config.Get(config.SigningKey)))
+ if err != nil {
+ logrus.WithError(err).Fatal("Could not connect to or init database")
+ }
+ r := routes.Build(d, db, a)
+ handler := cors.Default().Handler(r)
+
+ p := fmt.Sprintf(":%s", config.Get(config.Port))
+
+ if config.Get(config.UseSSL) == "false" {
+ logrus.WithField("port", p).Info("Serving without SSL")
+ err := http.ListenAndServe(p, handler)
+ logrus.Fatal(err)
+ } else {
+ logrus.Info("Serving with SSL")
+ err := http.ListenAndServeTLS(
+ p,
+ config.Get(config.CertKey),
+ config.Get(config.PrivKey),
+ handler,
+ )
+ // TODO: reroute http requests to https
+ logrus.Fatal(err)
+ }
+
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..976c106
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,6 @@
+
+
+
Welcome to the Conduit API Server. You probably want to go to https://conduit.suyash.io