Compare commits

..

9 commits

Author SHA1 Message Date
fd3cae1866 feat: SpaceAPI v15 support
Delete types/v14.go, replace with v15.go.

Rename internal types to not include message version.
I don't currently see the need to support multiple message
versions that have incompatible layouts.
v15 is backwards compatible with v14, so the config example now
specifies both.

Rename the "response" field in the config file to "static" as
dynamic/static makes more sense from the user's perspective.
(Internally it's still SpaceapidConfig.{Dynamic,Response}
to align with type names and usage.)
2026-07-25 22:51:26 +02:00
b5a286a237 feat: Log that version is unknown if version == "" 2026-07-25 22:51:26 +02:00
34d05ffa49 feat(handlers): Handle DELETE for environment sensors
It now works as advertised in the README! *ahem*...
2026-07-25 22:51:26 +02:00
abde10bfc9 style: Add .editorconfig 2026-07-25 22:51:26 +02:00
b33c266fb5 docs: Add missing backticks in README 2026-07-25 22:49:58 +02:00
b54d4a39cd fix(config): Mention what that list of paths in the log is 2026-07-25 22:49:58 +02:00
1d49948615 fix(config): Remove redundant space from log message 2026-07-25 21:16:49 +02:00
e714cca477 refactor(handlers): Use fmt.Errorf instead of errors.New(fmt.Sprintf()) 2026-07-25 21:16:40 +02:00
74fbef4ae8 refactor(handlers): Use http.Header.Set instead of direct map access 2026-07-25 21:16:06 +02:00
9 changed files with 62 additions and 22 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
trim_trailing_whitespace = true
insert_final_newline = true
[*.json]
indent_style = tab
tab_width = 4

View file

@ -6,7 +6,8 @@
$ curl http://localhost:8080 | jq $ curl http://localhost:8080 | jq
{ {
"api_compatibility": [ "api_compatibility": [
"14" "14",
"15"
], ],
"space": "CCCHH", "space": "CCCHH",
... ...
@ -15,16 +16,16 @@ $ curl http://localhost:8080 | jq
## Configuring ## Configuring
spaceapid has to be configured via one or multiple json files. `spaceapid` has to be configured via one or multiple json files.
A sample configuration is provided as [config-template.json](./config-template.json). A sample configuration is provided as [config-template.json](./config-template.json).
The config consists of three parts: The config consists of three parts:
- `"credentials"` - `"credentials"`
- List of Username/Password credentials for HTTP BasicAuth - List of Username/Password credentials for HTTP BasicAuth
- `"dynamic"` - `"dynamic"`
- The configuration for the dynamic parts of the message - The dynamic parts of the message
- `"response"` - `"static"`
- The static (pre-filled) parts of the response - The static (pre-filled) parts of the message
See [Running](#Running) for details. See [Running](#Running) for details.

View file

@ -66,7 +66,7 @@
} }
} }
}, },
"response": { "static": {
"api_compatibility": [ "api_compatibility": [
"14", "14",
"15" "15"

View file

@ -69,7 +69,7 @@ func ParseConfiguration() (conf SpaceapidConfig) {
log.Println("Parsing configuration files") log.Println("Parsing configuration files")
paths := getConfigPaths() paths := getConfigPaths()
log.Println(paths) log.Println("Config files:", paths)
for _, path := range paths { for _, path := range paths {
// Read file // Read file
file, err := os.ReadFile(path) file, err := os.ReadFile(path)
@ -86,9 +86,9 @@ func ParseConfiguration() (conf SpaceapidConfig) {
} }
} }
// Check if compatible with v14 // Sanity check: Config should declare compatibility with current SpaceAPI message version
if !slices.Contains(conf.Response.APICompatibility, "14") { if !slices.Contains(conf.Response.APICompatibility, types.CurrentMessageVersion) {
log.Fatalln("Provided file doesn't specify compatibility with API version 14") log.Fatalln("Provided config doesn't specify compatibility with current API version", types.CurrentMessageVersion)
} }
// Initialize fields for environment sensors // Initialize fields for environment sensors

View file

@ -34,5 +34,5 @@ type SpaceapidConfig struct {
// The dynamic part of the spaceapi JSON // The dynamic part of the spaceapi JSON
Dynamic DynamicStateConfig `json:"dynamic"` Dynamic DynamicStateConfig `json:"dynamic"`
// The static part of the spaceapi JSON // The static part of the spaceapi JSON
Response types.SpaceAPIResponse `json:"response"` Response types.SpaceAPIResponse `json:"static"`
} }

View file

@ -11,7 +11,7 @@ import (
"git.hamburg.ccc.de/ccchh/spaceapid/types" "git.hamburg.ccc.de/ccchh/spaceapid/types"
) )
func EnvironmentSensor( func EnvironmentSensorPUT(
authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID,
resp *types.EnvironmentSensor, resp *types.EnvironmentSensor,
) func(http.ResponseWriter, *http.Request) { ) func(http.ResponseWriter, *http.Request) {
@ -35,3 +35,20 @@ func EnvironmentSensor(
resp.LastChange = time.Now().Unix() resp.LastChange = time.Now().Unix()
} }
} }
func EnvironmentSensorDELETE(
authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID,
resp *types.EnvironmentSensor,
) 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.Value = 0
resp.LastChange = time.Now().Unix()
}
}

View file

@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -20,14 +19,14 @@ func updateEndpointValidator(
if !ok || !util.CheckCredentials(authDB, validCredentials, username, password) { if !ok || !util.CheckCredentials(authDB, validCredentials, username, password) {
w.Header().Set("WWW-Authenticate", "Basic realm=\"spaceapid\"") w.Header().Set("WWW-Authenticate", "Basic realm=\"spaceapid\"")
http.Error(w, "", http.StatusUnauthorized) http.Error(w, "", http.StatusUnauthorized)
return []byte{}, errors.New(fmt.Sprintf("Unauthorized request from %s Username: %s Password: %s", r.RemoteAddr, username, password)) return []byte{}, fmt.Errorf("Unauthorized request from %s Username: %s Password: %s", r.RemoteAddr, username, password)
} }
// Read request body // Read request body
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
http.Error(w, "", http.StatusInternalServerError) http.Error(w, "", http.StatusInternalServerError)
return []byte{}, errors.New(fmt.Sprintf("Failed to read request body from %s with error: %s", r.RemoteAddr, err)) return []byte{}, fmt.Errorf("Failed to read request body from %s with error: %s", r.RemoteAddr, err)
} }
return body, nil return body, nil

15
main.go
View file

@ -25,6 +25,9 @@ func init() {
} }
func main() { func main() {
if version == "" {
version = "unknown"
}
log.Println("SpaceAPId version", version) log.Println("SpaceAPId version", version)
flag.Parse() flag.Parse()
@ -68,12 +71,18 @@ func main() {
// Register handlers for environmental sensors // Register handlers for environmental sensors
for sensorType, envSensorConfigs := range conf.Dynamic.Sensors { for sensorType, envSensorConfigs := range conf.Dynamic.Sensors {
for i, envSensorConfig := range envSensorConfigs { for i, envSensorConfig := range envSensorConfigs {
urlPattern := "PUT " + util.GetSensorURLPath( urlPattern := util.GetSensorURLPath(
sensorType, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name, sensorType, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name,
) )
http.HandleFunc( http.HandleFunc(
urlPattern, "PUT "+urlPattern,
handlers.EnvironmentSensor( handlers.EnvironmentSensorPUT(
conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i],
),
)
http.HandleFunc(
"DELETE "+urlPattern,
handlers.EnvironmentSensorDELETE(
conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i], conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i],
), ),
) )

View file

@ -1,5 +1,10 @@
package types package types
const CurrentMessageVersion = "15"
// SpaceAPIResponse represents the contents of the SpaceAPI message
//
// Currently implemented version: 15
type SpaceAPIResponse struct { type SpaceAPIResponse struct {
APICompatibility []string `json:"api_compatibility"` // REQUIRED APICompatibility []string `json:"api_compatibility"` // REQUIRED
Space string `json:"space"` // REQUIRED Space string `json:"space"` // REQUIRED
@ -120,10 +125,10 @@ type SpaceAPIState struct {
} `json:"icon,omitzero"` } `json:"icon,omitzero"`
} }
func (s *SpaceAPIState) FromPersistentSpaceState(state PersistentSpaceState) { func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentSpaceState) {
s.Open = state.State.Open s.Open = ps.State.Open
s.LastChange = state.State.LastChange s.LastChange = ps.State.LastChange
s.Message = state.State.Message s.Message = ps.State.Message
} }
type SpaceAPIEvents struct { type SpaceAPIEvents struct {