diff --git a/Makefile b/Makefile index c13f722..796aa8a 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ SHELL := bash ARCH ?= amd64 ADDR ?= 0.0.0.0:8080 +default: build + generate: swagger generate server -f api/rcond.yaml -t api/ go mod tidy @@ -37,4 +39,4 @@ dev: go run cmd/rcond/main.go upload: - scp bin/rcond-${ARCH} pi@rpi-test:/home/pi/rcond + scp bin/rcond-${ARCH} pi@192.168.1.43:/home/pi/rcond diff --git a/README.md b/README.md index 7b5701a..c4a15bd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A simple daemon and REST API designed to simplify the management of various system components, including: - Network connections: Utilizing NetworkManager's D-Bus interface to dynamically configure network connections -- System hostname: Interacting with the hostname1 service to dynamically update the system's hostname +- 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 ## Requirements @@ -95,43 +95,34 @@ All endpoints use JSON for request and response payloads. ## Examples -### Setup an Access Point +### Connect to a WiFi Access Point + +This example will automatically connect to a WiFi access point with the given SSID and password on the interface "wlan0". ```bash -#!/usr/bin/env bash -set -euo pipefail - -# Example script to create and activate a WiFi access point -# Requires: -# - RCOND_ADDR (default: http://0.0.0.0:8080) -# - RCOND_API_TOKEN (your API token) - -API_URL="${RCOND_ADDR:-http://0.0.0.0:8080}" -API_TOKEN="${RCOND_API_TOKEN:-your_api_token}" -INTERFACE="wlan0" -SSID="MyAccessPoint" -PASSWORD="StrongPassword" - -echo "Creating access point '$SSID' on interface '$INTERFACE'..." -ap_response=$(curl -sSf -X POST "$API_URL/network/ap" \ +curl -sSf -X POST "http://rpi-test:8080/network/sta" \ -H "Content-Type: application/json" \ - -H "X-API-Token: $API_TOKEN" \ + -H "X-API-Token: 1234567890" \ -d '{ - "interface": "'"$INTERFACE"'", - "ssid": "'"$SSID"'", - "password": "'"$PASSWORD"'" - }') - -# Extract the UUID from the JSON response -AP_UUID=$(echo "$ap_response" | jq -r '.uuid') - -echo "Activating connection with UUID '$AP_UUID' on interface '$INTERFACE'..." -curl -sSf -X PUT "$API_URL/network/interface/$INTERFACE" \ - -H "Content-Type: application/json" \ - -H "X-API-Token: $API_TOKEN" \ - -d '{ - "uuid": "'"$AP_UUID"'" + "interface": "wlan0", + "ssid": "MyAccessPoint", + "password": "StrongPassword", + "autoconnect": true }' +``` -echo "Access point '$SSID' is now up and running on $INTERFACE." -``` \ No newline at end of file +### Setup an Access Point + +This example will create an access point on the interface "wlan0" with the given SSID and password. + +```bash +curl -sSf -X POST "http://rpi-test:8080/network/ap" \ + -H "Content-Type: application/json" \ + -H "X-API-Token: 1234567890" \ + -d '{ + "interface": "wlan0", + "ssid": "MyAccessPoint", + "password": "StrongPassword", + "autoconnect": true + }' +``` diff --git a/api/rcond.yaml b/api/rcond.yaml index 7a54dc0..56ce1b7 100644 --- a/api/rcond.yaml +++ b/api/rcond.yaml @@ -52,7 +52,71 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - + /network/sta: + post: + summary: Configure WiFi station + description: Creates a WiFi station (client) configuration on the specified interface + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - interface + - ssid + - password + properties: + interface: + type: string + description: Network interface name + example: "wlan0" + ssid: + type: string + description: WiFi network SSID + example: "MyNetworkSSID" + password: + type: string + description: WiFi network password + example: "SuperSecretPassword" + autoconnect: + type: boolean + description: Whether to automatically connect to the access point + example: true + responses: + '200': + description: WiFi station configured successfully + content: + application/json: + schema: + type: object + properties: + uuid: + type: string + description: UUID of the created connection profile + example: "7d706027-727c-4d4c-a816-f0e1b99db8ab" + status: + type: string + description: Status of the operation + example: "success" + '400': + description: Invalid request payload + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized - invalid or missing API token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /network/ap: post: summary: Configure WiFi access point @@ -80,6 +144,10 @@ paths: type: string description: WiFi network password example: "SuperSecretPassword" + autoconnect: + type: boolean + description: Whether to automatically start the access point + example: true responses: '200': description: Access point configured successfully diff --git a/pkg/http/handlers.go b/pkg/http/handlers.go index ec9a249..7d0bd0a 100644 --- a/pkg/http/handlers.go +++ b/pkg/http/handlers.go @@ -6,15 +6,23 @@ import ( "log" "net/http" - "github.com/0x1d/rcond/pkg/network" + network "github.com/0x1d/rcond/pkg/network" "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"` + 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 { @@ -40,6 +48,30 @@ func writeError(w http.ResponseWriter, message string, code int) { json.NewEncoder(w).Encode(errorResponse{Error: message}) } +func HandleConfigureSTA(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + writeError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + 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) { if r.Method != http.MethodPost { writeError(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -53,7 +85,7 @@ func HandleConfigureAP(w http.ResponseWriter, r *http.Request) { } log.Printf("Configuring access point on interface %s", req.Interface) - uuid, err := network.ConfigureAP(req.Interface, req.SSID, req.Password) + 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) diff --git a/pkg/http/server.go b/pkg/http/server.go index ad14f9d..15f3be9 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -59,6 +59,7 @@ func (s *Server) verifyToken(next http.HandlerFunc) http.HandlerFunc { func (s *Server) RegisterRoutes() { s.router.HandleFunc("/health", s.healthHandler).Methods(http.MethodGet) s.router.HandleFunc("/network/ap", s.verifyToken(HandleConfigureAP)).Methods(http.MethodPost) + s.router.HandleFunc("/network/sta", s.verifyToken(HandleConfigureSTA)).Methods(http.MethodPost) s.router.HandleFunc("/network/interface/{interface}", s.verifyToken(HandleNetworkUp)).Methods(http.MethodPut) s.router.HandleFunc("/network/interface/{interface}", s.verifyToken(HandleNetworkDown)).Methods(http.MethodDelete) s.router.HandleFunc("/network/connection/{uuid}", s.verifyToken(HandleNetworkRemove)).Methods(http.MethodDelete) diff --git a/pkg/network/nm.go b/pkg/network/network.go similarity index 84% rename from pkg/network/nm.go rename to pkg/network/network.go index a23b4ad..842a105 100644 --- a/pkg/network/nm.go +++ b/pkg/network/network.go @@ -31,13 +31,28 @@ type ConnectionConfig struct { IPv6Method string } -// DefaultAPConfig returns a default access point configuration -func DefaultAPConfig(uuid uuid.UUID, ssid string, password string) *ConnectionConfig { +func DefaultSTAConfig(uuid uuid.UUID, ssid string, password string, autoconnect bool) *ConnectionConfig { return &ConnectionConfig{ Type: "802-11-wireless", UUID: uuid.String(), ID: ssid, - AutoConnect: true, + AutoConnect: autoconnect, + SSID: ssid, + Mode: "infrastructure", + KeyMgmt: "wpa-psk", + PSK: password, + IPv4Method: "auto", + IPv6Method: "ignore", + } +} + +// DefaultAPConfig returns a default access point configuration +func DefaultAPConfig(uuid uuid.UUID, ssid string, password string, autoconnect bool) *ConnectionConfig { + return &ConnectionConfig{ + Type: "802-11-wireless", + UUID: uuid.String(), + ID: ssid, + AutoConnect: autoconnect, SSID: ssid, Mode: "ap", Band: "bg", @@ -219,8 +234,16 @@ func GetConnectionPath(conn *dbus.Conn, connUUID string) (dbus.ObjectPath, error // Takes a D-Bus connection, UUID string, SSID string, and password string as arguments. // Returns the D-Bus object path of the new connection profile. // Returns an error if the connection creation fails. -func AddAccessPointConnection(conn *dbus.Conn, uuid uuid.UUID, ssid string, password string) (dbus.ObjectPath, error) { - return AddConnectionWithConfig(conn, DefaultAPConfig(uuid, ssid, password)) +func AddAccessPointConnection(conn *dbus.Conn, uuid uuid.UUID, ssid string, password string, autoconnect bool) (dbus.ObjectPath, error) { + return AddConnectionWithConfig(conn, DefaultAPConfig(uuid, ssid, password, autoconnect)) +} + +// AddStationConnection creates a new NetworkManager connection profile for a WiFi station (client). +// Takes a D-Bus connection, UUID string, SSID string, and password string as arguments. +// Returns the D-Bus object path of the new connection profile. +// Returns an error if the connection creation fails. +func AddStationConnection(conn *dbus.Conn, uuid uuid.UUID, ssid string, password string, autoconnect bool) (dbus.ObjectPath, error) { + return AddConnectionWithConfig(conn, DefaultSTAConfig(uuid, ssid, password, autoconnect)) } // AddConnectionWithConfig creates a new NetworkManager connection profile with the given configuration. @@ -233,6 +256,19 @@ func AddConnectionWithConfig(conn *dbus.Conn, cfg *ConnectionConfig) (dbus.Objec "/org/freedesktop/NetworkManager/Settings", ) + var wirelessMap map[string]dbus.Variant + if cfg.Mode == "ap" { + wirelessMap = map[string]dbus.Variant{ + "mode": dbus.MakeVariant(cfg.Mode), + "band": dbus.MakeVariant(cfg.Band), + "channel": dbus.MakeVariant(cfg.Channel), + } + } else { + wirelessMap = map[string]dbus.Variant{ + "ssid": dbus.MakeVariant([]byte(cfg.SSID)), + } + } + settingsMap := map[string]map[string]dbus.Variant{ "connection": { "type": dbus.MakeVariant(cfg.Type), @@ -240,12 +276,7 @@ func AddConnectionWithConfig(conn *dbus.Conn, cfg *ConnectionConfig) (dbus.Objec "id": dbus.MakeVariant(cfg.ID), "autoconnect": dbus.MakeVariant(cfg.AutoConnect), }, - "802-11-wireless": { - "ssid": dbus.MakeVariant([]byte(cfg.SSID)), - "mode": dbus.MakeVariant(cfg.Mode), - "band": dbus.MakeVariant(cfg.Band), - "channel": dbus.MakeVariant(cfg.Channel), - }, + "802-11-wireless": wirelessMap, "802-11-wireless-security": { "key-mgmt": dbus.MakeVariant(cfg.KeyMgmt), "psk": dbus.MakeVariant(cfg.PSK), @@ -318,15 +349,37 @@ func SetHostname(newHost string) error { }) } +// ConfigureSTA connects to a WiFi access point with the specified settings. +// It takes the interface name, SSID and password as arguments. +// A new connection with a generated UUID will be created. +// Returns the UUID of the created connection and any error that occurred. +func ConfigureSTA(iface string, ssid string, password string, autoconnect bool) (string, error) { + uuid := uuid.New() + + err := 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) + } + return nil + }) + + if err != nil { + return "", err + } + + return uuid.String(), nil +} + // ConfigureAP creates a WiFi access point connection with the specified settings. // It takes the interface name, SSID and password as arguments. // A new connection with a generated UUID will be created. // Returns the UUID of the created connection and any error that occurred. -func ConfigureAP(iface string, ssid string, password string) (string, error) { +func ConfigureAP(iface string, ssid string, password string, autoconnect bool) (string, error) { uuid := uuid.New() err := withDbus(func(conn *dbus.Conn) error { - _, err := AddAccessPointConnection(conn, uuid, ssid, password) + _, err := AddAccessPointConnection(conn, uuid, ssid, password, autoconnect) if err != nil { return fmt.Errorf("failed to create access point connection: %v", err) }