Files
elmprodvpn/selective-vpn-api/app/transport_handlers_actions_virtual.go

215 lines
6.2 KiB
Go

package app
import (
"fmt"
"net/http"
"strings"
"time"
)
func handleTransportVirtualClientAction(w http.ResponseWriter, r *http.Request, id, action string) bool {
cid := sanitizeID(id)
if !isTransportPolicyVirtualClientID(cid) {
return false
}
switch action {
case "health":
handleTransportVirtualClientHealthAction(w, r, cid)
case "metrics":
handleTransportVirtualClientMetricsAction(w, r, cid)
case "provision":
handleTransportVirtualClientProvisionAction(w, r, cid)
case "start", "stop", "restart":
handleTransportVirtualClientLifecycleAction(w, r, cid, action)
default:
http.NotFound(w, r)
}
return true
}
func handleTransportVirtualClientCardGet(w http.ResponseWriter, r *http.Request, id string) bool {
cid := sanitizeID(id)
if !isTransportPolicyVirtualClientID(cid) {
return false
}
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return true
}
item := transportVirtualAdGuardSnapshot()
writeJSON(w, http.StatusOK, TransportClientsResponse{OK: true, Message: "ok", Item: &item})
return true
}
func transportVirtualClientReadOnlyResponse() (int, TransportClientsResponse) {
return http.StatusOK, TransportClientsResponse{
OK: false,
Message: "virtual control-plane client is read-only",
}
}
func transportVirtualAdGuardSnapshot() TransportClient {
if item, ok := resolveTransportPolicyVirtualClient(transportPolicyTargetAdGuardID); ok {
return item
}
now := time.Now().UTC()
item := buildTransportPolicyAdGuardTargetFromObservation("inactive", "DISCONNECTED", "", now)
item.Health.LastError = "adguard autoloop state unavailable"
item.Runtime.LastError = TransportClientError{
Code: "BACKEND_RUNTIME_ERROR",
Message: item.Health.LastError,
Retryable: true,
At: item.Health.LastCheck,
}
item.Runtime.LastAction = "observe"
item.Runtime.LastActionAt = item.Health.LastCheck
item.UpdatedAt = item.Health.LastCheck
return item
}
func handleTransportVirtualClientHealthAction(w http.ResponseWriter, r *http.Request, id string) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
item := transportVirtualAdGuardSnapshot()
resp := buildTransportHealthResponse(item, time.Now().UTC())
if normalizeTransportStatus(item.Status) == TransportClientDown {
resp.Code = "TRANSPORT_CLIENT_DOWN"
}
writeJSON(w, http.StatusOK, resp)
}
func handleTransportVirtualClientMetricsAction(w http.ResponseWriter, r *http.Request, id string) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
item := transportVirtualAdGuardSnapshot()
writeJSON(w, http.StatusOK, buildTransportMetricsResponse(item, time.Now().UTC()))
}
func handleTransportVirtualClientProvisionAction(w http.ResponseWriter, r *http.Request, id string) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
item := transportVirtualAdGuardSnapshot()
now := time.Now().UTC()
msg := "virtual adapter uses existing adguard autoloop runtime; explicit provision skipped"
writeJSON(w, http.StatusOK, TransportClientLifecycleResponse{
OK: true,
Message: msg,
Code: "",
ClientID: item.ID,
Kind: item.Kind,
Action: "provision",
StatusBefore: item.Status,
StatusAfter: item.Status,
Health: item.Health,
Runtime: transportRuntimeSnapshot(item, now),
})
}
func handleTransportVirtualClientLifecycleAction(w http.ResponseWriter, r *http.Request, id, action string) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
status, resp := runTransportVirtualClientLifecycleAction(id, action)
writeJSON(w, status, resp)
}
func runTransportVirtualClientLifecycleAction(id, action string) (int, TransportClientLifecycleResponse) {
status := http.StatusOK
resp := TransportClientLifecycleResponse{}
withTransportIfaceLock(transportPolicyTargetAdGuardIfaceID, func() {
status, resp = executeTransportVirtualClientLifecycleAction(id, action)
})
return status, resp
}
func executeTransportVirtualClientLifecycleAction(id, action string) (int, TransportClientLifecycleResponse) {
before := transportVirtualAdGuardSnapshot()
beforeStatus := normalizeTransportStatus(before.Status)
now := time.Now().UTC()
cmdAction, ok := transportVirtualAdGuardSystemdAction(action)
if !ok {
return http.StatusNotFound, TransportClientLifecycleResponse{
OK: false,
Message: "unknown action",
Code: "TRANSPORT_ACTION_UNKNOWN",
}
}
stdout, stderr, exitCode, err := runCommand("systemctl", cmdAction, adgvpnUnit)
actionOK := err == nil && exitCode == 0
after := transportVirtualAdGuardSnapshot()
afterStatus := normalizeTransportStatus(after.Status)
if !actionOK && afterStatus == beforeStatus {
afterStatus = TransportClientDegraded
after.Status = afterStatus
}
msg := strings.TrimSpace(stdout)
if msg == "" {
msg = strings.TrimSpace(stderr)
}
if msg == "" && err != nil {
msg = strings.TrimSpace(err.Error())
}
if msg == "" {
msg = fmt.Sprintf("adguardvpn %s done", cmdAction)
}
code := ""
if !actionOK {
code = "TRANSPORT_ADGUARD_ACTION_FAILED"
}
events.push("transport_client_state_changed", map[string]any{
"id": id,
"from": beforeStatus,
"to": afterStatus,
})
publishTransportRuntimeObservabilitySnapshotChanged(
"transport_client_state_changed",
[]string{id},
[]string{transportPolicyTargetAdGuardIfaceID},
)
_, _ = egressIdentitySWR.queueRefresh([]string{"adguardvpn", "system"}, true)
return http.StatusOK, TransportClientLifecycleResponse{
OK: actionOK,
Message: msg,
Code: code,
ExitCode: exitCode,
Stdout: stdout,
Stderr: stderr,
ClientID: id,
Kind: after.Kind,
Action: action,
StatusBefore: beforeStatus,
StatusAfter: afterStatus,
Health: after.Health,
Runtime: transportRuntimeSnapshot(after, now),
}
}
func transportVirtualAdGuardSystemdAction(action string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(action)) {
case "start":
return "start", true
case "stop":
return "stop", true
case "restart":
return "restart", true
default:
return "", false
}
}