package handlers import ( "encoding/base64" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/orohi/vpn-panel/internal/db" "github.com/orohi/vpn-panel/internal/models" ) func isBrowserRequest(r *http.Request) bool { if r.URL.Query().Get("format") == "raw" || r.URL.Query().Get("format") == "base64" { return false } if r.URL.Query().Get("format") == "html" { return true } ua := strings.ToLower(r.UserAgent()) browsers := []string{"mozilla", "chrome", "safari", "firefox", "edge", "opera"} for _, b := range browsers { if strings.Contains(ua, b) { // Exclude common VPN clients that include mozilla in UA sometimes clients := []string{"clash", "v2ray", "sing-box", "singbox", "shadowrocket", "stash", "quantumult", "surge", "hiddify", "nekobox", "streisand"} for _, c := range clients { if strings.Contains(ua, c) { return false } } return true } } return false } func writeSubHeaders(w http.ResponseWriter, username string, used, total int64, expireUnix int64) { w.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(username))) w.Header().Set("profile-update-interval", "12") w.Header().Set("subscription-userinfo", fmt.Sprintf( "upload=0; download=%d; total=%d; expire=%d", used, total, expireUnix, )) w.Header().Set("announce", "base64:"+base64.StdEncoding.EncodeToString([]byte("VPN Panel"))) } func (s *Server) subscriptionGet(w http.ResponseWriter, r *http.Request) { token := mux.Vars(r)["token"] c, err := db.GetClientBySubToken(s.DB, token) if err != nil || c == nil { http.Error(w, "not found", http.StatusNotFound) return } info, err := db.BuildSubscriptionInfo(s.DB, c) if err != nil { http.Error(w, "error", http.StatusInternalServerError) return } expireUnix := int64(0) if c.ExpireAt != nil { expireUnix = c.ExpireAt.Unix() } writeSubHeaders(w, c.Username, c.TrafficUsedBytes, c.TrafficLimitBytes, expireUnix) format := r.URL.Query().Get("format") if format == "json" || strings.Contains(r.Header.Get("Accept"), "application/json") { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(info) return } if isBrowserRequest(r) { if !c.IsActive() { s.render(w, "subscription.html", pageData{ "Title": c.Username, "Info": info, "SubURL": publicSubURL(r, token), "Expired": true, }) return } s.render(w, "subscription.html", pageData{ "Title": c.Username, "Info": info, "SubURL": publicSubURL(r, token), "Now": time.Now(), }) return } if !c.IsActive() { http.Error(w, "subscription inactive", http.StatusForbidden) return } body := strings.Join(info.Links, "\n") if body != "" { body += "\n" } if format == "plain" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(body)) return } // Default for VPN apps: base64 (common subscription format) w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(base64.StdEncoding.EncodeToString([]byte(body)))) } func (s *Server) subscriptionInfoGet(w http.ResponseWriter, r *http.Request) { token := mux.Vars(r)["token"] c, err := db.GetClientBySubToken(s.DB, token) if err != nil || c == nil { http.Error(w, "not found", http.StatusNotFound) return } info, err := db.BuildSubscriptionInfo(s.DB, c) if err != nil { http.Error(w, "error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "ok": true, "username": c.Username, "status": c.Status, "active": c.IsActive(), "uuid": c.UUID, "expire": info.ExpireUnix, "traffic": map[string]any{ "used": c.TrafficUsedBytes, "limit": c.TrafficLimitBytes, }, "hosts": info.Hosts, "inbounds": groupHostsByInbound(info.Hosts), "sub_url": publicSubURL(r, token), }) } func groupHostsByInbound(hosts []models.SubHost) []map[string]any { type agg struct { Tag, Protocol, Network, Security string Port int Servers []map[string]any } order := []uuid.UUID{} byID := map[uuid.UUID]*agg{} for _, h := range hosts { a, ok := byID[h.InboundID] if !ok { a = &agg{ Tag: h.Tag, Protocol: h.ProtocolName, Network: h.Network, Security: h.Security, Port: h.Port, } byID[h.InboundID] = a order = append(order, h.InboundID) } server := map[string]any{ "node": h.NodeName, "address": h.Address, "online": h.NodeOnline, "available": h.Available, "link": h.Link, "title": h.Title(), } if h.Address != "" || h.NodeName != "" { a.Servers = append(a.Servers, server) } } var out []map[string]any for _, id := range order { a := byID[id] out = append(out, map[string]any{ "id": id.String(), "tag": a.Tag, "protocol": a.Protocol, "port": a.Port, "network": a.Network, "security": a.Security, "servers": a.Servers, }) } return out } func publicSubURL(r *http.Request, token string) string { scheme := "https" if r.TLS == nil { if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" { scheme = xf } else { scheme = "http" } } return scheme + "://" + r.Host + "/sub/" + token }