feat: refactoring

This commit is contained in:
2025-05-17 20:32:53 +02:00
parent 956037fc03
commit fab01674d5
7 changed files with 364 additions and 330 deletions

View File

@@ -10,16 +10,18 @@ import (
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
) )
// Agent represents a Serf cluster agent.
type Agent struct { type Agent struct {
Serf *serf.Serf 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 { type ClusterEvent struct {
Name string Name string
Data []byte 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) { func NewAgent(clusterConfig *config.ClusterConfig, clusterEvents map[string]func([]byte)) (*Agent, error) {
config := serf.DefaultConfig() config := serf.DefaultConfig()
config.Init() config.Init()
@@ -65,11 +67,13 @@ func (a *Agent) Event(event ClusterEvent) error {
return nil return nil
} }
// Members returns a list of members in the Serf cluster.
func (a *Agent) Members() ([]serf.Member, error) { func (a *Agent) Members() ([]serf.Member, error) {
log.Printf("Getting members of the cluster") log.Printf("Getting members of the cluster")
return a.Serf.Members(), nil 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) { func (a *Agent) Join(addrs []string, ignoreOld bool) (int, error) {
log.Printf("Joining nodes in the cluster: %v", addrs) log.Printf("Joining nodes in the cluster: %v", addrs)
n, err := a.Serf.Join(addrs, ignoreOld) n, err := a.Serf.Join(addrs, ignoreOld)
@@ -81,16 +85,18 @@ func (a *Agent) Join(addrs []string, ignoreOld bool) (int, error) {
return n, nil return n, nil
} }
// Leave causes the agent to leave the Serf cluster.
func (a *Agent) Leave() error { func (a *Agent) Leave() error {
return a.Serf.Leave() return a.Serf.Leave()
} }
// Shutdown shuts down the Serf cluster agent.
func (a *Agent) Shutdown() error { func (a *Agent) Shutdown() error {
log.Printf("Shutting down cluster agent") log.Printf("Shutting down cluster agent")
return a.Serf.Shutdown() 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)) { func handleEvents(eventCh chan serf.Event, clusterEvents map[string]func([]byte)) {
for event := range eventCh { for event := range eventCh {
switch event.EventType() { switch event.EventType() {

View File

@@ -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"})
}

16
pkg/http/handle_error.go Normal file
View File

@@ -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})
}

153
pkg/http/handle_network.go Normal file
View File

@@ -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"})
}

28
pkg/http/handle_system.go Normal file
View File

@@ -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"})
}

63
pkg/http/handle_user.go Normal file
View File

@@ -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"})
}

View File

@@ -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"})
}