From fab01674d5ec5eae802456d4bda1b50a51a39f3b Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 17 May 2025 20:32:53 +0200 Subject: [PATCH] feat: refactoring --- pkg/cluster/agent.go | 10 +- pkg/http/handle_cluster.go | 96 +++++++++++ pkg/http/handle_error.go | 16 ++ pkg/http/handle_network.go | 153 +++++++++++++++++ pkg/http/handle_system.go | 28 ++++ pkg/http/handle_user.go | 63 +++++++ pkg/http/handlers.go | 328 ------------------------------------- 7 files changed, 364 insertions(+), 330 deletions(-) create mode 100644 pkg/http/handle_cluster.go create mode 100644 pkg/http/handle_error.go create mode 100644 pkg/http/handle_network.go create mode 100644 pkg/http/handle_system.go create mode 100644 pkg/http/handle_user.go delete mode 100644 pkg/http/handlers.go diff --git a/pkg/cluster/agent.go b/pkg/cluster/agent.go index 09fafb5..5826a3a 100644 --- a/pkg/cluster/agent.go +++ b/pkg/cluster/agent.go @@ -10,16 +10,18 @@ import ( "github.com/hashicorp/serf/serf" ) +// Agent represents a Serf cluster agent. type Agent struct { Serf *serf.Serf } -// ClusterEvent represents a custom event that will be sent to the Serf cluster +// ClusterEvent represents a custom event that will be sent to the Serf cluster. type ClusterEvent struct { Name string Data []byte } +// NewAgent creates a new Serf cluster agent with the given configuration and event handlers. func NewAgent(clusterConfig *config.ClusterConfig, clusterEvents map[string]func([]byte)) (*Agent, error) { config := serf.DefaultConfig() config.Init() @@ -65,11 +67,13 @@ func (a *Agent) Event(event ClusterEvent) error { return nil } +// Members returns a list of members in the Serf cluster. func (a *Agent) Members() ([]serf.Member, error) { log.Printf("Getting members of the cluster") return a.Serf.Members(), nil } +// Join attempts to join the Serf cluster with the given addresses, optionally ignoring old nodes. func (a *Agent) Join(addrs []string, ignoreOld bool) (int, error) { log.Printf("Joining nodes in the cluster: %v", addrs) n, err := a.Serf.Join(addrs, ignoreOld) @@ -81,16 +85,18 @@ func (a *Agent) Join(addrs []string, ignoreOld bool) (int, error) { return n, nil } +// Leave causes the agent to leave the Serf cluster. func (a *Agent) Leave() error { return a.Serf.Leave() } +// Shutdown shuts down the Serf cluster agent. func (a *Agent) Shutdown() error { log.Printf("Shutting down cluster agent") return a.Serf.Shutdown() } -// handleEvents handles Serf events received on the event channel +// handleEvents handles Serf events received on the event channel. func handleEvents(eventCh chan serf.Event, clusterEvents map[string]func([]byte)) { for event := range eventCh { switch event.EventType() { diff --git a/pkg/http/handle_cluster.go b/pkg/http/handle_cluster.go new file mode 100644 index 0000000..bd0e98b --- /dev/null +++ b/pkg/http/handle_cluster.go @@ -0,0 +1,96 @@ +package http + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/0x1d/rcond/pkg/cluster" +) + +type clusterEventRequest struct { + Name string `json:"name"` + Payload string `json:"payload,omitempty"` +} + +func ClusterAgentHandler(agent *cluster.Agent, handler func(http.ResponseWriter, *http.Request, *cluster.Agent)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + handler(w, r, agent) + } +} + +func HandleClusterJoin(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { + var joinRequest struct { + Join []string `json:"join"` + } + err := json.NewDecoder(r.Body).Decode(&joinRequest) + if err != nil { + WriteError(w, "Invalid request body", http.StatusBadRequest) + return + } + joinAddrs := strings.Join(joinRequest.Join, ",") + if joinAddrs == "" { + WriteError(w, "No join addresses provided", http.StatusBadRequest) + return + } + + addrs := strings.Split(joinAddrs, ",") + n, err := agent.Join(addrs, true) + if err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]int{"joined": n}) +} + +func HandleClusterLeave(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { + err := agent.Leave() + if err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func HandleClusterMembers(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { + if agent == nil { + WriteError(w, "cluster agent is not initialized", http.StatusInternalServerError) + return + } + members, err := agent.Members() + if err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(members) +} + +func HandleClusterEvent(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { + if agent == nil { + WriteError(w, "cluster agent is not initialized", http.StatusInternalServerError) + return + } + var req clusterEventRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteError(w, err.Error(), http.StatusBadRequest) + return + } + event := cluster.ClusterEvent{ + Name: req.Name, + Data: []byte(req.Payload), + } + err := agent.Event(event) + if err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/pkg/http/handle_error.go b/pkg/http/handle_error.go new file mode 100644 index 0000000..87619bb --- /dev/null +++ b/pkg/http/handle_error.go @@ -0,0 +1,16 @@ +package http + +import ( + "encoding/json" + "net/http" +) + +type ErrorResponse struct { + Error string `json:"error"` +} + +func WriteError(w http.ResponseWriter, message string, code int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(ErrorResponse{Error: message}) +} diff --git a/pkg/http/handle_network.go b/pkg/http/handle_network.go new file mode 100644 index 0000000..d775d1d --- /dev/null +++ b/pkg/http/handle_network.go @@ -0,0 +1,153 @@ +package http + +import ( + "encoding/json" + "log" + "net/http" + + network "github.com/0x1d/rcond/pkg/network" + "github.com/gorilla/mux" +) + +type configureAPRequest struct { + Interface string `json:"interface"` + SSID string `json:"ssid"` + Password string `json:"password"` + Autoconnect bool `json:"autoconnect"` +} + +type configureSTARequest struct { + Interface string `json:"interface"` + SSID string `json:"ssid"` + Password string `json:"password"` + Autoconnect bool `json:"autoconnect"` +} + +type networkUpRequest struct { + UUID string `json:"uuid"` +} + +type setHostnameRequest struct { + Hostname string `json:"hostname"` +} + +func HandleConfigureSTA(w http.ResponseWriter, r *http.Request) { + var req configureSTARequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteError(w, err.Error(), http.StatusBadRequest) + return + } + + log.Printf("Configuring station on interface %s", req.Interface) + uuid, err := network.ConfigureSTA(req.Interface, req.SSID, req.Password, req.Autoconnect) + if err != nil { + log.Printf("Failed to configure station on interface %s: %v", req.Interface, err) + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"uuid": uuid}) +} + +func HandleConfigureAP(w http.ResponseWriter, r *http.Request) { + var req configureAPRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteError(w, err.Error(), http.StatusBadRequest) + return + } + + log.Printf("Configuring access point on interface %s", req.Interface) + uuid, err := network.ConfigureAP(req.Interface, req.SSID, req.Password, req.Autoconnect) + if err != nil { + log.Printf("Failed to configure access point on interface %s: %v", req.Interface, err) + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("Successfully configured access point on interface %s with UUID %s", req.Interface, uuid) + + resp := struct { + UUID string `json:"uuid"` + }{ + UUID: uuid, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(resp); err != nil { + log.Printf("Failed to encode response: %v", err) + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func HandleNetworkUp(w http.ResponseWriter, r *http.Request) { + var req networkUpRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteError(w, err.Error(), http.StatusBadRequest) + return + } + vars := mux.Vars(r) + iface := vars["interface"] + + log.Printf("Bringing up network interface %s with UUID %s", iface, req.UUID) + if err := network.Up(iface, req.UUID); err != nil { + log.Printf("Failed to bring up network interface %s: %v", iface, err) + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("Successfully brought up network interface %s", iface) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func HandleNetworkDown(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + iface := vars["interface"] + if err := network.Down(iface); err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func HandleNetworkRemove(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + uuid := vars["uuid"] + if err := network.Remove(uuid); err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func HandleGetHostname(w http.ResponseWriter, r *http.Request) { + hostname, err := network.GetHostname() + if err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"hostname": hostname}) +} + +func HandleSetHostname(w http.ResponseWriter, r *http.Request) { + var req setHostnameRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteError(w, err.Error(), http.StatusBadRequest) + return + } + + if err := network.SetHostname(req.Hostname); err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/pkg/http/handle_system.go b/pkg/http/handle_system.go new file mode 100644 index 0000000..09b482c --- /dev/null +++ b/pkg/http/handle_system.go @@ -0,0 +1,28 @@ +package http + +import ( + "encoding/json" + "net/http" + + "github.com/0x1d/rcond/pkg/system" +) + +func HandleReboot(w http.ResponseWriter, r *http.Request) { + if err := system.Restart(); err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func HandleShutdown(w http.ResponseWriter, r *http.Request) { + if err := system.Shutdown(); err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/pkg/http/handle_user.go b/pkg/http/handle_user.go new file mode 100644 index 0000000..6b42217 --- /dev/null +++ b/pkg/http/handle_user.go @@ -0,0 +1,63 @@ +package http + +import ( + "encoding/base64" + "encoding/json" + "net/http" + + "github.com/0x1d/rcond/pkg/user" + "github.com/gorilla/mux" +) + +type authorizedKeyRequest struct { + User string `json:"user"` + PubKey string `json:"pubkey"` +} + +func HandleAddAuthorizedKey(w http.ResponseWriter, r *http.Request) { + var req authorizedKeyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteError(w, err.Error(), http.StatusBadRequest) + return + } + vars := mux.Vars(r) + username := vars["user"] + + fingerprint, err := user.AddAuthorizedKey(username, req.PubKey) + if err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"fingerprint": fingerprint}) +} + +func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + fingerprint := vars["fingerprint"] + if fingerprint == "" { + WriteError(w, "fingerprint parameter is required", http.StatusBadRequest) + return + } + + username := vars["user"] + if username == "" { + WriteError(w, "user parameter is required", http.StatusBadRequest) + return + } + + fingerprintBytes, err := base64.RawURLEncoding.DecodeString(fingerprint) + if err != nil { + WriteError(w, "invalid fingerprint base64", http.StatusBadRequest) + return + } + + if err := user.RemoveAuthorizedKey(username, string(fingerprintBytes)); err != nil { + WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/pkg/http/handlers.go b/pkg/http/handlers.go deleted file mode 100644 index 34d7644..0000000 --- a/pkg/http/handlers.go +++ /dev/null @@ -1,328 +0,0 @@ -package http - -import ( - "encoding/base64" - "encoding/json" - "log" - "net/http" - "strings" - - "github.com/0x1d/rcond/pkg/cluster" - network "github.com/0x1d/rcond/pkg/network" - "github.com/0x1d/rcond/pkg/system" - "github.com/0x1d/rcond/pkg/user" - "github.com/gorilla/mux" -) - -type configureAPRequest struct { - Interface string `json:"interface"` - SSID string `json:"ssid"` - Password string `json:"password"` - Autoconnect bool `json:"autoconnect"` -} - -type configureSTARequest struct { - Interface string `json:"interface"` - SSID string `json:"ssid"` - Password string `json:"password"` - Autoconnect bool `json:"autoconnect"` -} - -type networkUpRequest struct { - UUID string `json:"uuid"` -} - -type setHostnameRequest struct { - Hostname string `json:"hostname"` -} - -type authorizedKeyRequest struct { - User string `json:"user"` - PubKey string `json:"pubkey"` -} - -type clusterEventRequest struct { - Name string `json:"name"` - Payload string `json:"payload,omitempty"` -} - -type errorResponse struct { - Error string `json:"error"` -} - -func writeError(w http.ResponseWriter, message string, code int) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - json.NewEncoder(w).Encode(errorResponse{Error: message}) -} - -func HandleConfigureSTA(w http.ResponseWriter, r *http.Request) { - var req configureSTARequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, err.Error(), http.StatusBadRequest) - return - } - - log.Printf("Configuring station on interface %s", req.Interface) - uuid, err := network.ConfigureSTA(req.Interface, req.SSID, req.Password, req.Autoconnect) - if err != nil { - log.Printf("Failed to configure station on interface %s: %v", req.Interface, err) - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"uuid": uuid}) -} - -func HandleConfigureAP(w http.ResponseWriter, r *http.Request) { - var req configureAPRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, err.Error(), http.StatusBadRequest) - return - } - - log.Printf("Configuring access point on interface %s", req.Interface) - uuid, err := network.ConfigureAP(req.Interface, req.SSID, req.Password, req.Autoconnect) - if err != nil { - log.Printf("Failed to configure access point on interface %s: %v", req.Interface, err) - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - log.Printf("Successfully configured access point on interface %s with UUID %s", req.Interface, uuid) - - resp := struct { - UUID string `json:"uuid"` - }{ - UUID: uuid, - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - log.Printf("Failed to encode response: %v", err) - writeError(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func HandleNetworkUp(w http.ResponseWriter, r *http.Request) { - var req networkUpRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, err.Error(), http.StatusBadRequest) - return - } - vars := mux.Vars(r) - iface := vars["interface"] - - log.Printf("Bringing up network interface %s with UUID %s", iface, req.UUID) - if err := network.Up(iface, req.UUID); err != nil { - log.Printf("Failed to bring up network interface %s: %v", iface, err) - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - log.Printf("Successfully brought up network interface %s", iface) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func HandleNetworkDown(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - iface := vars["interface"] - if err := network.Down(iface); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func HandleNetworkRemove(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - uuid := vars["uuid"] - if err := network.Remove(uuid); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func HandleGetHostname(w http.ResponseWriter, r *http.Request) { - hostname, err := network.GetHostname() - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"hostname": hostname}) -} - -func HandleSetHostname(w http.ResponseWriter, r *http.Request) { - var req setHostnameRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, err.Error(), http.StatusBadRequest) - return - } - - if err := network.SetHostname(req.Hostname); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func HandleAddAuthorizedKey(w http.ResponseWriter, r *http.Request) { - var req authorizedKeyRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, err.Error(), http.StatusBadRequest) - return - } - vars := mux.Vars(r) - username := vars["user"] - - fingerprint, err := user.AddAuthorizedKey(username, req.PubKey) - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"fingerprint": fingerprint}) -} - -func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - fingerprint := vars["fingerprint"] - if fingerprint == "" { - writeError(w, "fingerprint parameter is required", http.StatusBadRequest) - return - } - - username := vars["user"] - if username == "" { - writeError(w, "user parameter is required", http.StatusBadRequest) - return - } - - fingerprintBytes, err := base64.RawURLEncoding.DecodeString(fingerprint) - if err != nil { - writeError(w, "invalid fingerprint base64", http.StatusBadRequest) - return - } - - if err := user.RemoveAuthorizedKey(username, string(fingerprintBytes)); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func HandleReboot(w http.ResponseWriter, r *http.Request) { - if err := system.Restart(); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func HandleShutdown(w http.ResponseWriter, r *http.Request) { - if err := system.Shutdown(); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func ClusterAgentHandler(agent *cluster.Agent, handler func(http.ResponseWriter, *http.Request, *cluster.Agent)) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - handler(w, r, agent) - } -} - -func HandleClusterJoin(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { - var joinRequest struct { - Join []string `json:"join"` - } - err := json.NewDecoder(r.Body).Decode(&joinRequest) - if err != nil { - writeError(w, "Invalid request body", http.StatusBadRequest) - return - } - joinAddrs := strings.Join(joinRequest.Join, ",") - if joinAddrs == "" { - writeError(w, "No join addresses provided", http.StatusBadRequest) - return - } - - addrs := strings.Split(joinAddrs, ",") - n, err := agent.Join(addrs, true) - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]int{"joined": n}) -} - -func HandleClusterLeave(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { - err := agent.Leave() - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -} - -func HandleClusterMembers(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { - if agent == nil { - writeError(w, "cluster agent is not initialized", http.StatusInternalServerError) - return - } - members, err := agent.Members() - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(members) -} - -func HandleClusterEvent(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { - if agent == nil { - writeError(w, "cluster agent is not initialized", http.StatusInternalServerError) - return - } - var req clusterEventRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, err.Error(), http.StatusBadRequest) - return - } - event := cluster.ClusterEvent{ - Name: req.Name, - Data: []byte(req.Payload), - } - err := agent.Event(event) - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) -}