Files

203 lines
5.2 KiB
Go

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
}