mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-11 03:36:37 +08:00
fix(federation): enforce local-only provider share nodes
This commit is contained in:
@@ -132,6 +132,10 @@ func (h *Handler) federationShareCreate(w http.ResponseWriter, r *http.Request)
|
|||||||
response.WriteJSON(w, response.ErrDefault("Node not found"))
|
response.WriteJSON(w, response.ErrDefault("Node not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if node.IsRemote == 1 {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("Only local nodes can be shared"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
token := randomToken(32)
|
token := randomToken(32)
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-backend/internal/http/response"
|
||||||
|
"go-backend/internal/store/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFederationShareCreateRejectsRemoteNode(t *testing.T) {
|
||||||
|
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = repo.Close() })
|
||||||
|
|
||||||
|
h := New(repo, "test-jwt-secret")
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
insertRes, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, "remote-share-node", "remote-share-secret", "10.10.10.1", "10.10.10.1", "", "20000-20010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://peer.example", "peer-token", `{"shareId":1}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert remote node: %v", err)
|
||||||
|
}
|
||||||
|
remoteNodeID, err := insertRes.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get remote node id: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(createPeerShareRequest{
|
||||||
|
Name: "remote-node-share",
|
||||||
|
NodeID: remoteNodeID,
|
||||||
|
MaxBandwidth: 0,
|
||||||
|
ExpiryTime: 0,
|
||||||
|
PortRangeStart: 20000,
|
||||||
|
PortRangeEnd: 20010,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/federation/share/create", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.federationShareCreate(res, req)
|
||||||
|
|
||||||
|
if res.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status %d, got %d", http.StatusOK, res.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload response.R
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Code != -1 {
|
||||||
|
t.Fatalf("expected response code -1, got %d", payload.Code)
|
||||||
|
}
|
||||||
|
if payload.Msg != "Only local nodes can be shared" {
|
||||||
|
t.Fatalf("expected rejection message %q, got %q", "Only local nodes can be shared", payload.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var shareCount int
|
||||||
|
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM peer_share WHERE node_id = ?`, remoteNodeID).Scan(&shareCount); err != nil {
|
||||||
|
t.Fatalf("query peer_share count: %v", err)
|
||||||
|
}
|
||||||
|
if shareCount != 0 {
|
||||||
|
t.Fatalf("expected no share rows for remote node, got %d", shareCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { Tabs, Tab } from "@heroui/tabs";
|
import { Tabs, Tab } from "@heroui/tabs";
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
interface Node {
|
interface Node {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
isRemote?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PeerShare {
|
interface PeerShare {
|
||||||
@@ -63,14 +64,7 @@ export default function PanelSharingPage() {
|
|||||||
token: "",
|
token: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const loadShares = useCallback(async () => {
|
||||||
if (selectedTab === "my-shares") {
|
|
||||||
loadShares();
|
|
||||||
loadNodes();
|
|
||||||
}
|
|
||||||
}, [selectedTab]);
|
|
||||||
|
|
||||||
const loadShares = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getPeerShareList();
|
const res = await getPeerShareList();
|
||||||
@@ -82,30 +76,54 @@ export default function PanelSharingPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const loadNodes = async () => {
|
const loadNodes = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getNodeList();
|
const res = await getNodeList();
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
setNodes(res.data || []);
|
const localNodes: Node[] = (res.data || []).filter(
|
||||||
|
(node: Node) => (node?.isRemote ?? 0) !== 1,
|
||||||
|
);
|
||||||
|
setNodes(localNodes);
|
||||||
|
setShareForm((prev) => {
|
||||||
|
if (!prev.nodeId) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const hasSelectedNode = localNodes.some(
|
||||||
|
(node: Node) => String(node.id) === prev.nodeId,
|
||||||
|
);
|
||||||
|
return hasSelectedNode ? prev : { ...prev, nodeId: "" };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTab === "my-shares") {
|
||||||
|
loadShares();
|
||||||
|
loadNodes();
|
||||||
|
}
|
||||||
|
}, [selectedTab, loadShares, loadNodes]);
|
||||||
|
|
||||||
const handleCreateShare = async () => {
|
const handleCreateShare = async () => {
|
||||||
if (!shareForm.name || !shareForm.nodeId) {
|
if (!shareForm.name || !shareForm.nodeId) {
|
||||||
toast.error("请填写必要信息");
|
toast.error("请填写必要信息");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const nodeId = parseInt(shareForm.nodeId, 10);
|
||||||
|
if (Number.isNaN(nodeId) || !nodes.some((node) => node.id === nodeId)) {
|
||||||
|
toast.error("仅可选择本地节点");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const expiryTime =
|
const expiryTime =
|
||||||
Date.now() + shareForm.expiryDays * 24 * 60 * 60 * 1000;
|
Date.now() + shareForm.expiryDays * 24 * 60 * 60 * 1000;
|
||||||
const res = await createPeerShare({
|
const res = await createPeerShare({
|
||||||
name: shareForm.name,
|
name: shareForm.name,
|
||||||
nodeId: parseInt(shareForm.nodeId),
|
nodeId,
|
||||||
maxBandwidth: shareForm.maxBandwidth * 1024 * 1024 * 1024,
|
maxBandwidth: shareForm.maxBandwidth * 1024 * 1024 * 1024,
|
||||||
expiryTime: shareForm.expiryDays === 0 ? 0 : expiryTime,
|
expiryTime: shareForm.expiryDays === 0 ? 0 : expiryTime,
|
||||||
portRangeStart: shareForm.portRangeStart,
|
portRangeStart: shareForm.portRangeStart,
|
||||||
@@ -249,7 +267,7 @@ export default function PanelSharingPage() {
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="选择节点"
|
label="选择节点"
|
||||||
placeholder="选择要分享的节点"
|
placeholder="选择要分享的本地节点"
|
||||||
selectedKeys={shareForm.nodeId ? [shareForm.nodeId] : []}
|
selectedKeys={shareForm.nodeId ? [shareForm.nodeId] : []}
|
||||||
onChange={(e) => setShareForm({ ...shareForm, nodeId: e.target.value })}
|
onChange={(e) => setShareForm({ ...shareForm, nodeId: e.target.value })}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user