mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 14:26:36 +08:00
[优化] go 引用调整
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user