diff --git a/config/config.go b/config/config.go index 7ad632c..05727aa 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,7 @@ package config import ( "bytes" - "encoding/json" + "encoding/json/v2" "flag" "fmt" "log" @@ -78,9 +78,7 @@ func ParseConfiguration() (conf SpaceapidConfig) { } // Parse JSON - dec := json.NewDecoder(bytes.NewReader(file)) - dec.DisallowUnknownFields() - err = dec.Decode(&conf) + err = json.UnmarshalRead(bytes.NewReader(file), &conf, json.RejectUnknownMembers(true)) if err != nil { log.Fatalln("Could not parse spaceapid config file:", err) } diff --git a/go.mod b/go.mod index 506613d..61b6d1f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module git.hamburg.ccc.de/ccchh/spaceapid -go 1.22 +go 1.27 diff --git a/handlers/root.go b/handlers/root.go index 88cdc55..e79023a 100644 --- a/handlers/root.go +++ b/handlers/root.go @@ -1,7 +1,7 @@ package handlers import ( - "encoding/json" + "encoding/json/v2" "log" "net/http" @@ -19,7 +19,7 @@ func Root(resp *types.SpaceAPIResponse) func(http.ResponseWriter, *http.Request) } // Serialize response - response, err := json.Marshal(resp) + response, err := json.Marshal(resp, json.Deterministic(true)) if err != nil { log.Println("Failed to serialize JSON response:", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/handlers/state.go b/handlers/state.go index d4962c1..47d7878 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -10,7 +10,7 @@ import ( "git.hamburg.ccc.de/ccchh/spaceapid/types" ) -func StateOpen( +func StateOpenPUT( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, resp *types.SpaceAPIState, ) func(http.ResponseWriter, *http.Request) { @@ -22,7 +22,7 @@ func StateOpen( } // Parse request body - newState, err := strconv.ParseBool(string(body)) + doorNowOpen, err := strconv.ParseBool(string(body)) if err != nil { log.Println("Failed to parse request body from", r.RemoteAddr, "with error:", err) http.Error(w, "HTTP request body should either be true or false", http.StatusBadRequest) @@ -30,7 +30,28 @@ func StateOpen( } // Set SpaceAPI response values - resp.Open = newState + if doorNowOpen { + resp.Open = types.SpaceAPIStateOpenIsOpen + } else { + resp.Open = types.SpaceAPIStateOpenIsClosed + } + resp.LastChange = time.Now().Unix() + } +} + +func StateOpenDELETE( + authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, + resp *types.SpaceAPIState, +) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + _, err := updateEndpointValidator(authDB, validCredentials, w, r) + if err != nil { + log.Println(err) + return + } + + // Set SpaceAPI response values + resp.Open = types.SpaceAPIStateOpenIsUnknown resp.LastChange = time.Now().Unix() } } diff --git a/main.go b/main.go index 4b01eee..23bd31b 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "log" "net/http" "os" @@ -16,11 +17,15 @@ import ( ) var ( - flagHelp bool - version string + flagHelp bool + listenAddress string + listenPort string + version string ) func init() { + flag.StringVar(&listenAddress, "i", "", "IP address the HTTP server should bind to (all available addresses by default)") + flag.StringVar(&listenPort, "p", "8080", "Port the HTTP server should listen on") flag.BoolVar(&flagHelp, "h", false, "Print help output") } @@ -59,7 +64,10 @@ func main() { ) // state->open http.HandleFunc("PUT /state/open", - handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), + handlers.StateOpenPUT(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), + ) + http.HandleFunc("DELETE /state/open", + handlers.StateOpenDELETE(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), ) // state->message http.HandleFunc("PUT /state/message", @@ -91,5 +99,5 @@ func main() { // Start webserver log.Println("Starting HTTP server...") - log.Fatalln(http.ListenAndServe(":8080", nil)) + log.Fatalln(http.ListenAndServe(fmt.Sprintf("%s:%s", listenAddress, listenPort), nil)) } diff --git a/persistence/persistentState.go b/persistence/persistentState.go index 87c3121..1f72394 100644 --- a/persistence/persistentState.go +++ b/persistence/persistentState.go @@ -1,7 +1,8 @@ package persistence import ( - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "errors" "log" "os" @@ -20,7 +21,7 @@ func MergeOldState(response *types.SpaceAPIResponse) { var ( err error oldState []byte - persistedState types.PersistentSpaceState + persistedState types.PersistentState ) log.Println("Merging old state from", persistentStatePath, "...") @@ -87,27 +88,38 @@ func SaveCurrentState(response types.SpaceAPIResponse) { defer file.Close() // Create persistent state - persistentStateV14 := types.PersistentSpaceState{ - State: struct { - Open bool `json:"open"` - LastChange int64 `json:"lastchange"` - Message string `json:"message,omitempty"` - }{Open: response.State.Open, LastChange: response.State.LastChange, Message: response.State.Message}, + var ( + isOpen bool + isUnknown bool + ) + switch response.State.Open { + case types.SpaceAPIStateOpenIsUnknown: + isUnknown = true + case types.SpaceAPIStateOpenIsClosed: + break + case types.SpaceAPIStateOpenIsOpen: + isOpen = true + } + persistentState := types.PersistentState{ + State: types.PersistentSpaceState{ + Open: isOpen, IsUnknown: isUnknown, + LastChange: response.State.LastChange, Message: response.State.Message, + }, + Sensors: make(map[string][]types.PersistentEnvironmentSensor), } // Save sensor state - persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor) for key, environmentSensors := range response.Sensors { - persistentStateV14.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors)) + persistentState.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 + persistentState.Sensors[key][i].Value = sensor.Value + persistentState.Sensors[key][i].Location = sensor.Location + persistentState.Sensors[key][i].Name = sensor.Name + persistentState.Sensors[key][i].LastChange = sensor.LastChange } } // Serialize persistent state - marshal, err := json.MarshalIndent(persistentStateV14, "", "\t") + marshal, err := json.Marshal(persistentState, json.JoinOptions(jsontext.Multiline(true), jsontext.SpaceAfterComma(true))) if err != nil { log.Fatalln("Failed serializing persistent state... error:", err) } diff --git a/types/v15.go b/types/v15.go index 5180ba6..b844175 100644 --- a/types/v15.go +++ b/types/v15.go @@ -1,5 +1,9 @@ package types +import ( + "encoding/json/v2" +) + const CurrentMessageVersion = "15" // SpaceAPIResponse represents the contents of the SpaceAPI message @@ -114,19 +118,50 @@ type SpaceAPILocationAreas struct { SquareMeters float64 `json:"square_meters"` // REQUIRED } +type SpaceAPIStateOpen int + +const ( + SpaceAPIStateOpenIsUnknown = iota + SpaceAPIStateOpenIsClosed + SpaceAPIStateOpenIsOpen +) + +func (state SpaceAPIStateOpen) MarshalJSON() ([]byte, error) { + switch state { + case SpaceAPIStateOpenIsClosed: + return json.Marshal(false) + case SpaceAPIStateOpenIsOpen: + return json.Marshal(true) + default: + return []byte{}, nil + } +} + +func (state SpaceAPIStateOpen) IsZero() bool { + return state == SpaceAPIStateOpenIsUnknown +} + type SpaceAPIState struct { - Open bool `json:"open,omitempty"` - LastChange int64 `json:"lastchange,omitempty"` - TriggerPerson string `json:"trigger_person,omitempty"` - Message string `json:"message,omitempty"` + Open SpaceAPIStateOpen `json:"open,omitzero"` + LastChange int64 `json:"lastchange,omitempty"` + TriggerPerson string `json:"trigger_person,omitempty"` + Message string `json:"message,omitempty"` Icon struct { Open string `json:"open,omitempty"` // REQUIRED Closed string `json:"closed,omitempty"` // REQUIRED } `json:"icon,omitzero"` } -func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentSpaceState) { - s.Open = ps.State.Open +func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentState) { + if ps.State.IsUnknown { + s.Open = SpaceAPIStateOpenIsUnknown + } else { + if ps.State.Open { + s.Open = SpaceAPIStateOpenIsOpen + } else { + s.Open = SpaceAPIStateOpenIsClosed + } + } s.LastChange = ps.State.LastChange s.Message = ps.State.Message } @@ -147,15 +182,18 @@ type EnvironmentSensor struct { LastChange int64 `json:"lastchange,omitempty"` } -type PersistentSpaceState struct { - State struct { - Open bool `json:"open"` - LastChange int64 `json:"lastchange"` - Message string `json:"message,omitempty"` - } `json:"state"` +type PersistentState struct { + State PersistentSpaceState `json:"state"` Sensors map[string][]PersistentEnvironmentSensor `json:"sensors,omitempty"` } +type PersistentSpaceState struct { + Open bool `json:"open"` + IsUnknown bool `json:"is_unknown"` + LastChange int64 `json:"lastchange"` + Message string `json:"message,omitempty"` +} + type PersistentEnvironmentSensor struct { Value float64 `json:"value"` Location string `json:"location,omitempty"`