commit
9510e8890a
|
@ -0,0 +1,23 @@
|
||||||
|
Copyright (c) 2016, Serge Bazanski <s@bazanski.pl>
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
Tog Switchifier Service
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This is a small service that collects status updates from the knife switch at Tog.
|
||||||
|
|
||||||
|
Building
|
||||||
|
--------
|
||||||
|
|
||||||
|
I assume you already have a GOPATH set up. This code was tested with and developed againat Go 1.7.
|
||||||
|
|
||||||
|
$ go get github.com/Tog-Hackerspace/switchifier
|
||||||
|
$ go build github.com/Tog-Hackerspace/switchifier
|
||||||
|
$ file switchifier
|
||||||
|
|
||||||
|
Runnning
|
||||||
|
--------
|
||||||
|
|
||||||
|
This binary binds to a TCP port and listens for incoming TCP requests. It uses a SQLite database to store historical state data, and keeps a few other things (like last ping from client) in memory.
|
||||||
|
|
||||||
|
Since this service authenticates its' client by a preshared secret, you should probably reverse-proxy this and wrap it in TLS or use another secure tunnel.
|
||||||
|
|
||||||
|
Here's a few useful flags:
|
||||||
|
|
||||||
|
- `--db_path`: Path to the SQLite database. The default is `switchifier.db` in the current working directory.
|
||||||
|
- `--bind_address`: Host/port to bind to. By default `:8080`, so port 8080 on all interfaces.
|
||||||
|
- `--secret`: The preshared secret used to authenticate the client.
|
||||||
|
|
||||||
|
|
||||||
|
Future work
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- SSL client cert authentication support.
|
||||||
|
- GRPC API.
|
||||||
|
- State overrides.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
See COPYING.
|
||||||
|
|
||||||
|
API
|
||||||
|
===
|
||||||
|
|
||||||
|
status
|
||||||
|
------
|
||||||
|
`GET /api/1/switchifier/status` - Get current switch status.
|
||||||
|
|
||||||
|
Takes no parameters.
|
||||||
|
|
||||||
|
Returns a JSON dictrionary with the following keys:
|
||||||
|
|
||||||
|
- `okay` - true if request was successful, false if an error occured.
|
||||||
|
- `error` - a string detailing the error that occured if `okay` is false, else empty.
|
||||||
|
- `data` - a dictionary containing response data if `okay` is true, else undefined.
|
||||||
|
- `open` - true if the space is currently marked as open, else false
|
||||||
|
- `since` - nanoseconds since Unix epoch of the last `data.open` state change
|
||||||
|
- `lastKeepalive` - nanoseconds since Unix epoch of the last client update. May be zero if data is unavailable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
$ curl 127.0.0.1:8080/api/1/switchifier/status
|
||||||
|
{"okay":true,"error":"","data":{"open":true,"since":1475744318547307236,"lastKeepalive":1475744318562059736}}
|
||||||
|
|
||||||
|
update
|
||||||
|
------
|
||||||
|
`POST /api/1/switchifier/update` - Update knife switch status from client.
|
||||||
|
|
||||||
|
Takes the following form parameters:
|
||||||
|
|
||||||
|
- `secret` - the preshared secret required for client updates.
|
||||||
|
- `status` - the state of the current status. Truthy (open) values are `[Tt]rue`, `1`. Falsey (closed) values are everything else.
|
||||||
|
|
||||||
|
Returns 200 if the update was succesfull. All other 5xx and 4xx codes shall be interpreted as errors. The client should periodically call this endpoint regardless of success.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import requests
|
||||||
|
|
||||||
|
r = requests.post('http://127.0.0.1:8080/api/1/switchifier/update', data={
|
||||||
|
'secret': "changeme",
|
||||||
|
'value': "true",
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
// Main package for Tog Switchifier. See accompanying README.md file.
|
||||||
|
|
||||||
|
// Copyright (c) 2016, Serge Bazanski <s@bazanski.pl>
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer.
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type switchApp struct {
|
||||||
|
db *sql.DB
|
||||||
|
secret string
|
||||||
|
lastUpdate int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSwitchApp(db *sql.DB) switchApp {
|
||||||
|
return switchApp{
|
||||||
|
db: db,
|
||||||
|
secret: secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *switchApp) RunSchemaUpdate() error {
|
||||||
|
sqlStmt := `
|
||||||
|
CREATE TABLE IF NOT EXISTS switch_state_change(
|
||||||
|
timestamp INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
interval INTEGER NOT NULL,
|
||||||
|
state BOOLEAN NOT NULL
|
||||||
|
);`
|
||||||
|
_, err := s.db.Exec(sqlStmt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *switchApp) UpdateState(state bool) error {
|
||||||
|
defer func() {
|
||||||
|
s.lastUpdate = time.Now().UnixNano()
|
||||||
|
}()
|
||||||
|
sqlStmt := `
|
||||||
|
SELECT state FROM switch_state_change
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 1;`
|
||||||
|
res, err := s.db.Query(sqlStmt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldStore := false
|
||||||
|
if !res.Next() {
|
||||||
|
// Always store a state if the database is empty
|
||||||
|
shouldStore = true
|
||||||
|
} else {
|
||||||
|
// Otherwise store if there was a state change
|
||||||
|
var lastState bool
|
||||||
|
if err = res.Scan(&lastState); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if lastState != state {
|
||||||
|
shouldStore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !shouldStore {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.Info("Storing state change in database...")
|
||||||
|
timestamp := time.Now().UnixNano()
|
||||||
|
interval := int64(0)
|
||||||
|
if s.lastUpdate != 0 {
|
||||||
|
interval = timestamp - s.lastUpdate
|
||||||
|
}
|
||||||
|
sqlStmt = `INSERT INTO switch_state_change(timestamp, interval, state) VALUES (?, ?, ?)`
|
||||||
|
_, err = s.db.Exec(sqlStmt, timestamp, interval, state)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *switchApp) HandleAPIUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
glog.Infof("%s: %s %s", r.RemoteAddr, r.Method, r.URL.String())
|
||||||
|
if r.Method != "POST" {
|
||||||
|
w.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Yes, this should be a constant-time comparison.
|
||||||
|
if secret := r.FormValue("secret"); secret != s.secret {
|
||||||
|
w.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newValueStr := strings.ToLower(r.FormValue("value"))
|
||||||
|
if newValueStr == "" {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var newValue bool
|
||||||
|
if strings.HasPrefix(newValueStr, "t") || newValueStr == "1" {
|
||||||
|
newValue = true
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.Infof("Switch state: %v.", newValue)
|
||||||
|
err := s.UpdateState(newValue)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
glog.Errorf("Error in handler: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiGetResponse struct {
|
||||||
|
Okay bool `json:"okay"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Data struct {
|
||||||
|
Open bool `json:"open"`
|
||||||
|
Since int64 `json:"since"`
|
||||||
|
LastKeepalive int64 `json:"lastKeepalive"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *switchApp) HandleAPIGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := apiGetResponse{}
|
||||||
|
defer func() {
|
||||||
|
if resp.Okay {
|
||||||
|
resp.Data.LastKeepalive = s.lastUpdate
|
||||||
|
}
|
||||||
|
respJson, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte(`{ okay: false, error: "Internal server error." }`))
|
||||||
|
glog.Errorf("Error in handler: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(respJson)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sqlStmt := `SELECT timestamp, state FROM switch_state_change
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 1`
|
||||||
|
data, err := s.db.Query(sqlStmt)
|
||||||
|
if err != nil {
|
||||||
|
resp.Okay = false
|
||||||
|
resp.Error = "Database error."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !data.Next() {
|
||||||
|
resp.Okay = false
|
||||||
|
resp.Error = "No switch state yet."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var timestamp int64
|
||||||
|
var state bool
|
||||||
|
if data.Scan(×tamp, &state) != nil {
|
||||||
|
resp.Okay = false
|
||||||
|
resp.Error = "Database error."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Okay = true
|
||||||
|
resp.Data.Open = state
|
||||||
|
resp.Data.Since = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbPath string
|
||||||
|
bindAddress string
|
||||||
|
secret string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&dbPath, "db_path", "./switchifier.db", "Path to the SQlite3 database.")
|
||||||
|
flag.StringVar(&bindAddress, "bind_address", ":8080", "Address to bind HTTP server to.")
|
||||||
|
flag.StringVar(&secret, "secret", "changeme", "Secret for state updates.")
|
||||||
|
flag.Parse()
|
||||||
|
if dbPath == "" {
|
||||||
|
glog.Exit("Please provide a database path.")
|
||||||
|
}
|
||||||
|
glog.Infof("Starting switchifier...")
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
glog.Exit(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
switchApp := newSwitchApp(db)
|
||||||
|
if err = switchApp.RunSchemaUpdate(); err != nil {
|
||||||
|
glog.Exitf("Could not run schema update: %v", err)
|
||||||
|
}
|
||||||
|
glog.Info("Schema updates applied.")
|
||||||
|
|
||||||
|
http.HandleFunc("/api/1/switchifier/update", switchApp.HandleAPIUpdate)
|
||||||
|
http.HandleFunc("/api/1/switchifier/status", switchApp.HandleAPIGet)
|
||||||
|
|
||||||
|
glog.Infof("Listening on %s.", bindAddress)
|
||||||
|
glog.Exit(http.ListenAndServe(bindAddress, nil))
|
||||||
|
}
|
Loading…
Reference in New Issue