diff --git a/.env.example b/.env.example index 3527629..484c0a6 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,5 @@ REDIS_DB=0 JWT_SECRET=change_me_to_a_long_random_secret APP_PORT=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 diff --git a/cmd/api/main_entry.go b/cmd/api/main_entry.go index aa3c2d4..c44de5d 100644 --- a/cmd/api/main_entry.go +++ b/cmd/api/main_entry.go @@ -1,6 +1,7 @@ package main import ( + "io" "log" "os" "path/filepath" @@ -16,23 +17,42 @@ import ( func main() { config.LoadConfig() + configureRuntimeLogging() database.InitDB() database.InitCache() 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") registerV1(api.Group("/v1")) registerV2(api.Group("/v2")) 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 { 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) { registerPassportRoutes(v1) registerGuestRoutes(v1) @@ -288,8 +308,10 @@ func registerWebRoutes(router *gin.Engine) { } securePath := "/" + service.GetAdminSecurePath() + subscribePath := "/" + service.GetSubscribePath() router.GET("/", 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+"/plugin-panel/:kind", handler.AdminPluginPanelPage) diff --git a/internal/config/config.go b/internal/config/config.go index b6c743b..d9a7f1c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,24 +4,26 @@ import ( "log" "os" "strconv" + "strings" "github.com/joho/godotenv" ) type Config struct { - DBHost string - DBPort string - DBUser string - DBPass string - DBName string - RedisHost string - RedisPort string - RedisPass string - RedisDB int - JWTSecret string - AppPort string - AppURL string - PluginRoot string + DBHost string + DBPort string + DBUser string + DBPass string + DBName string + RedisHost string + RedisPort string + RedisPass string + RedisDB int + JWTSecret string + AppPort string + AppURL string + LogLevel string + PluginRoot string } var AppConfig *Config @@ -45,10 +47,47 @@ func LoadConfig() { JWTSecret: getEnv("JWT_SECRET", "secret"), AppPort: getEnv("APP_PORT", "8080"), AppURL: getEnv("APP_URL", ""), + LogLevel: NormalizeLogLevel(getEnv("LOG_LEVEL", "info")), 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 { if value, exists := os.LookupEnv(key); exists { return value diff --git a/internal/database/cache.go b/internal/database/cache.go index 7ab7dfb..3bc8231 100644 --- a/internal/database/cache.go +++ b/internal/database/cache.go @@ -30,7 +30,9 @@ var fallbackCache = &memoryCache{ func InitCache() { 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 } @@ -46,12 +48,16 @@ func InitCache() { defer cancel() 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 } 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 { diff --git a/internal/database/db.go b/internal/database/db.go index 666f8c6..49eb6d1 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -24,7 +24,7 @@ func InitDB() { var err error 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, }) @@ -40,5 +40,22 @@ func InitDB() { 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 + } } diff --git a/internal/handler/admin_config_api.go b/internal/handler/admin_config_api.go index a845710..381296c 100644 --- a/internal/handler/admin_config_api.go +++ b/internal/handler/admin_config_api.go @@ -155,7 +155,7 @@ func getAllConfigMappings() gin.H { "show_protocol_to_server_enable": service.MustGetBool("show_protocol_to_server_enable", false), "default_remind_expire": service.MustGetBool("default_remind_expire", 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(), "frontend": gin.H{ diff --git a/internal/handler/request_url.go b/internal/handler/request_url.go new file mode 100644 index 0000000..258c3d0 --- /dev/null +++ b/internal/handler/request_url.go @@ -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 "" +} diff --git a/internal/handler/user_api.go b/internal/handler/user_api.go index 24b7ae9..e9ca55d 100644 --- a/internal/handler/user_api.go +++ b/internal/handler/user_api.go @@ -74,14 +74,8 @@ func UserGetSubscribe(c *gin.Context) { } } - baseURL := strings.TrimRight(service.GetAppURL(), "/") - if baseURL == "" { - scheme := "http" - if c.Request.TLS != nil { - scheme = "https" - } - baseURL = scheme + "://" + c.Request.Host - } + baseURL := requestBaseURL(c) + subscribePath := service.GetSubscribePath() Success(c, gin.H{ "plan_id": user.PlanID, @@ -96,7 +90,7 @@ func UserGetSubscribe(c *gin.Context) { "speed_limit": user.SpeedLimit, "next_reset_at": user.NextResetAt, "plan": user.Plan, - "subscribe_url": strings.TrimRight(baseURL, "/") + "/s/" + user.Token, + "subscribe_url": strings.TrimRight(baseURL, "/") + "/" + subscribePath + "/" + user.Token, "reset_day": nil, }) } @@ -129,15 +123,8 @@ func UserResetSecurity(c *gin.Context) { return } - baseURL := strings.TrimRight(service.GetAppURL(), "/") - if baseURL == "" { - scheme := "http" - if c.Request.TLS != nil { - scheme = "https" - } - baseURL = scheme + "://" + c.Request.Host - } - Success(c, strings.TrimRight(baseURL, "/")+"/s/"+newToken) + baseURL := requestBaseURL(c) + Success(c, strings.TrimRight(baseURL, "/")+"/"+service.GetSubscribePath()+"/"+newToken) } func UserUpdate(c *gin.Context) { diff --git a/internal/handler/user_extra_api.go b/internal/handler/user_extra_api.go index dcdd30b..f704339 100644 --- a/internal/handler/user_extra_api.go +++ b/internal/handler/user_extra_api.go @@ -206,10 +206,10 @@ func UserTrafficLog(c *gin.Context) { items := make([]gin.H, 0, len(records)) for _, record := range records { items = append(items, gin.H{ - "user_id": record.UserID, - "u": record.U, - "d": record.D, - "record_at": record.RecordAt, + "user_id": record.UserID, + "u": record.U, + "d": record.D, + "record_at": record.RecordAt, "server_rate": 1, }) } @@ -726,14 +726,10 @@ func quickLoginURL(c *gin.Context, userID int, redirectValues ...string) string } func baseURL(c *gin.Context) string { - if appURL := service.GetAppURL(); appURL != "" { - return strings.TrimRight(appURL, "/") + if baseURL := requestBaseURL(c); baseURL != "" { + return baseURL } - scheme := "http" - if c.Request.TLS != nil { - scheme = "https" - } - return scheme + "://" + c.Request.Host + return "" } func generateTradeNo() string { diff --git a/internal/handler/web_pages.go b/internal/handler/web_pages.go index 2827b04..f64b0ce 100644 --- a/internal/handler/web_pages.go +++ b/internal/handler/web_pages.go @@ -4,11 +4,9 @@ import ( "bytes" "encoding/json" "html/template" - "net" "net/http" "path/filepath" "strconv" - "strings" "time" "xboard-go/internal/service" @@ -65,15 +63,10 @@ func UserThemePage(c *gin.Context) { func AdminAppPage(c *gin.Context) { securePath := service.GetAdminSecurePath() - baseURL := service.GetAppURL() - if origin := requestOrigin(c.Request); origin != "" { - baseURL = origin - } - title := service.MustGetString("app_name", "XBoard") + " Admin" settings := map[string]string{ - "base_url": baseURL, + "base_url": requestBaseURL(c), "title": title, "version": service.MustGetString("app_version", "1.0.0"), "logo": service.MustGetString("logo", ""), @@ -113,38 +106,3 @@ func mustJSON(value any) template.JS { } 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 -} diff --git a/internal/service/settings.go b/internal/service/settings.go index 86c98ad..3594394 100644 --- a/internal/service/settings.go +++ b/internal/service/settings.go @@ -79,6 +79,13 @@ func GetAdminSecurePath() string { return "admin" } +func GetSubscribePath() string { + if subscribePath := strings.Trim(normalizeWrappedString(MustGetString("subscribe_path", "")), "/"); subscribePath != "" { + return subscribePath + } + return "s" +} + func GetAppURL() string { if appURL := normalizeWrappedString(MustGetString("app_url", "")); appURL != "" { return strings.TrimRight(appURL, "/")