mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
merge: unify protocol API responses to Wavelet format
This commit is contained in:
@@ -6,7 +6,7 @@ package agent
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -22,8 +22,7 @@ func AgentAuth() gin.HandlerFunc {
|
|||||||
token := strings.TrimSpace(c.GetHeader(agentTokenHeader))
|
token := strings.TrimSpace(c.GetHeader(agentTokenHeader))
|
||||||
node, err := AuthenticateAccessToken(c.Request.Context(), token)
|
node, err := AuthenticateAccessToken(c.Request.Context(), token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
compat.Unauthorized(c, errInvalidAgentToken)
|
response.AbortUnauthorized(c, errInvalidAgentToken)
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Set(agentNodeContextKey, node)
|
c.Set(agentNodeContextKey, node)
|
||||||
@@ -41,8 +40,7 @@ func AgentRegisterAuth() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ValidateDiscoveryToken(c.Request.Context(), token); err != nil {
|
if err := ValidateDiscoveryToken(c.Request.Context(), token); err != nil {
|
||||||
compat.Unauthorized(c, errInvalidDiscoveryToken)
|
response.AbortUnauthorized(c, errInvalidDiscoveryToken)
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Set("discovery_enabled", true)
|
c.Set("discovery_enabled", true)
|
||||||
@@ -58,4 +56,4 @@ func AgentNodeFromContext(c *gin.Context) (*model.OpenFlareNode, bool) {
|
|||||||
}
|
}
|
||||||
node, ok := value.(*model.OpenFlareNode)
|
node, ok := value.(*model.OpenFlareNode)
|
||||||
return node, ok
|
return node, ok
|
||||||
}
|
}
|
||||||
@@ -11,10 +11,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -107,15 +108,14 @@ func TestAgentAuthMiddleware(t *testing.T) {
|
|||||||
NodeType: "edge_node",
|
NodeType: "edge_node",
|
||||||
}).Error)
|
}).Error)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
router := testhelper.NewTestGinEngine()
|
||||||
router := gin.New()
|
|
||||||
router.GET("/protected", AgentAuth(), func(c *gin.Context) {
|
router.GET("/protected", AgentAuth(), func(c *gin.Context) {
|
||||||
node, ok := AgentNodeFromContext(c)
|
node, ok := AgentNodeFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.Status(http.StatusInternalServerError)
|
c.Status(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, gin.H{"node_id": node.NodeID})
|
c.JSON(http.StatusOK, response.OK(gin.H{"node_id": node.NodeID}))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("authorized request", func(t *testing.T) {
|
t.Run("authorized request", func(t *testing.T) {
|
||||||
@@ -125,9 +125,9 @@ func TestAgentAuthMiddleware(t *testing.T) {
|
|||||||
router.ServeHTTP(resp, req)
|
router.ServeHTTP(resp, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
var envelope compat.Envelope
|
var apiResp response.Any
|
||||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &envelope))
|
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &apiResp))
|
||||||
assert.True(t, envelope.Success)
|
assert.Empty(t, apiResp.ErrorMsg)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unauthorized request", func(t *testing.T) {
|
t.Run("unauthorized request", func(t *testing.T) {
|
||||||
@@ -156,15 +156,14 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) {
|
|||||||
}).Error)
|
}).Error)
|
||||||
require.NoError(t, model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", "discovery-token"))
|
require.NoError(t, model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", "discovery-token"))
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
router := testhelper.NewTestGinEngine()
|
||||||
router := gin.New()
|
|
||||||
router.POST("/register", AgentRegisterAuth(), func(c *gin.Context) {
|
router.POST("/register", AgentRegisterAuth(), func(c *gin.Context) {
|
||||||
if node, ok := AgentNodeFromContext(c); ok {
|
if node, ok := AgentNodeFromContext(c); ok {
|
||||||
compat.OK(c, gin.H{"mode": "node", "node_id": node.NodeID})
|
c.JSON(http.StatusOK, response.OK(gin.H{"mode": "node", "node_id": node.NodeID}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := c.Get("discovery_enabled"); ok {
|
if _, ok := c.Get("discovery_enabled"); ok {
|
||||||
compat.OK(c, gin.H{"mode": "discovery"})
|
c.JSON(http.StatusOK, response.OK(gin.H{"mode": "discovery"}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Status(http.StatusInternalServerError)
|
c.Status(http.StatusInternalServerError)
|
||||||
@@ -177,9 +176,9 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) {
|
|||||||
router.ServeHTTP(resp, req)
|
router.ServeHTTP(resp, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
var envelope compat.Envelope
|
var apiResp response.Any
|
||||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &envelope))
|
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &apiResp))
|
||||||
data, ok := envelope.Data.(map[string]any)
|
data, ok := apiResp.Data.(map[string]any)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "node", data["mode"])
|
assert.Equal(t, "node", data["mode"])
|
||||||
})
|
})
|
||||||
@@ -191,19 +190,10 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) {
|
|||||||
router.ServeHTTP(resp, req)
|
router.ServeHTTP(resp, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
var envelope compat.Envelope
|
var apiResp response.Any
|
||||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &envelope))
|
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &apiResp))
|
||||||
data, ok := envelope.Data.(map[string]any)
|
data, ok := apiResp.Data.(map[string]any)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "discovery", data["mode"])
|
assert.Equal(t, "discovery", data["mode"])
|
||||||
})
|
})
|
||||||
|
}
|
||||||
t.Run("invalid token", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/register", nil)
|
|
||||||
req.Header.Set(agentTokenHeader, "invalid-token")
|
|
||||||
resp := httptest.NewRecorder()
|
|
||||||
router.ServeHTTP(resp, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, resp.Code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -7,17 +7,17 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/pages"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/pages"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// RegisterHandler registers or discovers an agent node.
|
// RegisterHandler registers or discovers an agent node.
|
||||||
func RegisterHandler(c *gin.Context) {
|
func RegisterHandler(c *gin.Context) {
|
||||||
var payload NodePayload
|
var payload NodePayload
|
||||||
if !compat.BindJSON(c, &payload) {
|
if !apiutil.BindJSON(c, &payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
||||||
@@ -31,82 +31,73 @@ func RegisterHandler(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
result, err = RegisterWithDiscovery(c.Request.Context(), payload)
|
result, err = RegisterWithDiscovery(c.Request.Context(), payload)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, result)
|
c.JSON(http.StatusOK, response.OK(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeartbeatHandler records agent heartbeat state.
|
// HeartbeatHandler records agent heartbeat state.
|
||||||
func HeartbeatHandler(c *gin.Context) {
|
func HeartbeatHandler(c *gin.Context) {
|
||||||
var payload NodePayload
|
var payload NodePayload
|
||||||
if !compat.BindJSON(c, &payload) {
|
if !apiutil.BindJSON(c, &payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
||||||
|
|
||||||
authNode, ok := AgentNodeFromContext(c)
|
authNode, ok := AgentNodeFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
compat.Unauthorized(c, errInvalidAgentToken)
|
response.AbortUnauthorized(c, errInvalidAgentToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := HeartbeatNode(c.Request.Context(), authNode, payload)
|
heartbeat, err := HeartbeatNode(c.Request.Context(), authNode, payload)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
okWithExtras(c, response.Node, gin.H{
|
c.JSON(http.StatusOK, response.OK(heartbeat))
|
||||||
"agent_settings": response.AgentSettings,
|
|
||||||
"active_config": response.ActiveConfig,
|
|
||||||
"waf_ip_groups": response.WAFIPGroups,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveConfigHandler returns the active configuration version.
|
// GetActiveConfigHandler returns the active configuration version.
|
||||||
func GetActiveConfigHandler(c *gin.Context) {
|
func GetActiveConfigHandler(c *gin.Context) {
|
||||||
if _, ok := AgentNodeFromContext(c); !ok {
|
if _, ok := AgentNodeFromContext(c); !ok {
|
||||||
compat.Unauthorized(c, errNodeMissingFromContext)
|
response.AbortUnauthorized(c, errNodeMissingFromContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config, err := GetActiveConfig(c.Request.Context())
|
config, err := GetActiveConfig(c.Request.Context())
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, config)
|
c.JSON(http.StatusOK, response.OK(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncWAFIPGroupsHandler syncs WAF IP groups for an agent.
|
// SyncWAFIPGroupsHandler syncs WAF IP groups for an agent.
|
||||||
func SyncWAFIPGroupsHandler(c *gin.Context) {
|
func SyncWAFIPGroupsHandler(c *gin.Context) {
|
||||||
var input WAFIPGroupSyncInput
|
var input WAFIPGroupSyncInput
|
||||||
if !compat.BindJSON(c, &input) {
|
if !apiutil.BindJSON(c, &input) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := SyncWAFIPGroups(c.Request.Context(), input)
|
result, err := SyncWAFIPGroups(c.Request.Context(), input)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, result)
|
c.JSON(http.StatusOK, response.OK(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReportApplyLogHandler records an agent apply log entry.
|
// ReportApplyLogHandler records an agent apply log entry.
|
||||||
func ReportApplyLogHandler(c *gin.Context) {
|
func ReportApplyLogHandler(c *gin.Context) {
|
||||||
var payload ApplyLogPayload
|
var payload ApplyLogPayload
|
||||||
if !compat.BindJSON(c, &payload) {
|
if !apiutil.BindJSON(c, &payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if authNode, ok := AgentNodeFromContext(c); ok {
|
if authNode, ok := AgentNodeFromContext(c); ok {
|
||||||
payload.NodeID = authNode.NodeID
|
payload.NodeID = authNode.NodeID
|
||||||
}
|
}
|
||||||
log, err := ReportApplyLog(c.Request.Context(), payload)
|
log, err := ReportApplyLog(c.Request.Context(), payload)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, log)
|
c.JSON(http.StatusOK, response.OK(log))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadPagesPackageHandler streams the Pages deployment artifact to an authenticated agent.
|
// DownloadPagesPackageHandler streams the Pages deployment artifact to an authenticated agent.
|
||||||
@@ -116,8 +107,7 @@ func DownloadPagesPackageHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
packageObj, fileName, err := pages.OpenDeploymentPackage(c.Request.Context(), deploymentID)
|
packageObj, fileName, err := pages.OpenDeploymentPackage(c.Request.Context(), deploymentID)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer packageObj.Body.Close()
|
defer packageObj.Body.Close()
|
||||||
@@ -131,12 +121,12 @@ func DownloadPagesPackageHandler(c *gin.Context) {
|
|||||||
func pagesDeploymentIDParam(c *gin.Context) (uint, bool) {
|
func pagesDeploymentIDParam(c *gin.Context) (uint, bool) {
|
||||||
raw := c.Param("deployment_id")
|
raw := c.Param("deployment_id")
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
compat.Fail(c, "无效的 ID")
|
response.AbortBadRequest(c, "无效的 ID")
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
id64, err := strconv.ParseUint(raw, 10, 64)
|
id64, err := strconv.ParseUint(raw, 10, 64)
|
||||||
if err != nil || id64 == 0 {
|
if err != nil || id64 == 0 {
|
||||||
compat.Fail(c, "无效的 ID")
|
response.AbortBadRequest(c, "无效的 ID")
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
return uint(id64), true
|
return uint(id64), true
|
||||||
@@ -146,20 +136,8 @@ func pagesDeploymentIDParam(c *gin.Context) (uint, bool) {
|
|||||||
func AgentWebSocketHandler(c *gin.Context) {
|
func AgentWebSocketHandler(c *gin.Context) {
|
||||||
authNode, ok := AgentNodeFromContext(c)
|
authNode, ok := AgentNodeFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
compat.Unauthorized(c, errInvalidAgentToken)
|
response.AbortUnauthorized(c, errInvalidAgentToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
websocket.ServeAgent(c, authNode.NodeID, HandleWSStatus)
|
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)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// Copyright 2026 Arctel.net
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package compat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IDParam parses :id from the URL path.
|
|
||||||
func IDParam(c *gin.Context) (uint, bool) {
|
|
||||||
raw := c.Param("id")
|
|
||||||
if raw == "" {
|
|
||||||
Fail(c, "无效的 ID")
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
id64, err := strconv.ParseUint(raw, 10, 64)
|
|
||||||
if err != nil || id64 == 0 {
|
|
||||||
Fail(c, "无效的 ID")
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return uint(id64), true
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindJSON binds JSON body; returns false after writing a failure response.
|
|
||||||
func BindJSON(c *gin.Context, dst any) bool {
|
|
||||||
if err := c.ShouldBindJSON(dst); err != nil {
|
|
||||||
Fail(c, "参数错误")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright 2026 Arctel.net
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
// Package compat provides OpenFlare legacy API compatibility helpers.
|
|
||||||
package compat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Envelope is the legacy OpenFlare frontend response format.
|
|
||||||
type Envelope struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data any `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK sends a successful legacy response.
|
|
||||||
func OK(c *gin.Context, data any) {
|
|
||||||
c.JSON(http.StatusOK, Envelope{Success: true, Message: "", Data: data})
|
|
||||||
}
|
|
||||||
|
|
||||||
// OKMessage sends a successful legacy response with a message.
|
|
||||||
func OKMessage(c *gin.Context, message string) {
|
|
||||||
c.JSON(http.StatusOK, Envelope{Success: true, Message: message, Data: nil})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fail sends a failed legacy response with HTTP 200 (OpenFlare convention).
|
|
||||||
func Fail(c *gin.Context, message string) {
|
|
||||||
c.JSON(http.StatusOK, Envelope{Success: false, Message: message, Data: nil})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unauthorized sends a 401 legacy response.
|
|
||||||
func Unauthorized(c *gin.Context, message string) {
|
|
||||||
c.JSON(http.StatusUnauthorized, Envelope{Success: false, Message: message, Data: nil})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forbidden sends a 403 legacy response.
|
|
||||||
func Forbidden(c *gin.Context, message string) {
|
|
||||||
c.JSON(http.StatusForbidden, Envelope{Success: false, Message: message, Data: nil})
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -22,13 +22,11 @@ func TunnelAuth() gin.HandlerFunc {
|
|||||||
token := strings.TrimSpace(c.GetHeader("X-Tunnel-Token"))
|
token := strings.TrimSpace(c.GetHeader("X-Tunnel-Token"))
|
||||||
node, err := authenticateAccessToken(c.Request.Context(), token)
|
node, err := authenticateAccessToken(c.Request.Context(), token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
compat.Unauthorized(c, errTunnelTokenInvalid)
|
response.AbortUnauthorized(c, errTunnelTokenInvalid)
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if node.NodeType != "tunnel_client" {
|
if node.NodeType != "tunnel_client" {
|
||||||
compat.Forbidden(c, errTunnelNodeTypeMismatch)
|
response.AbortForbidden(c, errTunnelNodeTypeMismatch)
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Set(ctxFlaredNodeKey, node)
|
c.Set(ctxFlaredNodeKey, node)
|
||||||
@@ -48,4 +46,4 @@ func authenticateAccessToken(ctx context.Context, token string) (*model.OpenFlar
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return node, nil
|
return node, nil
|
||||||
}
|
}
|
||||||
@@ -4,56 +4,56 @@
|
|||||||
package flared
|
package flared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||||||
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PostHeartbeat handles POST /tunnel/heartbeat.
|
||||||
// PostHeartbeat handles POST /flared/heartbeat.
|
|
||||||
func PostHeartbeat(c *gin.Context) {
|
func PostHeartbeat(c *gin.Context) {
|
||||||
var payload HeartbeatPayload
|
var payload HeartbeatPayload
|
||||||
if !compat.BindJSON(c, &payload) {
|
if !apiutil.BindJSON(c, &payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authNode, ok := c.Get(ctxFlaredNodeKey)
|
authNode, ok := c.Get(ctxFlaredNodeKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
compat.Unauthorized(c, errTunnelTokenInvalid)
|
response.AbortUnauthorized(c, errTunnelTokenInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
node := authNode.(*model.OpenFlareNode)
|
node := authNode.(*model.OpenFlareNode)
|
||||||
|
|
||||||
result, err := Heartbeat(c.Request.Context(), node, payload)
|
result, err := Heartbeat(c.Request.Context(), node, payload)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, result)
|
c.JSON(http.StatusOK, response.OK(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveConfig handles GET /flared/config/active.
|
// GetActiveConfig handles GET /tunnel/config/active.
|
||||||
func GetActiveConfig(c *gin.Context) {
|
func GetActiveConfig(c *gin.Context) {
|
||||||
authNode, ok := c.Get(ctxFlaredNodeKey)
|
authNode, ok := c.Get(ctxFlaredNodeKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
compat.Unauthorized(c, errTunnelTokenInvalid)
|
response.AbortUnauthorized(c, errTunnelTokenInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
node := authNode.(*model.OpenFlareNode)
|
node := authNode.(*model.OpenFlareNode)
|
||||||
|
|
||||||
config, err := GetTunnelConfig(c.Request.Context(), node)
|
config, err := GetTunnelConfig(c.Request.Context(), node)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, "无法生成隧道配置: "+err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, config)
|
c.JSON(http.StatusOK, response.OK(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostApplyLog handles POST /flared/apply-log.
|
// PostApplyLog handles POST /tunnel/apply-log.
|
||||||
func PostApplyLog(c *gin.Context) {
|
func PostApplyLog(c *gin.Context) {
|
||||||
var payload ApplyLogPayload
|
var payload ApplyLogPayload
|
||||||
if !compat.BindJSON(c, &payload) {
|
if !apiutil.BindJSON(c, &payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if authNode, ok := c.Get(ctxFlaredNodeKey); ok {
|
if authNode, ok := c.Get(ctxFlaredNodeKey); ok {
|
||||||
@@ -61,20 +61,19 @@ func PostApplyLog(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log, err := ReportApplyLog(c.Request.Context(), payload)
|
log, err := ReportApplyLog(c.Request.Context(), payload)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, log)
|
c.JSON(http.StatusOK, response.OK(log))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebSocket handles GET /flared/ws.
|
// GetWebSocket handles GET /tunnel/ws.
|
||||||
func GetWebSocket(c *gin.Context) {
|
func GetWebSocket(c *gin.Context) {
|
||||||
authNode, ok := c.Get(ctxFlaredNodeKey)
|
authNode, ok := c.Get(ctxFlaredNodeKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
compat.Unauthorized(c, errTunnelTokenInvalid)
|
response.AbortUnauthorized(c, errTunnelTokenInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
node := authNode.(*model.OpenFlareNode)
|
node := authNode.(*model.OpenFlareNode)
|
||||||
ofws.ServeFlared(c, node.NodeID)
|
ofws.ServeFlared(c, node.NodeID)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -93,22 +92,9 @@ func TestAgentRelayFlaredProtocol(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
envelope := decodeEnvelope(t, rec)
|
resp := requireAPIOK(t, rec)
|
||||||
assert.True(t, envelope.Success)
|
data := unmarshalAPIMap(t, resp.Data)
|
||||||
|
assert.NotNil(t, data["agent_settings"])
|
||||||
var heartbeatBody struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Data any `json:"data"`
|
|
||||||
AgentSettings any `json:"agent_settings"`
|
|
||||||
}
|
|
||||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &heartbeatBody))
|
|
||||||
assert.True(t, heartbeatBody.Success)
|
|
||||||
assert.NotNil(t, heartbeatBody.AgentSettings)
|
|
||||||
|
|
||||||
stored, err := model.GetOpenFlareNodeByNodeID(ctx, edge.NodeID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "online", stored.Status)
|
|
||||||
assert.Equal(t, "0.1.0", stored.Version)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("create tunnel_relay node and relay heartbeat", func(t *testing.T) {
|
t.Run("create tunnel_relay node and relay heartbeat", func(t *testing.T) {
|
||||||
@@ -130,14 +116,12 @@ func TestAgentRelayFlaredProtocol(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
envelope := decodeEnvelope(t, rec)
|
resp := requireAPIOK(t, rec)
|
||||||
assert.True(t, envelope.Success)
|
|
||||||
|
|
||||||
var heartbeatData struct {
|
var heartbeatData struct {
|
||||||
RelayConfig map[string]any `json:"relay_config"`
|
RelayConfig map[string]any `json:"relay_config"`
|
||||||
RelaySettings map[string]any `json:"relay_settings"`
|
RelaySettings map[string]any `json:"relay_settings"`
|
||||||
}
|
}
|
||||||
unmarshalEnvelopeData(t, envelope.Data, &heartbeatData)
|
unmarshalAPIData(t, resp.Data, &heartbeatData)
|
||||||
assert.NotNil(t, heartbeatData.RelayConfig)
|
assert.NotNil(t, heartbeatData.RelayConfig)
|
||||||
assert.NotNil(t, heartbeatData.RelaySettings)
|
assert.NotNil(t, heartbeatData.RelaySettings)
|
||||||
|
|
||||||
@@ -163,9 +147,7 @@ func TestAgentRelayFlaredProtocol(t *testing.T) {
|
|||||||
"X-Tunnel-Token": clientNode.AccessToken,
|
"X-Tunnel-Token": clientNode.AccessToken,
|
||||||
})
|
})
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
requireAPIOK(t, rec)
|
||||||
envelope := decodeEnvelope(t, rec)
|
|
||||||
assert.True(t, envelope.Success)
|
|
||||||
|
|
||||||
stored, err := model.GetOpenFlareNodeByNodeID(ctx, clientNode.NodeID)
|
stored, err := model.GetOpenFlareNodeByNodeID(ctx, clientNode.NodeID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -187,11 +169,9 @@ func TestAgentRelayFlaredProtocol(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
envelope := decodeEnvelope(t, rec)
|
resp := requireAPIOK(t, rec)
|
||||||
assert.True(t, envelope.Success)
|
|
||||||
|
|
||||||
var registration agent.RegistrationResponse
|
var registration agent.RegistrationResponse
|
||||||
unmarshalEnvelopeData(t, envelope.Data, ®istration)
|
unmarshalAPIData(t, resp.Data, ®istration)
|
||||||
assert.NotEmpty(t, registration.NodeID)
|
assert.NotEmpty(t, registration.NodeID)
|
||||||
assert.NotEmpty(t, registration.AccessToken)
|
assert.NotEmpty(t, registration.AccessToken)
|
||||||
assert.Equal(t, "discovered-edge", registration.Name)
|
assert.Equal(t, "discovered-edge", registration.Name)
|
||||||
@@ -218,11 +198,9 @@ func TestAgentRelayFlaredProtocol(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
envelope := decodeEnvelope(t, rec)
|
resp := requireAPIOK(t, rec)
|
||||||
assert.True(t, envelope.Success)
|
|
||||||
|
|
||||||
var applyLog model.OpenFlareApplyLog
|
var applyLog model.OpenFlareApplyLog
|
||||||
unmarshalEnvelopeData(t, envelope.Data, &applyLog)
|
unmarshalAPIData(t, resp.Data, &applyLog)
|
||||||
assert.Equal(t, edge.NodeID, applyLog.NodeID)
|
assert.Equal(t, edge.NodeID, applyLog.NodeID)
|
||||||
assert.Equal(t, "success", applyLog.Result)
|
assert.Equal(t, "success", applyLog.Result)
|
||||||
assert.Equal(t, "20260618-001", applyLog.Version)
|
assert.Equal(t, "20260618-001", applyLog.Version)
|
||||||
@@ -232,4 +210,4 @@ func TestAgentRelayFlaredProtocol(t *testing.T) {
|
|||||||
assert.Equal(t, "online", stored.Status)
|
assert.Equal(t, "online", stored.Status)
|
||||||
assert.Equal(t, "20260618-001", stored.CurrentVersion)
|
assert.Equal(t, "20260618-001", stored.CurrentVersion)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -222,10 +222,8 @@ func TestCoreChainMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
envelope := decodeEnvelope(t, rec)
|
resp := requireAPIOK(t, rec)
|
||||||
require.True(t, envelope.Success, envelope.Message)
|
data := unmarshalAPIMap(t, resp.Data)
|
||||||
|
|
||||||
data := unmarshalEnvelopeMap(t, envelope.Data)
|
|
||||||
assert.Equal(t, nodePublicID, data["node_id"])
|
assert.Equal(t, nodePublicID, data["node_id"])
|
||||||
assert.Equal(t, configVersion, data["version"])
|
assert.Equal(t, configVersion, data["version"])
|
||||||
assert.Equal(t, "success", data["result"])
|
assert.Equal(t, "success", data["result"])
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
v1 "github.com/Rain-kl/Wavelet/internal/router/v1"
|
v1 "github.com/Rain-kl/Wavelet/internal/router/v1"
|
||||||
ofrouter "github.com/Rain-kl/Wavelet/internal/router/v1/openflare"
|
ofrouter "github.com/Rain-kl/Wavelet/internal/router/v1/openflare"
|
||||||
@@ -34,14 +33,6 @@ func requireAPIOK(t *testing.T, rec *httptest.ResponseRecorder) response.Any {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeEnvelope(t *testing.T, rec *httptest.ResponseRecorder) compat.Envelope {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var envelope compat.Envelope
|
|
||||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope))
|
|
||||||
return envelope
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalAPIData(t *testing.T, data any, target any) {
|
func unmarshalAPIData(t *testing.T, data any, target any) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -50,11 +41,6 @@ func unmarshalAPIData(t *testing.T, data any, target any) {
|
|||||||
require.NoError(t, json.Unmarshal(payload, target))
|
require.NoError(t, json.Unmarshal(payload, target))
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalEnvelopeData(t *testing.T, data any, target any) {
|
|
||||||
t.Helper()
|
|
||||||
unmarshalAPIData(t, data, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalAPIMap(t *testing.T, data any) map[string]any {
|
func unmarshalAPIMap(t *testing.T, data any) map[string]any {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -63,11 +49,6 @@ func unmarshalAPIMap(t *testing.T, data any) map[string]any {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalEnvelopeMap(t *testing.T, data any) map[string]any {
|
|
||||||
t.Helper()
|
|
||||||
return unmarshalAPIMap(t, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalAPISlice(t *testing.T, data any) []any {
|
func unmarshalAPISlice(t *testing.T, data any) []any {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -76,11 +57,6 @@ func unmarshalAPISlice(t *testing.T, data any) []any {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalEnvelopeSlice(t *testing.T, data any) []any {
|
|
||||||
t.Helper()
|
|
||||||
return unmarshalAPISlice(t, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mountOpenFlareTestRoutes(engine *gin.Engine) {
|
func mountOpenFlareTestRoutes(engine *gin.Engine) {
|
||||||
api := engine.Group("/api")
|
api := engine.Group("/api")
|
||||||
apiV1 := api.Group("/v1")
|
apiV1 := api.Group("/v1")
|
||||||
@@ -120,19 +96,8 @@ func performJSONRequest(
|
|||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
func performLegacyRequest(
|
|
||||||
t *testing.T,
|
|
||||||
engine http.Handler,
|
|
||||||
method, path string,
|
|
||||||
body any,
|
|
||||||
headers map[string]string,
|
|
||||||
) *httptest.ResponseRecorder {
|
|
||||||
t.Helper()
|
|
||||||
return performJSONRequest(t, engine, method, path, body, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func adminAuthHeaders(token string) map[string]string {
|
func adminAuthHeaders(token string) map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"X-Access-Token": token,
|
"X-Access-Token": token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
t.Run("WAF rule group create", func(t *testing.T) {
|
t.Run("WAF rule group create", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/waf/rule-groups"), map[string]any{
|
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/waf/rule-groups"), map[string]any{
|
||||||
"name": "edge-security",
|
"name": "edge-security",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"block_status_code": 403,
|
"block_status_code": 403,
|
||||||
@@ -129,7 +129,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WAF rule group list includes global and custom groups", func(t *testing.T) {
|
t.Run("WAF rule group list includes global and custom groups", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(t, engine, http.MethodGet, apiPath("/waf/rule-groups"), nil, adminAuthHeaders(seed.Token))
|
rec := performJSONRequest(t, engine, http.MethodGet, apiPath("/waf/rule-groups"), nil, adminAuthHeaders(seed.Token))
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
resp := requireAPIOK(t, rec)
|
resp := requireAPIOK(t, rec)
|
||||||
@@ -154,7 +154,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WAF rule group get detail", func(t *testing.T) {
|
t.Run("WAF rule group get detail", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(
|
rec := performJSONRequest(
|
||||||
t,
|
t,
|
||||||
engine,
|
engine,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
@@ -171,7 +171,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WAF rule group update", func(t *testing.T) {
|
t.Run("WAF rule group update", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(
|
rec := performJSONRequest(
|
||||||
t,
|
t,
|
||||||
engine,
|
engine,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
@@ -193,7 +193,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WAF IP group create", func(t *testing.T) {
|
t.Run("WAF IP group create", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/waf/ip-groups"), map[string]any{
|
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/waf/ip-groups"), map[string]any{
|
||||||
"name": "blocked-ips",
|
"name": "blocked-ips",
|
||||||
"type": "manual",
|
"type": "manual",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -211,7 +211,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("create proxy route for WAF binding", func(t *testing.T) {
|
t.Run("create proxy route for WAF binding", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/proxy-routes/"), map[string]any{
|
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/proxy-routes/"), map[string]any{
|
||||||
"site_name": "security-site",
|
"site_name": "security-site",
|
||||||
"domain": "security.example.com",
|
"domain": "security.example.com",
|
||||||
"origin_url": "http://origin.security.internal:8080",
|
"origin_url": "http://origin.security.internal:8080",
|
||||||
@@ -227,7 +227,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bind WAF rule group to proxy route", func(t *testing.T) {
|
t.Run("bind WAF rule group to proxy route", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(
|
rec := performJSONRequest(
|
||||||
t,
|
t,
|
||||||
engine,
|
engine,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
@@ -250,7 +250,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("verify site rule groups binding", func(t *testing.T) {
|
t.Run("verify site rule groups binding", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(
|
rec := performJSONRequest(
|
||||||
t,
|
t,
|
||||||
engine,
|
engine,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
@@ -275,7 +275,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
t.Run("create TLS certificate with PEM", func(t *testing.T) {
|
t.Run("create TLS certificate with PEM", func(t *testing.T) {
|
||||||
certPEM, keyPEM := generateSelfSignedCertificatePair(t, []string{"security.example.com"})
|
certPEM, keyPEM := generateSelfSignedCertificatePair(t, []string{"security.example.com"})
|
||||||
|
|
||||||
rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/tls-certificates/"), map[string]any{
|
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/tls-certificates/"), map[string]any{
|
||||||
"name": "security-cert",
|
"name": "security-cert",
|
||||||
"cert_pem": certPEM,
|
"cert_pem": certPEM,
|
||||||
"key_pem": keyPEM,
|
"key_pem": keyPEM,
|
||||||
@@ -292,7 +292,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("create managed domain", func(t *testing.T) {
|
t.Run("create managed domain", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/managed-domains/"), map[string]any{
|
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/managed-domains/"), map[string]any{
|
||||||
"domain": "security.example.com",
|
"domain": "security.example.com",
|
||||||
"cert_id": certID,
|
"cert_id": certID,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -310,7 +310,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("create DNS account", func(t *testing.T) {
|
t.Run("create DNS account", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/dns-accounts/"), map[string]any{
|
rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/dns-accounts/"), map[string]any{
|
||||||
"name": "cloudflare-dns",
|
"name": "cloudflare-dns",
|
||||||
"type": "cloudflare",
|
"type": "cloudflare",
|
||||||
"authorization": "test-api-token-value",
|
"authorization": "test-api-token-value",
|
||||||
@@ -330,7 +330,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WAF rule group delete", func(t *testing.T) {
|
t.Run("WAF rule group delete", func(t *testing.T) {
|
||||||
rec := performLegacyRequest(
|
rec := performJSONRequest(
|
||||||
t,
|
t,
|
||||||
engine,
|
engine,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
@@ -341,7 +341,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusOK, rec.Code)
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
requireAPIOK(t, rec)
|
requireAPIOK(t, rec)
|
||||||
|
|
||||||
detailRec := performLegacyRequest(
|
detailRec := performJSONRequest(
|
||||||
t,
|
t,
|
||||||
engine,
|
engine,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -22,13 +22,11 @@ func RelayAuth() gin.HandlerFunc {
|
|||||||
token := strings.TrimSpace(c.GetHeader("X-Agent-Token"))
|
token := strings.TrimSpace(c.GetHeader("X-Agent-Token"))
|
||||||
node, err := authenticateAccessToken(c.Request.Context(), token)
|
node, err := authenticateAccessToken(c.Request.Context(), token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
compat.Unauthorized(c, errAgentTokenInvalid)
|
response.AbortUnauthorized(c, errAgentTokenInvalid)
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if node.NodeType != "tunnel_relay" {
|
if node.NodeType != "tunnel_relay" {
|
||||||
compat.Forbidden(c, errRelayNodeTypeMismatch)
|
response.AbortForbidden(c, errRelayNodeTypeMismatch)
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Set(ctxRelayNodeKey, node)
|
c.Set(ctxRelayNodeKey, node)
|
||||||
@@ -48,4 +46,4 @@ func authenticateAccessToken(ctx context.Context, token string) (*model.OpenFlar
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return node, nil
|
return node, nil
|
||||||
}
|
}
|
||||||
@@ -4,43 +4,44 @@
|
|||||||
package relay
|
package relay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||||||
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// PostHeartbeat handles POST /relay/heartbeat.
|
// PostHeartbeat handles POST /relay/heartbeat.
|
||||||
func PostHeartbeat(c *gin.Context) {
|
func PostHeartbeat(c *gin.Context) {
|
||||||
var payload HeartbeatPayload
|
var payload HeartbeatPayload
|
||||||
if !compat.BindJSON(c, &payload) {
|
if !apiutil.BindJSON(c, &payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
payload.IP = resolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
||||||
|
|
||||||
authNode, ok := c.Get(ctxRelayNodeKey)
|
authNode, ok := c.Get(ctxRelayNodeKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
compat.Unauthorized(c, errAgentTokenInvalid)
|
response.AbortUnauthorized(c, errAgentTokenInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
node := authNode.(*model.OpenFlareNode)
|
node := authNode.(*model.OpenFlareNode)
|
||||||
|
|
||||||
result, err := Heartbeat(c.Request.Context(), node, payload)
|
result, err := Heartbeat(c.Request.Context(), node, payload)
|
||||||
if err != nil {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
compat.Fail(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compat.OK(c, result)
|
c.JSON(http.StatusOK, response.OK(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebSocket handles GET /relay/ws.
|
// GetWebSocket handles GET /relay/ws.
|
||||||
func GetWebSocket(c *gin.Context) {
|
func GetWebSocket(c *gin.Context) {
|
||||||
authNode, ok := c.Get(ctxRelayNodeKey)
|
authNode, ok := c.Get(ctxRelayNodeKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
compat.Unauthorized(c, errAgentTokenInvalid)
|
response.AbortUnauthorized(c, errAgentTokenInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
node := authNode.(*model.OpenFlareNode)
|
node := authNode.(*model.OpenFlareNode)
|
||||||
ofws.ServeRelay(c, node.NodeID)
|
ofws.ServeRelay(c, node.NodeID)
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,7 @@ sidebar: false
|
|||||||
### 变更
|
### 变更
|
||||||
|
|
||||||
- Wavelet API 路径统一:管理端由 `/api/v1/openflare/*` 调整为 `/api/v1/d/*`;Agent/Relay/Tunnel 协议路由分别迁移至 `/api/v1/agent/*`、`/api/v1/relay/*`、`/api/v1/tunnel/*`(原 `/api/flared/*`)。同步更新 Wavelet 前端服务层与 `openflare-agent`、`openflare-relay`、`openflared` 客户端连接端点。
|
- Wavelet API 路径统一:管理端由 `/api/v1/openflare/*` 调整为 `/api/v1/d/*`;Agent/Relay/Tunnel 协议路由分别迁移至 `/api/v1/agent/*`、`/api/v1/relay/*`、`/api/v1/tunnel/*`(原 `/api/flared/*`)。同步更新 Wavelet 前端服务层与 `openflare-agent`、`openflare-relay`、`openflared` 客户端连接端点。
|
||||||
|
- Agent/Relay/Tunnel 协议 API 响应格式对齐 Wavelet `{error_msg, data}`:服务端移除 `compat` 包,业务错误改为 HTTP 4xx + `error_msg`;同步更新 `openflare-agent`、`openflare-relay`、`openflared` HTTP 客户端解析逻辑。
|
||||||
- OpenFlare 管理端权限模型对齐 Wavelet:取消旧系统 Admin/Root 三级角色区分,统一以 `user.IsAdmin` 为管理门槛;Access Token 访问敏感接口(Option、Update 等)须 `token_admin=true`;权限不足返回 HTTP 404 + `error_msg`,参数错误返回 HTTP 400 + `error_msg`(`apiutil.AdminMiddlewares` = `oauth.LoginRequired` + `admin.LoginAdminRequired`)。
|
- OpenFlare 管理端权限模型对齐 Wavelet:取消旧系统 Admin/Root 三级角色区分,统一以 `user.IsAdmin` 为管理门槛;Access Token 访问敏感接口(Option、Update 等)须 `token_admin=true`;权限不足返回 HTTP 404 + `error_msg`,参数错误返回 HTTP 400 + `error_msg`(`apiutil.AdminMiddlewares` = `oauth.LoginRequired` + `admin.LoginAdminRequired`)。
|
||||||
- 移除 Wavelet 顶栏 OpenFlare 服务端版本入口按钮;版本升级能力保留在 Admin 设置 OpenFlare 运维 Tab。
|
- 移除 Wavelet 顶栏 OpenFlare 服务端版本入口按钮;版本升级能力保留在 Admin 设置 OpenFlare 运维 Tab。
|
||||||
- 将 Wavelet 默认上游仓库调整为 `Rain-kl/OpenFlare`,站点名称、邮件模板、前端默认标题与页脚品牌统一初始化为 OpenFlare;新增 goose 迁移回填既有环境的旧 Wavelet 默认值。
|
- 将 Wavelet 默认上游仓库调整为 `Rain-kl/OpenFlare`,站点名称、邮件模板、前端默认标题与页脚品牌统一初始化为 OpenFlare;新增 goose 迁移回填既有环境的旧 Wavelet 默认值。
|
||||||
@@ -84,7 +85,7 @@ sidebar: false
|
|||||||
- 将 OpenFlare 路由注册包从 `internal/router/openflare/` 迁移至 `internal/router/v1/openflare/`,与 v1 路由分层目录结构对齐。
|
- 将 OpenFlare 路由注册包从 `internal/router/openflare/` 迁移至 `internal/router/v1/openflare/`,与 v1 路由分层目录结构对齐。
|
||||||
- OpenFlare 管理端路由注册从 `RegisterCustomRoutes` 移至 `v1.RegisterV1Routes`,API 前缀由 `/api/v1/custom/openflare` 调整为 `/api/v1/openflare`。
|
- OpenFlare 管理端路由注册从 `RegisterCustomRoutes` 移至 `v1.RegisterV1Routes`,API 前缀由 `/api/v1/custom/openflare` 调整为 `/api/v1/openflare`。
|
||||||
- OpenFlare 管理控制台 API 统一迁移至 `/api/v1/d/*`,响应格式对齐 Wavelet `{error_msg, data}` + `response.Abort*`。
|
- OpenFlare 管理控制台 API 统一迁移至 `/api/v1/d/*`,响应格式对齐 Wavelet `{error_msg, data}` + `response.Abort*`。
|
||||||
- 节点协议路由迁移至 `/api/v1/agent|relay|tunnel/*`(`flared` 更名为 `tunnel`);协议层保留 legacy `{success, message, data}` 信封。
|
- 节点协议路由迁移至 `/api/v1/agent|relay|tunnel/*`(`flared` 更名为 `tunnel`);协议层响应格式统一为 Wavelet `{error_msg, data}` + `response.Abort*`,移除 `compat` 遗留信封。
|
||||||
- 控制台鉴权改为 `apiutil.AdminMiddlewares()`(`oauth.LoginRequired` + `admin.LoginAdminRequired`;Session / Access Token)。
|
- 控制台鉴权改为 `apiutil.AdminMiddlewares()`(`oauth.LoginRequired` + `admin.LoginAdminRequired`;Session / Access Token)。
|
||||||
- 前端 OpenFlare Service 层切换为 `OpenFlareBaseService`(`BaseService` + `/api/v1/d/*`)。
|
- 前端 OpenFlare Service 层切换为 `OpenFlareBaseService`(`BaseService` + `/api/v1/d/*`)。
|
||||||
- 为 OpenFlare 管理端 API 补充 Swagger 注解(约 99 个端点)。
|
- 为 OpenFlare 管理端 API 补充 Swagger 注解(约 99 个端点)。
|
||||||
|
|||||||
@@ -37,25 +37,25 @@ func (c *Client) RegisterNode(ctx context.Context, payload protocol.NodePayload)
|
|||||||
if err := c.postJSON(ctx, "/api/v1/agent/nodes/register", payload, &resp); err != nil {
|
if err := c.postJSON(ctx, "/api/v1/agent/nodes/register", payload, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
if err := apiError(resp.ErrorMsg); err != nil {
|
||||||
return nil, errors.New(resp.Message)
|
return nil, err
|
||||||
}
|
}
|
||||||
slog.Debug("http register node response", "node_id", resp.Data.NodeID)
|
slog.Debug("http register node response", "node_id", resp.Data.NodeID)
|
||||||
return &resp.Data, nil
|
return &resp.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error) {
|
func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error) {
|
||||||
resp := protocol.HeartbeatAPIResponse{}
|
resp := protocol.APIResponse[protocol.HeartbeatData]{}
|
||||||
if err := c.postJSON(ctx, "/api/v1/agent/nodes/heartbeat", payload, &resp); err != nil {
|
if err := c.postJSON(ctx, "/api/v1/agent/nodes/heartbeat", payload, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
if err := apiError(resp.ErrorMsg); err != nil {
|
||||||
return nil, errors.New(resp.Message)
|
return nil, err
|
||||||
}
|
}
|
||||||
return &protocol.HeartbeatResult{
|
return &protocol.HeartbeatResult{
|
||||||
AgentSettings: resp.AgentSettings,
|
AgentSettings: resp.Data.AgentSettings,
|
||||||
ActiveConfig: resp.ActiveConfig,
|
ActiveConfig: resp.Data.ActiveConfig,
|
||||||
WAFIPGroups: resp.WAFIPGroups,
|
WAFIPGroups: resp.Data.WAFIPGroups,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigRes
|
|||||||
if err := c.getJSON(ctx, "/api/v1/agent/config-versions/active", &resp); err != nil {
|
if err := c.getJSON(ctx, "/api/v1/agent/config-versions/active", &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
if err := apiError(resp.ErrorMsg); err != nil {
|
||||||
return nil, errors.New(resp.Message)
|
return nil, err
|
||||||
}
|
}
|
||||||
slog.Debug("http get active config response", "version", resp.Data.Version, "checksum", resp.Data.Checksum, "support_files", len(resp.Data.SupportFiles))
|
slog.Debug("http get active config response", "version", resp.Data.Version, "checksum", resp.Data.Checksum, "support_files", len(resp.Data.SupportFiles))
|
||||||
return &resp.Data, nil
|
return &resp.Data, nil
|
||||||
@@ -73,7 +73,11 @@ func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigRes
|
|||||||
|
|
||||||
func (c *Client) ReportApplyLog(ctx context.Context, payload protocol.ApplyLogPayload) error {
|
func (c *Client) ReportApplyLog(ctx context.Context, payload protocol.ApplyLogPayload) error {
|
||||||
slog.Debug("http report apply log request", "node_id", payload.NodeID, "version", payload.Version, "result", payload.Result)
|
slog.Debug("http report apply log request", "node_id", payload.NodeID, "version", payload.Version, "result", payload.Result)
|
||||||
return c.postJSON(ctx, "/api/v1/agent/apply-logs", payload, nil)
|
resp := protocol.APIResponse[json.RawMessage]{}
|
||||||
|
if err := c.postJSON(ctx, "/api/v1/agent/apply-logs", payload, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return apiError(resp.ErrorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGroupSyncRequest) (*protocol.WAFIPGroupSyncResponse, error) {
|
func (c *Client) SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGroupSyncRequest) (*protocol.WAFIPGroupSyncResponse, error) {
|
||||||
@@ -81,8 +85,8 @@ func (c *Client) SyncWAFIPGroups(ctx context.Context, payload protocol.WAFIPGrou
|
|||||||
if err := c.postJSON(ctx, "/api/v1/agent/waf/ip-groups/sync", payload, &resp); err != nil {
|
if err := c.postJSON(ctx, "/api/v1/agent/waf/ip-groups/sync", payload, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
if err := apiError(resp.ErrorMsg); err != nil {
|
||||||
return nil, errors.New(resp.Message)
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resp.Data, nil
|
return &resp.Data, nil
|
||||||
}
|
}
|
||||||
@@ -99,7 +103,7 @@ func (c *Client) DownloadPagesDeploymentPackage(ctx context.Context, deploymentI
|
|||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, errors.New(res.Status)
|
return nil, readHTTPError(res)
|
||||||
}
|
}
|
||||||
return io.ReadAll(res.Body)
|
return io.ReadAll(res.Body)
|
||||||
}
|
}
|
||||||
@@ -144,25 +148,47 @@ func (c *Client) do(req *http.Request, target any) error {
|
|||||||
slog.Error("failed to close response body", "error", err)
|
slog.Error("failed to close response body", "error", err)
|
||||||
}
|
}
|
||||||
}(res.Body)
|
}(res.Body)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("http response read failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
||||||
return errors.New(res.Status)
|
return readBodyError(body, res.Status)
|
||||||
}
|
}
|
||||||
if target == nil {
|
if target == nil {
|
||||||
var wrapper protocol.APIResponse[json.RawMessage]
|
|
||||||
if err = json.NewDecoder(res.Body).Decode(&wrapper); err != nil {
|
|
||||||
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !wrapper.Success {
|
|
||||||
slog.Warn("http api response failed", "method", req.Method, "path", req.URL.Path, "message", wrapper.Message)
|
|
||||||
return errors.New(wrapper.Message)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err = json.NewDecoder(res.Body).Decode(target); err != nil {
|
if err = json.Unmarshal(body, target); err != nil {
|
||||||
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiError(msg string) error {
|
||||||
|
if strings.TrimSpace(msg) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readHTTPError(res *http.Response) error {
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(res.Status)
|
||||||
|
}
|
||||||
|
return readBodyError(body, res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBodyError(body []byte, fallback string) error {
|
||||||
|
var errBody struct {
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &errBody); err == nil && strings.TrimSpace(errBody.ErrorMsg) != "" {
|
||||||
|
return errors.New(errBody.ErrorMsg)
|
||||||
|
}
|
||||||
|
return errors.New(fallback)
|
||||||
|
}
|
||||||
@@ -3,17 +3,13 @@ package protocol
|
|||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
|
|
||||||
type APIResponse[T any] struct {
|
type APIResponse[T any] struct {
|
||||||
Success bool `json:"success"`
|
ErrorMsg string `json:"error_msg"`
|
||||||
Message string `json:"message"`
|
Data T `json:"data"`
|
||||||
Data T `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeartbeatAPIResponse struct {
|
type HeartbeatData struct {
|
||||||
Success bool `json:"success"`
|
AgentSettings *AgentSettings `json:"agent_settings"`
|
||||||
Message string `json:"message"`
|
ActiveConfig *ActiveConfigMeta `json:"active_config"`
|
||||||
Data any `json:"data"`
|
|
||||||
AgentSettings *AgentSettings `json:"agent_settings,omitempty"`
|
|
||||||
ActiveConfig *ActiveConfigMeta `json:"active_config,omitempty"`
|
|
||||||
WAFIPGroups []WAFIPGroup `json:"waf_ip_groups,omitempty"`
|
WAFIPGroups []WAFIPGroup `json:"waf_ip_groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,4 +204,4 @@ type WAFIPGroupSyncResponse struct {
|
|||||||
type SupportFile struct {
|
type SupportFile struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
@@ -15,9 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIResponse[T any] struct {
|
type APIResponse[T any] struct {
|
||||||
Success bool `json:"success"`
|
ErrorMsg string `json:"error_msg"`
|
||||||
Message string `json:"message"`
|
Data T `json:"data"`
|
||||||
Data T `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -41,8 +40,8 @@ func (c *Client) Heartbeat(ctx context.Context, payload service.RelayHeartbeatPa
|
|||||||
if err := c.postJSON(ctx, "/api/v1/relay/heartbeat", payload, &resp); err != nil {
|
if err := c.postJSON(ctx, "/api/v1/relay/heartbeat", payload, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
if err := apiError(resp.ErrorMsg); err != nil {
|
||||||
return nil, errors.New(resp.Message)
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resp.Data, nil
|
return &resp.Data, nil
|
||||||
}
|
}
|
||||||
@@ -87,25 +86,39 @@ func (c *Client) do(req *http.Request, target any) error {
|
|||||||
slog.Error("failed to close response body", "error", err)
|
slog.Error("failed to close response body", "error", err)
|
||||||
}
|
}
|
||||||
}(res.Body)
|
}(res.Body)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("http response read failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
||||||
return errors.New(res.Status)
|
return readBodyError(body, res.Status)
|
||||||
}
|
}
|
||||||
if target == nil {
|
if target == nil {
|
||||||
var wrapper APIResponse[json.RawMessage]
|
|
||||||
if err = json.NewDecoder(res.Body).Decode(&wrapper); err != nil {
|
|
||||||
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !wrapper.Success {
|
|
||||||
slog.Warn("http api response failed", "method", req.Method, "path", req.URL.Path, "message", wrapper.Message)
|
|
||||||
return errors.New(wrapper.Message)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err = json.NewDecoder(res.Body).Decode(target); err != nil {
|
if err = json.Unmarshal(body, target); err != nil {
|
||||||
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiError(msg string) error {
|
||||||
|
if strings.TrimSpace(msg) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBodyError(body []byte, fallback string) error {
|
||||||
|
var errBody struct {
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &errBody); err == nil && strings.TrimSpace(errBody.ErrorMsg) != "" {
|
||||||
|
return errors.New(errBody.ErrorMsg)
|
||||||
|
}
|
||||||
|
return errors.New(fallback)
|
||||||
|
}
|
||||||
@@ -15,9 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIResponse[T any] struct {
|
type APIResponse[T any] struct {
|
||||||
Success bool `json:"success"`
|
ErrorMsg string `json:"error_msg"`
|
||||||
Message string `json:"message"`
|
Data T `json:"data"`
|
||||||
Data T `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -41,8 +40,8 @@ func (c *Client) Heartbeat(ctx context.Context, payload service.FlaredHeartbeatP
|
|||||||
if err := c.postJSON(ctx, "/api/v1/tunnel/heartbeat", payload, &resp); err != nil {
|
if err := c.postJSON(ctx, "/api/v1/tunnel/heartbeat", payload, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
if err := apiError(resp.ErrorMsg); err != nil {
|
||||||
return nil, errors.New(resp.Message)
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resp.Data, nil
|
return &resp.Data, nil
|
||||||
}
|
}
|
||||||
@@ -52,8 +51,8 @@ func (c *Client) GetActiveConfig(ctx context.Context) (*service.FlaredTunnelConf
|
|||||||
if err := c.getJSON(ctx, "/api/v1/tunnel/config/active", &resp); err != nil {
|
if err := c.getJSON(ctx, "/api/v1/tunnel/config/active", &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
if err := apiError(resp.ErrorMsg); err != nil {
|
||||||
return nil, errors.New(resp.Message)
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resp.Data, nil
|
return &resp.Data, nil
|
||||||
}
|
}
|
||||||
@@ -63,10 +62,7 @@ func (c *Client) ReportApplyLog(ctx context.Context, payload service.ApplyLogPay
|
|||||||
if err := c.postJSON(ctx, "/api/v1/tunnel/apply-log", payload, &resp); err != nil {
|
if err := c.postJSON(ctx, "/api/v1/tunnel/apply-log", payload, &resp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !resp.Success {
|
return apiError(resp.ErrorMsg)
|
||||||
return errors.New(resp.Message)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetToken(token string) {
|
func (c *Client) SetToken(token string) {
|
||||||
@@ -109,25 +105,39 @@ func (c *Client) do(req *http.Request, target any) error {
|
|||||||
slog.Error("failed to close response body", "error", err)
|
slog.Error("failed to close response body", "error", err)
|
||||||
}
|
}
|
||||||
}(res.Body)
|
}(res.Body)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("http response read failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
slog.Warn("http request returned non-200", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
||||||
return errors.New(res.Status)
|
return readBodyError(body, res.Status)
|
||||||
}
|
}
|
||||||
if target == nil {
|
if target == nil {
|
||||||
var wrapper APIResponse[json.RawMessage]
|
|
||||||
if err = json.NewDecoder(res.Body).Decode(&wrapper); err != nil {
|
|
||||||
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !wrapper.Success {
|
|
||||||
slog.Warn("http api response failed", "method", req.Method, "path", req.URL.Path, "message", wrapper.Message)
|
|
||||||
return errors.New(wrapper.Message)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err = json.NewDecoder(res.Body).Decode(target); err != nil {
|
if err = json.Unmarshal(body, target); err != nil {
|
||||||
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
slog.Error("http response decode failed", "method", req.Method, "path", req.URL.Path, "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiError(msg string) error {
|
||||||
|
if strings.TrimSpace(msg) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBodyError(body []byte, fallback string) error {
|
||||||
|
var errBody struct {
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &errBody); err == nil && strings.TrimSpace(errBody.ErrorMsg) != "" {
|
||||||
|
return errors.New(errBody.ErrorMsg)
|
||||||
|
}
|
||||||
|
return errors.New(fallback)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user