[优化] go 引用调整

This commit is contained in:
ryan
2026-06-06 10:26:20 +08:00
parent ee1110b752
commit 3cfefb4367
552 changed files with 1642 additions and 2185 deletions
+39
View File
@@ -0,0 +1,39 @@
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/rain-kl/openflare/openflare-server/common/response"
"github.com/rain-kl/openflare/openflare-server/service"
)
func AgentAuth() func(c *gin.Context) {
return func(c *gin.Context) {
token := c.GetHeader("X-Agent-Token")
node, err := service.AuthenticateAccessToken(token)
if err != nil {
response.RespondUnauthorized(c, "无权进行此操作,Agent Token 无效")
c.Abort()
return
}
c.Set("agent_node", node)
c.Next()
}
}
func AgentRegisterAuth() func(c *gin.Context) {
return func(c *gin.Context) {
token := c.GetHeader("X-Agent-Token")
if node, err := service.AuthenticateAccessToken(token); err == nil {
c.Set("agent_node", node)
c.Next()
return
}
if err := service.ValidateDiscoveryToken(token); err != nil {
response.RespondUnauthorized(c, "无权进行此操作,注册 Token 无效")
c.Abort()
return
}
c.Set("discovery_enabled", true)
c.Next()
}
}
+101
View File
@@ -0,0 +1,101 @@
package middleware
import (
"github.com/rain-kl/openflare/openflare-server/common"
"github.com/rain-kl/openflare/openflare-server/common/response"
"github.com/rain-kl/openflare/openflare-server/model"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
const OpenFlareTokenHeader = "OpenFlare-Token"
func authHelper(c *gin.Context, minRole int) {
tokenStr := c.GetHeader(OpenFlareTokenHeader)
if tokenStr == "" {
response.RespondUnauthorized(c, "无权进行此操作,未登录或 token 无效")
c.Abort()
return
}
token, err := JWTMiddleware.ParseTokenString(tokenStr)
if err != nil {
response.RespondUnauthorized(c, "无权进行此操作,token 无效: "+err.Error())
c.Abort()
return
}
claims := jwt.ExtractClaimsFromToken(token)
id, ok := claims["id"].(float64)
if !ok {
response.RespondUnauthorized(c, "无权进行此操作,token 格式错误")
c.Abort()
return
}
dbUser := &model.User{}
dbErr := model.DB.Select([]string{"id", "username", "display_name", "role", "status", "token"}).
First(dbUser, "id = ?", int(id)).Error
if dbErr != nil || dbUser.Username == "" {
response.RespondUnauthorized(c, "无权进行此操作,用户不存在")
c.Abort()
return
}
if dbUser.Token != tokenStr {
response.RespondUnauthorized(c, "无权进行此操作,token 已失效或已登出")
c.Abort()
return
}
if dbUser.Status == common.UserStatusDisabled {
response.RespondFailure(c, "用户已被封禁")
c.Abort()
return
}
if int(dbUser.Role) < minRole {
response.RespondFailure(c, "无权进行此操作,权限不足")
c.Abort()
return
}
c.Set("username", dbUser.Username)
c.Set("role", dbUser.Role)
c.Set("id", dbUser.Id)
c.Set("authByToken", true)
c.Next()
}
func UserAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authHelper(c, common.RoleCommonUser)
}
}
func AdminAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authHelper(c, common.RoleAdminUser)
}
}
func RootAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authHelper(c, common.RoleRootUser)
}
}
// NoTokenAuth is kept as a compatibility no-op because admin APIs now always use OPENFLARE_TOKEN.
func NoTokenAuth() func(c *gin.Context) {
return func(c *gin.Context) {
c.Next()
}
}
// TokenOnlyAuth is kept as a compatibility no-op because admin APIs now always use OPENFLARE_TOKEN.
func TokenOnlyAuth() func(c *gin.Context) {
return func(c *gin.Context) {
c.Next()
}
}
+37
View File
@@ -0,0 +1,37 @@
package middleware
import (
"path"
"strings"
"github.com/gin-gonic/gin"
)
func Cache() func(c *gin.Context) {
return func(c *gin.Context) {
requestPath := c.Request.URL.Path
switch {
case strings.HasPrefix(requestPath, "/_next/static/"):
c.Header("Cache-Control", "public, max-age=31536000, immutable")
case isStaticPublicAsset(requestPath):
c.Header("Cache-Control", "public, max-age=86400")
default:
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
}
c.Next()
}
}
func isStaticPublicAsset(requestPath string) bool {
ext := strings.ToLower(path.Ext(requestPath))
switch ext {
case ".ico", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".css", ".js":
return true
default:
return false
}
}
+27
View File
@@ -0,0 +1,27 @@
package middleware
import (
"strings"
"github.com/rain-kl/openflare/openflare-server/common"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func CORS() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowCredentials = true
config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "OpenFlare-Token", "X-Agent-Token", "Accept"}
config.AllowOriginFunc = func(origin string) bool {
serverAddr := strings.TrimRight(common.ServerAddress, "/")
if serverAddr == "" {
return true
}
if origin == serverAddr {
return true
}
return false
}
return cors.New(config)
}
+72
View File
@@ -0,0 +1,72 @@
package middleware
import (
"log"
"time"
"github.com/rain-kl/openflare/openflare-server/common"
"github.com/rain-kl/openflare/openflare-server/common/response"
"github.com/rain-kl/openflare/openflare-server/model"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
var JWTMiddleware *jwt.GinJWTMiddleware
// jwtSigningKey returns JWT_SECRET when set, falling back to SESSION_SECRET
// for backward compatibility with deployments that only configure SESSION_SECRET.
func jwtSigningKey() []byte {
if common.JWTSecret != "" {
return []byte(common.JWTSecret)
}
return []byte(common.SessionSecret)
}
func InitJWTMiddleware() {
var err error
JWTMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{
Realm: "openflare",
Key: jwtSigningKey(),
Timeout: 24 * time.Hour,
MaxRefresh: 24 * time.Hour,
IdentityKey: "identity",
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*model.User); ok {
return jwt.MapClaims{
"id": v.Id,
"username": v.Username,
"role": v.Role,
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
id, ok := claims["id"].(float64)
if !ok {
return nil
}
username, _ := claims["username"].(string)
role, _ := claims["role"].(float64)
return &model.User{
Id: int(id),
Username: username,
Role: int(role),
}
},
Authorizator: func(data interface{}, c *gin.Context) bool {
return data != nil
},
Unauthorized: func(c *gin.Context, code int, message string) {
response.RespondErrorWithStatus(c, code, "无权进行此操作,未登录或 token 无效: "+message)
},
TokenLookup: "header: OpenFlare-Token",
TokenHeadName: "", // Empty for raw token value directly
SendCookie: false,
})
if err != nil {
log.Fatalf("JWT Init Error: %s", err.Error())
}
}
+98
View File
@@ -0,0 +1,98 @@
package middleware
import (
"context"
"log/slog"
"net/http"
"time"
"github.com/rain-kl/openflare/openflare-server/common"
"github.com/rain-kl/openflare/openflare-server/utils/ratelimit"
"github.com/gin-gonic/gin"
)
var timeFormat = "2006-01-02T15:04:05.000Z"
var inMemoryRateLimiter ratelimit.InMemoryRateLimiter
func redisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
ctx := context.Background()
rdb := common.RDB
key := "rateLimit:" + mark + c.ClientIP()
listLength, err := rdb.LLen(ctx, key).Result()
if err != nil {
slog.Error("redis rate limiter llen failed", "error", err)
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
if listLength < int64(maxRequestNum) {
rdb.LPush(ctx, key, time.Now().Format(timeFormat))
rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
} else {
oldTimeStr, _ := rdb.LIndex(ctx, key, -1).Result()
oldTime, err := time.Parse(timeFormat, oldTimeStr)
if err != nil {
slog.Error("parse redis rate limiter old timestamp failed", "error", err)
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
nowTimeStr := time.Now().Format(timeFormat)
nowTime, err := time.Parse(timeFormat, nowTimeStr)
if err != nil {
slog.Error("parse redis rate limiter current timestamp failed", "error", err)
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
// time.Since will return negative number!
// See: https://stackoverflow.com/questions/50970900/why-is-time-since-returning-negative-durations-on-windows
if int64(nowTime.Sub(oldTime).Seconds()) < duration {
rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
c.Status(http.StatusTooManyRequests)
c.Abort()
return
}
rdb.LPush(ctx, key, time.Now().Format(timeFormat))
rdb.LTrim(ctx, key, 0, int64(maxRequestNum-1))
rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
}
}
func memoryRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
key := mark + c.ClientIP()
if !inMemoryRateLimiter.Request(key, maxRequestNum, duration) {
c.Status(http.StatusTooManyRequests)
c.Abort()
return
}
}
func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) {
if common.RedisEnabled {
return func(c *gin.Context) {
redisRateLimiter(c, maxRequestNum, duration, mark)
}
}
// It's safe to call multi times.
inMemoryRateLimiter.Init(common.RateLimitKeyExpirationDuration)
return func(c *gin.Context) {
memoryRateLimiter(c, maxRequestNum, duration, mark)
}
}
func GlobalWebRateLimit() func(c *gin.Context) {
return rateLimitFactory(common.GlobalWebRateLimitNum, common.GlobalWebRateLimitDuration, "GW")
}
func GlobalAPIRateLimit() func(c *gin.Context) {
return rateLimitFactory(common.GlobalApiRateLimitNum, common.GlobalApiRateLimitDuration, "GA")
}
func CriticalRateLimit() func(c *gin.Context) {
return rateLimitFactory(common.CriticalRateLimitNum, common.CriticalRateLimitDuration, "CT")
}
+28
View File
@@ -0,0 +1,28 @@
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/rain-kl/openflare/openflare-server/common/response"
"github.com/rain-kl/openflare/openflare-server/service"
)
// RelayAuth authenticates Relay requests using the shared agent token,
// and verifies the node is a tunnel_relay type.
func RelayAuth() func(c *gin.Context) {
return func(c *gin.Context) {
token := c.GetHeader("X-Agent-Token")
node, err := service.AuthenticateAccessToken(token)
if err != nil {
response.RespondUnauthorized(c, "无权进行此操作,Agent Token 无效")
c.Abort()
return
}
if node.NodeType != "tunnel_relay" {
response.RespondForbidden(c, "此节点不是 TunnelRelay 类型")
c.Abort()
return
}
c.Set("relay_node", node)
c.Next()
}
}
@@ -0,0 +1,30 @@
package middleware
import (
"github.com/rain-kl/openflare/openflare-server/common/response"
"github.com/rain-kl/openflare/openflare-server/service"
"github.com/gin-gonic/gin"
)
// TunnelAuth authenticates OpenFlared client requests using the per-node
// tunnel_token carried in the X-Tunnel-Token header, and verifies the node is
// of the tunnel_client type.
func TunnelAuth() func(c *gin.Context) {
return func(c *gin.Context) {
token := c.GetHeader("X-Tunnel-Token")
node, err := service.AuthenticateAccessToken(token)
if err != nil {
response.RespondUnauthorized(c, "无权进行此操作,Tunnel Token 无效")
c.Abort()
return
}
if node.NodeType != "tunnel_client" {
response.RespondForbidden(c, "此节点不是 TunnelClient 类型")
c.Abort()
return
}
c.Set("flared_node", node)
c.Next()
}
}