264 lines
7.9 KiB
Go
264 lines
7.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"xboard-go/internal/model"
|
|
"xboard-go/internal/protocol"
|
|
"xboard-go/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func ClientSubscribe(c *gin.Context) {
|
|
user, ok := currentUser(c)
|
|
if !ok {
|
|
Fail(c, http.StatusForbidden, "invalid token")
|
|
return
|
|
}
|
|
|
|
FulfillSubscription(c, user)
|
|
}
|
|
|
|
func FulfillSubscription(c *gin.Context, user *model.User) {
|
|
servers, err := service.AvailableServersForUser(user)
|
|
if err != nil {
|
|
Fail(c, 500, "failed to fetch servers")
|
|
return
|
|
}
|
|
|
|
// 1. Filter servers
|
|
types := c.Query("types")
|
|
filter := c.Query("filter")
|
|
servers = filterServers(servers, types, filter)
|
|
|
|
ua := c.GetHeader("User-Agent")
|
|
flag := c.Query("flag")
|
|
uaLower := strings.ToLower(ua)
|
|
|
|
// 2. Handle specialized configs
|
|
if strings.Contains(uaLower, "stash") || flag == "stash" {
|
|
config, _ := protocol.GenerateClashWithTemplate("stash", servers, *user)
|
|
c.Header("Content-Type", "application/octet-stream")
|
|
c.String(http.StatusOK, config)
|
|
return
|
|
}
|
|
|
|
if strings.Contains(uaLower, "clashmeta") ||
|
|
strings.Contains(uaLower, "clash meta") ||
|
|
strings.Contains(uaLower, "metacubex") ||
|
|
strings.Contains(uaLower, "verge") ||
|
|
strings.Contains(uaLower, "flclash") ||
|
|
strings.Contains(uaLower, "nekobox") ||
|
|
flag == "clashmeta" || flag == "clash-meta" {
|
|
config, _ := protocol.GenerateClashWithTemplate("clashmeta", servers, *user)
|
|
c.Header("Content-Type", "application/octet-stream")
|
|
c.String(http.StatusOK, config)
|
|
return
|
|
}
|
|
|
|
if strings.Contains(ua, "Clash") || flag == "clash" {
|
|
config, _ := protocol.GenerateClashWithTemplate("clash", servers, *user)
|
|
c.Header("Content-Type", "application/octet-stream")
|
|
c.String(http.StatusOK, config)
|
|
return
|
|
}
|
|
|
|
if strings.Contains(uaLower, "sing-box") || flag == "sing-box" {
|
|
config, _ := protocol.GenerateSingBox(servers, *user)
|
|
c.Header("Content-Type", "application/json; charset=utf-8")
|
|
c.String(http.StatusOK, config)
|
|
return
|
|
}
|
|
|
|
// 3. General Subscription (Links)
|
|
var finalLinks []string
|
|
|
|
// 3a. Add Info Nodes (match reference behavior)
|
|
if service.MustGetBool("show_info_to_server_enable", true) {
|
|
finalLinks = append(finalLinks, generateInfoNodes(user)...)
|
|
}
|
|
|
|
// 3b. Add Actual Node Links
|
|
nodeLinks := protocol.GenerateGeneralLinks(servers, *user)
|
|
if nodeLinks != "" {
|
|
finalLinks = append(finalLinks, strings.Split(nodeLinks, "\n")...)
|
|
}
|
|
|
|
c.Header("Content-Type", "text/plain; charset=utf-8")
|
|
c.Header("Subscription-Userinfo", subscriptionUserInfo(*user))
|
|
c.String(http.StatusOK, base64.StdEncoding.EncodeToString([]byte(strings.Join(finalLinks, "\n"))))
|
|
}
|
|
|
|
func filterServers(servers []model.Server, types, filter string) []model.Server {
|
|
var filtered []model.Server
|
|
allowed := make(map[string]bool)
|
|
if types != "" && types != "all" {
|
|
for _, t := range strings.Split(types, ",") {
|
|
allowed[strings.TrimSpace(t)] = true
|
|
}
|
|
}
|
|
|
|
for _, s := range servers {
|
|
if len(allowed) > 0 && !allowed[s.Type] {
|
|
continue
|
|
}
|
|
if filter != "" {
|
|
if !strings.Contains(strings.ToLower(s.Name), strings.ToLower(filter)) {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, s)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func generateInfoNodes(user *model.User) []string {
|
|
var nodes []string
|
|
|
|
// Expire Info
|
|
expireDate := "长期有效"
|
|
if user.ExpiredAt != nil {
|
|
expireDate = time.Unix(*user.ExpiredAt, 0).Format("2006-01-02")
|
|
}
|
|
nodes = append(nodes, buildInfoLink("套餐到期:"+expireDate))
|
|
|
|
// Traffic Info
|
|
remaining := int64(user.TransferEnable) - (int64(user.U) + int64(user.D))
|
|
if remaining < 0 {
|
|
remaining = 0
|
|
}
|
|
nodes = append(nodes, buildInfoLink("剩余流量:"+formatTraffic(remaining)))
|
|
|
|
return nodes
|
|
}
|
|
|
|
func buildInfoLink(name string) string {
|
|
m := map[string]string{
|
|
"v": "2",
|
|
"ps": name,
|
|
"add": "1.2.3.4",
|
|
"port": "443",
|
|
"id": "00000000-0000-0000-0000-000000000000",
|
|
"aid": "0",
|
|
"scy": "auto",
|
|
"net": "tcp",
|
|
"type": "none",
|
|
"host": "",
|
|
"path": "",
|
|
"tls": "",
|
|
}
|
|
b, _ := json.Marshal(m)
|
|
return "vmess://" + base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
|
|
func formatTraffic(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
func ClientAppConfigV1(c *gin.Context) {
|
|
user, ok := currentUser(c)
|
|
if !ok {
|
|
Fail(c, http.StatusForbidden, "invalid token")
|
|
return
|
|
}
|
|
|
|
servers, err := service.AvailableServersForUser(user)
|
|
if err != nil {
|
|
Fail(c, 500, "failed to fetch servers")
|
|
return
|
|
}
|
|
|
|
config, _ := protocol.GenerateClash(servers, *user)
|
|
c.Header("Content-Type", "text/yaml; charset=utf-8")
|
|
c.String(http.StatusOK, config)
|
|
}
|
|
|
|
func ClientAppConfigV2(c *gin.Context) {
|
|
config := gin.H{
|
|
"app_info": gin.H{
|
|
"app_name": service.MustGetString("app_name", "XBoard"),
|
|
"app_description": service.MustGetString("app_description", ""),
|
|
"app_url": service.GetAppURL(),
|
|
"logo": service.MustGetString("logo", ""),
|
|
"version": service.MustGetString("app_version", "1.0.0"),
|
|
},
|
|
"features": gin.H{
|
|
"enable_register": service.MustGetBool("app_enable_register", true),
|
|
"enable_invite_system": service.MustGetBool("app_enable_invite_system", true),
|
|
"enable_telegram_bot": service.MustGetBool("telegram_bot_enable", false),
|
|
"enable_ticket_system": service.MustGetBool("app_enable_ticket_system", true),
|
|
"enable_commission_system": service.MustGetBool("app_enable_commission_system", true),
|
|
"enable_traffic_log": service.MustGetBool("app_enable_traffic_log", true),
|
|
"enable_knowledge_base": service.MustGetBool("app_enable_knowledge_base", true),
|
|
"enable_announcements": service.MustGetBool("app_enable_announcements", true),
|
|
},
|
|
"security_config": gin.H{
|
|
"tos_url": service.MustGetString("tos_url", ""),
|
|
"privacy_policy_url": service.MustGetString("app_privacy_policy_url", ""),
|
|
"is_email_verify": service.MustGetInt("email_verify", 0),
|
|
"is_invite_force": service.MustGetInt("invite_force", 0),
|
|
"email_whitelist_suffix": service.MustGetString("email_whitelist_suffix", ""),
|
|
"is_captcha": service.MustGetInt("captcha_enable", 0),
|
|
"captcha_type": service.MustGetString("captcha_type", "recaptcha"),
|
|
"recaptcha_site_key": service.MustGetString("recaptcha_site_key", ""),
|
|
"turnstile_site_key": service.MustGetString("turnstile_site_key", ""),
|
|
},
|
|
}
|
|
|
|
Success(c, config)
|
|
}
|
|
|
|
func ClientAppVersion(c *gin.Context) {
|
|
ua := strings.ToLower(c.GetHeader("User-Agent"))
|
|
if strings.Contains(ua, "tidalab/4.0.0") || strings.Contains(ua, "tunnelab/4.0.0") {
|
|
if strings.Contains(ua, "win64") {
|
|
Success(c, gin.H{
|
|
"version": service.MustGetString("windows_version", ""),
|
|
"download_url": service.MustGetString("windows_download_url", ""),
|
|
})
|
|
return
|
|
}
|
|
|
|
Success(c, gin.H{
|
|
"version": service.MustGetString("macos_version", ""),
|
|
"download_url": service.MustGetString("macos_download_url", ""),
|
|
})
|
|
return
|
|
}
|
|
|
|
Success(c, gin.H{
|
|
"windows_version": service.MustGetString("windows_version", ""),
|
|
"windows_download_url": service.MustGetString("windows_download_url", ""),
|
|
"macos_version": service.MustGetString("macos_version", ""),
|
|
"macos_download_url": service.MustGetString("macos_download_url", ""),
|
|
"android_version": service.MustGetString("android_version", ""),
|
|
"android_download_url": service.MustGetString("android_download_url", ""),
|
|
})
|
|
}
|
|
|
|
func subscriptionUserInfo(user model.User) string {
|
|
expire := int64(0)
|
|
if user.ExpiredAt != nil {
|
|
expire = *user.ExpiredAt
|
|
}
|
|
return "upload=" + strconv.FormatInt(int64(user.U), 10) +
|
|
"; download=" + strconv.FormatInt(int64(user.D), 10) +
|
|
"; total=" + strconv.FormatInt(int64(user.TransferEnable), 10) +
|
|
"; expire=" + strconv.FormatInt(expire, 10)
|
|
}
|