200 lines
3.8 KiB
Go
200 lines
3.8 KiB
Go
package transportcfg
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ConfigDiff struct {
|
|
Added int
|
|
Removed int
|
|
Changed int
|
|
}
|
|
|
|
func SingBoxTypedProtocolSupported(proto string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(proto)) {
|
|
case "vless", "trojan", "shadowsocks", "wireguard", "hysteria2", "tuic":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func ParsePort(v any) (int, bool) {
|
|
switch x := v.(type) {
|
|
case int:
|
|
return x, true
|
|
case int64:
|
|
return int(x), true
|
|
case float64:
|
|
return int(x), true
|
|
case string:
|
|
s := strings.TrimSpace(x)
|
|
if s == "" {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func DigestJSONMap(config map[string]any) string {
|
|
if config == nil {
|
|
return ""
|
|
}
|
|
b, err := json.Marshal(config)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
h := sha256.Sum256(b)
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
func DiffConfigMaps(prev, next map[string]any) (ConfigDiff, bool) {
|
|
diff := ConfigDiff{}
|
|
changed := false
|
|
if prev == nil {
|
|
diff.Added = len(next)
|
|
return diff, diff.Added > 0
|
|
}
|
|
seen := map[string]struct{}{}
|
|
for k, pv := range prev {
|
|
seen[k] = struct{}{}
|
|
nv, ok := next[k]
|
|
if !ok {
|
|
diff.Removed++
|
|
changed = true
|
|
continue
|
|
}
|
|
if !reflect.DeepEqual(pv, nv) {
|
|
diff.Changed++
|
|
changed = true
|
|
}
|
|
}
|
|
for k := range next {
|
|
if _, ok := seen[k]; ok {
|
|
continue
|
|
}
|
|
diff.Added++
|
|
changed = true
|
|
}
|
|
return diff, changed
|
|
}
|
|
|
|
func ProfileConfigPath(rootDir, profileID string, sanitizeID func(string) string) string {
|
|
id := profileID
|
|
if sanitizeID != nil {
|
|
id = sanitizeID(id)
|
|
}
|
|
if strings.TrimSpace(id) == "" {
|
|
id = "profile"
|
|
}
|
|
return filepath.Join(strings.TrimSpace(rootDir), id+".json")
|
|
}
|
|
|
|
func WriteJSONConfigFile(path string, config map[string]any) error {
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, append(data, '\n'), 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, path)
|
|
}
|
|
|
|
func ReadJSONMapFile(path string) map[string]any {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ReadFileOptional(path string) ([]byte, bool, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, false, nil
|
|
}
|
|
return nil, false, err
|
|
}
|
|
return data, true, nil
|
|
}
|
|
|
|
func RestoreFileOptional(path string, data []byte, exists bool) error {
|
|
if !exists {
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, path)
|
|
}
|
|
|
|
func SanitizeHistoryStamp(ts string, now time.Time) string {
|
|
s := strings.TrimSpace(ts)
|
|
if s == "" {
|
|
s = now.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
repl := strings.NewReplacer(":", "", "-", "", "T", "_", "Z", "", ".", "")
|
|
out := repl.Replace(s)
|
|
out = strings.Trim(out, "_")
|
|
if out == "" {
|
|
out = strconv.FormatInt(now.UTC().UnixNano(), 10)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func JoinMessages(messages []string) string {
|
|
if len(messages) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(messages))
|
|
for _, msg := range messages {
|
|
v := strings.TrimSpace(msg)
|
|
if v == "" {
|
|
continue
|
|
}
|
|
parts = append(parts, v)
|
|
}
|
|
return strings.Join(parts, "; ")
|
|
}
|
|
|
|
func SortRecordsDescByAt[T any](items []T, extractAt func(T) string) {
|
|
if len(items) < 2 || extractAt == nil {
|
|
return
|
|
}
|
|
sort.Slice(items, func(i, j int) bool {
|
|
return extractAt(items[i]) > extractAt(items[j])
|
|
})
|
|
}
|