mirror of
https://github.com/0x1d/rcond.git
synced 2025-12-14 18:25:21 +01:00
feat: add backend for configuring WiFi STA
This commit is contained in:
4
Makefile
4
Makefile
@@ -2,6 +2,8 @@ SHELL := bash
|
|||||||
ARCH ?= amd64
|
ARCH ?= amd64
|
||||||
ADDR ?= 0.0.0.0:8080
|
ADDR ?= 0.0.0.0:8080
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
swagger generate server -f api/rcond.yaml -t api/
|
swagger generate server -f api/rcond.yaml -t api/
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@@ -37,4 +39,4 @@ dev:
|
|||||||
go run cmd/rcond/main.go
|
go run cmd/rcond/main.go
|
||||||
|
|
||||||
upload:
|
upload:
|
||||||
scp bin/rcond-${ARCH} pi@rpi-test:/home/pi/rcond
|
scp bin/rcond-${ARCH} pi@192.168.1.43:/home/pi/rcond
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A simple daemon and REST API designed to simplify the management of various system components, including:
|
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
|
- 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
|
- Authorized SSH keys: Directly managing the user's authorized_keys file to securely add, remove, or modify authorized SSH keys
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -95,43 +95,34 @@ All endpoints use JSON for request and response payloads.
|
|||||||
|
|
||||||
## Examples
|
## 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
|
```bash
|
||||||
#!/usr/bin/env bash
|
curl -sSf -X POST "http://rpi-test:8080/network/sta" \
|
||||||
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" \
|
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "X-API-Token: $API_TOKEN" \
|
-H "X-API-Token: 1234567890" \
|
||||||
-d '{
|
-d '{
|
||||||
"interface": "'"$INTERFACE"'",
|
"interface": "wlan0",
|
||||||
"ssid": "'"$SSID"'",
|
"ssid": "MyAccessPoint",
|
||||||
"password": "'"$PASSWORD"'"
|
"password": "StrongPassword",
|
||||||
}')
|
"autoconnect": true
|
||||||
|
|
||||||
# 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"'"
|
|
||||||
}'
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
echo "Access point '$SSID' is now up and running on $INTERFACE."
|
### 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
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|||||||
@@ -52,7 +52,71 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Error'
|
$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:
|
/network/ap:
|
||||||
post:
|
post:
|
||||||
summary: Configure WiFi access point
|
summary: Configure WiFi access point
|
||||||
@@ -80,6 +144,10 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
description: WiFi network password
|
description: WiFi network password
|
||||||
example: "SuperSecretPassword"
|
example: "SuperSecretPassword"
|
||||||
|
autoconnect:
|
||||||
|
type: boolean
|
||||||
|
description: Whether to automatically start the access point
|
||||||
|
example: true
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Access point configured successfully
|
description: Access point configured successfully
|
||||||
|
|||||||
@@ -6,15 +6,23 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/0x1d/rcond/pkg/network"
|
network "github.com/0x1d/rcond/pkg/network"
|
||||||
"github.com/0x1d/rcond/pkg/user"
|
"github.com/0x1d/rcond/pkg/user"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
type configureAPRequest struct {
|
type configureAPRequest struct {
|
||||||
Interface string `json:"interface"`
|
Interface string `json:"interface"`
|
||||||
SSID string `json:"ssid"`
|
SSID string `json:"ssid"`
|
||||||
Password string `json:"password"`
|
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 {
|
type networkUpRequest struct {
|
||||||
@@ -40,6 +48,30 @@ func writeError(w http.ResponseWriter, message string, code int) {
|
|||||||
json.NewEncoder(w).Encode(errorResponse{Error: message})
|
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) {
|
func HandleConfigureAP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
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)
|
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 {
|
if err != nil {
|
||||||
log.Printf("Failed to configure access point on interface %s: %v", req.Interface, err)
|
log.Printf("Failed to configure access point on interface %s: %v", req.Interface, err)
|
||||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func (s *Server) verifyToken(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
func (s *Server) RegisterRoutes() {
|
func (s *Server) RegisterRoutes() {
|
||||||
s.router.HandleFunc("/health", s.healthHandler).Methods(http.MethodGet)
|
s.router.HandleFunc("/health", s.healthHandler).Methods(http.MethodGet)
|
||||||
s.router.HandleFunc("/network/ap", s.verifyToken(HandleConfigureAP)).Methods(http.MethodPost)
|
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(HandleNetworkUp)).Methods(http.MethodPut)
|
||||||
s.router.HandleFunc("/network/interface/{interface}", s.verifyToken(HandleNetworkDown)).Methods(http.MethodDelete)
|
s.router.HandleFunc("/network/interface/{interface}", s.verifyToken(HandleNetworkDown)).Methods(http.MethodDelete)
|
||||||
s.router.HandleFunc("/network/connection/{uuid}", s.verifyToken(HandleNetworkRemove)).Methods(http.MethodDelete)
|
s.router.HandleFunc("/network/connection/{uuid}", s.verifyToken(HandleNetworkRemove)).Methods(http.MethodDelete)
|
||||||
|
|||||||
@@ -31,13 +31,28 @@ type ConnectionConfig struct {
|
|||||||
IPv6Method string
|
IPv6Method string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAPConfig returns a default access point configuration
|
func DefaultSTAConfig(uuid uuid.UUID, ssid string, password string, autoconnect bool) *ConnectionConfig {
|
||||||
func DefaultAPConfig(uuid uuid.UUID, ssid string, password string) *ConnectionConfig {
|
|
||||||
return &ConnectionConfig{
|
return &ConnectionConfig{
|
||||||
Type: "802-11-wireless",
|
Type: "802-11-wireless",
|
||||||
UUID: uuid.String(),
|
UUID: uuid.String(),
|
||||||
ID: ssid,
|
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,
|
SSID: ssid,
|
||||||
Mode: "ap",
|
Mode: "ap",
|
||||||
Band: "bg",
|
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.
|
// 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 the D-Bus object path of the new connection profile.
|
||||||
// Returns an error if the connection creation fails.
|
// Returns an error if the connection creation fails.
|
||||||
func AddAccessPointConnection(conn *dbus.Conn, uuid uuid.UUID, ssid string, password string) (dbus.ObjectPath, error) {
|
func AddAccessPointConnection(conn *dbus.Conn, uuid uuid.UUID, ssid string, password string, autoconnect bool) (dbus.ObjectPath, error) {
|
||||||
return AddConnectionWithConfig(conn, DefaultAPConfig(uuid, ssid, password))
|
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.
|
// 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",
|
"/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{
|
settingsMap := map[string]map[string]dbus.Variant{
|
||||||
"connection": {
|
"connection": {
|
||||||
"type": dbus.MakeVariant(cfg.Type),
|
"type": dbus.MakeVariant(cfg.Type),
|
||||||
@@ -240,12 +276,7 @@ func AddConnectionWithConfig(conn *dbus.Conn, cfg *ConnectionConfig) (dbus.Objec
|
|||||||
"id": dbus.MakeVariant(cfg.ID),
|
"id": dbus.MakeVariant(cfg.ID),
|
||||||
"autoconnect": dbus.MakeVariant(cfg.AutoConnect),
|
"autoconnect": dbus.MakeVariant(cfg.AutoConnect),
|
||||||
},
|
},
|
||||||
"802-11-wireless": {
|
"802-11-wireless": wirelessMap,
|
||||||
"ssid": dbus.MakeVariant([]byte(cfg.SSID)),
|
|
||||||
"mode": dbus.MakeVariant(cfg.Mode),
|
|
||||||
"band": dbus.MakeVariant(cfg.Band),
|
|
||||||
"channel": dbus.MakeVariant(cfg.Channel),
|
|
||||||
},
|
|
||||||
"802-11-wireless-security": {
|
"802-11-wireless-security": {
|
||||||
"key-mgmt": dbus.MakeVariant(cfg.KeyMgmt),
|
"key-mgmt": dbus.MakeVariant(cfg.KeyMgmt),
|
||||||
"psk": dbus.MakeVariant(cfg.PSK),
|
"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.
|
// ConfigureAP creates a WiFi access point connection with the specified settings.
|
||||||
// It takes the interface name, SSID and password as arguments.
|
// It takes the interface name, SSID and password as arguments.
|
||||||
// A new connection with a generated UUID will be created.
|
// A new connection with a generated UUID will be created.
|
||||||
// Returns the UUID of the created connection and any error that occurred.
|
// 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()
|
uuid := uuid.New()
|
||||||
|
|
||||||
err := withDbus(func(conn *dbus.Conn) error {
|
err := withDbus(func(conn *dbus.Conn) error {
|
||||||
_, err := AddAccessPointConnection(conn, uuid, ssid, password)
|
_, err := AddAccessPointConnection(conn, uuid, ssid, password, autoconnect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create access point connection: %v", err)
|
return fmt.Errorf("failed to create access point connection: %v", err)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user