feat: refactor REST API

This commit is contained in:
2025-05-04 16:26:04 +02:00
parent bdd5f7bea5
commit d15ec1f8cb
8 changed files with 336 additions and 229 deletions

View File

@@ -25,13 +25,14 @@ All endpoints except `/health` require authentication via an API token passed in
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|
| GET | `/health` | Health check endpoint that returns status | | GET | `/health` | Health check endpoint that returns status |
| POST | `/network/up` | Create and activate a WiFi access point | | POST | `/network/ap` | Create and activate a WiFi access point |
| POST | `/network/down` | Deactivate a WiFi interface | | PUT | `/network/interface/{interface}` | Activate a connection |
| POST | `/network/remove` | Remove the stored connection profile | | DELETE | `/network/interface/{interface}` | Deactivate a connection |
| DELETE | `/network/connection/{uuid}` | Remove a connection |
| GET | `/hostname` | Get the hostname | | GET | `/hostname` | Get the hostname |
| POST | `/hostname` | Set the hostname | | POST | `/hostname` | Set the hostname |
| POST | `/authorized-key` | Add an authorized SSH key | | POST | `/users/{user}/keys` | Add an authorized SSH key |
| DELETE | `/authorized-key` | Remove an authorized SSH key | | DELETE | `/users/{user}/keys/{fingerprint}` | Remove an authorized SSH key |
### Response Codes ### Response Codes
@@ -42,79 +43,3 @@ All endpoints except `/health` require authentication via an API token passed in
### Request/Response Format ### Request/Response Format
All endpoints use JSON for request and response payloads. All endpoints use JSON for request and response payloads.
### Bring a network up
```bash
curl -v -X POST http://localhost:8080/network/up \
-H "Content-Type: application/json" \
-H "X-API-Token: 1234567890" \
-d '{
"interface": "wlan0",
"ssid": "MyNetworkSSID",
"password": "SuperSecretPassword"
}'
```
### Bring a network down
```bash
curl -v -X POST http://localhost:8080/network/down \
-H "Content-Type: application/json" \
-H "X-API-Token: 1234567890" \
-d '{
"interface": "wlan0"
}'
```
### Remove the stored connection
```bash
curl -v -X POST http://localhost:8080/network/remove \
-H "X-API-Token: 1234567890" \
-d '{
"interface": "wlan0"
}'
```
### Get the hostname
```bash
curl -v http://localhost:8080/hostname \
-H "X-API-Token: 1234567890"
```
### Set the hostname
```bash
curl -v -X POST http://localhost:8080/hostname \
-H "Content-Type: application/json" \
-H "X-API-Token: 1234567890" \
-d '{
"hostname": "MyHostname"
}'
```
### Add an authorized SSH key
```bash
curl -v -X POST http://localhost:8080/authorized-key \
-H "Content-Type: application/json" \
-H "X-API-Token: 1234567890" \
-d '{
"user": "pi",
"pubkey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1234567890"
}'
```
### Remove an authorized SSH key
```bash
curl -v -X DELETE http://localhost:8080/authorized-key \
-H "Content-Type: application/json" \
-H "X-API-Token: 1234567890" \
-d '{
"user": "pi",
"pubkey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1234567890"
}'
```

View File

@@ -24,6 +24,7 @@ paths:
get: get:
summary: Health check endpoint summary: Health check endpoint
description: Returns the health status of the service description: Returns the health status of the service
security: []
responses: responses:
'200': '200':
description: Service is healthy description: Service is healthy
@@ -36,10 +37,10 @@ paths:
type: string type: string
example: "healthy" example: "healthy"
/network/up: /network/ap:
post: post:
summary: Create and activate WiFi access point summary: Configure WiFi access point
description: Creates and activates a WiFi access point on the specified interface description: Creates a WiFi access point configuration on the specified interface
requestBody: requestBody:
required: true required: true
content: content:
@@ -65,7 +66,16 @@ paths:
example: "SuperSecretPassword" example: "SuperSecretPassword"
responses: responses:
'200': '200':
description: Network interface brought up successfully description: Access point 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"
'400': '400':
description: Invalid request payload description: Invalid request payload
'401': '401':
@@ -73,10 +83,18 @@ paths:
'500': '500':
description: Internal server error description: Internal server error
/network/down: /network/interface/{interface}:
post: put:
summary: Deactivate network interface summary: Activate network connection
description: Deactivates the specified network interface description: Activates an existing network connection on the specified interface
parameters:
- name: interface
in: path
required: true
schema:
type: string
description: Network interface name
example: "wlan0"
requestBody: requestBody:
required: true required: true
content: content:
@@ -84,12 +102,32 @@ paths:
schema: schema:
type: object type: object
required: required:
- interface - uuid
properties: properties:
interface: uuid:
type: string type: string
description: Network interface name description: UUID of the connection profile
example: "wlan0" example: "7d706027-727c-4d4c-a816-f0e1b99db8ab"
responses:
'200':
description: Network interface brought up successfully
'400':
description: Invalid request payload
'401':
description: Unauthorized - invalid or missing API token
'500':
description: Internal server error
delete:
summary: Deactivate network connection
description: Deactivates the specified network connection
parameters:
- name: interface
in: path
required: true
schema:
type: string
description: Network interface name
example: "wlan0"
responses: responses:
'200': '200':
description: Network interface brought down successfully description: Network interface brought down successfully
@@ -100,13 +138,23 @@ paths:
'500': '500':
description: Internal server error description: Internal server error
/network/remove: /network/connection/{uuid}:
post: delete:
summary: Remove stored connection profile summary: Remove stored connection profile
description: Removes the stored NetworkManager connection profile description: Removes the stored NetworkManager connection profile
parameters:
- name: uuid
in: path
required: true
schema:
type: string
description: UUID of the connection profile to remove
example: "7d706027-727c-4d4c-a816-f0e1b99db8ab"
responses: responses:
'200': '200':
description: Connection profile removed successfully description: Connection profile removed successfully
'400':
description: Invalid request payload
'401': '401':
description: Unauthorized - invalid or missing API token description: Unauthorized - invalid or missing API token
'500': '500':
@@ -154,10 +202,19 @@ paths:
description: Unauthorized - invalid or missing API token description: Unauthorized - invalid or missing API token
'500': '500':
description: Internal server error description: Internal server error
/authorized-key:
/users/{user}/keys:
post: post:
summary: Add SSH authorized key summary: Add SSH authorized key
description: Adds an SSH public key to a user's authorized_keys file description: Adds an SSH public key to a user's authorized_keys file
parameters:
- name: user
in: path
required: true
schema:
type: string
description: Username to add key for
example: "pi"
requestBody: requestBody:
required: true required: true
content: content:
@@ -165,13 +222,8 @@ paths:
schema: schema:
type: object type: object
required: required:
- user
- pubkey - pubkey
properties: properties:
user:
type: string
description: Username to add key for
example: "pi"
pubkey: pubkey:
type: string type: string
description: SSH public key to add description: SSH public key to add
@@ -179,36 +231,12 @@ paths:
responses: responses:
'200': '200':
description: Key added successfully description: Key added successfully
'400': content:
description: Invalid request payload or SSH key format text/plain:
'401': schema:
description: Unauthorized - invalid or missing API token type: string
'500': description: Fingerprint of the added key
description: Internal server error example: "SHA256:abcdef1234567890..."
delete:
summary: Remove SSH authorized key
description: Removes an SSH public key from a user's authorized_keys file
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- user
- pubkey
properties:
user:
type: string
description: Username to remove key for
example: "pi"
pubkey:
type: string
description: SSH public key to remove
example: "ssh-rsa AAAAB3NzaC1yc2E... user@host"
responses:
'200':
description: Key removed successfully
'400': '400':
description: Invalid request payload or SSH key format description: Invalid request payload or SSH key format
'401': '401':
@@ -216,3 +244,32 @@ paths:
'500': '500':
description: Internal server error description: Internal server error
/users/{user}/keys/{fingerprint}:
delete:
summary: Remove SSH authorized key
description: Removes an SSH public key from a user's authorized_keys file
parameters:
- name: user
in: path
required: true
schema:
type: string
description: Username to remove key for
example: "pi"
- name: fingerprint
in: path
required: true
schema:
type: string
format: string
description: URL-safe Base64 encoded fingerprint of the key to remove
example: "U0hBMjU2OmFiY2RlZjEyMzQ1Njc4OTAuLi4="
responses:
'200':
description: Key removed successfully
'400':
description: Invalid request payload or fingerprint
'401':
description: Unauthorized - invalid or missing API token
'500':
description: Internal server error

1
go.mod
View File

@@ -8,6 +8,7 @@ replace github.com/0x1d/rcond/pkg => ./pkg
require ( require (
github.com/godbus/dbus/v5 v5.1.0 github.com/godbus/dbus/v5 v5.1.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.37.0
) )

2
go.sum
View File

@@ -1,5 +1,7 @@
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=

View File

@@ -1,62 +1,107 @@
package http package http
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"github.com/0x1d/rcond/pkg/network" "github.com/0x1d/rcond/pkg/network"
"github.com/0x1d/rcond/pkg/user" "github.com/0x1d/rcond/pkg/user"
"github.com/gorilla/mux"
) )
const ( const (
NETWORK_CONNECTION_UUID = "7d706027-727c-4d4c-a816-f0e1b99db8ab" NETWORK_CONNECTION_UUID = "7d706027-727c-4d4c-a816-f0e1b99db8ab"
) )
func HandleNetworkUp(w http.ResponseWriter, r *http.Request) { type configureAPRequest struct {
Interface string `json:"interface"`
SSID string `json:"ssid"`
Password string `json:"password"`
}
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"`
}
func HandleConfigureAP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
var req struct { var req configureAPRequest
Interface string `json:"interface"`
SSID string `json:"ssid"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
log.Printf("Bringing up network interface %s with SSID %s", req.Interface, req.SSID) log.Printf("Configuring access point on interface %s", req.Interface)
if err := network.Up(req.Interface, req.SSID, req.Password, NETWORK_CONNECTION_UUID); err != nil { uuid, err := network.ConfigureAP(req.Interface, req.SSID, req.Password)
log.Printf("Failed to bring up network interface %s: %v", req.Interface, err) if err != nil {
log.Printf("Failed to configure access point on interface %s: %v", req.Interface, err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
log.Printf("Successfully brought up network interface %s", req.Interface) 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)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func HandleNetworkUp(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req networkUpRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(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)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Successfully brought up network interface %s", iface)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func HandleNetworkDown(w http.ResponseWriter, r *http.Request) { func HandleNetworkDown(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
var req struct { vars := mux.Vars(r)
Interface string `json:"interface"` iface := vars["interface"]
} if err := network.Down(iface); err != nil {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := network.Down(req.Interface); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@@ -65,12 +110,14 @@ func HandleNetworkDown(w http.ResponseWriter, r *http.Request) {
} }
func HandleNetworkRemove(w http.ResponseWriter, r *http.Request) { func HandleNetworkRemove(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
if err := network.Remove(NETWORK_CONNECTION_UUID); err != nil { vars := mux.Vars(r)
uuid := vars["uuid"]
if err := network.Remove(uuid); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@@ -93,10 +140,7 @@ func HandleSetHostname(w http.ResponseWriter, r *http.Request) {
return return
} }
var req struct { var req setHostnameRequest
Hostname string `json:"hostname"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
@@ -116,22 +160,21 @@ func HandleAddAuthorizedKey(w http.ResponseWriter, r *http.Request) {
return return
} }
var req struct { var req authorizedKeyRequest
User string `json:"user"`
PubKey string `json:"pubkey"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
vars := mux.Vars(r)
username := vars["user"]
if err := user.AddAuthorizedKey(req.User, req.PubKey); err != nil { fingerprint, err := user.AddAuthorizedKey(username, req.PubKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.WriteHeader(http.StatusOK) w.Write([]byte(fingerprint))
} }
func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) { func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) {
@@ -140,17 +183,26 @@ func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) {
return return
} }
var req struct { vars := mux.Vars(r)
User string `json:"user"` fingerprint := vars["fingerprint"]
PubKey string `json:"pubkey"` if fingerprint == "" {
} http.Error(w, "fingerprint parameter is required", http.StatusBadRequest)
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if err := user.RemoveAuthorizedKey(req.User, req.PubKey); err != nil { username := vars["user"]
if username == "" {
http.Error(w, "user parameter is required", http.StatusBadRequest)
return
}
fingerprintBytes, err := base64.RawURLEncoding.DecodeString(fingerprint)
if err != nil {
http.Error(w, "invalid fingerprint base64", http.StatusBadRequest)
return
}
if err := user.RemoveAuthorizedKey(username, string(fingerprintBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }

View File

@@ -53,13 +53,14 @@ 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/up", s.verifyToken(HandleNetworkUp)).Methods(http.MethodPost) s.router.HandleFunc("/network/ap", s.verifyToken(HandleConfigureAP)).Methods(http.MethodPost)
s.router.HandleFunc("/network/down", s.verifyToken(HandleNetworkDown)).Methods(http.MethodPost) s.router.HandleFunc("/network/interface/{interface}", s.verifyToken(HandleNetworkUp)).Methods(http.MethodPut)
s.router.HandleFunc("/network/remove", s.verifyToken(HandleNetworkRemove)).Methods(http.MethodPost) 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("/hostname", s.verifyToken(HandleGetHostname)).Methods(http.MethodGet) s.router.HandleFunc("/hostname", s.verifyToken(HandleGetHostname)).Methods(http.MethodGet)
s.router.HandleFunc("/hostname", s.verifyToken(HandleSetHostname)).Methods(http.MethodPost) s.router.HandleFunc("/hostname", s.verifyToken(HandleSetHostname)).Methods(http.MethodPost)
s.router.HandleFunc("/authorized-key", s.verifyToken(HandleAddAuthorizedKey)).Methods(http.MethodPost) s.router.HandleFunc("/users/{user}/keys", s.verifyToken(HandleAddAuthorizedKey)).Methods(http.MethodPost)
s.router.HandleFunc("/authorized-key", s.verifyToken(HandleRemoveAuthorizedKey)).Methods(http.MethodDelete) s.router.HandleFunc("/users/{user}/keys/{fingerprint}", s.verifyToken(HandleRemoveAuthorizedKey)).Methods(http.MethodDelete)
} }
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -7,10 +7,7 @@ import (
"time" "time"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) "github.com/google/uuid"
const (
ourUUID = "7d706027-727c-4d4c-a816-f0e1b99db8ab"
) )
var ( var (
@@ -18,6 +15,40 @@ var (
defaultPassword = "raspberry" defaultPassword = "raspberry"
) )
// ConnectionConfig holds the configuration for a NetworkManager connection
type ConnectionConfig struct {
Type string
UUID string
ID string
AutoConnect bool
SSID string
Mode string
Band string
Channel uint32
KeyMgmt string
PSK string
IPv4Method string
IPv6Method string
}
// DefaultAPConfig returns a default access point configuration
func DefaultAPConfig(uuid uuid.UUID, ssid string, password string) *ConnectionConfig {
return &ConnectionConfig{
Type: "802-11-wireless",
UUID: uuid.String(),
ID: ssid,
AutoConnect: true,
SSID: ssid,
Mode: "ap",
Band: "bg",
Channel: 1,
KeyMgmt: "wpa-psk",
PSK: password,
IPv4Method: "shared",
IPv6Method: "ignore",
}
}
// withDbus executes the given function with a D-Bus system connection // withDbus executes the given function with a D-Bus system connection
// and handles any connection errors // and handles any connection errors
func withDbus(fn func(*dbus.Conn) error) error { func withDbus(fn func(*dbus.Conn) error) error {
@@ -185,10 +216,18 @@ func GetConnectionPath(conn *dbus.Conn, connUUID string) (dbus.ObjectPath, error
} }
// AddConnection creates a new NetworkManager connection profile for a WiFi access point. // AddConnection creates a new NetworkManager connection profile for a WiFi access point.
// Takes a D-Bus connection, 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 AddConnection(conn *dbus.Conn, ssid string, password string) (dbus.ObjectPath, error) { func AddAccessPointConnection(conn *dbus.Conn, uuid uuid.UUID, ssid string, password string) (dbus.ObjectPath, error) {
return AddConnectionWithConfig(conn, DefaultAPConfig(uuid, ssid, password))
}
// AddConnectionWithConfig creates a new NetworkManager connection profile with the given configuration.
// Takes a D-Bus connection and ConnectionConfig struct as arguments.
// Returns the D-Bus object path of the new connection profile.
// Returns an error if the connection creation fails.
func AddConnectionWithConfig(conn *dbus.Conn, cfg *ConnectionConfig) (dbus.ObjectPath, error) {
settingsObj := conn.Object( settingsObj := conn.Object(
"org.freedesktop.NetworkManager", "org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager/Settings", "/org/freedesktop/NetworkManager/Settings",
@@ -196,26 +235,26 @@ func AddConnection(conn *dbus.Conn, ssid string, password string) (dbus.ObjectPa
settingsMap := map[string]map[string]dbus.Variant{ settingsMap := map[string]map[string]dbus.Variant{
"connection": { "connection": {
"type": dbus.MakeVariant("802-11-wireless"), "type": dbus.MakeVariant(cfg.Type),
"uuid": dbus.MakeVariant(ourUUID), "uuid": dbus.MakeVariant(cfg.UUID),
"id": dbus.MakeVariant(ssid), "id": dbus.MakeVariant(cfg.ID),
"autoconnect": dbus.MakeVariant(true), "autoconnect": dbus.MakeVariant(cfg.AutoConnect),
}, },
"802-11-wireless": { "802-11-wireless": {
"ssid": dbus.MakeVariant([]byte(ssid)), "ssid": dbus.MakeVariant([]byte(cfg.SSID)),
"mode": dbus.MakeVariant("ap"), "mode": dbus.MakeVariant(cfg.Mode),
"band": dbus.MakeVariant("bg"), "band": dbus.MakeVariant(cfg.Band),
"channel": dbus.MakeVariant(uint32(1)), "channel": dbus.MakeVariant(cfg.Channel),
}, },
"802-11-wireless-security": { "802-11-wireless-security": {
"key-mgmt": dbus.MakeVariant("wpa-psk"), "key-mgmt": dbus.MakeVariant(cfg.KeyMgmt),
"psk": dbus.MakeVariant(password), "psk": dbus.MakeVariant(cfg.PSK),
}, },
"ipv4": { "ipv4": {
"method": dbus.MakeVariant("shared"), "method": dbus.MakeVariant(cfg.IPv4Method),
}, },
"ipv6": { "ipv6": {
"method": dbus.MakeVariant("ignore"), "method": dbus.MakeVariant(cfg.IPv6Method),
}, },
} }
@@ -279,13 +318,34 @@ func SetHostname(newHost string) error {
}) })
} }
// Up creates and activates a WiFi access point connection. // ConfigureAP creates a WiFi access point connection with the specified settings.
// It takes the interface name, SSID, password and UUID as arguments. // It takes the interface name, SSID and password as arguments.
// If a connection with the given UUID exists, it will be reused. // A new connection with a generated UUID will be created.
// Otherwise, a new connection 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) {
uuid := uuid.New()
err := withDbus(func(conn *dbus.Conn) error {
_, err := AddAccessPointConnection(conn, uuid, ssid, password)
if err != nil {
return fmt.Errorf("failed to create access point connection: %v", err)
}
return nil
})
if err != nil {
return "", err
}
return uuid.String(), nil
}
// Up activates a connection.
// It takes the interface name and UUID as arguments.
// The connection with the given UUID must exist.
// The connection will be activated on the specified interface. // The connection will be activated on the specified interface.
// Returns an error if any operation fails. // Returns an error if any operation fails.
func Up(iface string, ssid string, password string, uuid string) error { func Up(iface string, uuid string) error {
return withDbus(func(conn *dbus.Conn) error { return withDbus(func(conn *dbus.Conn) error {
connPath, err := GetConnectionPath(conn, uuid) connPath, err := GetConnectionPath(conn, uuid)
if err != nil { if err != nil {
@@ -293,10 +353,7 @@ func Up(iface string, ssid string, password string, uuid string) error {
} }
if connPath == "" { if connPath == "" {
connPath, err = AddConnection(conn, ssid, password) return fmt.Errorf("connection with UUID %s not found", uuid)
if err != nil {
return err
}
} }
log.Printf("Getting device path for interface %s", iface) log.Printf("Getting device path for interface %s", iface)

View File

@@ -9,18 +9,19 @@ import (
) )
// AddAuthorizedKey verifies and adds an SSH public key to /home/<user>/.ssh/authorized_keys // AddAuthorizedKey verifies and adds an SSH public key to /home/<user>/.ssh/authorized_keys
// if it doesn't already exist // if it doesn't already exist. Returns the key's fingerprint.
func AddAuthorizedKey(user string, pubKey string) error { func AddAuthorizedKey(user string, pubKey string) (string, error) {
// Verify the public key format // Verify the public key format and get fingerprint
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
if err != nil { if err != nil {
return fmt.Errorf("invalid SSH public key: %v", err) return "", fmt.Errorf("invalid SSH public key: %v", err)
} }
fingerprint := ssh.FingerprintSHA256(parsed)
// Ensure .ssh directory exists // Ensure .ssh directory exists
sshDir := fmt.Sprintf("/home/%s/.ssh", user) sshDir := fmt.Sprintf("/home/%s/.ssh", user)
if err := os.MkdirAll(sshDir, 0700); err != nil { if err := os.MkdirAll(sshDir, 0700); err != nil {
return fmt.Errorf("failed to create .ssh directory: %v", err) return "", fmt.Errorf("failed to create .ssh directory: %v", err)
} }
// Check if key already exists // Check if key already exists
@@ -28,43 +29,45 @@ func AddAuthorizedKey(user string, pubKey string) error {
if _, err := os.Stat(keyFile); err == nil { if _, err := os.Stat(keyFile); err == nil {
existingKeys, err := os.ReadFile(keyFile) existingKeys, err := os.ReadFile(keyFile)
if err != nil { if err != nil {
return fmt.Errorf("failed to read authorized_keys: %v", err) return "", fmt.Errorf("failed to read authorized_keys: %v", err)
} }
if string(existingKeys) != "" { if string(existingKeys) != "" {
for _, line := range strings.Split(string(existingKeys), "\n") { for _, line := range strings.Split(string(existingKeys), "\n") {
if line == pubKey { if line == "" {
// Key already exists, nothing to do continue
return nil }
parsed, err := ssh.ParsePublicKey([]byte(line))
if err != nil {
continue
}
if ssh.FingerprintSHA256(parsed) == fingerprint {
// Key already exists, return fingerprint
return fingerprint, nil
} }
} }
} }
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return fmt.Errorf("failed to check authorized_keys: %v", err) return "", fmt.Errorf("failed to check authorized_keys: %v", err)
} }
// Open authorized_keys file in append mode // Open authorized_keys file in append mode
f, err := os.OpenFile(keyFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) f, err := os.OpenFile(keyFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil { if err != nil {
return fmt.Errorf("failed to open authorized_keys: %v", err) return "", fmt.Errorf("failed to open authorized_keys: %v", err)
} }
defer f.Close() defer f.Close()
// Write the public key // Write the public key
if _, err := f.WriteString(pubKey + "\n"); err != nil { if _, err := f.WriteString(pubKey + "\n"); err != nil {
return fmt.Errorf("failed to write public key: %v", err) return "", fmt.Errorf("failed to write public key: %v", err)
} }
return nil return fingerprint, nil
} }
// RemoveAuthorizedKey removes an authorized SSH key from /home/<user>/.ssh/authorized_keys // RemoveAuthorizedKey removes an authorized SSH key from /home/<user>/.ssh/authorized_keys
func RemoveAuthorizedKey(user string, pubKey string) error { // using the key's fingerprint to identify which key to remove
// Verify the public key format func RemoveAuthorizedKey(user string, fingerprint string) error {
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
if err != nil {
return fmt.Errorf("invalid SSH public key: %v", err)
}
// Check if authorized_keys file exists // Check if authorized_keys file exists
keyFile := fmt.Sprintf("/home/%s/.ssh/authorized_keys", user) keyFile := fmt.Sprintf("/home/%s/.ssh/authorized_keys", user)
if _, err := os.Stat(keyFile); err != nil { if _, err := os.Stat(keyFile); err != nil {
@@ -80,10 +83,19 @@ func RemoveAuthorizedKey(user string, pubKey string) error {
return fmt.Errorf("failed to read authorized_keys: %v", err) return fmt.Errorf("failed to read authorized_keys: %v", err)
} }
// Filter out the key to remove // Filter out the key with matching fingerprint
var newLines []string var newLines []string
for _, line := range strings.Split(string(existingKeys), "\n") { for _, line := range strings.Split(string(existingKeys), "\n") {
if line != "" && line != pubKey { if line == "" {
continue
}
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
if err != nil {
// Keep lines we can't parse
newLines = append(newLines, line)
continue
}
if ssh.FingerprintSHA256(parsed) != fingerprint {
newLines = append(newLines, line) newLines = append(newLines, line)
} }
} }