mirror of
https://github.com/0x1d/rcond.git
synced 2025-12-14 18:25:21 +01:00
feat: refactor REST API
This commit is contained in:
87
README.md
87
README.md
@@ -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"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
157
api/rcond.yaml
157
api/rcond.yaml
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user