mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-06 10:06:36 +08:00
Merge pull request #122 from Sagit-chu/claude/stupefied-jemison
fix(backend): enforce port range in federation runtime commands
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -1232,6 +1233,13 @@ func (h *Handler) federationRuntimeCommand(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isFederationServiceCommand(cmd) {
|
||||||
|
if err := validateFederationCommandPorts(share, req.Data); err != nil {
|
||||||
|
response.WriteJSON(w, response.Err(403, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res, err := h.sendNodeCommand(share.NodeID, cmd, req.Data, false, false)
|
res, err := h.sendNodeCommand(share.NodeID, cmd, req.Data, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
@@ -1249,6 +1257,55 @@ func isFederationRuntimeCommandAllowed(commandType string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isFederationServiceCommand(commandType string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(commandType)) {
|
||||||
|
case "addservice", "updateservice":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFederationCommandPorts(share *sqlite.PeerShare, data interface{}) error {
|
||||||
|
if share == nil || (share.PortRangeStart <= 0 && share.PortRangeEnd <= 0) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dataMap, ok := data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
services, ok := dataMap["services"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
serviceList, ok := services.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, svc := range serviceList {
|
||||||
|
svcMap, ok := svc.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, ok := svcMap["addr"].(string)
|
||||||
|
if !ok || addr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil || port <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if port < share.PortRangeStart || port > share.PortRangeEnd {
|
||||||
|
return fmt.Errorf("port %d out of allowed range %d-%d", port, share.PortRangeStart, share.PortRangeEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) pickPeerSharePort(share *sqlite.PeerShare, requestedPort int) (int, error) {
|
func (h *Handler) pickPeerSharePort(share *sqlite.PeerShare, requestedPort int) (int, error) {
|
||||||
if share == nil {
|
if share == nil {
|
||||||
return 0, fmt.Errorf("share not found")
|
return 0, fmt.Errorf("share not found")
|
||||||
|
|||||||
@@ -688,3 +688,112 @@ func valueAsBool(v interface{}) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFederationRuntimeCommandPortRangeEnforcement(t *testing.T) {
|
||||||
|
providerSecret := "provider-portrange-jwt"
|
||||||
|
providerRouter, providerRepo := setupContractRouter(t, providerSecret)
|
||||||
|
providerServer := httptest.NewServer(providerRouter)
|
||||||
|
defer providerServer.Close()
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
providerNodeID := insertContractNode(t, providerRepo, "provider-portrange-node", "198.51.100.50", "44000-44010", "provider-portrange-secret", 1)
|
||||||
|
|
||||||
|
insertPeerShare(t, providerRepo, &sqlite.PeerShare{
|
||||||
|
Name: "portrange-share",
|
||||||
|
NodeID: providerNodeID,
|
||||||
|
Token: "share-portrange-token",
|
||||||
|
PortRangeStart: 44000,
|
||||||
|
PortRangeEnd: 44010,
|
||||||
|
IsActive: 1,
|
||||||
|
CreatedTime: now,
|
||||||
|
UpdatedTime: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
stopNode := startMockNodeSession(t, providerServer.URL, "provider-portrange-secret")
|
||||||
|
defer stopNode()
|
||||||
|
|
||||||
|
sendCommand := func(token string, cmdType string, data interface{}) *httptest.ResponseRecorder {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"commandType": cmdType,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal command payload: %v", err)
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/federation/runtime/command", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
providerRouter.ServeHTTP(res, req)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: AddService with port OUTSIDE allowed range should be rejected
|
||||||
|
outOfRangeData := map[string]interface{}{
|
||||||
|
"services": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"name": "test_service_tcp",
|
||||||
|
"addr": "[::]:55555",
|
||||||
|
"handler": map[string]interface{}{
|
||||||
|
"type": "tcp",
|
||||||
|
},
|
||||||
|
"listener": map[string]interface{}{
|
||||||
|
"type": "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
res := sendCommand("share-portrange-token", "AddService", outOfRangeData)
|
||||||
|
var out response.R
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code != 403 {
|
||||||
|
t.Fatalf("expected code 403 for out-of-range port, got %d (msg: %s)", out.Code, out.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: UpdateService with port OUTSIDE allowed range should be rejected
|
||||||
|
res = sendCommand("share-portrange-token", "UpdateService", outOfRangeData)
|
||||||
|
out = response.R{}
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code != 403 {
|
||||||
|
t.Fatalf("expected code 403 for out-of-range UpdateService, got %d (msg: %s)", out.Code, out.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: AddService with port INSIDE allowed range should succeed
|
||||||
|
inRangeData := map[string]interface{}{
|
||||||
|
"services": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"name": "test_service_ok_tcp",
|
||||||
|
"addr": "[::]:44005",
|
||||||
|
"handler": map[string]interface{}{
|
||||||
|
"type": "tcp",
|
||||||
|
},
|
||||||
|
"listener": map[string]interface{}{
|
||||||
|
"type": "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
res = sendCommand("share-portrange-token", "AddService", inRangeData)
|
||||||
|
out = response.R{}
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code != 0 {
|
||||||
|
t.Fatalf("expected code 0 for in-range port, got %d (msg: %s)", out.Code, out.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: Non-service commands should pass through without port validation
|
||||||
|
res = sendCommand("share-portrange-token", "reload", nil)
|
||||||
|
out = response.R{}
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code != 0 {
|
||||||
|
t.Fatalf("expected code 0 for reload command, got %d (msg: %s)", out.Code, out.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user