mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-05 17:46:37 +08:00
feat: allow user custom inport with range validation
Entire-Checkpoint: fcd76aac10e9
This commit is contained in:
@@ -1158,13 +1158,6 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.Err(-1, "普通用户无法设置限速规则"))
|
||||
return
|
||||
}
|
||||
if inPortVal, ok := req["inPort"]; ok {
|
||||
port := asInt(inPortVal, 0)
|
||||
if port > 0 {
|
||||
response.WriteJSON(w, response.Err(-1, "普通用户无法设置自定义端口"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
speedID := asAnyToInt64Ptr(req["speedId"])
|
||||
speedID, err = h.normalizeSpeedLimitReference(speedID)
|
||||
@@ -1194,6 +1187,10 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := validateLocalNodePort(node, port); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
inx := h.repo.NextIndex("forward")
|
||||
@@ -1281,13 +1278,6 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.Err(-1, "普通用户无法修改限速规则"))
|
||||
return
|
||||
}
|
||||
if inPortVal, ok := req["inPort"]; ok {
|
||||
port := asInt(inPortVal, 0)
|
||||
if port > 0 {
|
||||
response.WriteJSON(w, response.Err(-1, "普通用户无法修改自定义端口"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
speedID := asAnyToInt64Ptr(req["speedId"])
|
||||
speedID, err = h.normalizeSpeedLimitReference(speedID)
|
||||
@@ -1332,6 +1322,10 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := validateLocalNodePort(node, port); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
if err := h.repo.UpdateForward(id, name, tunnelID, remoteAddr, strategy, now, newSpeedID); err != nil {
|
||||
@@ -3519,3 +3513,68 @@ func asInt64Slice(v interface{}) []int64 {
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func validateLocalNodePort(node *nodeRecord, port int) error {
|
||||
if node == nil || node.IsRemote == 1 || port <= 0 {
|
||||
return nil
|
||||
}
|
||||
portRange := strings.TrimSpace(node.PortRange)
|
||||
if portRange == "" {
|
||||
return nil
|
||||
}
|
||||
minPort, maxPort := parsePortRangeMinMax(portRange)
|
||||
if minPort <= 0 || maxPort <= 0 {
|
||||
return nil
|
||||
}
|
||||
if port < minPort || port > maxPort {
|
||||
return fmt.Errorf("端口 %d 超出节点 %s 允许范围 %d-%d", port, node.Name, minPort, maxPort)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePortRangeMinMax(input string) (int, int) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return 0, 0
|
||||
}
|
||||
minPort, maxPort := 0, 0
|
||||
parts := strings.Split(input, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(part, "-") {
|
||||
r := strings.SplitN(part, "-", 2)
|
||||
if len(r) != 2 {
|
||||
continue
|
||||
}
|
||||
start, err1 := strconv.Atoi(strings.TrimSpace(r[0]))
|
||||
end, err2 := strconv.Atoi(strings.TrimSpace(r[1]))
|
||||
if err1 != nil || err2 != nil || start <= 0 || end <= 0 {
|
||||
continue
|
||||
}
|
||||
if end < start {
|
||||
start, end = end, start
|
||||
}
|
||||
if minPort == 0 || start < minPort {
|
||||
minPort = start
|
||||
}
|
||||
if maxPort == 0 || end > maxPort {
|
||||
maxPort = end
|
||||
}
|
||||
continue
|
||||
}
|
||||
p, err := strconv.Atoi(part)
|
||||
if err != nil || p <= 0 {
|
||||
continue
|
||||
}
|
||||
if minPort == 0 || p < minPort {
|
||||
minPort = p
|
||||
}
|
||||
if maxPort == 0 || p > maxPort {
|
||||
maxPort = p
|
||||
}
|
||||
}
|
||||
return minPort, maxPort
|
||||
}
|
||||
|
||||
@@ -767,9 +767,21 @@ func (r *Repository) ListUserAccessibleTunnels(userID int64) ([]map[string]inter
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tunnelIDs := make([]int64, 0, len(rows))
|
||||
for _, rw := range rows {
|
||||
tunnelIDs = append(tunnelIDs, rw.ID)
|
||||
}
|
||||
portRangeMap := r.getTunnelEntryPortRanges(tunnelIDs)
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
items = append(items, map[string]interface{}{"id": r.ID, "name": r.Name})
|
||||
for _, rw := range rows {
|
||||
item := map[string]interface{}{"id": rw.ID, "name": rw.Name}
|
||||
if pr, ok := portRangeMap[rw.ID]; ok {
|
||||
item["portRangeMin"] = pr.min
|
||||
item["portRangeMax"] = pr.max
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -788,13 +800,146 @@ func (r *Repository) ListEnabledTunnelSummaries() ([]map[string]interface{}, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tunnelIDs := make([]int64, 0, len(rows))
|
||||
for _, rw := range rows {
|
||||
tunnelIDs = append(tunnelIDs, rw.ID)
|
||||
}
|
||||
portRangeMap := r.getTunnelEntryPortRanges(tunnelIDs)
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
items = append(items, map[string]interface{}{"id": r.ID, "name": r.Name})
|
||||
for _, rw := range rows {
|
||||
item := map[string]interface{}{"id": rw.ID, "name": rw.Name}
|
||||
if pr, ok := portRangeMap[rw.ID]; ok {
|
||||
item["portRangeMin"] = pr.min
|
||||
item["portRangeMax"] = pr.max
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
type tunnelPortRange struct {
|
||||
min int
|
||||
max int
|
||||
}
|
||||
|
||||
func (r *Repository) getTunnelEntryPortRanges(tunnelIDs []int64) map[int64]tunnelPortRange {
|
||||
result := make(map[int64]tunnelPortRange)
|
||||
if len(tunnelIDs) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
type entryNode struct {
|
||||
TunnelID int64
|
||||
NodeID int64
|
||||
}
|
||||
var entries []entryNode
|
||||
r.db.Model(&model.ChainTunnel{}).
|
||||
Select("tunnel_id, node_id").
|
||||
Where("tunnel_id IN (?) AND chain_type = ?", tunnelIDs, "1").
|
||||
Find(&entries)
|
||||
|
||||
nodeIDs := make([]int64, 0, len(entries))
|
||||
nodeSet := make(map[int64]struct{})
|
||||
for _, e := range entries {
|
||||
if _, exists := nodeSet[e.NodeID]; !exists {
|
||||
nodeSet[e.NodeID] = struct{}{}
|
||||
nodeIDs = append(nodeIDs, e.NodeID)
|
||||
}
|
||||
}
|
||||
|
||||
type nodePort struct {
|
||||
ID int64
|
||||
Port string
|
||||
}
|
||||
var nodePorts []nodePort
|
||||
if len(nodeIDs) > 0 {
|
||||
r.db.Model(&model.Node{}).Select("id, port").Where("id IN (?)", nodeIDs).Find(&nodePorts)
|
||||
}
|
||||
|
||||
nodePortMap := make(map[int64]string)
|
||||
for _, np := range nodePorts {
|
||||
nodePortMap[np.ID] = np.Port
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
portSpec := nodePortMap[e.NodeID]
|
||||
if portSpec == "" {
|
||||
continue
|
||||
}
|
||||
minP, maxP := parsePortRangeMinMax(portSpec)
|
||||
if minP <= 0 || maxP <= 0 {
|
||||
continue
|
||||
}
|
||||
pr, exists := result[e.TunnelID]
|
||||
if !exists {
|
||||
result[e.TunnelID] = tunnelPortRange{min: minP, max: maxP}
|
||||
} else {
|
||||
if minP < pr.min {
|
||||
pr.min = minP
|
||||
}
|
||||
if maxP > pr.max {
|
||||
pr.max = maxP
|
||||
}
|
||||
result[e.TunnelID] = pr
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parsePortRangeMinMax(input string) (int, int) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return 0, 0
|
||||
}
|
||||
minPort, maxPort := 0, 0
|
||||
parts := strings.Split(input, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(part, "-") {
|
||||
r := strings.SplitN(part, "-", 2)
|
||||
if len(r) != 2 {
|
||||
continue
|
||||
}
|
||||
start, end := parseIntPort(r[0]), parseIntPort(r[1])
|
||||
if start <= 0 || end <= 0 {
|
||||
continue
|
||||
}
|
||||
if end < start {
|
||||
start, end = end, start
|
||||
}
|
||||
if minPort == 0 || start < minPort {
|
||||
minPort = start
|
||||
}
|
||||
if maxPort == 0 || end > maxPort {
|
||||
maxPort = end
|
||||
}
|
||||
continue
|
||||
}
|
||||
p := parseIntPort(part)
|
||||
if p <= 0 {
|
||||
continue
|
||||
}
|
||||
if minPort == 0 || p < minPort {
|
||||
minPort = p
|
||||
}
|
||||
if maxPort == 0 || p > maxPort {
|
||||
maxPort = p
|
||||
}
|
||||
}
|
||||
return minPort, maxPort
|
||||
}
|
||||
|
||||
func parseIntPort(s string) int {
|
||||
var p int
|
||||
fmt.Sscanf(strings.TrimSpace(s), "%d", &p)
|
||||
return p
|
||||
}
|
||||
|
||||
func (r *Repository) ListTunnels() ([]map[string]interface{}, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("repository not initialized")
|
||||
|
||||
@@ -998,9 +998,9 @@ func TestNonAdminCannotSetSpeedIdOrPort(t *testing.T) {
|
||||
assertCodeMsg(t, res, -1, "普通用户无法设置限速规则")
|
||||
})
|
||||
|
||||
t.Run("non-admin cannot set inPort on create", func(t *testing.T) {
|
||||
t.Run("non-admin cannot set inPort out of range on create", func(t *testing.T) {
|
||||
createPayload := map[string]interface{}{
|
||||
"name": "perm-forward-port",
|
||||
"name": "perm-forward-port-out",
|
||||
"tunnelId": tunnelID,
|
||||
"remoteAddr": "1.2.3.4:443",
|
||||
"strategy": "fifo",
|
||||
@@ -1015,7 +1015,33 @@ func TestNonAdminCannotSetSpeedIdOrPort(t *testing.T) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
assertCodeMsg(t, res, -1, "普通用户无法设置自定义端口")
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code >= 0 {
|
||||
t.Errorf("expected port out of range error, got code=%d msg=%s", out.Code, out.Msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-admin can set inPort within range on create", func(t *testing.T) {
|
||||
createPayload := map[string]interface{}{
|
||||
"name": "perm-forward-port-in",
|
||||
"tunnelId": tunnelID,
|
||||
"remoteAddr": "1.2.3.4:443",
|
||||
"strategy": "fifo",
|
||||
"inPort": 30005,
|
||||
}
|
||||
createBody, err := json.Marshal(createPayload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal create payload: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/create", bytes.NewReader(createBody))
|
||||
req.Header.Set("Authorization", userToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("non-admin can create without speedId and inPort", func(t *testing.T) {
|
||||
@@ -1059,7 +1085,7 @@ func TestNonAdminCannotSetSpeedIdOrPort(t *testing.T) {
|
||||
assertCodeMsg(t, res, -1, "普通用户无法修改限速规则")
|
||||
})
|
||||
|
||||
t.Run("non-admin cannot update inPort", func(t *testing.T) {
|
||||
t.Run("non-admin cannot update inPort out of range", func(t *testing.T) {
|
||||
updatePayload := map[string]interface{}{
|
||||
"id": forwardID,
|
||||
"name": "perm-forward-updated2",
|
||||
@@ -1076,7 +1102,33 @@ func TestNonAdminCannotSetSpeedIdOrPort(t *testing.T) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
assertCodeMsg(t, res, -1, "普通用户无法修改自定义端口")
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code >= 0 {
|
||||
t.Errorf("expected port out of range error, got code=%d msg=%s", out.Code, out.Msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-admin can update inPort within range", func(t *testing.T) {
|
||||
updatePayload := map[string]interface{}{
|
||||
"id": forwardID,
|
||||
"name": "perm-forward-updated3",
|
||||
"tunnelId": tunnelID,
|
||||
"remoteAddr": "5.6.7.8:443",
|
||||
"inPort": 30006,
|
||||
}
|
||||
updateBody, err := json.Marshal(updatePayload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal update payload: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/update", bytes.NewReader(updateBody))
|
||||
req.Header.Set("Authorization", userToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("non-admin can update without speedId and inPort", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user