summaryrefslogtreecommitdiff
path: root/internal/control/catalog.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/control/catalog.go')
-rw-r--r--internal/control/catalog.go229
1 files changed, 229 insertions, 0 deletions
diff --git a/internal/control/catalog.go b/internal/control/catalog.go
new file mode 100644
index 0000000..9ef3c35
--- /dev/null
+++ b/internal/control/catalog.go
@@ -0,0 +1,229 @@
+package control
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "vpnem/internal/models"
+)
+
+func BuildLegacyCatalog(nodes []Node) (*models.ServersResponse, error) {
+ servers := make([]models.Server, 0)
+
+ for _, node := range nodes {
+ if !node.Enabled {
+ continue
+ }
+
+ publicHost := node.Host
+ if strings.TrimSpace(node.Domain) != "" {
+ publicHost = node.Domain
+ }
+
+ for _, protocol := range node.Protocols {
+ if !protocol.Enabled {
+ continue
+ }
+ if err := ensureRealityProfile(&protocol); err != nil {
+ return nil, err
+ }
+
+ server, err := legacyServerFromNode(node, publicHost, protocol)
+ if err != nil {
+ return nil, err
+ }
+ servers = append(servers, server)
+ }
+ }
+
+ sort.Slice(servers, func(i, j int) bool {
+ return servers[i].Tag < servers[j].Tag
+ })
+
+ return &models.ServersResponse{Servers: servers}, nil
+}
+
+func WriteLegacyCatalog(path string, nodes []Node) error {
+ resp, err := BuildLegacyCatalog(nodes)
+ if err != nil {
+ return err
+ }
+ staticResp, err := LoadStaticLegacyCatalog(filepath.Join(filepath.Dir(path), "static-servers.json"))
+ if err != nil {
+ return err
+ }
+ resp.Servers = MergeLegacyServers(staticResp.Servers, resp.Servers)
+
+ data, err := json.MarshalIndent(resp, "", " ")
+ if err != nil {
+ return err
+ }
+ data = append(data, '\n')
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return err
+ }
+ tmpPath := path + ".tmp"
+ if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
+ return err
+ }
+ return os.Rename(tmpPath, path)
+}
+
+func LoadStaticLegacyCatalog(path string) (*models.ServersResponse, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return &models.ServersResponse{Servers: nil}, nil
+ }
+ return nil, err
+ }
+
+ var resp models.ServersResponse
+ if err := json.Unmarshal(data, &resp); err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
+
+func MergeLegacyServers(primary, secondary []models.Server) []models.Server {
+ merged := make([]models.Server, 0, len(primary)+len(secondary))
+ seen := make(map[string]struct{}, len(primary)+len(secondary))
+ for _, item := range primary {
+ if _, ok := seen[item.Tag]; ok {
+ continue
+ }
+ seen[item.Tag] = struct{}{}
+ merged = append(merged, item)
+ }
+ for _, item := range secondary {
+ if _, ok := seen[item.Tag]; ok {
+ continue
+ }
+ seen[item.Tag] = struct{}{}
+ merged = append(merged, item)
+ }
+ sort.Slice(merged, func(i, j int) bool {
+ return merged[i].Tag < merged[j].Tag
+ })
+ return merged
+}
+
+func legacyServerFromNode(node Node, publicHost string, protocol ProtocolProfile) (models.Server, error) {
+ switch protocol.Type {
+ case "socks", "socks5":
+ return models.Server{
+ Tag: node.ID + "-socks5",
+ Region: node.Region,
+ Type: "socks",
+ Server: publicHost,
+ ServerPort: protocol.Port,
+ }, nil
+ case "vless":
+ if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.UUID) == "" {
+ return models.Server{}, fmt.Errorf("node %s protocol vless requires auth.uuid", node.ID)
+ }
+ server := models.Server{
+ Tag: node.ID + "-vless",
+ Region: node.Region,
+ Type: "vless",
+ Server: publicHost,
+ ServerPort: protocol.Port,
+ UUID: protocol.Auth.UUID,
+ }
+ if protocol.TLS != nil {
+ server.TLS = &models.TLS{
+ Enabled: protocol.TLS.Enabled,
+ ServerName: protocol.TLS.ServerName,
+ Insecure: false,
+ }
+ }
+ if transportType, _ := protocol.Extra["transport_type"].(string); transportType != "" {
+ server.Transport = &models.Transport{
+ Type: transportType,
+ Path: stringFromExtra(protocol.Extra, "path"),
+ }
+ }
+ return server, nil
+ case "vless-reality":
+ if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.UUID) == "" {
+ return models.Server{}, fmt.Errorf("node %s protocol vless-reality requires auth.uuid", node.ID)
+ }
+ if protocol.Reality == nil {
+ return models.Server{}, fmt.Errorf("node %s protocol vless-reality requires reality settings", node.ID)
+ }
+ server := models.Server{
+ Tag: node.ID + "-vless-reality",
+ Region: node.Region,
+ Type: "vless-reality",
+ Server: publicHost,
+ ServerPort: protocol.Port,
+ UUID: protocol.Auth.UUID,
+ TLS: &models.TLS{
+ Enabled: true,
+ ServerName: protocol.Reality.ServerName,
+ Reality: &models.Reality{
+ Enabled: true,
+ PublicKey: protocol.Reality.PublicKey,
+ ShortID: protocol.Reality.ShortID,
+ Fingerprint: protocol.Reality.Fingerprint,
+ },
+ },
+ }
+ return server, nil
+ case "shadowsocks":
+ if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.Method) == "" || strings.TrimSpace(protocol.Auth.Password) == "" {
+ return models.Server{}, fmt.Errorf("node %s protocol shadowsocks requires auth.method and auth.password", node.ID)
+ }
+ return models.Server{
+ Tag: node.ID + "-shadowsocks",
+ Region: node.Region,
+ Type: "shadowsocks",
+ Server: publicHost,
+ ServerPort: protocol.Port,
+ Method: protocol.Auth.Method,
+ Password: protocol.Auth.Password,
+ }, nil
+ case "hysteria2":
+ if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.Password) == "" {
+ return models.Server{}, fmt.Errorf("node %s protocol hysteria2 requires auth.password", node.ID)
+ }
+ server := models.Server{
+ Tag: node.ID + "-hysteria2",
+ Region: node.Region,
+ Type: "hysteria2",
+ Server: publicHost,
+ ServerPort: protocol.Port,
+ Password: protocol.Auth.Password,
+ ObfsPassword: stringFromExtra(protocol.Extra, "obfs_password"),
+ UpMbps: intFromExtra(protocol.Extra, "up_mbps", 0),
+ DownMbps: intFromExtra(protocol.Extra, "down_mbps", 0),
+ TLS: &models.TLS{
+ Enabled: true,
+ Insecure: true,
+ ServerName: "",
+ ALPN: []string{defaultHysteria2ALPN},
+ MinVersion: "1.3",
+ MaxVersion: "1.3",
+ },
+ }
+ if protocol.TLS != nil && protocol.TLS.ServerName != "" {
+ server.TLS.ServerName = protocol.TLS.ServerName
+ }
+ return server, nil
+ default:
+ return models.Server{}, fmt.Errorf("node %s uses unsupported legacy protocol %q", node.ID, protocol.Type)
+ }
+}
+
+func stringFromExtra(extra map[string]any, key string) string {
+ if extra == nil {
+ return ""
+ }
+ value, _ := extra[key].(string)
+ return value
+}