baseline: api+gui traffic mode + candidates picker

Snapshot before app-launcher (cgroup/mark) work; ignore binaries/backups.
This commit is contained in:
beckline
2026-02-14 15:32:25 +03:00
parent 50e2999cad
commit 10a10f44a8
55 changed files with 16488 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
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
}