修复订阅无法正常获取的错误
Some checks failed
build / build (api, amd64, linux) (push) Failing after -50s
build / build (api, arm64, linux) (push) Failing after -51s
build / build (api.exe, amd64, windows) (push) Failing after -51s

This commit is contained in:
CN-JS-HuiBai
2026-04-17 23:07:47 +08:00
parent 9f6a4515c0
commit 97f0672729
11 changed files with 273 additions and 95 deletions

View File

@@ -12,6 +12,5 @@ REDIS_DB=0
JWT_SECRET=change_me_to_a_long_random_secret JWT_SECRET=change_me_to_a_long_random_secret
APP_PORT=8080 APP_PORT=8080
APP_URL=http://127.0.0.1:8080 APP_URL=http://127.0.0.1:8080
LOG_LEVEL=info
# Plugin source reference directory, only needed during development.
PLUGIN_ROOT=reference/LDNET-GA-Theme/plugin

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"io"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -16,23 +17,42 @@ import (
func main() { func main() {
config.LoadConfig() config.LoadConfig()
configureRuntimeLogging()
database.InitDB() database.InitDB()
database.InitCache() database.InitCache()
router := gin.New() router := gin.New()
router.Use(gin.Logger(), gin.Recovery()) if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
router.Use(gin.Logger())
}
router.Use(gin.Recovery())
api := router.Group("/api") api := router.Group("/api")
registerV1(api.Group("/v1")) registerV1(api.Group("/v1"))
registerV2(api.Group("/v2")) registerV2(api.Group("/v2"))
registerWebRoutes(router) registerWebRoutes(router)
log.Printf("server starting on port %s", config.AppConfig.AppPort) if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
log.Printf("server starting on port %s", config.AppConfig.AppPort)
}
if err := router.Run(":" + config.AppConfig.AppPort); err != nil { if err := router.Run(":" + config.AppConfig.AppPort); err != nil {
log.Fatalf("failed to start server: %v", err) log.Fatalf("failed to start server: %v", err)
} }
} }
func configureRuntimeLogging() {
if config.NormalizeLogLevel(config.AppConfig.LogLevel) == "debug" {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
if config.NormalizeLogLevel(config.AppConfig.LogLevel) == "silent" {
gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
}
}
func registerV1(v1 *gin.RouterGroup) { func registerV1(v1 *gin.RouterGroup) {
registerPassportRoutes(v1) registerPassportRoutes(v1)
registerGuestRoutes(v1) registerGuestRoutes(v1)
@@ -288,8 +308,10 @@ func registerWebRoutes(router *gin.Engine) {
} }
securePath := "/" + service.GetAdminSecurePath() securePath := "/" + service.GetAdminSecurePath()
subscribePath := "/" + service.GetSubscribePath()
router.GET("/", handler.UserThemePage) router.GET("/", handler.UserThemePage)
router.GET("/dashboard", handler.UserThemePage) router.GET("/dashboard", handler.UserThemePage)
router.GET(subscribePath+"/:token", handler.Subscribe)
router.GET(securePath, handler.AdminAppPage) router.GET(securePath, handler.AdminAppPage)
router.GET(securePath+"/", handler.AdminAppPage) router.GET(securePath+"/", handler.AdminAppPage)
router.GET(securePath+"/plugin-panel/:kind", handler.AdminPluginPanelPage) router.GET(securePath+"/plugin-panel/:kind", handler.AdminPluginPanelPage)

View File

@@ -4,24 +4,26 @@ import (
"log" "log"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
type Config struct { type Config struct {
DBHost string DBHost string
DBPort string DBPort string
DBUser string DBUser string
DBPass string DBPass string
DBName string DBName string
RedisHost string RedisHost string
RedisPort string RedisPort string
RedisPass string RedisPass string
RedisDB int RedisDB int
JWTSecret string JWTSecret string
AppPort string AppPort string
AppURL string AppURL string
PluginRoot string LogLevel string
PluginRoot string
} }
var AppConfig *Config var AppConfig *Config
@@ -45,10 +47,47 @@ func LoadConfig() {
JWTSecret: getEnv("JWT_SECRET", "secret"), JWTSecret: getEnv("JWT_SECRET", "secret"),
AppPort: getEnv("APP_PORT", "8080"), AppPort: getEnv("APP_PORT", "8080"),
AppURL: getEnv("APP_URL", ""), AppURL: getEnv("APP_URL", ""),
LogLevel: NormalizeLogLevel(getEnv("LOG_LEVEL", "info")),
PluginRoot: getEnv("PLUGIN_ROOT", "reference\\LDNET-GA-Theme\\plugin"), PluginRoot: getEnv("PLUGIN_ROOT", "reference\\LDNET-GA-Theme\\plugin"),
} }
} }
func NormalizeLogLevel(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "debug":
return "debug"
case "warn", "warning":
return "warn"
case "error":
return "error"
case "silent":
return "silent"
default:
return "info"
}
}
func IsLogLevelEnabled(currentLevel, targetLevel string) bool {
logLevelOrder := map[string]int{
"debug": 0,
"info": 1,
"warn": 2,
"error": 3,
"silent": 4,
}
currentRank, ok := logLevelOrder[NormalizeLogLevel(currentLevel)]
if !ok {
currentRank = logLevelOrder["info"]
}
targetRank, ok := logLevelOrder[NormalizeLogLevel(targetLevel)]
if !ok {
targetRank = logLevelOrder["info"]
}
return currentRank <= targetRank
}
func getEnv(key, defaultValue string) string { func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists { if value, exists := os.LookupEnv(key); exists {
return value return value

View File

@@ -30,7 +30,9 @@ var fallbackCache = &memoryCache{
func InitCache() { func InitCache() {
if config.AppConfig.RedisHost == "" { if config.AppConfig.RedisHost == "" {
log.Printf("Redis host not configured, using in-memory cache") if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "warn") {
log.Printf("Redis host not configured, using in-memory cache")
}
return return
} }
@@ -46,12 +48,16 @@ func InitCache() {
defer cancel() defer cancel()
if err := client.Ping(ctx).Err(); err != nil { if err := client.Ping(ctx).Err(); err != nil {
log.Printf("Redis unavailable at %s, falling back to in-memory: %v", addr, err) if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "warn") {
log.Printf("Redis unavailable at %s, falling back to in-memory: %v", addr, err)
}
return return
} }
Redis = client Redis = client
log.Printf("Redis connection established at %s", addr) if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
log.Printf("Redis connection established at %s", addr)
}
} }
func CacheSet(key string, value any, ttl time.Duration) error { func CacheSet(key string, value any, ttl time.Duration) error {

View File

@@ -24,7 +24,7 @@ func InitDB() {
var err error var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), Logger: logger.Default.LogMode(gormLogLevel(config.AppConfig.LogLevel)),
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
}) })
@@ -40,5 +40,22 @@ func InitDB() {
log.Fatalf("Failed to migrate database tables: %v", err) log.Fatalf("Failed to migrate database tables: %v", err)
} }
log.Println("Database connection established") if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
log.Println("Database connection established")
}
}
func gormLogLevel(level string) logger.LogLevel {
switch config.NormalizeLogLevel(level) {
case "debug", "info":
return logger.Info
case "warn":
return logger.Warn
case "error":
return logger.Error
case "silent":
return logger.Silent
default:
return logger.Info
}
} }

View File

@@ -155,7 +155,7 @@ func getAllConfigMappings() gin.H {
"show_protocol_to_server_enable": service.MustGetBool("show_protocol_to_server_enable", false), "show_protocol_to_server_enable": service.MustGetBool("show_protocol_to_server_enable", false),
"default_remind_expire": service.MustGetBool("default_remind_expire", true), "default_remind_expire": service.MustGetBool("default_remind_expire", true),
"default_remind_traffic": service.MustGetBool("default_remind_traffic", true), "default_remind_traffic": service.MustGetBool("default_remind_traffic", true),
"subscribe_path": service.MustGetString("subscribe_path", "s"), "subscribe_path": service.GetSubscribePath(),
}, },
"subscribe_template": service.GetAllSubscribeTemplates(), "subscribe_template": service.GetAllSubscribeTemplates(),
"frontend": gin.H{ "frontend": gin.H{

View File

@@ -0,0 +1,147 @@
package handler
import (
"net"
"net/http"
"net/url"
"strings"
"xboard-go/internal/service"
"github.com/gin-gonic/gin"
)
func requestBaseURL(c *gin.Context) string {
if c != nil {
if origin := requestOrigin(c.Request); origin != "" {
return origin
}
}
if appURL := service.GetAppURL(); appURL != "" {
return strings.TrimRight(appURL, "/")
}
return ""
}
func requestOrigin(r *http.Request) string {
if r == nil {
return ""
}
scheme, host := forwardedOrigin(r)
if scheme != "" && host != "" {
return scheme + "://" + host
}
if host == "" {
host = firstHeaderValue(r.Header.Get("X-Forwarded-Host"))
}
if host == "" {
host = strings.TrimSpace(r.Host)
}
if host == "" {
host = originHostFallback(r)
}
if host == "" {
return ""
}
if scheme == "" {
scheme = firstHeaderValue(r.Header.Get("X-Forwarded-Proto"))
}
if scheme == "" {
scheme = firstHeaderValue(r.Header.Get("X-Forwarded-Scheme"))
}
if scheme == "" {
scheme = firstHeaderValue(r.Header.Get("X-Scheme"))
}
if scheme == "" && strings.EqualFold(strings.TrimSpace(r.Header.Get("Front-End-Https")), "on") {
scheme = "https"
}
if scheme == "" && strings.EqualFold(strings.TrimSpace(r.Header.Get("X-Forwarded-Ssl")), "on") {
scheme = "https"
}
if scheme == "" {
switch strings.TrimSpace(firstHeaderValue(r.Header.Get("X-Forwarded-Port"))) {
case "443":
scheme = "https"
case "80":
scheme = "http"
}
}
if scheme == "" {
scheme = originSchemeFallback(r)
}
if scheme == "" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
if forwardedPort := strings.TrimSpace(firstHeaderValue(r.Header.Get("X-Forwarded-Port"))); forwardedPort != "" && !strings.Contains(host, ":") {
defaultPort := map[string]string{
"http": "80",
"https": "443",
}[scheme]
if forwardedPort != defaultPort {
host = net.JoinHostPort(host, forwardedPort)
}
}
return scheme + "://" + host
}
func forwardedOrigin(r *http.Request) (scheme string, host string) {
forwarded := strings.TrimSpace(firstHeaderValue(r.Header.Get("Forwarded")))
if forwarded == "" {
return "", ""
}
for _, segment := range strings.Split(forwarded, ";") {
parts := strings.SplitN(strings.TrimSpace(segment), "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToLower(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
switch key {
case "proto":
if value != "" {
scheme = value
}
case "host":
if value != "" {
host = value
}
}
}
return scheme, host
}
func firstHeaderValue(value string) string {
if value == "" {
return ""
}
parts := strings.Split(value, ",")
return strings.TrimSpace(parts[0])
}
func originHostFallback(r *http.Request) string {
for _, raw := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} {
if parsed, err := url.Parse(strings.TrimSpace(raw)); err == nil && parsed.Host != "" {
return parsed.Host
}
}
return ""
}
func originSchemeFallback(r *http.Request) string {
for _, raw := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} {
if parsed, err := url.Parse(strings.TrimSpace(raw)); err == nil && parsed.Scheme != "" {
return parsed.Scheme
}
}
return ""
}

View File

@@ -74,14 +74,8 @@ func UserGetSubscribe(c *gin.Context) {
} }
} }
baseURL := strings.TrimRight(service.GetAppURL(), "/") baseURL := requestBaseURL(c)
if baseURL == "" { subscribePath := service.GetSubscribePath()
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
baseURL = scheme + "://" + c.Request.Host
}
Success(c, gin.H{ Success(c, gin.H{
"plan_id": user.PlanID, "plan_id": user.PlanID,
@@ -96,7 +90,7 @@ func UserGetSubscribe(c *gin.Context) {
"speed_limit": user.SpeedLimit, "speed_limit": user.SpeedLimit,
"next_reset_at": user.NextResetAt, "next_reset_at": user.NextResetAt,
"plan": user.Plan, "plan": user.Plan,
"subscribe_url": strings.TrimRight(baseURL, "/") + "/s/" + user.Token, "subscribe_url": strings.TrimRight(baseURL, "/") + "/" + subscribePath + "/" + user.Token,
"reset_day": nil, "reset_day": nil,
}) })
} }
@@ -129,15 +123,8 @@ func UserResetSecurity(c *gin.Context) {
return return
} }
baseURL := strings.TrimRight(service.GetAppURL(), "/") baseURL := requestBaseURL(c)
if baseURL == "" { Success(c, strings.TrimRight(baseURL, "/")+"/"+service.GetSubscribePath()+"/"+newToken)
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
baseURL = scheme + "://" + c.Request.Host
}
Success(c, strings.TrimRight(baseURL, "/")+"/s/"+newToken)
} }
func UserUpdate(c *gin.Context) { func UserUpdate(c *gin.Context) {

View File

@@ -206,10 +206,10 @@ func UserTrafficLog(c *gin.Context) {
items := make([]gin.H, 0, len(records)) items := make([]gin.H, 0, len(records))
for _, record := range records { for _, record := range records {
items = append(items, gin.H{ items = append(items, gin.H{
"user_id": record.UserID, "user_id": record.UserID,
"u": record.U, "u": record.U,
"d": record.D, "d": record.D,
"record_at": record.RecordAt, "record_at": record.RecordAt,
"server_rate": 1, "server_rate": 1,
}) })
} }
@@ -726,14 +726,10 @@ func quickLoginURL(c *gin.Context, userID int, redirectValues ...string) string
} }
func baseURL(c *gin.Context) string { func baseURL(c *gin.Context) string {
if appURL := service.GetAppURL(); appURL != "" { if baseURL := requestBaseURL(c); baseURL != "" {
return strings.TrimRight(appURL, "/") return baseURL
} }
scheme := "http" return ""
if c.Request.TLS != nil {
scheme = "https"
}
return scheme + "://" + c.Request.Host
} }
func generateTradeNo() string { func generateTradeNo() string {

View File

@@ -4,11 +4,9 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"html/template" "html/template"
"net"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
"xboard-go/internal/service" "xboard-go/internal/service"
@@ -65,15 +63,10 @@ func UserThemePage(c *gin.Context) {
func AdminAppPage(c *gin.Context) { func AdminAppPage(c *gin.Context) {
securePath := service.GetAdminSecurePath() securePath := service.GetAdminSecurePath()
baseURL := service.GetAppURL()
if origin := requestOrigin(c.Request); origin != "" {
baseURL = origin
}
title := service.MustGetString("app_name", "XBoard") + " Admin" title := service.MustGetString("app_name", "XBoard") + " Admin"
settings := map[string]string{ settings := map[string]string{
"base_url": baseURL, "base_url": requestBaseURL(c),
"title": title, "title": title,
"version": service.MustGetString("app_version", "1.0.0"), "version": service.MustGetString("app_version", "1.0.0"),
"logo": service.MustGetString("logo", ""), "logo": service.MustGetString("logo", ""),
@@ -113,38 +106,3 @@ func mustJSON(value any) template.JS {
} }
return template.JS(payload) return template.JS(payload)
} }
func requestOrigin(r *http.Request) string {
if r == nil {
return ""
}
scheme := r.Header.Get("X-Forwarded-Proto")
if scheme == "" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
if host == "" {
return ""
}
if forwardedPort := r.Header.Get("X-Forwarded-Port"); forwardedPort != "" && !strings.Contains(host, ":") {
defaultPort := map[string]string{
"http": "80",
"https": "443",
}[scheme]
if forwardedPort != "" && forwardedPort != defaultPort {
host = net.JoinHostPort(host, forwardedPort)
}
}
return scheme + "://" + host
}

View File

@@ -79,6 +79,13 @@ func GetAdminSecurePath() string {
return "admin" return "admin"
} }
func GetSubscribePath() string {
if subscribePath := strings.Trim(normalizeWrappedString(MustGetString("subscribe_path", "")), "/"); subscribePath != "" {
return subscribePath
}
return "s"
}
func GetAppURL() string { func GetAppURL() string {
if appURL := normalizeWrappedString(MustGetString("app_url", "")); appURL != "" { if appURL := normalizeWrappedString(MustGetString("app_url", "")); appURL != "" {
return strings.TrimRight(appURL, "/") return strings.TrimRight(appURL, "/")