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
{
"api_compatibility": [
"14"
"14",
"15"
],
"space": "CCCHH",
...
@ -15,16 +16,16 @@ $ curl http://localhost:8080 | jq
## 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).
The config consists of three parts:
- `"credentials"`
- List of Username/Password credentials for HTTP BasicAuth
- `"dynamic"`
- The configuration for the dynamic parts of the message
- `"response"`
- The static (pre-filled) parts of the response
- The dynamic parts of the message
- `"static"`
- The static (pre-filled) parts of the message
See [Running](#Running) for details.

View file

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

View file

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

View file

@ -34,5 +34,5 @@ type SpaceapidConfig struct {
// The dynamic part of the spaceapi JSON
Dynamic DynamicStateConfig `json:"dynamic"`
// 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"
)
func EnvironmentSensor(
func EnvironmentSensorPUT(
authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID,
resp *types.EnvironmentSensor,
) func(http.ResponseWriter, *http.Request) {
@ -35,3 +35,20 @@ func EnvironmentSensor(
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
import (
"errors"
"fmt"
"io"
"net/http"
@ -20,14 +19,14 @@ func updateEndpointValidator(
if !ok || !util.CheckCredentials(authDB, validCredentials, username, password) {
w.Header().Set("WWW-Authenticate", "Basic realm=\"spaceapid\"")
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
body, err := io.ReadAll(r.Body)
if err != nil {
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

15
main.go
View file

@ -25,6 +25,9 @@ func init() {
}
func main() {
if version == "" {
version = "unknown"
}
log.Println("SpaceAPId version", version)
flag.Parse()
@ -68,12 +71,18 @@ func main() {
// Register handlers for environmental sensors
for sensorType, envSensorConfigs := range conf.Dynamic.Sensors {
for i, envSensorConfig := range envSensorConfigs {
urlPattern := "PUT " + util.GetSensorURLPath(
urlPattern := util.GetSensorURLPath(
sensorType, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name,
)
http.HandleFunc(
urlPattern,
handlers.EnvironmentSensor(
"PUT "+urlPattern,
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],
),
)

View file

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