203 lines
5.2 KiB
Go
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
|
|
}
|