From 4b41acfa7b467708c9728fbdabfb5cb0b59fb343 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 5 Nov 2023 20:23:31 +0100 Subject: [PATCH] Major Refactoring - Move components to separate packages - Fix HTTP header for 401 response - Add documentation --- config/config.go | 11 +++++ handlers/handlers.go | 87 +++++++++++++++++++++++++++++++++ main.go | 112 ++++--------------------------------------- util/config.go | 48 +++++++++++++++++++ util/util.go | 36 ++++++++++++++ 5 files changed, 191 insertions(+), 103 deletions(-) create mode 100644 config/config.go create mode 100644 handlers/handlers.go create mode 100644 util/config.go create mode 100644 util/util.go diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..2a40332 --- /dev/null +++ b/config/config.go @@ -0,0 +1,11 @@ +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 +} diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..e0bdfeb --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,87 @@ +package handlers + +import ( + "encoding/json" + "io" + "log" + "net/http" + "strconv" + "time" + + "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) + } +} + +func StateOpen( + validUsername, validPassword string, 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 { + log.Println("Unauthorized request from", r.RemoteAddr) + w.Header().Set("WWW-Authenticate", "Basic realm=\"space-api\"") + w.WriteHeader(http.StatusUnauthorized) + return + } + + // Check if PUT method + if r.Method != http.MethodPut { + log.Println("Wrong METHOD from", r.RemoteAddr) + w.Header().Set("Allow", http.MethodPut) + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // Read request body + body, err := io.ReadAll(r.Body) + if err != nil { + log.Println("Failed to read request body from", r.RemoteAddr) + w.WriteHeader(http.StatusInternalServerError) + _, _ = io.WriteString(w, "Failed reading HTTP request body") + return + } + + // Parse request body + newState, err := strconv.ParseBool(string(body)) + if err != nil { + log.Println("Failed to parse request body from", r.RemoteAddr) + w.WriteHeader(http.StatusBadRequest) + _, _ = io.WriteString(w, "HTTP request body should either be true or false") + return + } + + // Set SpaceAPI response values + resp.State.Open = newState + resp.State.LastChange = time.Now().Unix() + + // Respond with OK + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, "Update Successful") + } +} diff --git a/main.go b/main.go index 9759334..6d8ebe0 100644 --- a/main.go +++ b/main.go @@ -1,117 +1,23 @@ package main import ( - "encoding/json" - "io" "log" "net/http" - "os" - "path/filepath" - "slices" - "strconv" - "time" - "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/handlers" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/util" ) func main() { - var err error + log.Println("Reading configuration values") + config := util.GetConfiguration() - validUsername, success := os.LookupEnv("DOORIS_USERNAME") - if !success || validUsername == "" { - log.Fatalln("Could not retrieve DOORIS_API_KEY env variable or variable is empty") - } + log.Println("Reading initial SpaceAPI response from", config.TemplatePath) + spaceApiResponse := util.ParseTemplate(config.TemplatePath) - validPassword, success := os.LookupEnv("DOORIS_PASSWORD") - if !success || validPassword == "" { - log.Fatalln("Could not retrieve DOORIS_API_KEY env variable or variable is empty") - } - - templatePath, success := os.LookupEnv("SPACE_API_JSON_TEMPLATE_PATH") - if !success || templatePath == "" { - log.Fatalln("Could not retrieve SPACE_API_JSON_TEMPLATE_PATH env variable or variable is empty") - } - - templatePathAbs, err := filepath.Abs(templatePath) - if err != nil { - log.Fatalln("Failed converting", templatePath, "to absolute path:", err) - } - - log.Println("Reading initial SpaceAPI response from", templatePathAbs) - initialJson, err := os.ReadFile(templatePathAbs) - if err != nil { - log.Fatalln("Failed reading file:", err) - } - - spaceApiResponse := new(types.SpaceAPIResponseV14) - err = json.Unmarshal(initialJson, spaceApiResponse) - if err != nil { - log.Fatalln("Could not parse SpaceAPI response template:", err) - } - - if !slices.Contains(spaceApiResponse.APICompatibility, "14") { - log.Fatalln("Provided SpaceAPI response doesn't specify compatibility with API version 14") - } - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - log.Println("Wrong METHOD from", r.RemoteAddr) - w.Header().Set("allow", http.MethodGet) - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - response, err := json.Marshal(spaceApiResponse) - if err != nil { - log.Println("Failed to serialize JSON response:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(response) - }) - - http.HandleFunc("/state/open", func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - if !ok || username != validUsername || password != validPassword { - log.Println("Unauthorized request from", r.RemoteAddr) - w.Header().Set("www-authentication", "Basic realm=\"space-api\"") - w.WriteHeader(http.StatusUnauthorized) - return - } - - if r.Method != http.MethodPut { - log.Println("Wrong METHOD from", r.RemoteAddr) - w.Header().Set("allow", http.MethodPut) - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - log.Println("Failed to read request body from", r.RemoteAddr) - w.WriteHeader(http.StatusInternalServerError) - _, _ = io.WriteString(w, "Failed reading HTTP request body") - return - } - - newState, err := strconv.ParseBool(string(body)) - if err != nil { - log.Println("Failed to parse request body from", r.RemoteAddr) - w.WriteHeader(http.StatusBadRequest) - _, _ = io.WriteString(w, "HTTP request body should either be true or false") - return - } - - spaceApiResponse.State.Open = newState - spaceApiResponse.State.LastChange = time.Now().Unix() - - w.WriteHeader(http.StatusOK) - _, _ = io.WriteString(w, "Update Successful") - }) + // Register HTTP handlers + http.HandleFunc("/", handlers.Root(&spaceApiResponse)) + http.HandleFunc("/state/open", handlers.StateOpen(config.BAUsername, config.BAPassword, &spaceApiResponse)) log.Println("Starting HTTP server...") log.Fatalln(http.ListenAndServe(":8080", nil)) diff --git a/util/config.go b/util/config.go new file mode 100644 index 0000000..f42eaa1 --- /dev/null +++ b/util/config.go @@ -0,0 +1,48 @@ +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/util.go b/util/util.go new file mode 100644 index 0000000..cca50dd --- /dev/null +++ b/util/util.go @@ -0,0 +1,36 @@ +package util + +import ( + "bytes" + "encoding/json" + "log" + "os" + "slices" + + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" +) + +// 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 +}