From b2f62c7bb04978d5f9154946d160abad7d960529 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 13 Jan 2024 23:26:04 +0100 Subject: [PATCH] First implementation of new config format - Functionally equivalent to old version at present - Temperature and Humidity sensors not handled yet - Moved HTTP handlers around --- config/config.go | 59 ++++++++++++++++++++++++++---- handlers/root.go | 35 ++++++++++++++++++ handlers/{handlers.go => state.go} | 34 +++-------------- main.go | 20 +++++----- util/config.go | 48 ------------------------ util/credentials.go | 20 ++++++++++ util/util.go | 30 ++------------- 7 files changed, 125 insertions(+), 121 deletions(-) create mode 100644 handlers/root.go rename handlers/{handlers.go => state.go} (62%) delete mode 100644 util/config.go create mode 100644 util/credentials.go diff --git a/config/config.go b/config/config.go index 2a40332..8092560 100644 --- a/config/config.go +++ b/config/config.go @@ -1,11 +1,54 @@ package config -// Configuration represents the settings needed to configure spaceapid -type Configuration struct { - // The HTTP BasicAuth username for door status updates - BAUsername string - // The HTTP BasicAuth password for door status updates - BAPassword string - // The path to the JSON with initial values - TemplatePath string +import ( + "bytes" + "encoding/json" + "log" + "os" + "path/filepath" + "slices" +) + +const ( + envConfigPath = "CONFIG_PATH" +) + +// getConfigPath gets the spaceapid configuration from the respective environment variables +func getConfigPath() string { + // JSON template path + configPath, ok := os.LookupEnv(envConfigPath) + if !ok || configPath == "" { + log.Fatalln("Could not retrieve", envConfigPath, "env variable or variable is empty") + } + // Save as absolute path + absConfigPath, err := filepath.Abs(configPath) + if err != nil { + log.Fatalln("Failed converting", configPath, "to absolute path:", err) + } + return absConfigPath +} + +// ParseConfiguration returns the config from file +func ParseConfiguration() (conf SpaceapidConfig) { + log.Println("Parsing configuration file") + // Read file + file, err := os.ReadFile(getConfigPath()) + if err != nil { + log.Fatalln("Failed reading file:", err) + } + + // Parse JSON + dec := json.NewDecoder(bytes.NewReader(file)) + dec.DisallowUnknownFields() + err = dec.Decode(&conf) + if err != nil { + log.Fatalln("Could not parse spaceapid config file:", err) + } + + // Check if compatible with v14 + if !slices.Contains(conf.Response.APICompatibility, "14") { + log.Fatalln("Provided file doesn't specify compatibility with API version 14") + } + + return } diff --git a/handlers/root.go b/handlers/root.go new file mode 100644 index 0000000..1015f49 --- /dev/null +++ b/handlers/root.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" +) + +func Root(resp *types.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Check if GET method + if r.Method != http.MethodGet { + log.Println("Wrong METHOD from", r.RemoteAddr) + w.Header().Set("Allow", http.MethodGet) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // Serialize response + response, err := json.Marshal(resp) + if err != nil { + log.Println("Failed to serialize JSON response:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Respond with OK + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(response) + } +} diff --git a/handlers/handlers.go b/handlers/state.go similarity index 62% rename from handlers/handlers.go rename to handlers/state.go index e0bdfeb..dcd191a 100644 --- a/handlers/handlers.go +++ b/handlers/state.go @@ -1,49 +1,25 @@ package handlers import ( - "encoding/json" "io" "log" "net/http" "strconv" "time" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/util" ) -func Root(resp *types.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Check if GET method - if r.Method != http.MethodGet { - log.Println("Wrong METHOD from", r.RemoteAddr) - w.Header().Set("Allow", http.MethodGet) - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - // Serialize response - response, err := json.Marshal(resp) - if err != nil { - log.Println("Failed to serialize JSON response:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Respond with OK - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(response) - } -} - func StateOpen( - validUsername, validPassword string, resp *types.SpaceAPIResponseV14, + authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, + resp *types.SpaceAPIResponseV14, ) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // Check BasicAuth credentials username, password, ok := r.BasicAuth() - if !ok || username != validUsername || password != validPassword { + if !ok || !util.CheckCredentials(authDB, validCredentials, username, password) { log.Println("Unauthorized request from", r.RemoteAddr) w.Header().Set("WWW-Authenticate", "Basic realm=\"space-api\"") w.WriteHeader(http.StatusUnauthorized) diff --git a/main.go b/main.go index 56ac017..98b8b05 100644 --- a/main.go +++ b/main.go @@ -7,20 +7,18 @@ import ( "os/signal" "syscall" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" "gitlab.hamburg.ccc.de/ccchh/spaceapid/handlers" "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" "gitlab.hamburg.ccc.de/ccchh/spaceapid/util" ) func main() { - log.Println("Reading configuration values") - config := util.GetConfiguration() - - log.Println("Reading initial SpaceAPI response from", config.TemplatePath) - spaceApiResponse := util.ParseTemplate(config.TemplatePath) + // Get spaceapid configuration + conf := config.ParseConfiguration() // Merge old state if present - util.MergeOldState(&spaceApiResponse) + util.MergeOldState(&conf.Response) // Register signal handler sc := make(chan os.Signal, 1) @@ -30,11 +28,15 @@ func main() { log.Println("Saving state and shutting down...") util.SaveCurrentState(*resp) os.Exit(0) - }(&spaceApiResponse) + }(&conf.Response) // Register HTTP handlers - http.HandleFunc("/", handlers.Root(&spaceApiResponse)) - http.HandleFunc("/state/open", handlers.StateOpen(config.BAUsername, config.BAPassword, &spaceApiResponse)) + http.HandleFunc("/", + handlers.Root(&conf.Response), + ) + http.HandleFunc("/state/open", + handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response), + ) // Start webserver log.Println("Starting HTTP server...") diff --git a/util/config.go b/util/config.go deleted file mode 100644 index f42eaa1..0000000 --- a/util/config.go +++ /dev/null @@ -1,48 +0,0 @@ -package util - -import ( - "log" - "os" - "path/filepath" - - "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" -) - -const ( - envBAUsername = "BA_USERNAME" - envBAPassword = "BA_PASSWORD" - envJSONTemplatePath = "JSON_TEMPLATE_PATH" -) - -// GetConfiguration gets the spaceapid configuration from the respective environment variables -func GetConfiguration() (c config.Configuration) { - var ( - success bool - err error - ) - - // HTTP BasicAuth username - c.BAUsername, success = os.LookupEnv(envBAUsername) - if !success || c.BAUsername == "" { - log.Fatalln("Could not retrieve env variable", envBAUsername, "or variable is empty") - } - - // HTTP BasicAuth password - c.BAPassword, success = os.LookupEnv(envBAPassword) - if !success || c.BAPassword == "" { - log.Fatalln("Could not retrieve", envBAPassword, "env variable or variable is empty") - } - - // JSON template path - templatePath, success := os.LookupEnv(envJSONTemplatePath) - if !success || templatePath == "" { - log.Fatalln("Could not retrieve", envJSONTemplatePath, "env variable or variable is empty") - } - // Save as absolute path - c.TemplatePath, err = filepath.Abs(templatePath) - if err != nil { - log.Fatalln("Failed converting", templatePath, "to absolute path:", err) - } - - return -} diff --git a/util/credentials.go b/util/credentials.go new file mode 100644 index 0000000..9352d5e --- /dev/null +++ b/util/credentials.go @@ -0,0 +1,20 @@ +package util + +import ( + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" +) + +// CheckCredentials validates whether a given username/password pair matches an +// entry in the authDB whose id is present in the validCredentials list +func CheckCredentials( + authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, username, password string, +) bool { + for _, id := range validCredentials { + if cred, present := authDB[id]; present { + if cred.Username == username && cred.Password == password { + return true + } + } + } + return false +} diff --git a/util/util.go b/util/util.go index e009bb2..726d885 100644 --- a/util/util.go +++ b/util/util.go @@ -1,42 +1,18 @@ package util import ( - "bytes" "encoding/json" "errors" "log" "os" "path" - "slices" "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) -const savedStateJSONPath = "/var/lib/spaceapid/spaceapid-state.json" - -// ParseTemplate parses the given file and -func ParseTemplate(file string) (resp types.SpaceAPIResponseV14) { - // Read template file - template, err := os.ReadFile(file) - if err != nil { - log.Fatalln("Failed reading file:", err) - } - - // Parse JSON - dec := json.NewDecoder(bytes.NewReader(template)) - dec.DisallowUnknownFields() - err = dec.Decode(&resp) - if err != nil { - log.Fatalln("Could not parse SpaceAPI response template:", err) - } - - // Check if compatible with v14 - if !slices.Contains(resp.APICompatibility, "14") { - log.Fatalln("Provided template doesn't specify compatibility with API version 14") - } - - return -} +const ( + savedStateJSONPath = "/var/lib/spaceapid/spaceapid-state.json" +) // MergeOldState merges a given SpaceAPIResponse with the state saved at the time of last program exit and then deletes // the file containing the old state.