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) } }() }