platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View File

@@ -0,0 +1,178 @@
package trafficprofiles
import (
"path/filepath"
"strings"
)
func CanonicalizeAppKey(appKey string, command string) string {
key := strings.TrimSpace(appKey)
cmd := strings.TrimSpace(command)
fields := SplitCommandTokens(cmd)
if len(fields) == 0 && key != "" {
fields = []string{key}
}
primary := key
if len(fields) > 0 {
primary = fields[0]
}
primary = stripOuterQuotes(strings.TrimSpace(primary))
if primary == "" {
return ""
}
base := strings.ToLower(filepath.Base(primary))
clean := make([]string, 0, len(fields))
for _, f := range fields {
f = stripOuterQuotes(strings.TrimSpace(f))
if f == "" {
continue
}
clean = append(clean, f)
}
switch base {
case "flatpak":
if id := extractRunTarget(clean); id != "" {
return "flatpak:" + strings.ToLower(strings.TrimSpace(id))
}
return "flatpak"
case "snap":
if name := extractRunTarget(clean); name != "" {
return "snap:" + strings.ToLower(strings.TrimSpace(name))
}
return "snap"
case "gtk-launch":
if len(clean) >= 2 {
id := strings.TrimSpace(clean[1])
if id != "" && !strings.HasPrefix(id, "-") {
return "desktop:" + strings.ToLower(id)
}
}
case "env":
for i := 1; i < len(clean); i++ {
tok := strings.TrimSpace(clean[i])
if tok == "" {
continue
}
if strings.HasPrefix(tok, "-") {
continue
}
if strings.Contains(tok, "=") {
continue
}
return CanonicalizeAppKey(tok, strings.Join(clean[i:], " "))
}
return "env"
}
if strings.Contains(primary, "/") {
b := filepath.Base(primary)
if b != "" && b != "." && b != "/" {
return strings.ToLower(strings.TrimSpace(b))
}
}
return strings.ToLower(strings.TrimSpace(primary))
}
func stripOuterQuotes(s string) string {
in := strings.TrimSpace(s)
if len(in) >= 2 {
if (in[0] == '"' && in[len(in)-1] == '"') || (in[0] == '\'' && in[len(in)-1] == '\'') {
return strings.TrimSpace(in[1 : len(in)-1])
}
}
return in
}
func extractRunTarget(fields []string) string {
if len(fields) == 0 {
return ""
}
idx := -1
for i := 0; i < len(fields); i++ {
if strings.TrimSpace(fields[i]) == "run" {
idx = i
break
}
}
if idx < 0 {
return ""
}
for j := idx + 1; j < len(fields); j++ {
tok := strings.TrimSpace(fields[j])
if tok == "" {
continue
}
if tok == "--" {
continue
}
if strings.HasPrefix(tok, "-") {
continue
}
return tok
}
return ""
}
func SplitCommandTokens(raw string) []string {
s := strings.TrimSpace(raw)
if s == "" {
return nil
}
out := make([]string, 0, 8)
var cur strings.Builder
inSingle := false
inDouble := false
escaped := false
flush := func() {
if cur.Len() == 0 {
return
}
out = append(out, cur.String())
cur.Reset()
}
for _, r := range s {
if escaped {
cur.WriteRune(r)
escaped = false
continue
}
switch r {
case '\\':
if inSingle {
cur.WriteRune(r)
} else {
escaped = true
}
case '\'':
if inDouble {
cur.WriteRune(r)
} else {
inSingle = !inSingle
}
case '"':
if inSingle {
cur.WriteRune(r)
} else {
inDouble = !inDouble
}
case ' ', '\t', '\n', '\r':
if inSingle || inDouble {
cur.WriteRune(r)
} else {
flush()
}
default:
cur.WriteRune(r)
}
}
flush()
return out
}

View File

@@ -0,0 +1,411 @@
package trafficprofiles
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
type Profile struct {
ID string
Name string
AppKey string
Command string
Target string
TTLSec int
VPNProfile string
CreatedAt string
UpdatedAt string
}
type UpsertRequest struct {
ID string
Name string
AppKey string
Command string
Target string
TTLSec int
VPNProfile string
}
type Deps struct {
CanonicalizeAppKey func(appKey, command string) string
SanitizeID func(string) string
DefaultTTLSec int
}
type Store struct {
mu sync.Mutex
statePath string
canonicalizeAppKey func(appKey, command string) string
sanitizeID func(string) string
defaultTTLSec int
}
type state struct {
Version int `json:"version"`
UpdatedAt string `json:"updated_at"`
Profiles []Profile `json:"profiles,omitempty"`
}
func NewStore(statePath string, deps Deps) *Store {
canon := deps.CanonicalizeAppKey
if canon == nil {
canon = func(appKey, _ string) string { return strings.TrimSpace(appKey) }
}
sanitize := deps.SanitizeID
if sanitize == nil {
sanitize = defaultSanitizeID
}
return &Store{
statePath: strings.TrimSpace(statePath),
canonicalizeAppKey: canon,
sanitizeID: sanitize,
defaultTTLSec: deps.DefaultTTLSec,
}
}
func (s *Store) List() []Profile {
s.mu.Lock()
defer s.mu.Unlock()
st := s.loadStateLocked()
out := append([]Profile(nil), st.Profiles...)
sort.Slice(out, func(i, j int) bool {
return out[i].UpdatedAt > out[j].UpdatedAt
})
return out
}
func (s *Store) Upsert(req UpsertRequest) (Profile, error) {
s.mu.Lock()
defer s.mu.Unlock()
st := s.loadStateLocked()
target := strings.ToLower(strings.TrimSpace(req.Target))
if target == "" {
target = "vpn"
}
if target != "vpn" && target != "direct" {
return Profile{}, fmt.Errorf("target must be vpn|direct")
}
cmd := strings.TrimSpace(req.Command)
if cmd == "" {
return Profile{}, fmt.Errorf("missing command")
}
appKey := strings.TrimSpace(req.AppKey)
if appKey == "" {
fields := strings.Fields(cmd)
if len(fields) > 0 {
appKey = strings.TrimSpace(fields[0])
}
}
appKey = s.canonicalizeAppKey(appKey, cmd)
if appKey == "" {
return Profile{}, fmt.Errorf("cannot infer app_key")
}
id := strings.TrimSpace(req.ID)
if id == "" {
for _, p := range st.Profiles {
if strings.TrimSpace(p.AppKey) == appKey && strings.ToLower(strings.TrimSpace(p.Target)) == target {
id = strings.TrimSpace(p.ID)
break
}
}
}
if id == "" {
id = deriveProfileID(appKey, target, st.Profiles, s.sanitizeID)
}
if id == "" {
return Profile{}, fmt.Errorf("cannot derive profile id")
}
name := strings.TrimSpace(req.Name)
if name == "" {
name = filepath.Base(appKey)
if name == "" || name == "/" || name == "." {
name = id
}
}
ttl := req.TTLSec
if ttl <= 0 {
ttl = s.defaultTTLSec
}
vpnProfile := strings.TrimSpace(req.VPNProfile)
now := time.Now().UTC().Format(time.RFC3339)
prof := Profile{
ID: id,
Name: name,
AppKey: appKey,
Command: cmd,
Target: target,
TTLSec: ttl,
VPNProfile: vpnProfile,
UpdatedAt: now,
}
updated := false
for i := range st.Profiles {
if strings.TrimSpace(st.Profiles[i].ID) != id {
continue
}
prof.CreatedAt = strings.TrimSpace(st.Profiles[i].CreatedAt)
if prof.CreatedAt == "" {
prof.CreatedAt = now
}
st.Profiles[i] = prof
updated = true
break
}
if !updated {
prof.CreatedAt = now
st.Profiles = append(st.Profiles, prof)
}
if err := s.saveStateLocked(st); err != nil {
return Profile{}, err
}
return prof, nil
}
func (s *Store) Delete(id string) (bool, string) {
s.mu.Lock()
defer s.mu.Unlock()
id = strings.TrimSpace(id)
if id == "" {
return false, "empty id"
}
st := s.loadStateLocked()
kept := st.Profiles[:0]
found := false
for _, p := range st.Profiles {
if strings.TrimSpace(p.ID) == id {
found = true
continue
}
kept = append(kept, p)
}
st.Profiles = kept
if !found {
return true, "not found"
}
if err := s.saveStateLocked(st); err != nil {
return false, err.Error()
}
return true, "deleted"
}
func Dedupe(in []Profile, canonicalize func(appKey, command string) string) ([]Profile, bool) {
if canonicalize == nil {
canonicalize = func(appKey, _ string) string { return strings.TrimSpace(appKey) }
}
if len(in) <= 1 {
return in, false
}
out := make([]Profile, 0, len(in))
byID := map[string]int{}
byAppTarget := map[string]int{}
changed := false
for _, raw := range in {
p := raw
p.ID = strings.TrimSpace(p.ID)
p.Target = strings.ToLower(strings.TrimSpace(p.Target))
p.AppKey = canonicalize(p.AppKey, p.Command)
if p.ID == "" {
changed = true
continue
}
if p.Target != "vpn" && p.Target != "direct" {
p.Target = "vpn"
changed = true
}
if idx, ok := byID[p.ID]; ok {
if preferProfile(p, out[idx]) {
out[idx] = p
}
changed = true
continue
}
if p.AppKey != "" {
key := p.Target + "|" + p.AppKey
if idx, ok := byAppTarget[key]; ok {
if preferProfile(p, out[idx]) {
byID[p.ID] = idx
out[idx] = p
}
changed = true
continue
}
byAppTarget[key] = len(out)
}
byID[p.ID] = len(out)
out = append(out, p)
}
return out, changed
}
func (s *Store) loadStateLocked() state {
st := state{Version: 1}
if strings.TrimSpace(s.statePath) == "" {
return st
}
data, err := os.ReadFile(s.statePath)
if err != nil {
return st
}
if err := json.Unmarshal(data, &st); err != nil {
return state{Version: 1}
}
if st.Version == 0 {
st.Version = 1
}
if st.Profiles == nil {
st.Profiles = nil
}
changed := false
for i := range st.Profiles {
canon := s.canonicalizeAppKey(st.Profiles[i].AppKey, st.Profiles[i].Command)
if canon != "" && strings.TrimSpace(st.Profiles[i].AppKey) != canon {
st.Profiles[i].AppKey = canon
changed = true
}
st.Profiles[i].Target = strings.ToLower(strings.TrimSpace(st.Profiles[i].Target))
}
if deduped, dedupChanged := Dedupe(st.Profiles, s.canonicalizeAppKey); dedupChanged {
st.Profiles = deduped
changed = true
}
if changed {
_ = s.saveStateLocked(st)
}
return st
}
func (s *Store) saveStateLocked(st state) error {
st.Version = 1
st.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
data, err := json.MarshalIndent(st, "", " ")
if err != nil {
return err
}
if strings.TrimSpace(s.statePath) == "" {
return fmt.Errorf("state path is empty")
}
if err := os.MkdirAll(filepath.Dir(s.statePath), 0o755); err != nil {
return err
}
tmp := s.statePath + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
return os.Rename(tmp, s.statePath)
}
func deriveProfileID(appKey string, target string, existing []Profile, sanitize func(string) string) string {
if sanitize == nil {
sanitize = defaultSanitizeID
}
base := filepath.Base(strings.TrimSpace(appKey))
if base == "" || base == "/" || base == "." {
base = "app"
}
base = sanitize(base)
if base == "" {
base = "app"
}
idBase := base + "-" + strings.ToLower(strings.TrimSpace(target))
id := idBase
used := map[string]struct{}{}
for _, p := range existing {
used[strings.TrimSpace(p.ID)] = struct{}{}
}
if _, ok := used[id]; !ok {
return id
}
for i := 2; i < 1000; i++ {
cand := fmt.Sprintf("%s-%d", idBase, i)
if _, ok := used[cand]; !ok {
return cand
}
}
return ""
}
func preferProfile(cand, cur Profile) bool {
cu := strings.TrimSpace(cand.UpdatedAt)
ou := strings.TrimSpace(cur.UpdatedAt)
if cu != ou {
if cu == "" {
return false
}
if ou == "" {
return true
}
return cu > ou
}
cc := strings.TrimSpace(cand.CreatedAt)
oc := strings.TrimSpace(cur.CreatedAt)
if cc != oc {
if cc == "" {
return false
}
if oc == "" {
return true
}
return cc > oc
}
if strings.TrimSpace(cand.Command) != "" && strings.TrimSpace(cur.Command) == "" {
return true
}
return false
}
func defaultSanitizeID(s string) string {
in := strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
b.Grow(len(in))
lastDash := false
for i := 0; i < len(in); i++ {
ch := in[i]
isAZ := ch >= 'a' && ch <= 'z'
is09 := ch >= '0' && ch <= '9'
if isAZ || is09 {
b.WriteByte(ch)
lastDash = false
continue
}
if !lastDash {
b.WriteByte('-')
lastDash = true
}
}
return strings.Trim(b.String(), "-")
}