2026-05-25 17:06:22 +08:00

131 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"crypto/subtle"
"encoding/base64"
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
)
const agentAuthRealm = "FLNXI Agent"
const agentAuthConfigRequiredMessage = "Agent 管理认证未配置,管理面默认关闭"
type AgentAuthConfig struct {
Username string
Password string
IsConfigured bool
IsPartial bool
}
func LoadAgentAuthConfig() AgentAuthConfig {
username := strings.TrimSpace(os.Getenv("AGENT_AUTH_USERNAME"))
password := os.Getenv("AGENT_AUTH_PASSWORD")
return AgentAuthConfig{
Username: username,
Password: password,
IsConfigured: username != "" && password != "",
IsPartial: (username != "" && password == "") || (username == "" && password != ""),
}
}
func LogAgentSecurityConfig(config AgentAuthConfig) {
if config.IsPartial {
logMessage := "[Security] AGENT_AUTH_USERNAME 和 AGENT_AUTH_PASSWORD 需要同时设置;当前仅保留 /api/health,其余 Agent 管理面已关闭"
println(logMessage)
return
}
if config.IsConfigured {
println("[Security] Agent 管理认证已启用")
return
}
println("[Security] Agent 管理认证未配置;当前仅保留 /api/health,其余 Agent 管理面默认关闭")
}
func safeEqual(left, right string) bool {
leftBytes := []byte(left)
rightBytes := []byte(right)
if len(leftBytes) != len(rightBytes) {
return false
}
return subtle.ConstantTimeCompare(leftBytes, rightBytes) == 1
}
func parseBasicAuth(header string) (string, string, bool) {
if !strings.HasPrefix(header, "Basic ") {
return "", "", false
}
decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(header, "Basic "))
if err != nil {
return "", "", false
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
return "", "", false
}
return parts[0], parts[1], true
}
func isAuthorized(header string, config AgentAuthConfig) bool {
if !config.IsConfigured {
return false
}
username, password, ok := parseBasicAuth(header)
if !ok {
return false
}
return safeEqual(username, config.Username) && safeEqual(password, config.Password)
}
func AgentSecurityMiddleware(config AgentAuthConfig) gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Frame-Options", "SAMEORIGIN")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("Referrer-Policy", "same-origin")
c.Header("Cache-Control", "no-store")
if c.Request.URL.Path == "/api/health" {
c.Next()
return
}
if !config.IsConfigured {
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": agentAuthConfigRequiredMessage})
} else {
c.String(http.StatusServiceUnavailable, agentAuthConfigRequiredMessage)
}
c.Abort()
return
}
if !isAuthorized(c.GetHeader("Authorization"), config) {
c.Header("WWW-Authenticate", `Basic realm="`+agentAuthRealm+`", charset="UTF-8"`)
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "需要 Agent 管理认证"})
} else {
c.String(http.StatusUnauthorized, "需要 Agent 管理认证")
}
c.Abort()
return
}
c.Next()
}
}