diff --git a/README.md b/README.md index f299b2c..42400dc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A simple daemon and REST API designed to simplify the management of various syst - Network connections: Utilizing NetworkManager's D-Bus interface to dynamically configure network connections - System hostname: Dynamically update the system's hostname - Authorized SSH keys: Directly managing the user's authorized_keys file to securely add, remove, or modify authorized SSH keys +- System state: Restart and shutdown the system ## Requirements @@ -85,6 +86,8 @@ All endpoints except `/health` require authentication via an API token passed in | POST | `/hostname` | Set the hostname | | POST | `/users/{user}/keys` | Add an authorized SSH key | | DELETE | `/users/{user}/keys/{fingerprint}` | Remove an authorized SSH key | +| POST | `/system/restart` | Restart the system | +| POST | `/system/shutdown` | Shutdown the system | ### Response Codes diff --git a/api/rcond.yaml b/api/rcond.yaml index 56ce1b7..434af38 100644 --- a/api/rcond.yaml +++ b/api/rcond.yaml @@ -494,3 +494,46 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /system/restart: + post: + summary: Restart system + description: Restarts the system + responses: + '200': + description: System restarted successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /system/shutdown: + post: + summary: Shutdown system + description: Shuts down the system + responses: + '200': + description: System shut down successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + \ No newline at end of file diff --git a/pkg/http/handlers.go b/pkg/http/handlers.go index 7d0bd0a..425ba6c 100644 --- a/pkg/http/handlers.go +++ b/pkg/http/handlers.go @@ -7,6 +7,7 @@ import ( "net/http" network "github.com/0x1d/rcond/pkg/network" + "github.com/0x1d/rcond/pkg/system" "github.com/0x1d/rcond/pkg/user" "github.com/gorilla/mux" ) @@ -256,3 +257,33 @@ func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) { 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 r.Method != http.MethodPost { + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + 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 r.Method != http.MethodPost { + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + 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/server.go b/pkg/http/server.go index 15f3be9..6d8a676 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -67,6 +67,8 @@ func (s *Server) RegisterRoutes() { s.router.HandleFunc("/hostname", s.verifyToken(HandleSetHostname)).Methods(http.MethodPost) s.router.HandleFunc("/users/{user}/keys", s.verifyToken(HandleAddAuthorizedKey)).Methods(http.MethodPost) s.router.HandleFunc("/users/{user}/keys/{fingerprint}", s.verifyToken(HandleRemoveAuthorizedKey)).Methods(http.MethodDelete) + s.router.HandleFunc("/system/restart", s.verifyToken(HandleReboot)).Methods(http.MethodPost) + s.router.HandleFunc("/system/shutdown", s.verifyToken(HandleShutdown)).Methods(http.MethodPost) } func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/network/network.go b/pkg/network/network.go index 842a105..48afd5f 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -6,6 +6,7 @@ import ( "os" "time" + "github.com/0x1d/rcond/pkg/system" "github.com/godbus/dbus/v5" "github.com/google/uuid" ) @@ -64,21 +65,6 @@ func DefaultAPConfig(uuid uuid.UUID, ssid string, password string, autoconnect b } } -// withDbus executes the given function with a D-Bus system connection -// and handles any connection errors -func withDbus(fn func(*dbus.Conn) error) error { - conn, err := dbus.SystemBus() - if err != nil { - log.Printf("Failed to connect to system bus: %v", err) - return err - } - if err := fn(conn); err != nil { - log.Print(err) - return err - } - return nil -} - // ActivateConnection activates a NetworkManager connection profile. // It takes a D-Bus connection, connection profile path, and device path as arguments. // The function waits up to 10 seconds for the connection to become active. @@ -259,6 +245,7 @@ func AddConnectionWithConfig(conn *dbus.Conn, cfg *ConnectionConfig) (dbus.Objec var wirelessMap map[string]dbus.Variant if cfg.Mode == "ap" { wirelessMap = map[string]dbus.Variant{ + "ssid": dbus.MakeVariant([]byte(cfg.SSID)), "mode": dbus.MakeVariant(cfg.Mode), "band": dbus.MakeVariant(cfg.Band), "channel": dbus.MakeVariant(cfg.Channel), @@ -266,6 +253,7 @@ func AddConnectionWithConfig(conn *dbus.Conn, cfg *ConnectionConfig) (dbus.Objec } else { wirelessMap = map[string]dbus.Variant{ "ssid": dbus.MakeVariant([]byte(cfg.SSID)), + "mode": dbus.MakeVariant(cfg.Mode), } } @@ -335,7 +323,7 @@ func GetHostname() (string, error) { // SetHostname changes the static hostname via the system bus. // newHost is your desired hostname, interactive=false skips any prompt. func SetHostname(newHost string) error { - return withDbus(func(conn *dbus.Conn) error { + return system.WithDbus(func(conn *dbus.Conn) error { obj := conn.Object( "org.freedesktop.hostname1", dbus.ObjectPath("/org/freedesktop/hostname1"), @@ -356,7 +344,7 @@ func SetHostname(newHost string) error { func ConfigureSTA(iface string, ssid string, password string, autoconnect bool) (string, error) { uuid := uuid.New() - err := withDbus(func(conn *dbus.Conn) error { + err := system.WithDbus(func(conn *dbus.Conn) error { _, err := AddStationConnection(conn, uuid, ssid, password, autoconnect) if err != nil { return fmt.Errorf("failed to create station connection: %v", err) @@ -378,7 +366,7 @@ func ConfigureSTA(iface string, ssid string, password string, autoconnect bool) func ConfigureAP(iface string, ssid string, password string, autoconnect bool) (string, error) { uuid := uuid.New() - err := withDbus(func(conn *dbus.Conn) error { + err := system.WithDbus(func(conn *dbus.Conn) error { _, err := AddAccessPointConnection(conn, uuid, ssid, password, autoconnect) if err != nil { return fmt.Errorf("failed to create access point connection: %v", err) @@ -399,7 +387,7 @@ func ConfigureAP(iface string, ssid string, password string, autoconnect bool) ( // The connection will be activated on the specified interface. // Returns an error if any operation fails. func Up(iface string, uuid string) error { - return withDbus(func(conn *dbus.Conn) error { + return system.WithDbus(func(conn *dbus.Conn) error { connPath, err := GetConnectionPath(conn, uuid) if err != nil { return err @@ -429,7 +417,7 @@ func Up(iface string, uuid string) error { // It takes the interface name as an argument. // Returns an error if the device cannot be found or disconnected. func Down(iface string) error { - return withDbus(func(conn *dbus.Conn) error { + return system.WithDbus(func(conn *dbus.Conn) error { devPath, err := GetDeviceByIpIface(conn, iface) if err != nil { return err @@ -447,7 +435,7 @@ func Down(iface string) error { // If no connection with the UUID exists, it returns nil. // Returns an error if the connection exists but cannot be deleted. func Remove(uuid string) error { - return withDbus(func(conn *dbus.Conn) error { + return system.WithDbus(func(conn *dbus.Conn) error { connPath, err := GetConnectionPath(conn, uuid) if err != nil { return err diff --git a/pkg/system/dbus.go b/pkg/system/dbus.go new file mode 100644 index 0000000..801c881 --- /dev/null +++ b/pkg/system/dbus.go @@ -0,0 +1,23 @@ +package system + +import ( + "log" + + "github.com/godbus/dbus/v5" +) + +// WithDbus executes the given function with a D-Bus system connection +// and handles any connection errors +func WithDbus(fn func(*dbus.Conn) error) error { + conn, err := dbus.SystemBus() + if err != nil { + log.Printf("Failed to connect to system bus: %v", err) + return err + } + if err := fn(conn); err != nil { + log.Print(err) + return err + } + conn.Close() + return nil +} diff --git a/pkg/system/state.go b/pkg/system/state.go new file mode 100644 index 0000000..1d22e60 --- /dev/null +++ b/pkg/system/state.go @@ -0,0 +1,33 @@ +package system + +import ( + "log" + + "github.com/godbus/dbus/v5" +) + +// Restart restarts the system. +func Restart() error { + return WithDbus(func(conn *dbus.Conn) error { + obj := conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") + log.Println("Rebooting system...") + call := obj.Call("org.freedesktop.systemd1.Manager.Reboot", 0) + if call.Err != nil { + log.Fatal(call.Err) + } + return nil + }) +} + +// Shutdown shuts down the system. +func Shutdown() error { + return WithDbus(func(conn *dbus.Conn) error { + obj := conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") + log.Println("Shutting down system...") + call := obj.Call("org.freedesktop.systemd1.Manager.PowerOff", 0) + if call.Err != nil { + log.Fatal(call.Err) + } + return nil + }) +}