// Main package for Tog Switchifier. See accompanying README.md file. // Copyright (c) 2016, Serge Bazanski // 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" "io/ioutil" "net/http" "os" "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 { res.Close() return err } if lastState != state { shouldStore = true } } res.Close() 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) defer data.Close() 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 secret_path 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.StringVar(&secret_path, "secret_path", "", "File with secret for state updates.") flag.Parse() if dbPath == "" { glog.Exit("Please provide a database path.") } if secret_path != "" { file, err := os.Open(secret_path) if err != nil { glog.Exit(err) } secretData, err := ioutil.ReadAll(file) if err != nil { glog.Exit(err) } secretParts := strings.Split(string(secretData), "\n") secret = secretParts[0] } glog.Infof("Starting switchifier...") db, err := sql.Open("sqlite3", dbPath) if err != nil { glog.Exit(err) } defer db.Close() db.SetMaxOpenConns(1) 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)) }