Initial commit.

Signed-off-by: Sergiusz 'q3k' Bazański <q3k@q3k.org>
master
q3k 2016-10-06 12:01:49 +03:00
commit 9510e8890a
3 changed files with 329 additions and 0 deletions

23
COPYING Normal file
View File

@ -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

84
README.md Normal file
View File

@ -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",
})

222
main.go Normal file
View File

@ -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(&timestamp, &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))
}