package api import ( "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "strings" "vpnem/internal/models" ) func (h *Handler) Subscribe(w http.ResponseWriter, r *http.Request) { links := make([]string, 0) catalog, err := h.store.LoadCatalogV2OrLegacy() if err == nil { for _, node := range catalog.Nodes { for _, protocol := range node.Protocols { link, ok := subscriptionLinkV2(node, protocol) if !ok { continue } links = append(links, link) } } } else { http.Error(w, "internal error", http.StatusInternalServerError) return } if r.URL.Query().Get("format") == "plain" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(strings.Join(links, "\n"))) return } payload := base64.StdEncoding.EncodeToString([]byte(strings.Join(links, "\n"))) w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(payload)) } func subscriptionLink(server models.Server) (string, bool) { switch server.Type { case "vless": if strings.TrimSpace(server.UUID) == "" { return "", false } query := url.Values{} security := "none" if server.TLS != nil && server.TLS.Enabled { security = "tls" if strings.TrimSpace(server.TLS.ServerName) != "" { query.Set("sni", server.TLS.ServerName) } } query.Set("security", security) if server.Transport != nil { if strings.TrimSpace(server.Transport.Type) != "" { query.Set("type", server.Transport.Type) } if strings.TrimSpace(server.Transport.Path) != "" { query.Set("path", server.Transport.Path) } } return fmt.Sprintf( "vless://%s@%s:%d?%s#%s", server.UUID, server.Server, server.ServerPort, query.Encode(), url.QueryEscape(server.Tag), ), true case "vless-reality": if strings.TrimSpace(server.UUID) == "" || server.TLS == nil || server.TLS.Reality == nil { return "", false } query := url.Values{} query.Set("encryption", "none") query.Set("security", "reality") query.Set("sni", server.TLS.ServerName) query.Set("fp", firstNonEmpty(server.TLS.Reality.Fingerprint, "chrome")) query.Set("pbk", server.TLS.Reality.PublicKey) query.Set("sid", server.TLS.Reality.ShortID) query.Set("type", "tcp") return fmt.Sprintf( "vless://%s@%s:%d?%s#%s", server.UUID, server.Server, server.ServerPort, query.Encode(), url.QueryEscape(server.Tag), ), true case "shadowsocks": if strings.TrimSpace(server.Method) == "" || strings.TrimSpace(server.Password) == "" { return "", false } userInfo := base64.StdEncoding.EncodeToString([]byte(server.Method + ":" + server.Password)) return fmt.Sprintf( "ss://%s@%s:%d#%s", userInfo, server.Server, server.ServerPort, url.QueryEscape(server.Tag), ), true case "socks": return fmt.Sprintf( "socks5://%s:%d#%s", server.Server, server.ServerPort, url.QueryEscape(server.Tag), ), true default: return "", false } } func subscriptionLinkV2(node models.CatalogNode, protocol models.CatalogProtocol) (string, bool) { host := node.PublicHost if strings.TrimSpace(host) == "" { if strings.TrimSpace(node.Domain) != "" { host = node.Domain } else { host = node.Host } } tag := subscriptionTag(node, protocol) switch protocol.Type { case "vless": if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.UUID) == "" { return "", false } query := url.Values{} security := "none" if protocol.TLS != nil && protocol.TLS.Enabled { security = "tls" if strings.TrimSpace(protocol.TLS.ServerName) != "" { query.Set("sni", protocol.TLS.ServerName) } } query.Set("security", security) if transportType, _ := protocol.Extra["transport_type"].(string); transportType != "" { query.Set("type", transportType) } if path, _ := protocol.Extra["path"].(string); path != "" { query.Set("path", path) } return fmt.Sprintf( "vless://%s@%s:%d?%s#%s", protocol.Auth.UUID, host, protocol.Port, query.Encode(), url.QueryEscape(tag), ), true case "vless-reality": if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.UUID) == "" || protocol.TLS == nil || protocol.TLS.Reality == nil { return "", false } query := url.Values{} query.Set("encryption", "none") query.Set("security", "reality") query.Set("sni", protocol.TLS.ServerName) query.Set("fp", firstNonEmpty(protocol.TLS.Reality.Fingerprint, "chrome")) query.Set("pbk", protocol.TLS.Reality.PublicKey) query.Set("sid", protocol.TLS.Reality.ShortID) query.Set("type", "tcp") return fmt.Sprintf( "vless://%s@%s:%d?%s#%s", protocol.Auth.UUID, host, protocol.Port, query.Encode(), url.QueryEscape(tag), ), true case "shadowsocks": if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.Method) == "" || strings.TrimSpace(protocol.Auth.Password) == "" { return "", false } userInfo := base64.StdEncoding.EncodeToString([]byte(protocol.Auth.Method + ":" + protocol.Auth.Password)) return fmt.Sprintf( "ss://%s@%s:%d#%s", userInfo, host, protocol.Port, url.QueryEscape(tag), ), true case "socks", "socks5": return fmt.Sprintf( "socks5://%s:%d#%s", host, protocol.Port, url.QueryEscape(tag), ), true case "vmess": if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.UUID) == "" { return "", false } payload := map[string]string{ "v": "2", "ps": tag, "add": host, "port": fmt.Sprintf("%d", protocol.Port), "id": protocol.Auth.UUID, "aid": "0", "scy": "auto", "net": "ws", "type": "none", "host": strings.TrimSpace(protocol.TLS.ServerName), "path": stringFromExtraMap(protocol.Extra, "path", "/vmess"), "tls": vmessTLSValue(protocol.TLS), "sni": strings.TrimSpace(protocol.TLS.ServerName), } if payload["host"] == "" { payload["host"] = host } if payload["sni"] == "" { payload["sni"] = host } data, err := json.Marshal(payload) if err != nil { return "", false } return "vmess://" + base64.StdEncoding.EncodeToString(data), true case "hysteria2": if protocol.Auth == nil || strings.TrimSpace(protocol.Auth.Password) == "" { return "", false } query := url.Values{} sni := "" if protocol.TLS != nil && strings.TrimSpace(protocol.TLS.ServerName) != "" { sni = protocol.TLS.ServerName } if sni != "" { query.Set("sni", sni) } query.Set("alpn", "h3") query.Set("insecure", "1") if obfsPassword, _ := protocol.Extra["obfs_password"].(string); obfsPassword != "" { query.Set("obfs", "salamander") query.Set("obfs-password", obfsPassword) } return fmt.Sprintf( "hysteria2://%s@%s:%d/?%s#%s", url.QueryEscape(protocol.Auth.Password), host, protocol.Port, query.Encode(), url.QueryEscape(tag), ), true default: return "", false } } func subscriptionTag(node models.CatalogNode, protocol models.CatalogProtocol) string { if legacy := stringFromExtraMap(protocol.Extra, "legacy_tag", ""); legacy != "" { return legacy } return node.ID + "-" + protocol.Type } func stringFromExtraMap(extra map[string]any, key, fallback string) string { if extra == nil { return fallback } value, _ := extra[key].(string) if strings.TrimSpace(value) == "" { return fallback } return value } func vmessTLSValue(tls *models.TLS) string { if tls != nil && tls.Enabled { return "tls" } return "" } func firstNonEmpty(value, fallback string) string { if strings.TrimSpace(value) != "" { return value } return fallback }