diff --git a/go-backend/internal/http/handler/federation.go b/go-backend/internal/http/handler/federation.go index aa6c33f..2c34c68 100644 --- a/go-backend/internal/http/handler/federation.go +++ b/go-backend/internal/http/handler/federation.go @@ -1277,26 +1277,31 @@ type federationForwardServiceBinding struct { Port int } -func parseFederationForwardServiceBindings(data interface{}) []federationForwardServiceBinding { +func extractFederationServiceEntries(data interface{}) []map[string]interface{} { + if data == nil { + return nil + } + + if entries := asMapSlice(data); len(entries) > 0 { + return entries + } + dataMap, ok := data.(map[string]interface{}) if !ok { return nil } - rawServices, ok := dataMap["services"] - if !ok { - return nil - } - serviceList, ok := rawServices.([]interface{}) - if !ok { - return nil + + if entries := asMapSlice(dataMap["services"]); len(entries) > 0 { + return entries } + return nil +} + +func parseFederationForwardServiceBindings(data interface{}) []federationForwardServiceBinding { + serviceList := extractFederationServiceEntries(data) bindings := make([]federationForwardServiceBinding, 0, len(serviceList)) - for _, item := range serviceList { - svcMap, ok := item.(map[string]interface{}) - if !ok { - continue - } + for _, svcMap := range serviceList { name := normalizeForwardRuntimeServiceName(asString(svcMap["name"])) if name == "" { continue @@ -1365,36 +1370,26 @@ func validateFederationCommandPorts(share *repo.PeerShare, data interface{}) err if share == nil || (share.PortRangeStart <= 0 && share.PortRangeEnd <= 0) { return nil } - dataMap, ok := data.(map[string]interface{}) - if !ok { + + serviceList := extractFederationServiceEntries(data) + if len(serviceList) == 0 { return nil } - - if services, ok := dataMap["services"]; ok { - serviceList, ok := services.([]interface{}) - if !ok { - return fmt.Errorf("invalid services format") + for _, svcMap := range serviceList { + addr := asString(svcMap["addr"]) + if addr == "" { + continue } - for _, svc := range serviceList { - svcMap, ok := svc.(map[string]interface{}) - if !ok { - return fmt.Errorf("invalid service entry format") - } - addr, ok := svcMap["addr"].(string) - if !ok || addr == "" { - continue - } - _, portStr, err := net.SplitHostPort(addr) - if err != nil { - return fmt.Errorf("invalid service address: %s", addr) - } - port, err := strconv.Atoi(portStr) - if err != nil || port <= 0 { - return fmt.Errorf("invalid port in service address: %s", addr) - } - if port < share.PortRangeStart || port > share.PortRangeEnd { - return fmt.Errorf("port %d out of allowed range %d-%d", port, share.PortRangeStart, share.PortRangeEnd) - } + _, portStr, err := net.SplitHostPort(addr) + if err != nil { + return fmt.Errorf("invalid service address: %s", addr) + } + port, err := strconv.Atoi(portStr) + if err != nil || port <= 0 { + return fmt.Errorf("invalid port in service address: %s", addr) + } + if port < share.PortRangeStart || port > share.PortRangeEnd { + return fmt.Errorf("port %d out of allowed range %d-%d", port, share.PortRangeStart, share.PortRangeEnd) } } diff --git a/go-backend/internal/http/handler/federation_share_test.go b/go-backend/internal/http/handler/federation_share_test.go index 55a8360..2071155 100644 --- a/go-backend/internal/http/handler/federation_share_test.go +++ b/go-backend/internal/http/handler/federation_share_test.go @@ -670,6 +670,72 @@ func TestBindPeerShareForwardRuntimeServicesOnlyBindsForwardRole(t *testing.T) { } } +func TestBindPeerShareForwardRuntimeServicesAcceptsTopLevelServiceArray(t *testing.T) { + r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db")) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + t.Cleanup(func() { _ = r.Close() }) + + h := New(r, "test-jwt-secret") + now := time.Now().UnixMilli() + + if err := r.CreatePeerShare(&repo.PeerShare{ + Name: "bind-array-share", + NodeID: 1, + Token: "bind-array-token", + MaxBandwidth: 0, + CurrentFlow: 0, + PortRangeStart: 26100, + PortRangeEnd: 26120, + IsActive: 1, + CreatedTime: now, + UpdatedTime: now, + }); err != nil { + t.Fatalf("create share: %v", err) + } + share, err := r.GetPeerShareByToken("bind-array-token") + if err != nil || share == nil { + t.Fatalf("load share: %v", err) + } + + if err := r.DB().Exec(` + INSERT INTO peer_share_runtime(id, share_id, node_id, reservation_id, resource_key, binding_id, role, chain_name, service_name, protocol, strategy, port, target, applied, status, created_time, updated_time) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + 1, share.ID, share.NodeID, "bind-array-r1", "bind-array-rk1", "", "forward", "", "", "tcp", "fifo", 26101, "", 0, 1, now, now, + ).Error; err != nil { + t.Fatalf("insert runtime: %v", err) + } + + h.bindPeerShareForwardRuntimeServices(share, []interface{}{ + map[string]interface{}{"name": "99_2_10_tcp", "addr": "[::]:26101"}, + map[string]interface{}{"name": "99_2_10_udp", "addr": "[::]:26101"}, + }) + + forwardServiceName := "" + if err := r.DB().Raw(`SELECT service_name FROM peer_share_runtime WHERE id = 1`).Scan(&forwardServiceName).Error; err != nil { + t.Fatalf("load forward runtime service name: %v", err) + } + if forwardServiceName != "99_2_10" { + t.Fatalf("expected forward runtime service name bound from top-level array, got %q", forwardServiceName) + } +} + +func TestValidateFederationCommandPortsAcceptsTopLevelServiceArray(t *testing.T) { + share := &repo.PeerShare{ + PortRangeStart: 26200, + PortRangeEnd: 26210, + } + err := validateFederationCommandPorts(share, []interface{}{ + map[string]interface{}{"name": "11_2_10_tcp", "addr": "[::]:26201"}, + map[string]interface{}{"name": "11_2_10_udp", "addr": "[::]:26201"}, + }) + if err != nil { + t.Fatalf("expected top-level service array to pass port validation, got: %v", err) + } +} + func TestFederationRemoteUsageList(t *testing.T) { r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db")) if err != nil {