From 1a0499d81f24f28afd0f6d1ac7903f13b7a34d73 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 3 May 2025 23:04:47 +0200 Subject: [PATCH] feat: add API token authentication --- .env | 2 ++ Makefile | 5 +++-- README.md | 19 ++++++++++++++----- api/rcond.yaml | 22 +++++++++++++++++++++- cmd/rcond/main.go | 15 ++++++++++----- pkg/http/server.go | 33 +++++++++++++++++++++++---------- 6 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..1659e48 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +RCOND_ADDR=0.0.0.0:8080 +RCOND_API_TOKEN=your_api_token diff --git a/Makefile b/Makefile index 6d39571..682d2f7 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +SHELL := bash ARCH ?= arm64 ADDR ?= 0.0.0.0:8080 @@ -10,10 +11,10 @@ build: env GOOS=linux GOARCH=${ARCH} go build -o bin/rcond-${ARCH} ./cmd/rcond/main.go run: - bin/rcond-${ARCH} ${ADDR} + source .env && bin/rcond-${ARCH} ${ADDR} dev: - go run cmd/rcond/main.go ${ADDR} + RCOND_API_TOKEN=1234567890 go run cmd/rcond/main.go upload: scp rcond-${ARCH} pi@rpi-40ac:/home/pi/rcond diff --git a/README.md b/README.md index 89e0a2b..dcba914 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # rcond -A simple daemon to manage network connections through NetworkManager's D-Bus interface. +A simple daemon to manage +- network connections through NetworkManager's D-Bus interface +- system hostname through the hostname1 service It provides a REST API to: - Create and activate WiFi connections - Deactivate WiFi connections - Remove stored connection profiles - -The daemon is designed to run on Linux systems with NetworkManager. +- Get and set the system hostname ## Build and Run @@ -46,6 +47,7 @@ All endpoints use JSON for request and response payloads. ```bash curl -v -X POST http://localhost:8080/network/up \ -H "Content-Type: application/json" \ + -H "X-API-Token: 1234567890" \ -d '{ "interface": "wlan0", "ssid": "MyNetworkSSID", @@ -58,6 +60,7 @@ curl -v -X POST http://localhost:8080/network/up \ ```bash curl -v -X POST http://localhost:8080/network/down \ -H "Content-Type: application/json" \ + -H "X-API-Token: 1234567890" \ -d '{ "interface": "wlan0" }' @@ -66,13 +69,18 @@ curl -v -X POST http://localhost:8080/network/down \ ### Remove the stored connection ```bash -curl -v -X POST http://localhost:8080/network/remove +curl -v -X POST http://localhost:8080/network/remove \ + -H "X-API-Token: 1234567890" \ + -d '{ + "interface": "wlan0" + }' ``` ### Get the hostname ```bash -curl -v http://localhost:8080/hostname +curl -v http://localhost:8080/hostname \ + -H "X-API-Token: 1234567890" ``` ### Set the hostname @@ -80,6 +88,7 @@ curl -v http://localhost:8080/hostname ```bash curl -v -X POST http://localhost:8080/hostname \ -H "Content-Type: application/json" \ + -H "X-API-Token: 1234567890" \ -d '{ "hostname": "MyHostname" }' diff --git a/api/rcond.yaml b/api/rcond.yaml index c7ad5cd..009c0f7 100644 --- a/api/rcond.yaml +++ b/api/rcond.yaml @@ -8,6 +8,17 @@ servers: - url: http://localhost:8080 description: Local development server +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Token + description: API token for authentication + +security: + - ApiKeyAuth: [] + paths: /health: get: @@ -57,6 +68,8 @@ paths: description: Network interface brought up successfully '400': description: Invalid request payload + '401': + description: Unauthorized - invalid or missing API token '500': description: Internal server error @@ -82,6 +95,8 @@ paths: description: Network interface brought down successfully '400': description: Invalid request payload + '401': + description: Unauthorized - invalid or missing API token '500': description: Internal server error @@ -92,6 +107,8 @@ paths: responses: '200': description: Connection profile removed successfully + '401': + description: Unauthorized - invalid or missing API token '500': description: Internal server error @@ -108,6 +125,8 @@ paths: type: string description: Current hostname example: "MyHostname" + '401': + description: Unauthorized - invalid or missing API token '500': description: Internal server error post: @@ -131,6 +150,7 @@ paths: description: Hostname set successfully '400': description: Invalid request payload + '401': + description: Unauthorized - invalid or missing API token '500': description: Internal server error - diff --git a/cmd/rcond/main.go b/cmd/rcond/main.go index 9352574..c499bb6 100644 --- a/cmd/rcond/main.go +++ b/cmd/rcond/main.go @@ -1,4 +1,4 @@ -// Usage: rcond
+// Usage: rcond
package main @@ -20,12 +20,17 @@ func usage() { } func main() { - if len(os.Args) < 2 { - usage() + + addr := "0.0.0.0:8080" + if len(os.Args) > 1 { + addr = os.Args[1] + } + apiToken := os.Getenv("RCOND_API_TOKEN") + if apiToken == "" { + log.Fatal("RCOND_API_TOKEN environment variable not set") } - addr := os.Args[1] - srv := http.NewServer(addr) + srv := http.NewServer(addr, apiToken) srv.RegisterRoutes() log.Printf("Starting server on %s", addr) diff --git a/pkg/http/server.go b/pkg/http/server.go index 20cf0bc..e9665f1 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -10,11 +10,12 @@ import ( ) type Server struct { - router *mux.Router - srv *http.Server + router *mux.Router + srv *http.Server + apiToken string } -func NewServer(addr string) *Server { +func NewServer(addr string, apiToken string) *Server { router := mux.NewRouter() srv := &http.Server{ @@ -25,8 +26,9 @@ func NewServer(addr string) *Server { } return &Server{ - router: router, - srv: srv, + router: router, + srv: srv, + apiToken: apiToken, } } @@ -38,13 +40,24 @@ func (s *Server) Shutdown(ctx context.Context) error { return s.srv.Shutdown(ctx) } +func (s *Server) verifyToken(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-API-Token") + if token == "" || token != s.apiToken { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next(w, r) + } +} + func (s *Server) RegisterRoutes() { s.router.HandleFunc("/health", s.healthHandler).Methods(http.MethodGet) - s.router.HandleFunc("/network/up", HandleNetworkUp).Methods(http.MethodPost) - s.router.HandleFunc("/network/down", HandleNetworkDown).Methods(http.MethodPost) - s.router.HandleFunc("/network/remove", HandleNetworkRemove).Methods(http.MethodPost) - s.router.HandleFunc("/hostname", HandleGetHostname).Methods(http.MethodGet) - s.router.HandleFunc("/hostname", HandleSetHostname).Methods(http.MethodPost) + s.router.HandleFunc("/network/up", s.verifyToken(HandleNetworkUp)).Methods(http.MethodPost) + s.router.HandleFunc("/network/down", s.verifyToken(HandleNetworkDown)).Methods(http.MethodPost) + s.router.HandleFunc("/network/remove", s.verifyToken(HandleNetworkRemove)).Methods(http.MethodPost) + s.router.HandleFunc("/hostname", s.verifyToken(HandleGetHostname)).Methods(http.MethodGet) + s.router.HandleFunc("/hostname", s.verifyToken(HandleSetHostname)).Methods(http.MethodPost) } func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {