mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
refactor(backend): rename OpenFlare directory to lowercase openflare
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package protocol defines the communication protocol between OpenFlare server, agent, and relay components.
|
||||
package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// APIResponse is a generic API response wrapper.
|
||||
type APIResponse[T any] struct {
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
// HeartbeatData is the heartbeat request payload from agent.
|
||||
type HeartbeatData struct {
|
||||
AgentSettings *AgentSettings `json:"agent_settings"`
|
||||
ActiveConfig *ActiveConfigMeta `json:"active_config"`
|
||||
WAFIPGroups []WAFIPGroup `json:"waf_ip_groups,omitempty"`
|
||||
}
|
||||
|
||||
// HeartbeatResult is the heartbeat response payload.
|
||||
type HeartbeatResult struct {
|
||||
AgentSettings *AgentSettings
|
||||
ActiveConfig *ActiveConfigMeta
|
||||
WAFIPGroups []WAFIPGroup
|
||||
}
|
||||
|
||||
// AgentSettings holds agent configuration settings.
|
||||
type AgentSettings struct {
|
||||
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||
WebsocketUpgradeEnabled bool `json:"websocket_upgrade_enabled"`
|
||||
AutoUpdate bool `json:"auto_update"`
|
||||
UpdateRepo string `json:"update_repo"`
|
||||
UpdateNow bool `json:"update_now"`
|
||||
UpdateChannel string `json:"update_channel"`
|
||||
UpdateTag string `json:"update_tag"`
|
||||
RestartOpenrestyNow bool `json:"restart_openresty_now"`
|
||||
}
|
||||
|
||||
// WSMessageType constants define WebSocket message types.
|
||||
const (
|
||||
WSMessageTypeStatus = "status"
|
||||
WSMessageTypeSettings = "settings"
|
||||
WSMessageTypeActiveConfig = "active_config"
|
||||
WSMessageTypeForceSyncConfig = "force_sync_config"
|
||||
WSMessageTypeWAFIPGroups = "waf_ip_groups"
|
||||
WSMessageTypePing = "ping"
|
||||
WSMessageTypePong = "pong"
|
||||
)
|
||||
|
||||
// WSMessage represents a WebSocket message.
|
||||
type WSMessage struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// WSOutboundMessage represents an outbound WebSocket message.
|
||||
type WSOutboundMessage struct {
|
||||
Type string `json:"type"`
|
||||
Payload any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// WebSocketConnection defines the WebSocket connection interface.
|
||||
type WebSocketConnection interface {
|
||||
URL() string
|
||||
SendStatus(payload NodePayload) error
|
||||
SendPong() error
|
||||
Receive() (WSMessage, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// OpenrestyStatus constants define OpenResty health status values.
|
||||
const (
|
||||
OpenrestyStatusHealthy = "healthy"
|
||||
OpenrestyStatusUnhealthy = "unhealthy"
|
||||
OpenrestyStatusUnknown = "unknown"
|
||||
)
|
||||
|
||||
// NodePayload is the agent node registration / heartbeat payload.
|
||||
// schema_version 2: host_metrics + edge_health + access_logs facts only (no business pre-aggregation).
|
||||
// Agents are destroy/rebuild upgraded; no wire-level compatibility aliases.
|
||||
type NodePayload struct {
|
||||
SchemaVersion int `json:"schema_version,omitempty"`
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Version string `json:"version"`
|
||||
ExtVersion string `json:"ext_version"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
LastError string `json:"last_error"`
|
||||
OpenrestyStatus string `json:"openresty_status"`
|
||||
OpenrestyMessage string `json:"openresty_message"`
|
||||
Profile *NodeSystemProfile `json:"profile,omitempty"`
|
||||
HostMetrics *NodeMetricSnapshot `json:"host_metrics,omitempty"`
|
||||
EdgeHealth *NodeEdgeHealth `json:"edge_health,omitempty"`
|
||||
AccessLogs []NodeAccessLog `json:"access_logs,omitempty"`
|
||||
Buffered []BufferedObservabilityRecord `json:"buffered,omitempty"`
|
||||
HealthEvents []NodeHealthEvent `json:"health_events"`
|
||||
WAFIPGroupChecksums map[string]string `json:"waf_ip_group_checksums,omitempty"`
|
||||
}
|
||||
|
||||
// NodeEdgeHealth is an instantaneous OpenResty health snapshot (L2).
|
||||
type NodeEdgeHealth struct {
|
||||
CapturedAtUnix int64 `json:"captured_at_unix"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Connections int64 `json:"connections"`
|
||||
}
|
||||
|
||||
// NodeSystemProfile describes the system profile of a node.
|
||||
type NodeSystemProfile struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OSName string `json:"os_name"`
|
||||
OSVersion string `json:"os_version"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
Architecture string `json:"architecture"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
TotalMemoryBytes int64 `json:"total_memory_bytes"`
|
||||
TotalDiskBytes int64 `json:"total_disk_bytes"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
ReportedAtUnix int64 `json:"reported_at_unix"`
|
||||
}
|
||||
|
||||
// NodeMetricSnapshot is a metric snapshot of a node.
|
||||
type NodeMetricSnapshot struct {
|
||||
CapturedAtUnix int64 `json:"captured_at_unix"`
|
||||
CPUUsagePercent float64 `json:"cpu_usage_percent"`
|
||||
MemoryUsedBytes int64 `json:"memory_used_bytes"`
|
||||
MemoryTotalBytes int64 `json:"memory_total_bytes"`
|
||||
StorageUsedBytes int64 `json:"storage_used_bytes"`
|
||||
StorageTotalBytes int64 `json:"storage_total_bytes"`
|
||||
DiskReadBytes int64 `json:"disk_read_bytes"`
|
||||
DiskWriteBytes int64 `json:"disk_write_bytes"`
|
||||
}
|
||||
|
||||
// NodeAccessLog is an access log entry from agent (L1 business fact).
|
||||
type NodeAccessLog struct {
|
||||
LoggedAtUnix int64 `json:"logged_at_unix"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
CacheStatus string `json:"cache_status,omitempty"` // $upstream_cache_status
|
||||
StatusCode int `json:"status_code"`
|
||||
BytesSent int64 `json:"bytes_sent"` // body bytes = 已提供数据
|
||||
RequestLength int64 `json:"request_length"` // 接收数据
|
||||
RequestTimeMs int64 `json:"request_time_ms"` // optional
|
||||
}
|
||||
|
||||
// BufferedObservabilityRecord is a buffered observability record (facts only).
|
||||
type BufferedObservabilityRecord struct {
|
||||
CapturedAtUnix int64 `json:"captured_at_unix,omitempty"`
|
||||
HostMetrics *NodeMetricSnapshot `json:"host_metrics,omitempty"`
|
||||
EdgeHealth *NodeEdgeHealth `json:"edge_health,omitempty"`
|
||||
AccessLogs []NodeAccessLog `json:"access_logs,omitempty"`
|
||||
}
|
||||
|
||||
// NodeHealthEvent represents a node health event.
|
||||
type NodeHealthEvent struct {
|
||||
EventType string `json:"event_type"`
|
||||
Severity string `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
TriggeredAtUnix int64 `json:"triggered_at_unix"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterNodeResponse is the node registration response.
|
||||
type RegisterNodeResponse struct {
|
||||
NodeID string `json:"node_id"`
|
||||
AccessToken string `json:"agent_token"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ActiveConfigResponse is the active configuration response.
|
||||
type ActiveConfigResponse struct {
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
SourceConfigJSON string `json:"source_config_json"`
|
||||
SupportFiles []SupportFile `json:"support_files"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// WAFIPGroup defines a WAF IP group.
|
||||
type WAFIPGroup struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IPList []string `json:"ip_list"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
// WAFIPGroupSyncRequest is a WAF IP group sync request.
|
||||
type WAFIPGroupSyncRequest struct {
|
||||
IDs []uint `json:"ids,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
}
|
||||
|
||||
// WAFIPGroupSyncResponse is a WAF IP group sync response.
|
||||
type WAFIPGroupSyncResponse struct {
|
||||
Groups []WAFIPGroup `json:"groups"`
|
||||
}
|
||||
|
||||
// SupportFile represents a support file for relay.
|
||||
type SupportFile struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// PagesDeploymentHashResponse is the upload SHA-256 hash for a Pages deployment package.
|
||||
type PagesDeploymentHashResponse struct {
|
||||
DeploymentID uint `json:"deployment_id"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// PagesProjectLatestHashResponse is the hash of a project's currently active Pages deployment.
|
||||
// Agents poll this like a "latest" pointer without caring about historical deployment IDs.
|
||||
type PagesProjectLatestHashResponse struct {
|
||||
ProjectID uint `json:"project_id"`
|
||||
DeploymentID uint `json:"deployment_id"`
|
||||
Hash string `json:"hash"`
|
||||
PackageSize int64 `json:"package_size"`
|
||||
FileCount int `json:"file_count"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAgentProtocolJSONTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
value any
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "NodePayload",
|
||||
value: NodePayload{},
|
||||
expected: map[string]string{
|
||||
"NodeID": "node_id",
|
||||
"Name": "name",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AgentSettings",
|
||||
value: AgentSettings{},
|
||||
expected: map[string]string{
|
||||
"HeartbeatInterval": "heartbeat_interval",
|
||||
"WebsocketUpgradeEnabled": "websocket_upgrade_enabled",
|
||||
"RestartOpenrestyNow": "restart_openresty_now",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "WSMessage",
|
||||
value: WSMessage{},
|
||||
expected: map[string]string{
|
||||
"Type": "type",
|
||||
"Payload": "payload,omitempty",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "RegisterNodeResponse",
|
||||
value: RegisterNodeResponse{},
|
||||
expected: map[string]string{
|
||||
"NodeID": "node_id",
|
||||
"AccessToken": "agent_token",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PagesProjectLatestHashResponse",
|
||||
value: PagesProjectLatestHashResponse{},
|
||||
expected: map[string]string{
|
||||
"ProjectID": "project_id",
|
||||
"DeploymentID": "deployment_id",
|
||||
"Hash": "hash",
|
||||
"PackageSize": "package_size",
|
||||
"FileCount": "file_count",
|
||||
"TotalSize": "total_size",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
typ := reflect.TypeOf(tc.value)
|
||||
for field, wantTag := range tc.expected {
|
||||
structField, ok := typ.FieldByName(field)
|
||||
if !ok {
|
||||
t.Fatalf("field %q not found on %s", field, tc.name)
|
||||
}
|
||||
gotTag := structField.Tag.Get("json")
|
||||
if gotTag != wantTag {
|
||||
t.Fatalf("field %q json tag = %q, want %q", field, gotTag, wantTag)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodePayloadJSONRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload := NodePayload{
|
||||
NodeID: "node-1",
|
||||
Name: "edge-a",
|
||||
OpenrestyStatus: OpenrestyStatusHealthy,
|
||||
HealthEvents: []NodeHealthEvent{},
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded NodePayload
|
||||
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if decoded.NodeID != payload.NodeID || decoded.Name != payload.Name {
|
||||
t.Fatalf("round trip mismatch: %+v", decoded)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package protocol
|
||||
|
||||
// AgentNodeSystemProfile is an alias for NodeSystemProfile used by server.
|
||||
type AgentNodeSystemProfile = NodeSystemProfile
|
||||
|
||||
// AgentNodeMetricSnapshot is an alias for NodeMetricSnapshot used by server.
|
||||
type AgentNodeMetricSnapshot = NodeMetricSnapshot
|
||||
|
||||
// AgentNodeHealthEvent is an alias for NodeHealthEvent used by server.
|
||||
type AgentNodeHealthEvent = NodeHealthEvent
|
||||
|
||||
// RelayProxyStat holds relay proxy statistics.
|
||||
type RelayProxyStat struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
ClientVersion string `json:"client_version"`
|
||||
LastStartTime string `json:"last_start_time"`
|
||||
LastCloseTime string `json:"last_close_time"`
|
||||
ClientAddr string `json:"client_addr"`
|
||||
}
|
||||
|
||||
// RelayHeartbeatPayload is the relay heartbeat payload.
|
||||
type RelayHeartbeatPayload struct {
|
||||
Version string `json:"version"`
|
||||
ExtVersion string `json:"frp_version"`
|
||||
RelayStatus string `json:"relay_status"`
|
||||
FrpsConnCount int `json:"frps_connections"`
|
||||
FrpsProxyCount int `json:"frps_proxy_count"`
|
||||
FrpsClientCount int `json:"frps_client_count"`
|
||||
FrpsProxies []RelayProxyStat `json:"frps_proxies,omitempty"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Profile *AgentNodeSystemProfile `json:"profile,omitempty"`
|
||||
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
|
||||
HealthEvents []AgentNodeHealthEvent `json:"health_events,omitempty"`
|
||||
}
|
||||
|
||||
// RelayConfig holds relay configuration.
|
||||
type RelayConfig struct {
|
||||
BindPort int `json:"bind_port"`
|
||||
VhostHTTPPort int `json:"vhost_http_port"`
|
||||
AuthToken string `json:"auth_token"`
|
||||
LogLevel string `json:"log_level"`
|
||||
WebServerEnabled bool `json:"web_server_enabled"`
|
||||
WebServerPort int `json:"web_server_port"`
|
||||
}
|
||||
|
||||
// RelaySettings holds relay runtime settings.
|
||||
type RelaySettings struct {
|
||||
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||
WebsocketUpgradeEnabled bool `json:"websocket_upgrade_enabled"`
|
||||
AutoUpdate bool `json:"auto_update"`
|
||||
UpdateRepo string `json:"update_repo"`
|
||||
UpdateNow bool `json:"update_now"`
|
||||
UpdateChannel string `json:"update_channel"`
|
||||
UpdateTag string `json:"update_tag"`
|
||||
}
|
||||
|
||||
// RelayHeartbeatResponse is the relay heartbeat response.
|
||||
type RelayHeartbeatResponse struct {
|
||||
RelayConfig *RelayConfig `json:"relay_config"`
|
||||
RelaySettings *RelaySettings `json:"relay_settings"`
|
||||
}
|
||||
|
||||
// ActiveConfigMeta holds active configuration metadata.
|
||||
type ActiveConfigMeta struct {
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
// FlaredConnectedRelay describes a connected relay info for flared.
|
||||
type FlaredConnectedRelay struct {
|
||||
RelayNodeID string `json:"relay_node_id"`
|
||||
Status string `json:"status"`
|
||||
ProxyCount int `json:"proxy_count"`
|
||||
}
|
||||
|
||||
// FlaredHeartbeatPayload is the flared heartbeat payload.
|
||||
type FlaredHeartbeatPayload struct {
|
||||
ClientVersion string `json:"client_version"`
|
||||
FrpVersion string `json:"frp_version"`
|
||||
IP string `json:"ip"`
|
||||
TunnelStatus string `json:"tunnel_status"`
|
||||
ConnectedRelays []FlaredConnectedRelay `json:"connected_relays"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
CurrentChecksum string `json:"current_checksum"`
|
||||
}
|
||||
|
||||
// FlaredHeartbeatResponse is the flared heartbeat response.
|
||||
type FlaredHeartbeatResponse struct {
|
||||
ActiveConfig *ActiveConfigMeta `json:"active_config"`
|
||||
TunnelSettings *RelaySettings `json:"tunnel_settings"`
|
||||
}
|
||||
|
||||
// FlaredTunnelConfigResponse is the flared tunnel configuration response.
|
||||
type FlaredTunnelConfigResponse struct {
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
Relays []FlaredRelayInfo `json:"relays"`
|
||||
Proxies []FlaredProxyEntry `json:"proxies"`
|
||||
}
|
||||
|
||||
// FlaredRelayInfo holds flared relay information.
|
||||
type FlaredRelayInfo struct {
|
||||
RelayNodeID string `json:"relay_node_id"`
|
||||
Address string `json:"address"`
|
||||
AuthToken string `json:"auth_token"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
}
|
||||
|
||||
// FlaredProxyEntry represents a flared proxy entry.
|
||||
type FlaredProxyEntry struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
LocalPort int `json:"local_port"`
|
||||
CustomDomains []string `json:"custom_domains"`
|
||||
}
|
||||
|
||||
// ApplyLogPayload is the apply log payload for flared.
|
||||
type ApplyLogPayload struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Version string `json:"version"`
|
||||
Result string `json:"result"`
|
||||
Message string `json:"message"`
|
||||
Checksum string `json:"checksum"`
|
||||
MainConfigChecksum string `json:"main_config_checksum"`
|
||||
RouteConfigChecksum string `json:"route_config_checksum"`
|
||||
SupportFileCount int `json:"support_file_count"`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package protocol
|
||||
|
||||
import "strings"
|
||||
|
||||
// TOMLQuote renders s as a quoted TOML basic string, escaping characters that
|
||||
// would otherwise break the document or allow key injection (quotes,
|
||||
// backslashes, control/newline characters). Use it for every interpolated
|
||||
// value written into frps/frpc TOML configs.
|
||||
func TOMLQuote(s string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\\':
|
||||
b.WriteString(`\\`)
|
||||
case '"':
|
||||
b.WriteString(`\"`)
|
||||
case '\n':
|
||||
b.WriteString(`\n`)
|
||||
case '\r':
|
||||
b.WriteString(`\r`)
|
||||
case '\t':
|
||||
b.WriteString(`\t`)
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package protocol
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTOMLQuote(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`plain`: `"plain"`,
|
||||
`a"b`: `"a\"b"`,
|
||||
`a\b`: `"a\\b"`,
|
||||
"injection\"\n": `"injection\"\n"`,
|
||||
"": `""`,
|
||||
"a\tb": `"a\tb"`,
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := TOMLQuote(in); got != want {
|
||||
t.Errorf("TOMLQuote(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MaxWAFIPGroupSnapshotBytes is the maximum serialized size accepted for the
|
||||
// complete Agent/OpenResty WAF IP group runtime document.
|
||||
const MaxWAFIPGroupSnapshotBytes = 20 << 20
|
||||
|
||||
type wafIPGroupSnapshot struct {
|
||||
Groups map[string]WAFIPGroup `json:"groups"`
|
||||
}
|
||||
|
||||
// MarshalWAFIPGroupSnapshot serializes the exact document written by the
|
||||
// Agent to waf_ip_groups.json.
|
||||
func MarshalWAFIPGroupSnapshot(groups map[string]WAFIPGroup) ([]byte, error) {
|
||||
if groups == nil {
|
||||
groups = map[string]WAFIPGroup{}
|
||||
}
|
||||
return json.Marshal(wafIPGroupSnapshot{Groups: groups})
|
||||
}
|
||||
|
||||
// ValidateWAFIPGroupSnapshotSize rejects a complete runtime document that
|
||||
// cannot be published safely to the OpenResty shared-memory snapshot.
|
||||
func ValidateWAFIPGroupSnapshotSize(groups map[string]WAFIPGroup) error {
|
||||
data, err := MarshalWAFIPGroupSnapshot(groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) > MaxWAFIPGroupSnapshotBytes {
|
||||
return fmt.Errorf("WAF IP 组快照大小 %d 字节超过上限 %d 字节", len(data), MaxWAFIPGroupSnapshotBytes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalWAFIPGroupSnapshotMatchesAgentRuntimeDocument(t *testing.T) {
|
||||
data, err := MarshalWAFIPGroupSnapshot(map[string]WAFIPGroup{
|
||||
"7": {ID: 7, Name: "deny", Type: "manual", Enabled: true, IPList: []string{"192.0.2.7"}, Checksum: "sum"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalWAFIPGroupSnapshot failed: %v", err)
|
||||
}
|
||||
want := `{"groups":{"7":{"id":7,"name":"deny","type":"manual","enabled":true,"ip_list":["192.0.2.7"],"checksum":"sum"}}}`
|
||||
if string(data) != want {
|
||||
t.Fatalf("snapshot = %s, want %s", data, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWAFIPGroupSnapshotSizeBoundary(t *testing.T) {
|
||||
groups := map[string]WAFIPGroup{
|
||||
"1": {ID: 1, Type: "manual", Enabled: true, IPList: []string{"192.0.2.1"}, Checksum: strings.Repeat("a", 64)},
|
||||
}
|
||||
base, err := MarshalWAFIPGroupSnapshot(groups)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal base snapshot: %v", err)
|
||||
}
|
||||
groups["1"] = WAFIPGroup{
|
||||
ID: 1,
|
||||
Name: strings.Repeat("x", MaxWAFIPGroupSnapshotBytes-len(base)),
|
||||
Type: "manual",
|
||||
Enabled: true,
|
||||
IPList: []string{"192.0.2.1"},
|
||||
Checksum: strings.Repeat("a", 64),
|
||||
}
|
||||
atLimit, err := MarshalWAFIPGroupSnapshot(groups)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal boundary snapshot: %v", err)
|
||||
}
|
||||
if len(atLimit) != MaxWAFIPGroupSnapshotBytes {
|
||||
t.Fatalf("boundary snapshot size = %d, want %d", len(atLimit), MaxWAFIPGroupSnapshotBytes)
|
||||
}
|
||||
if err := ValidateWAFIPGroupSnapshotSize(groups); err != nil {
|
||||
t.Fatalf("boundary snapshot rejected: %v", err)
|
||||
}
|
||||
|
||||
group := groups["1"]
|
||||
group.Name += "x"
|
||||
groups["1"] = group
|
||||
if err := ValidateWAFIPGroupSnapshotSize(groups); err == nil {
|
||||
t.Fatal("oversized snapshot was accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user