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