From 883d25898e36e46ebcebe252d73993b54c211093 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 6 Jan 2024 19:43:51 +0100 Subject: [PATCH 01/15] Small refactoring --- .gitignore | 3 +++ main.go | 6 +++--- util/util.go | 7 ++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 2e1f8da..8a7f400 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ # go build output spaceapid + +# Saved state +spaceapid-state.json diff --git a/main.go b/main.go index 7678238..56ac017 100644 --- a/main.go +++ b/main.go @@ -25,12 +25,12 @@ func main() { // Register signal handler sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM) - go func(ch chan os.Signal, resp *types.SpaceAPIResponseV14) { - <-ch + go func(resp *types.SpaceAPIResponseV14) { + <-sc log.Println("Saving state and shutting down...") util.SaveCurrentState(*resp) os.Exit(0) - }(sc, &spaceApiResponse) + }(&spaceApiResponse) // Register HTTP handlers http.HandleFunc("/", handlers.Root(&spaceApiResponse)) diff --git a/util/util.go b/util/util.go index e43123d..23e1773 100644 --- a/util/util.go +++ b/util/util.go @@ -16,7 +16,6 @@ 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 { @@ -84,14 +83,12 @@ func SaveCurrentState(response types.SpaceAPIResponseV14) { log.Fatalln("Failed creating", savedStateJSONPath, ", aborting... error:", err) } - // Open persistent state file for reading + // Open persistent state file file, err := os.OpenFile(savedStateJSONPath, os.O_RDWR|os.O_CREATE, 0644) if err != nil { log.Fatalln("Failed opening", savedStateJSONPath, "while trying to save current state... error:", err) } - defer func(file *os.File) { - _ = file.Close() - }(file) + defer file.Close() // Create persistent state persistentStateV14 := types.PersistentStateV14{ From ae9b429950fc164adf0d142e83b9f911c7d7e1ef Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 13 Jan 2024 23:14:56 +0100 Subject: [PATCH 02/15] Add log output in util.MergeOldState --- util/util.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/util/util.go b/util/util.go index 23e1773..e009bb2 100644 --- a/util/util.go +++ b/util/util.go @@ -46,6 +46,8 @@ func MergeOldState(response *types.SpaceAPIResponseV14) { oldState []byte ) + log.Println("Merging old state from", savedStateJSONPath, "...") + // Check if state.json is present _, err = os.Stat(savedStateJSONPath) if err != nil { From 951b4edc17d23ff35f30d4f5fe22bb94d6fc823f Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 13 Jan 2024 23:25:54 +0100 Subject: [PATCH 03/15] Rename config template and switch to new format --- ccchh-template.json | 42 ------------------------ config-template.json | 77 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 42 deletions(-) delete mode 100644 ccchh-template.json create mode 100644 config-template.json diff --git a/ccchh-template.json b/ccchh-template.json deleted file mode 100644 index 70d7a62..0000000 --- a/ccchh-template.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "api_compatibility": [ - "14" - ], - "space": "CCCHH", - "logo": "https://next.hamburg.ccc.de/images/logo.svg", - "ext_ccc": "erfa", - "url": "https://hamburg.ccc.de/", - "location": { - "address": "Zeiseweg 9, 22765 Hamburg, Germany", - "lon": 9.9445899999999998, - "lat": 53.55836 - }, - "contact": { - "phone": "+494023830150", - "irc": "ircs://irc.hackint.org:6697/#ccchh", - "mastodon": "@ccchh@chaos.social", - "email": "mail@hamburg.ccc.de", - "ml": "talk@hamburg.ccc.de", - "matrix": "#ccchh:hamburg.ccc.de" - }, - "feeds": { - "blog": { - "type": "application/atom+xml", - "url": "https://hamburg.ccc.de/feed.xml" - }, - "calendar": { - "type": "ical", - "url": "webcal://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g/?export" - } - }, - "links": [ - { - "name": "Wiki", - "url": "https://wiki.ccchh.net" - }, - { - "name": "GitLab", - "url": "https://gitlab.hamburg.ccc.de" - } - ] -} diff --git a/config-template.json b/config-template.json new file mode 100644 index 0000000..49bc770 --- /dev/null +++ b/config-template.json @@ -0,0 +1,77 @@ +{ + "credentials": { + "home-assistant": { + "username": "home-assistant", + "password": "hamiau" + }, + "dooris-hauptraum": { + "username": "dooris-hauptraum", + "password": "doorimiau" + } + }, + "dynamic": { + "sensors": { + "temperature": [ + { + "sensor_data": { + "name": "Küche", + "location": "Hauptraum", + "unit": "C" + }, + "allowed_credentials": [ + "home-assistant" + ] + } + ] + }, + "state": { + "open": { + "allowed_credentials": [ + "dooris-hauptraum" + ] + } + } + }, + "static": { + "api_compatibility": [ + "14" + ], + "space": "CCCHH", + "logo": "https://next.hamburg.ccc.de/images/logo.svg", + "ext_ccc": "erfa", + "url": "https://hamburg.ccc.de/", + "location": { + "address": "Zeiseweg 9, 22765 Hamburg, Germany", + "lon": 9.9445899999999998, + "lat": 53.55836 + }, + "contact": { + "phone": "+49 40 23830150", + "irc": "ircs://irc.hackint.org:6697/#ccchh", + "mastodon": "@ccchh@chaos.social", + "email": "mail@hamburg.ccc.de", + "ml": "talk@hamburg.ccc.de", + "matrix": "#ccchh:hamburg.ccc.de" + }, + "feeds": { + "blog": { + "type": "application/atom+xml", + "url": "https://hamburg.ccc.de/feed.xml" + }, + "calendar": { + "type": "ical", + "url": "webcal://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g/?export" + } + }, + "links": [ + { + "name": "Wiki", + "url": "https://wiki.ccchh.net" + }, + { + "name": "GitLab", + "url": "https://gitlab.hamburg.ccc.de" + } + ] + } +} From ab0a91d5f23f43d44ece459fe653f79e2ae0da92 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 13 Jan 2024 23:13:56 +0100 Subject: [PATCH 04/15] Add fields for Temperature and Humidity sensors to SpaceAPIResponseV14 --- types/v14.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/types/v14.go b/types/v14.go index 5f014ba..341013c 100644 --- a/types/v14.go +++ b/types/v14.go @@ -23,6 +23,10 @@ type SpaceAPIResponseV14 struct { ML string `json:"ml"` Matrix string `json:"matrix"` } `json:"contact"` + Sensors struct { + Temperature []EnvironmentSensor `json:"temperature"` + Humidity []EnvironmentSensor `json:"humidity"` + } `json:"sensors"` Feeds struct { Blog struct { Type string `json:"type"` @@ -39,6 +43,14 @@ type SpaceAPIResponseV14 struct { } `json:"links"` } +type EnvironmentSensor struct { + Value float32 `json:"value"` + Unit string `json:"unit"` + Location string `json:"location"` + Name string `json:"name"` + Description string `json:"description"` +} + type PersistentStateV14 struct { State struct { Open bool `json:"open"` From 7bb676887fa62d7a5056f7f9ab4f4f8af88dcc4e Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 13 Jan 2024 23:16:10 +0100 Subject: [PATCH 05/15] Add type definitions for new config format --- config/types.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 config/types.go diff --git a/config/types.go b/config/types.go new file mode 100644 index 0000000..a6a3728 --- /dev/null +++ b/config/types.go @@ -0,0 +1,41 @@ +package config + +import ( + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" +) + +type HTTPBACredentialID string + +type HTTPBACredentials map[HTTPBACredentialID]HTTPBACredential + +type HTTPBACredential struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type EnvironmentSensorConfig struct { + SensorData types.EnvironmentSensor `json:"sensor_data"` + AllowedCredentials []HTTPBACredentialID `json:"allowed_credentials"` +} + +type DynamicStateConfig struct { + Sensors struct { + Temperature []EnvironmentSensorConfig `json:"temperature"` + Humidity []EnvironmentSensorConfig `json:"humidity"` + } `json:"sensors"` + State struct { + Open struct { + AllowedCredentials []HTTPBACredentialID `json:"allowed_credentials"` + } `json:"open"` + } `json:"state"` +} + +// SpaceapidConfig is the representation of the config.json file +type SpaceapidConfig struct { + // A list of username/password pairs for BasicAuth endpoints + Credentials HTTPBACredentials `json:"credentials"` + // The dynamic part of the spaceapi JSON + Dynamic DynamicStateConfig `json:"dynamic"` + // The static part of the spaceapi JSON + Response types.SpaceAPIResponseV14 `json:"static"` +} From b2f62c7bb04978d5f9154946d160abad7d960529 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 13 Jan 2024 23:26:04 +0100 Subject: [PATCH 06/15] 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. From c3f51f2e36f7cf37fd05c78386bee55865c5dc22 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 00:59:57 +0100 Subject: [PATCH 07/15] Rename "static" to "response" for consistency --- config-template.json | 2 +- config/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config-template.json b/config-template.json index 49bc770..9be6055 100644 --- a/config-template.json +++ b/config-template.json @@ -32,7 +32,7 @@ } } }, - "static": { + "response": { "api_compatibility": [ "14" ], diff --git a/config/types.go b/config/types.go index a6a3728..c8d9524 100644 --- a/config/types.go +++ b/config/types.go @@ -37,5 +37,5 @@ type SpaceapidConfig struct { // The dynamic part of the spaceapi JSON Dynamic DynamicStateConfig `json:"dynamic"` // The static part of the spaceapi JSON - Response types.SpaceAPIResponseV14 `json:"static"` + Response types.SpaceAPIResponseV14 `json:"response"` } From daac0b3b9ec247947e7c0cc06a93488a66ada448 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 01:01:12 +0100 Subject: [PATCH 08/15] Change Sensors field type in config and response to map --- config/types.go | 7 ++----- types/v14.go | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/config/types.go b/config/types.go index c8d9524..c2b3f12 100644 --- a/config/types.go +++ b/config/types.go @@ -19,11 +19,8 @@ type EnvironmentSensorConfig struct { } type DynamicStateConfig struct { - Sensors struct { - Temperature []EnvironmentSensorConfig `json:"temperature"` - Humidity []EnvironmentSensorConfig `json:"humidity"` - } `json:"sensors"` - State struct { + Sensors map[string][]EnvironmentSensorConfig `json:"sensors"` + State struct { Open struct { AllowedCredentials []HTTPBACredentialID `json:"allowed_credentials"` } `json:"open"` diff --git a/types/v14.go b/types/v14.go index 341013c..81b92a6 100644 --- a/types/v14.go +++ b/types/v14.go @@ -23,11 +23,8 @@ type SpaceAPIResponseV14 struct { ML string `json:"ml"` Matrix string `json:"matrix"` } `json:"contact"` - Sensors struct { - Temperature []EnvironmentSensor `json:"temperature"` - Humidity []EnvironmentSensor `json:"humidity"` - } `json:"sensors"` - Feeds struct { + Sensors map[string][]EnvironmentSensor `json:"sensors"` + Feeds struct { Blog struct { Type string `json:"type"` URL string `json:"url"` From 6e1a8ac0e6074429e86270c75c902cea68fd64d2 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 01:02:30 +0100 Subject: [PATCH 09/15] Initialize sensors map with static parts when parsing config --- config/config.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/config.go b/config/config.go index 8092560..20c0419 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,8 @@ import ( "os" "path/filepath" "slices" + + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) const ( @@ -50,5 +52,14 @@ func ParseConfiguration() (conf SpaceapidConfig) { log.Fatalln("Provided file doesn't specify compatibility with API version 14") } + // Initialise fields for environment sensors + conf.Response.Sensors = make(map[string][]types.EnvironmentSensor) + for key, sensorConfigs := range conf.Dynamic.Sensors { + conf.Response.Sensors[key] = make([]types.EnvironmentSensor, len(sensorConfigs)) + for i, sensorConfig := range sensorConfigs { + conf.Response.Sensors[key][i] = sensorConfig.SensorData + } + } + return } From 38710484f9d5b5beb558050ddf997af351f866c6 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 01:04:01 +0100 Subject: [PATCH 10/15] Generate HTTP endpoints for environment sensors - Move update request sanity checks to new method in handlers/util.go - Change EnvironmentSensor.Value type because ParseFloat returns float64 --- handlers/sensors.go | 39 +++++++++++++++++++++++++++++++++++++++ handlers/state.go | 27 +-------------------------- handlers/util.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 15 +++++++++++++++ types/v14.go | 3 ++- 5 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 handlers/sensors.go create mode 100644 handlers/util.go diff --git a/handlers/sensors.go b/handlers/sensors.go new file mode 100644 index 0000000..7441699 --- /dev/null +++ b/handlers/sensors.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "io" + "log" + "math" + "net/http" + "strconv" + "time" + + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" +) + +func EnvironmentSensor( + authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, + resp *types.EnvironmentSensor, +) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + body := updateEndpointValidator(authDB, validCredentials, w, r) + + // Parse request body + newState, err := strconv.ParseFloat(string(body), 64) + if err != nil || math.IsInf(newState, 0) { + log.Println("Failed to parse request body from", r.RemoteAddr) + w.WriteHeader(http.StatusBadRequest) + _, _ = io.WriteString(w, "HTTP request body has to be a valid float64 value != +/-Inf") + return + } + + // Set SpaceAPI response values + resp.Value = newState + resp.LastChange = time.Now().Unix() + + // Respond with OK + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, "Update Successful") + } +} diff --git a/handlers/state.go b/handlers/state.go index dcd191a..13f37c0 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -9,7 +9,6 @@ import ( "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" - "gitlab.hamburg.ccc.de/ccchh/spaceapid/util" ) func StateOpen( @@ -17,31 +16,7 @@ func StateOpen( 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 || !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) - 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 - } + body := updateEndpointValidator(authDB, validCredentials, w, r) // Parse request body newState, err := strconv.ParseBool(string(body)) diff --git a/handlers/util.go b/handlers/util.go new file mode 100644 index 0000000..625a9c0 --- /dev/null +++ b/handlers/util.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "io" + "log" + "net/http" + + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/util" +) + +// updateEndpointValidator checks BasicAuth credentials, +// checks for correct HTTP method and then returns the request body +func updateEndpointValidator( + authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, + w http.ResponseWriter, r *http.Request, +) (body []byte) { + // Check BasicAuth credentials + username, password, ok := r.BasicAuth() + 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) + return + } + + // Check if PUT method + if r.Method != http.MethodPut { + log.Println("Wrong Method: ", r.Method, "from", r.RemoteAddr, "at", r.RequestURI) + 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 + } + + return +} diff --git a/main.go b/main.go index 98b8b05..c9060fb 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,12 @@ package main import ( + "fmt" "log" "net/http" "os" "os/signal" + "strings" "syscall" "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" @@ -37,6 +39,19 @@ func main() { http.HandleFunc("/state/open", handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response), ) + // Register handlers for Environmental Sensors + for key, envSensorConfigs := range conf.Dynamic.Sensors { + for i, envSensorConfig := range envSensorConfigs { + http.HandleFunc( + strings.ToLower(fmt.Sprintf( + "/sensors/%s/%s/%s", key, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name, + )), + handlers.EnvironmentSensor( + conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[key][i], + ), + ) + } + } // Start webserver log.Println("Starting HTTP server...") diff --git a/types/v14.go b/types/v14.go index 81b92a6..2c354bf 100644 --- a/types/v14.go +++ b/types/v14.go @@ -41,11 +41,12 @@ type SpaceAPIResponseV14 struct { } type EnvironmentSensor struct { - Value float32 `json:"value"` + Value float64 `json:"value"` Unit string `json:"unit"` Location string `json:"location"` Name string `json:"name"` Description string `json:"description"` + LastChange int64 `json:"lastchange"` } type PersistentStateV14 struct { From 42483df78ccacaf36be9470e7982704d9f51dd4c Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 01:04:32 +0100 Subject: [PATCH 11/15] Update config-template.json with placeholders for environment sensors --- config-template.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/config-template.json b/config-template.json index 9be6055..bd1ecfb 100644 --- a/config-template.json +++ b/config-template.json @@ -14,9 +14,23 @@ "temperature": [ { "sensor_data": { - "name": "Küche", + "unit": "C", "location": "Hauptraum", - "unit": "C" + "name": "Kueche", + "description": "Sensor im Ofen" + }, + "allowed_credentials": [ + "home-assistant" + ] + } + ], + "humidity": [ + { + "sensor_data": { + "unit": "hPa", + "location": "Hauptraum", + "name": "Kueche", + "description": "Sensor im Wasserhahn" }, "allowed_credentials": [ "home-assistant" From 44143c0c2d632cd819d7ae6f09f2283a58d2c499 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 01:29:41 +0100 Subject: [PATCH 12/15] Save sensor values to persistent-state.json --- types/v14.go | 8 ++++++++ util/util.go | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/types/v14.go b/types/v14.go index 2c354bf..63e6765 100644 --- a/types/v14.go +++ b/types/v14.go @@ -54,4 +54,12 @@ type PersistentStateV14 struct { Open bool `json:"open"` LastChange int64 `json:"lastchange"` } `json:"state"` + Sensors map[string][]PersistentEnvironmentSensor `json:"sensors"` +} + +type PersistentEnvironmentSensor struct { + Value float64 `json:"value"` + Location string `json:"location"` + Name string `json:"name"` + LastChange int64 `json:"lastchange"` } diff --git a/util/util.go b/util/util.go index 726d885..282f2e9 100644 --- a/util/util.go +++ b/util/util.go @@ -18,8 +18,9 @@ const ( // the file containing the old state. func MergeOldState(response *types.SpaceAPIResponseV14) { var ( - err error - oldState []byte + err error + oldState []byte + persistedState types.PersistentStateV14 ) log.Println("Merging old state from", savedStateJSONPath, "...") @@ -34,18 +35,35 @@ func MergeOldState(response *types.SpaceAPIResponseV14) { goto removeOld } - // Read file and merge + // Read file and load persisted state oldState, err = os.ReadFile(savedStateJSONPath) if err != nil { log.Println("Error reading old state from", savedStateJSONPath, ", skipping merge... error:", err) goto removeOld } - err = json.Unmarshal(oldState, response) + err = json.Unmarshal(oldState, &persistedState) if err != nil { log.Println(savedStateJSONPath, "doesn't seem to contain valid data... error:", err) goto removeOld } + // Merge state + response.State = persistedState.State + + // Merge sensors + for key, environmentSensors := range persistedState.Sensors { + for _, sensor := range environmentSensors { + // Order or amount of sensors might have changed, so check sensors already present in response + // and then look for matching values in persisted state + for i := range response.Sensors[key] { + if rs := &response.Sensors[key][i]; rs.Location == sensor.Location && rs.Name == sensor.Name { + rs.Value = sensor.Value + rs.LastChange = sensor.LastChange + } + } + } + } + // Delete old state json removeOld: err = os.RemoveAll(savedStateJSONPath) @@ -75,6 +93,17 @@ func SaveCurrentState(response types.SpaceAPIResponseV14) { LastChange int64 `json:"lastchange"` }{Open: response.State.Open, LastChange: response.State.LastChange}, } + // Save sensor state + persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor) + for key, environmentSensors := range response.Sensors { + persistentStateV14.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors)) + for i, sensor := range environmentSensors { + persistentStateV14.Sensors[key][i].Value = sensor.Value + persistentStateV14.Sensors[key][i].Location = sensor.Location + persistentStateV14.Sensors[key][i].Name = sensor.Name + persistentStateV14.Sensors[key][i].LastChange = sensor.LastChange + } + } // Serialize persistent state marshal, err := json.MarshalIndent(persistentStateV14, "", "\t") From 70b2e8069b9ba724a0dc53d646656a2ad5bb5fd7 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 01:53:36 +0100 Subject: [PATCH 13/15] Only pass State struct pointer to StateOpen, not entire response --- handlers/state.go | 6 +++--- main.go | 2 +- types/v14.go | 10 ++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/handlers/state.go b/handlers/state.go index 13f37c0..47bc6ab 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -13,7 +13,7 @@ import ( func StateOpen( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, - resp *types.SpaceAPIResponseV14, + resp *types.SpaceState, ) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { body := updateEndpointValidator(authDB, validCredentials, w, r) @@ -28,8 +28,8 @@ func StateOpen( } // Set SpaceAPI response values - resp.State.Open = newState - resp.State.LastChange = time.Now().Unix() + resp.Open = newState + resp.LastChange = time.Now().Unix() // Respond with OK w.WriteHeader(http.StatusOK) diff --git a/main.go b/main.go index c9060fb..044ceed 100644 --- a/main.go +++ b/main.go @@ -37,7 +37,7 @@ func main() { handlers.Root(&conf.Response), ) http.HandleFunc("/state/open", - handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response), + handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), ) // Register handlers for Environmental Sensors for key, envSensorConfigs := range conf.Dynamic.Sensors { diff --git a/types/v14.go b/types/v14.go index 63e6765..8540fe7 100644 --- a/types/v14.go +++ b/types/v14.go @@ -11,10 +11,7 @@ type SpaceAPIResponseV14 struct { Lat float64 `json:"lat"` Lon float64 `json:"lon"` } `json:"location"` - State struct { - Open bool `json:"open"` - LastChange int64 `json:"lastchange"` - } `json:"state"` + State SpaceState `json:"state"` Contact struct { Phone string `json:"phone"` IRC string `json:"irc"` @@ -40,6 +37,11 @@ type SpaceAPIResponseV14 struct { } `json:"links"` } +type SpaceState struct { + Open bool `json:"open"` + LastChange int64 `json:"lastchange"` +} + type EnvironmentSensor struct { Value float64 `json:"value"` Unit string `json:"unit"` From 04b7efd74a1ec107addff9da055e4e0104d705e0 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 21:52:23 +0100 Subject: [PATCH 14/15] Open the persistent file just for writing when saving --- util/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/util.go b/util/util.go index 282f2e9..086ee2f 100644 --- a/util/util.go +++ b/util/util.go @@ -80,7 +80,7 @@ func SaveCurrentState(response types.SpaceAPIResponseV14) { } // Open persistent state file - file, err := os.OpenFile(savedStateJSONPath, os.O_RDWR|os.O_CREATE, 0644) + file, err := os.OpenFile(savedStateJSONPath, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { log.Fatalln("Failed opening", savedStateJSONPath, "while trying to save current state... error:", err) } From 0241a506d4e85a014760264ec86f78b232172917 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sun, 14 Jan 2024 21:54:01 +0100 Subject: [PATCH 15/15] Handle sensors that don't have a name, just a location --- config-template.json | 10 ++++++++++ main.go | 9 +++++---- types/v14.go | 10 +++++----- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/config-template.json b/config-template.json index bd1ecfb..6fbaf81 100644 --- a/config-template.json +++ b/config-template.json @@ -22,6 +22,16 @@ "allowed_credentials": [ "home-assistant" ] + }, + { + "sensor_data": { + "unit": "C", + "location": "Hauptraum", + "description": "Sensor im Hauptraum" + }, + "allowed_credentials": [ + "home-assistant" + ] } ], "humidity": [ diff --git a/main.go b/main.go index 044ceed..5f516ea 100644 --- a/main.go +++ b/main.go @@ -42,10 +42,11 @@ func main() { // Register handlers for Environmental Sensors for key, envSensorConfigs := range conf.Dynamic.Sensors { for i, envSensorConfig := range envSensorConfigs { - http.HandleFunc( - strings.ToLower(fmt.Sprintf( - "/sensors/%s/%s/%s", key, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name, - )), + pattern := fmt.Sprintf("/sensors/%s/%s", key, envSensorConfig.SensorData.Location) + if envSensorConfig.SensorData.Name != "" { + pattern += "/" + envSensorConfig.SensorData.Name + } + http.HandleFunc(strings.ToLower(pattern), handlers.EnvironmentSensor( conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[key][i], ), diff --git a/types/v14.go b/types/v14.go index 8540fe7..2bcdc06 100644 --- a/types/v14.go +++ b/types/v14.go @@ -46,9 +46,9 @@ type EnvironmentSensor struct { Value float64 `json:"value"` Unit string `json:"unit"` Location string `json:"location"` - Name string `json:"name"` - Description string `json:"description"` - LastChange int64 `json:"lastchange"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + LastChange int64 `json:"lastchange,omitempty"` } type PersistentStateV14 struct { @@ -56,12 +56,12 @@ type PersistentStateV14 struct { Open bool `json:"open"` LastChange int64 `json:"lastchange"` } `json:"state"` - Sensors map[string][]PersistentEnvironmentSensor `json:"sensors"` + Sensors map[string][]PersistentEnvironmentSensor `json:"sensors,omitempty"` } type PersistentEnvironmentSensor struct { Value float64 `json:"value"` Location string `json:"location"` - Name string `json:"name"` + Name string `json:"name,omitempty"` LastChange int64 `json:"lastchange"` }