package app import ( "encoding/json" "io" "io/fs" "net/http" "os" "path/filepath" "strings" ) // --------------------------------------------------------------------- // domains editor + smartdns wildcards // --------------------------------------------------------------------- // EN: Domain and SmartDNS configuration endpoints. // EN: Provides CRUD-style file access for domain lists, current nft/ipset table dump, // EN: and persisted SmartDNS wildcard configuration. // RU: Эндпоинты конфигурации доменов и SmartDNS. // RU: Предоставляет доступ к файлам списков доменов, дамп текущей таблицы nft/ipset // RU: и сохранение конфигурации wildcard-доменов SmartDNS. var domainFiles = map[string]string{ "bases": domainDir + "/bases.txt", "meta": domainDir + "/meta-special.txt", "subs": domainDir + "/subs.txt", "static": staticIPsFile, "last-ips-map": lastIPsMapPath, "last-ips-map-direct": lastIPsMapDirect, "last-ips-map-wildcard": lastIPsMapDyn, } // --------------------------------------------------------------------- // domains table // --------------------------------------------------------------------- // GET /api/v1/domains/table -> { "lines": [ ... ] } func handleDomainsTable(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } stdout, _, _, err := runCommand("ipset", "list", "agvpn4") lines := []string{} if err == nil { for _, l := range strings.Split(stdout, "\n") { l = strings.TrimRight(l, "\r") if l != "" { lines = append(lines, l) } } } writeJSON(w, http.StatusOK, map[string]any{"lines": lines}) } // --------------------------------------------------------------------- // domains file // --------------------------------------------------------------------- // GET /api/v1/domains/file?name=bases|meta|subs|static|smartdns|last-ips-map|last-ips-map-direct|last-ips-map-wildcard // POST /api/v1/domains/file { "name": "...", "content": "..." } func handleDomainsFile(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: name := strings.TrimSpace(r.URL.Query().Get("name")) if name == "smartdns" { domains, source := loadSmartDNSWildcardDomainsState(nil) writeJSON(w, http.StatusOK, map[string]string{ "content": renderSmartDNSDomainsContent(domains), "source": source, }) return } path, ok := domainFiles[name] if !ok { http.Error(w, "unknown file name", http.StatusBadRequest) return } source := "file" if strings.HasPrefix(name, "last-ips-map") { source = "artifact" } data, err := os.ReadFile(path) if err != nil { if !os.IsNotExist(err) { http.Error(w, "read error", http.StatusInternalServerError) return } switch name { case "bases", "meta", "subs": // fallback to embedded seed embedName := name + ".txt" if name == "meta" { embedName = "meta-special.txt" } data, _ = fs.ReadFile(embeddedDomains, "assets/domains/"+embedName) source = "embedded" default: data = []byte{} } } writeJSON(w, http.StatusOK, map[string]string{ "content": string(data), "source": source, }) case http.MethodPost: var body struct { Name string `json:"name"` Content string `json:"content"` } 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 } } if strings.TrimSpace(body.Name) == "smartdns" { domains := parseSmartDNSDomainsContent(body.Content) if err := saveSmartDNSWildcardDomainsState(domains); err != nil { http.Error(w, "write error", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) return } if body.Name == "last-ips-map-direct" || body.Name == "last-ips-map-wildcard" { http.Error(w, "read-only file name", http.StatusBadRequest) return } path, ok := domainFiles[strings.TrimSpace(body.Name)] if !ok { http.Error(w, "unknown file name", http.StatusBadRequest) return } _ = os.MkdirAll(filepath.Dir(path), 0o755) if err := os.WriteFile(path, []byte(body.Content), 0o644); err != nil { http.Error(w, "write error", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } // --------------------------------------------------------------------- // smartdns wildcards // --------------------------------------------------------------------- func handleSmartdnsWildcards(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: payload := struct { Domains []string `json:"domains"` }{Domains: readSmartDNSWildcardDomains()} writeJSON(w, http.StatusOK, payload) case http.MethodPost: var payload struct { Domains []string `json:"domains"` } if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&payload); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } } if err := saveSmartDNSWildcardDomainsState(payload.Domains); err != nil { http.Error(w, "write error", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func readSmartDNSWildcardDomains() []string { domains, _ := loadSmartDNSWildcardDomainsState(nil) return domains }