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

140 lines
3.7 KiB
Go

package app
import (
"encoding/json"
"io"
"net/http"
"os"
"strings"
"time"
)
func handleVPNAutoconnect(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
Action string `json:"action"`
}
if r.Body != nil {
defer r.Body.Close()
_ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body)
}
action := strings.ToLower(strings.TrimSpace(body.Action))
switch action {
case "start", "stop", "restart":
default:
http.Error(w, "unknown action", http.StatusBadRequest)
return
}
_, lifecycle := runTransportVirtualClientLifecycleAction(transportPolicyTargetAdGuardID, action)
res := cmdResult{
OK: lifecycle.OK,
Message: lifecycle.Message,
ExitCode: lifecycle.ExitCode,
Stdout: lifecycle.Stdout,
Stderr: lifecycle.Stderr,
}
writeJSON(w, http.StatusOK, res)
}
func handleVPNListLocations(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
force := false
switch strings.ToLower(strings.TrimSpace(r.URL.Query().Get("refresh"))) {
case "1", "true", "yes", "on":
force = true
}
writeJSON(w, http.StatusOK, getVPNLocationsSnapshot(force))
}
func handleVPNSetLocation(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
ISO string `json:"iso"`
Target string `json:"target"`
Label string `json:"label"`
}
if r.Body != nil {
defer r.Body.Close()
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
}
reqTarget := strings.TrimSpace(body.Target)
reqISO := strings.ToUpper(strings.TrimSpace(body.ISO))
reqLabel := strings.TrimSpace(body.Label)
if reqTarget == "" && reqISO == "" {
http.Error(w, "target or iso is required", http.StatusBadRequest)
return
}
snap := getVPNLocationsSnapshot(false)
val, iso, resolvedLabel, validated, err := resolveVPNLocationSelection(
reqTarget, reqISO, reqLabel, snap.Locations,
)
if err != nil {
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
"status": "error",
"error": err.Error(),
"requested_target": reqTarget,
"requested_iso": reqISO,
"requested_label": reqLabel,
})
return
}
_ = os.MkdirAll(stateDir, 0o755)
stored := val
if isISO2(iso) && !strings.EqualFold(val, iso) {
stored = val + "|" + iso
}
if err := os.WriteFile(desiredLocation, []byte(stored+"\n"), 0o644); err != nil {
http.Error(w, "write error", http.StatusInternalServerError)
return
}
// Force location switch for already connected tunnels:
// disconnect first so autoloop reconnects using the new desired location.
_, _, _, _ = runCommandTimeout(8*time.Second, adgvpnCLI, "disconnect")
// как старый GUI: сразу рестартуем автоконнект
_, _, _, _ = runCommand("systemctl", "restart", adgvpnUnit)
triggerVPNEgressRefreshBurst()
writeJSON(w, http.StatusOK, map[string]any{
"status": "ok",
"iso": iso,
"target": val,
"label": resolvedLabel,
"validated": validated,
})
}
func triggerVPNEgressRefreshBurst() {
go func() {
// Multiple forced refreshes are used because service restart can report
// old egress for a short period before tunnel re-establishes.
delays := []time.Duration{
0,
2500 * time.Millisecond,
4500 * time.Millisecond,
}
for _, d := range delays {
if d > 0 {
time.Sleep(d)
}
_, _ = egressIdentitySWR.queueRefresh([]string{"adguardvpn"}, true)
}
}()
}