This commit is contained in:
ryan
2026-06-19 15:13:24 +08:00
parent 0b34792709
commit 32861c5db9
376 changed files with 3648 additions and 19957 deletions
@@ -1,6 +1,8 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package agent implements the OpenFlare agent protocol: node registration,
// heartbeat processing, access-log ingestion, and related middleware.
package agent
import (
@@ -11,12 +13,12 @@ import (
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
)
var accessLogGeoProviderFactory = func() (pkggeoip.GeoIPService, error) {
var accessLogGeoProviderFactory = func() (pkggeoip.Service, error) {
return pkggeoip.NewMaxMindGeoIPService()
}
type accessLogRegionResolver struct {
provider pkggeoip.GeoIPService
provider pkggeoip.Service
cache map[string]string
}
+4 -6
View File
@@ -36,12 +36,10 @@ var tokenCache = newAccessTokenAuthCache()
func newAccessTokenAuthCache() *accessTokenAuthCache {
return &accessTokenAuthCache{
positive: make(map[string]cachedAgentNode),
negative: make(map[string]time.Time),
now: time.Now,
loadNodeByToken: func(ctx context.Context, token string) (*model.OpenFlareNode, error) {
return model.GetOpenFlareNodeByAccessToken(ctx, token)
},
positive: make(map[string]cachedAgentNode),
negative: make(map[string]time.Time),
now: time.Now,
loadNodeByToken: model.GetOpenFlareNodeByAccessToken,
}
}
+3 -3
View File
@@ -4,9 +4,9 @@
package agent
const (
errMissingAgentToken = "缺少 Agent Token"
errInvalidAgentToken = "无权进行此操作,Agent Token 无效"
errInvalidDiscoveryToken = "无权进行此操作,注册 Token 无效"
errMissingAgentToken = "缺少 Agent Token" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
errInvalidAgentToken = "无权进行此操作,Agent Token 无效" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
errInvalidDiscoveryToken = "无权进行此操作,注册 Token 无效" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
errNodeMissingFromContext = "Node object missing from context"
errNoActiveConfig = "当前没有激活版本"
errNodeNotFound = "节点不存在"
+13 -11
View File
@@ -20,10 +20,12 @@ const (
openrestyStatusUnhealthy = "unhealthy"
openrestyStatusUnknown = "unknown"
releaseChannelStable = "stable"
randomTokenBytes = 16
maxDatabaseTextLength = 16000
)
func newRandomToken() (string, error) {
buf := make([]byte, 16)
buf := make([]byte, randomTokenBytes)
if _, err := rand.Read(buf); err != nil {
return "", err
}
@@ -55,9 +57,9 @@ func normalizeNodePayload(payload NodePayload) NodePayload {
payload.Version = strings.TrimSpace(payload.Version)
payload.ExtVersion = strings.TrimSpace(payload.ExtVersion)
payload.CurrentVersion = strings.TrimSpace(payload.CurrentVersion)
payload.LastError = truncateForDatabase(payload.LastError, 16000)
payload.LastError = truncateForDatabase(payload.LastError, maxDatabaseTextLength)
payload.OpenrestyStatus = normalizeOpenrestyStatus(payload.OpenrestyStatus)
payload.OpenrestyMessage = truncateForDatabase(payload.OpenrestyMessage, 16000)
payload.OpenrestyMessage = truncateForDatabase(payload.OpenrestyMessage, maxDatabaseTextLength)
return payload
}
@@ -92,12 +94,12 @@ func applyNodeRuntime(node *model.OpenFlareNode, payload NodePayload, preserveNa
node.Version = strings.TrimSpace(payload.Version)
node.ExtVersion = strings.TrimSpace(payload.ExtVersion)
node.OpenrestyStatus = normalizeOpenrestyStatus(payload.OpenrestyStatus)
node.OpenrestyMessage = truncateForDatabase(payload.OpenrestyMessage, 16000)
node.OpenrestyMessage = truncateForDatabase(payload.OpenrestyMessage, maxDatabaseTextLength)
node.Status = nodeStatusOnline
node.CurrentVersion = strings.TrimSpace(payload.CurrentVersion)
now := time.Now()
node.LastSeenAt = &now
node.LastError = truncateForDatabase(payload.LastError, 16000)
node.LastError = truncateForDatabase(payload.LastError, maxDatabaseTextLength)
if !node.GeoManualOverride {
applyGeoInfoFromIP(node, node.IP)
}
@@ -135,15 +137,15 @@ func cloneCoordinate(value *float64) *float64 {
return &cloned
}
func truncateForDatabase(value string, max int) string {
if max <= 0 {
func truncateForDatabase(value string, maxVal int) string {
if maxVal <= 0 {
return ""
}
runes := []rune(strings.TrimSpace(value))
if len(runes) <= max {
if len(runes) <= maxVal {
return string(runes)
}
return string(runes[:max])
return string(runes[:maxVal])
}
func resolveReportedNodeIP(reportedIP string, remoteAddr string) string {
@@ -277,7 +279,7 @@ func normalizeApplyLogPayload(payload ApplyLogPayload) ApplyLogPayload {
payload.NodeID = strings.TrimSpace(payload.NodeID)
payload.Version = strings.TrimSpace(payload.Version)
payload.Result = strings.ToLower(strings.TrimSpace(payload.Result))
payload.Message = truncateForDatabase(strings.TrimSpace(payload.Message), 16000)
payload.Message = truncateForDatabase(strings.TrimSpace(payload.Message), maxDatabaseTextLength)
payload.Checksum = strings.TrimSpace(payload.Checksum)
payload.MainConfigChecksum = strings.TrimSpace(payload.MainConfigChecksum)
payload.RouteConfigChecksum = strings.TrimSpace(payload.RouteConfigChecksum)
@@ -292,7 +294,7 @@ func isUniqueConstraintError(err error) bool {
}
// RefreshAccessTokenCache updates the in-memory node cache after heartbeat mutations.
func RefreshAccessTokenCache(ctx context.Context, node *model.OpenFlareNode) {
func RefreshAccessTokenCache(_ context.Context, node *model.OpenFlareNode) {
if node == nil {
return
}
+8 -8
View File
@@ -12,12 +12,12 @@ import (
)
const (
agentTokenHeader = "X-Agent-Token"
agentTokenHeader = "X-Agent-Token" //nolint:gosec // HTTP header name, not a credential value
agentNodeContextKey = "agent_node"
)
// AgentAuth validates X-Agent-Token against of_nodes.access_token.
func AgentAuth() gin.HandlerFunc {
// Auth validates X-Agent-Token against of_nodes.access_token.
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := strings.TrimSpace(c.GetHeader(agentTokenHeader))
node, err := AuthenticateAccessToken(c.Request.Context(), token)
@@ -30,8 +30,8 @@ func AgentAuth() gin.HandlerFunc {
}
}
// AgentRegisterAuth accepts either a node access token or the global discovery token.
func AgentRegisterAuth() gin.HandlerFunc {
// RegisterAuth accepts either a node access token or the global discovery token.
func RegisterAuth() gin.HandlerFunc {
return func(c *gin.Context) {
token := strings.TrimSpace(c.GetHeader(agentTokenHeader))
if node, err := AuthenticateAccessToken(c.Request.Context(), token); err == nil {
@@ -48,12 +48,12 @@ func AgentRegisterAuth() gin.HandlerFunc {
}
}
// AgentNodeFromContext returns the authenticated agent node.
func AgentNodeFromContext(c *gin.Context) (*model.OpenFlareNode, bool) {
// NodeFromContext returns the authenticated agent node.
func NodeFromContext(c *gin.Context) (*model.OpenFlareNode, bool) {
value, ok := c.Get(agentNodeContextKey)
if !ok {
return nil, false
}
node, ok := value.(*model.OpenFlareNode)
return node, ok
}
}
@@ -109,8 +109,8 @@ func TestAgentAuthMiddleware(t *testing.T) {
}).Error)
router := testhelper.NewTestGinEngine()
router.GET("/protected", AgentAuth(), func(c *gin.Context) {
node, ok := AgentNodeFromContext(c)
router.GET("/protected", Auth(), func(c *gin.Context) {
node, ok := NodeFromContext(c)
if !ok {
c.Status(http.StatusInternalServerError)
return
@@ -157,8 +157,8 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) {
require.NoError(t, model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", "discovery-token"))
router := testhelper.NewTestGinEngine()
router.POST("/register", AgentRegisterAuth(), func(c *gin.Context) {
if node, ok := AgentNodeFromContext(c); ok {
router.POST("/register", RegisterAuth(), func(c *gin.Context) {
if node, ok := NodeFromContext(c); ok {
c.JSON(http.StatusOK, response.OK(gin.H{"mode": "node", "node_id": node.NodeID}))
return
}
@@ -196,4 +196,4 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) {
require.True(t, ok)
assert.Equal(t, "discovery", data["mode"])
})
}
}
@@ -27,6 +27,7 @@ const (
nodeAccessLogRetentionDays = 90
nodeAccessLogRetentionWindow = nodeAccessLogRetentionDays * 24 * time.Hour
accessLogPathMaxLength = 100
healthEventMessageMaxLength = 4096
)
// PersistHeartbeatObservability stores profile, snapshots, traffic, access logs, and health events.
@@ -396,7 +397,7 @@ func normalizeHealthSeverity(severity string) string {
}
func normalizeHealthEventMessage(message string) string {
return truncateForDatabase(message, 4096)
return truncateForDatabase(message, healthEventMessageMaxLength)
}
func timeFromUnix(unixSeconds int64, fallback time.Time) time.Time {
@@ -5,22 +5,55 @@ package agent
import pkgprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
// NodePayload is the data sent by an agent on registration or heartbeat.
type NodePayload = pkgprotocol.NodePayload
// NodeSystemProfile carries static host information reported by an agent.
type NodeSystemProfile = pkgprotocol.NodeSystemProfile
// NodeMetricSnapshot holds a point-in-time resource-usage sample from an agent.
type NodeMetricSnapshot = pkgprotocol.NodeMetricSnapshot
// NodeOpenrestyObservation reports the OpenResty process health observed by an agent.
type NodeOpenrestyObservation = pkgprotocol.NodeOpenrestyObservation
// NodeTrafficReport aggregates traffic counters collected by an agent.
type NodeTrafficReport = pkgprotocol.NodeTrafficReport
// NodeAccessLog is a single access-log record forwarded by an agent.
type NodeAccessLog = pkgprotocol.NodeAccessLog
// BufferedObservabilityRecord bundles multiple observability payloads into one upload.
type BufferedObservabilityRecord = pkgprotocol.BufferedObservabilityRecord
// NodeHealthEvent represents a discrete health-state change on an agent node.
type NodeHealthEvent = pkgprotocol.NodeHealthEvent
// ApplyLogPayload carries the result of a configuration-apply attempt reported by an agent.
type ApplyLogPayload = pkgprotocol.ApplyLogPayload
// Settings contains remote-control directives sent from the server to an agent.
type Settings = pkgprotocol.AgentSettings
// ActiveConfigMeta describes the currently active configuration version on the server.
type ActiveConfigMeta = pkgprotocol.ActiveConfigMeta
// SupportFile represents a supplementary file bundled with an agent configuration package.
type SupportFile = pkgprotocol.SupportFile
// WAFIPGroup is a named IP-address group used in WAF allow/block rules.
type WAFIPGroup = pkgprotocol.WAFIPGroup
// WAFIPGroupSyncRequest is sent by an agent to request an incremental WAF IP-group sync.
type WAFIPGroupSyncRequest = pkgprotocol.WAFIPGroupSyncRequest
// WAFIPGroupSyncResponse carries the server's reply to a WAF IP-group sync request.
type WAFIPGroupSyncResponse = pkgprotocol.WAFIPGroupSyncResponse
// Backward-compatible names used by server routers and handlers.
// WAFIPGroupSyncInput is an alias for WAFIPGroupSyncRequest kept for backward compatibility.
type WAFIPGroupSyncInput = WAFIPGroupSyncRequest
type WAFIPGroupSyncResult = WAFIPGroupSyncResponse
// WAFIPGroupSyncResult is an alias for WAFIPGroupSyncResponse kept for backward compatibility.
type WAFIPGroupSyncResult = WAFIPGroupSyncResponse
+9 -9
View File
@@ -37,7 +37,7 @@ func RegisterHandler(c *gin.Context) {
result *RegistrationResponse
err error
)
if authNode, ok := AgentNodeFromContext(c); ok {
if authNode, ok := NodeFromContext(c); ok {
result, err = RegisterWithAccessToken(c.Request.Context(), authNode, payload)
} else {
result, err = RegisterWithDiscovery(c.Request.Context(), payload)
@@ -67,7 +67,7 @@ func HeartbeatHandler(c *gin.Context) {
}
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
authNode, ok := AgentNodeFromContext(c)
authNode, ok := NodeFromContext(c)
if !ok {
response.AbortUnauthorized(c, errInvalidAgentToken)
return
@@ -91,7 +91,7 @@ func HeartbeatHandler(c *gin.Context) {
// @Failure 401 {object} response.Any "Token 无效"
// @Router /api/v1/agent/config-versions/active [get]
func GetActiveConfigHandler(c *gin.Context) {
if _, ok := AgentNodeFromContext(c); !ok {
if _, ok := NodeFromContext(c); !ok {
response.AbortUnauthorized(c, errNodeMissingFromContext)
return
}
@@ -143,7 +143,7 @@ func ReportApplyLogHandler(c *gin.Context) {
if !apiutil.BindJSON(c, &payload) {
return
}
if authNode, ok := AgentNodeFromContext(c); ok {
if authNode, ok := NodeFromContext(c); ok {
payload.NodeID = authNode.NodeID
}
log, err := ReportApplyLog(c.Request.Context(), payload)
@@ -173,7 +173,7 @@ func DownloadPagesPackageHandler(c *gin.Context) {
if apiutil.AbortBadRequestOnError(c, err) {
return
}
defer packageObj.Body.Close()
defer func() { _ = packageObj.Body.Close() }()
c.Header("Content-Disposition", "attachment; filename="+fileName)
if packageObj.ContentType != "" {
c.Header("Content-Type", packageObj.ContentType)
@@ -195,18 +195,18 @@ func pagesDeploymentIDParam(c *gin.Context) (uint, bool) {
return uint(id64), true
}
// AgentWebSocketHandler upgrades an authenticated agent websocket connection.
// WebSocketHandler upgrades an authenticated agent websocket connection.
// @Summary Agent WebSocket 连接
// @Description 升级为 WebSocket 长连接,用于实时推送配置同步、WAF IP 组等指令;需携带 X-Agent-Token
// @Tags openflare-agent
// @Security AgentTokenAuth
// @Failure 401 {object} response.Any "Token 无效"
// @Router /api/v1/agent/ws [get]
func AgentWebSocketHandler(c *gin.Context) {
authNode, ok := AgentNodeFromContext(c)
func WebSocketHandler(c *gin.Context) {
authNode, ok := NodeFromContext(c)
if !ok {
response.AbortUnauthorized(c, errInvalidAgentToken)
return
}
websocket.ServeAgent(c, authNode.NodeID, HandleWSStatus)
}
}