mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
184 lines
4.7 KiB
Go
184 lines
4.7 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package agent
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/pages"
|
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// RegisterRoutes mounts agent API routes under /agent.
|
|
func RegisterRoutes(apiGroup *gin.RouterGroup) {
|
|
agentRoute := apiGroup.Group("/agent")
|
|
{
|
|
discoveryRoute := agentRoute.Group("/")
|
|
discoveryRoute.Use(AgentRegisterAuth())
|
|
{
|
|
discoveryRoute.POST("/nodes/register", RegisterHandler)
|
|
}
|
|
|
|
authorizedRoute := agentRoute.Group("/")
|
|
authorizedRoute.Use(AgentAuth())
|
|
{
|
|
authorizedRoute.GET("/ws", AgentWebSocketHandler)
|
|
authorizedRoute.POST("/nodes/heartbeat", HeartbeatHandler)
|
|
authorizedRoute.GET("/config-versions/active", GetActiveConfigHandler)
|
|
authorizedRoute.GET("/pages/deployments/:deployment_id/package", DownloadPagesPackageHandler)
|
|
authorizedRoute.POST("/waf/ip-groups/sync", SyncWAFIPGroupsHandler)
|
|
authorizedRoute.POST("/apply-logs", ReportApplyLogHandler)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RegisterHandler registers or discovers an agent node.
|
|
func RegisterHandler(c *gin.Context) {
|
|
var payload NodePayload
|
|
if !compat.BindJSON(c, &payload) {
|
|
return
|
|
}
|
|
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
|
|
|
var (
|
|
result *RegistrationResponse
|
|
err error
|
|
)
|
|
if authNode, ok := AgentNodeFromContext(c); ok {
|
|
result, err = RegisterWithAccessToken(c.Request.Context(), authNode, payload)
|
|
} else {
|
|
result, err = RegisterWithDiscovery(c.Request.Context(), payload)
|
|
}
|
|
if err != nil {
|
|
compat.Fail(c, err.Error())
|
|
return
|
|
}
|
|
compat.OK(c, result)
|
|
}
|
|
|
|
// HeartbeatHandler records agent heartbeat state.
|
|
func HeartbeatHandler(c *gin.Context) {
|
|
var payload NodePayload
|
|
if !compat.BindJSON(c, &payload) {
|
|
return
|
|
}
|
|
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
|
|
|
authNode, ok := AgentNodeFromContext(c)
|
|
if !ok {
|
|
compat.Unauthorized(c, errInvalidAgentToken)
|
|
return
|
|
}
|
|
|
|
response, err := HeartbeatNode(c.Request.Context(), authNode, payload)
|
|
if err != nil {
|
|
compat.Fail(c, err.Error())
|
|
return
|
|
}
|
|
okWithExtras(c, response.Node, gin.H{
|
|
"agent_settings": response.AgentSettings,
|
|
"active_config": response.ActiveConfig,
|
|
"waf_ip_groups": response.WAFIPGroups,
|
|
})
|
|
}
|
|
|
|
// GetActiveConfigHandler returns the active configuration version.
|
|
func GetActiveConfigHandler(c *gin.Context) {
|
|
if _, ok := AgentNodeFromContext(c); !ok {
|
|
compat.Unauthorized(c, errNodeMissingFromContext)
|
|
return
|
|
}
|
|
config, err := GetActiveConfig(c.Request.Context())
|
|
if err != nil {
|
|
compat.Fail(c, err.Error())
|
|
return
|
|
}
|
|
compat.OK(c, config)
|
|
}
|
|
|
|
// SyncWAFIPGroupsHandler syncs WAF IP groups for an agent.
|
|
func SyncWAFIPGroupsHandler(c *gin.Context) {
|
|
var input WAFIPGroupSyncInput
|
|
if !compat.BindJSON(c, &input) {
|
|
return
|
|
}
|
|
result, err := SyncWAFIPGroups(c.Request.Context(), input)
|
|
if err != nil {
|
|
compat.Fail(c, err.Error())
|
|
return
|
|
}
|
|
compat.OK(c, result)
|
|
}
|
|
|
|
// ReportApplyLogHandler records an agent apply log entry.
|
|
func ReportApplyLogHandler(c *gin.Context) {
|
|
var payload ApplyLogPayload
|
|
if !compat.BindJSON(c, &payload) {
|
|
return
|
|
}
|
|
if authNode, ok := AgentNodeFromContext(c); ok {
|
|
payload.NodeID = authNode.NodeID
|
|
}
|
|
log, err := ReportApplyLog(c.Request.Context(), payload)
|
|
if err != nil {
|
|
compat.Fail(c, err.Error())
|
|
return
|
|
}
|
|
compat.OK(c, log)
|
|
}
|
|
|
|
// DownloadPagesPackageHandler streams the Pages deployment artifact to an authenticated agent.
|
|
func DownloadPagesPackageHandler(c *gin.Context) {
|
|
deploymentID, ok := pagesDeploymentIDParam(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
filePath, fileName, err := pages.GetDeploymentPackagePath(c.Request.Context(), deploymentID)
|
|
if err != nil {
|
|
compat.Fail(c, err.Error())
|
|
return
|
|
}
|
|
c.Header("Content-Disposition", "attachment; filename="+fileName)
|
|
c.File(filePath)
|
|
}
|
|
|
|
func pagesDeploymentIDParam(c *gin.Context) (uint, bool) {
|
|
raw := c.Param("deployment_id")
|
|
if raw == "" {
|
|
compat.Fail(c, "无效的 ID")
|
|
return 0, false
|
|
}
|
|
id64, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil || id64 == 0 {
|
|
compat.Fail(c, "无效的 ID")
|
|
return 0, false
|
|
}
|
|
return uint(id64), true
|
|
}
|
|
|
|
// AgentWebSocketHandler upgrades an authenticated agent websocket connection.
|
|
func AgentWebSocketHandler(c *gin.Context) {
|
|
authNode, ok := AgentNodeFromContext(c)
|
|
if !ok {
|
|
compat.Unauthorized(c, errInvalidAgentToken)
|
|
return
|
|
}
|
|
websocket.ServeAgent(c, authNode.NodeID, HandleWSStatus)
|
|
}
|
|
|
|
func okWithExtras(c *gin.Context, data any, extras gin.H) {
|
|
payload := gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": data,
|
|
}
|
|
for key, value := range extras {
|
|
payload[key] = value
|
|
}
|
|
c.JSON(http.StatusOK, payload)
|
|
}
|