diff --git a/go-backend/internal/http/handler/mutations.go b/go-backend/internal/http/handler/mutations.go index 09781f6..4089766 100644 --- a/go-backend/internal/http/handler/mutations.go +++ b/go-backend/internal/http/handler/mutations.go @@ -1764,6 +1764,18 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) { response.WriteJSON(w, response.Err(-2, err.Error())) return } + if roleID != 0 { + if ipSpeedIDVal, ok := req["ipSpeedId"]; ok && ipSpeedIDVal != nil { + response.WriteJSON(w, response.Err(-1, "普通用户无法设置每 IP 限速规则")) + return + } + } + ipSpeedID := asAnyToInt64Ptr(req["ipSpeedId"]) + ipSpeedID, err = h.normalizeSpeedLimitReference(ipSpeedID) + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } port := asInt(req["inPort"], 0) if port <= 0 { port = h.pickTunnelPort(tunnelID) @@ -1802,9 +1814,13 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) { userName = "user" } maxConn := asInt(req["maxConn"], 0) + ipMaxConn := asInt(req["ipMaxConn"], 0) + if ipMaxConn < 0 { + ipMaxConn = 0 + } proxyProtocol := asInt(req["proxyProtocol"], 0) - forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn, 0, nil, proxyProtocol) + forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn, ipMaxConn, nullableInt(ipSpeedID), proxyProtocol) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return @@ -1903,6 +1919,24 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) { } else if _, ok := req["speedId"]; ok { newSpeedID = sql.NullInt64{Valid: false} } + rawIPSpeedID, hasIPSpeedID := req["ipSpeedId"] + requestedIPSpeedID := asAnyToInt64Ptr(rawIPSpeedID) + if actorRole != 0 && hasIPSpeedID && requestedIPSpeedID != nil && !sameSpeedLimitSelection(forward.IPSpeedID, requestedIPSpeedID) { + response.WriteJSON(w, response.Err(-1, "普通用户无法修改每 IP 限速规则")) + return + } + ipSpeedID := requestedIPSpeedID + ipSpeedID, err = h.normalizeSpeedLimitReference(ipSpeedID) + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + newIPSpeedID := forward.IPSpeedID + if ipSpeedID != nil { + newIPSpeedID = sql.NullInt64{Int64: *ipSpeedID, Valid: true} + } else if _, ok := req["ipSpeedId"]; ok { + newIPSpeedID = sql.NullInt64{Valid: false} + } port := asInt(req["inPort"], 0) if port <= 0 { @@ -1956,9 +1990,13 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) { } now := time.Now().UnixMilli() maxConn := asInt(req["maxConn"], forward.MaxConn) + ipMaxConn := asInt(req["ipMaxConn"], forward.IPMaxConn) + if ipMaxConn < 0 { + ipMaxConn = 0 + } proxyProtocol := asInt(req["proxyProtocol"], forward.ProxyProtocol) - if err := h.repo.UpdateForward(id, name, tunnelID, remoteAddr, strategy, now, newSpeedID, maxConn, forward.IPMaxConn, forward.IPSpeedID, proxyProtocol); err != nil { + if err := h.repo.UpdateForward(id, name, tunnelID, remoteAddr, strategy, now, newSpeedID, maxConn, ipMaxConn, newIPSpeedID, proxyProtocol); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } diff --git a/go-backend/tests/contract/forward_contract_test.go b/go-backend/tests/contract/forward_contract_test.go index 2bf72fc..909bb51 100644 --- a/go-backend/tests/contract/forward_contract_test.go +++ b/go-backend/tests/contract/forward_contract_test.go @@ -1085,6 +1085,70 @@ func jsonNumber(v int64) string { return strconv.FormatInt(v, 10) } +func TestForwardIPSpeedLimitPermission(t *testing.T) { + secret := "contract-jwt-secret" + router, repo := setupContractRouter(t, secret) + now := time.Now().UnixMilli() + + if err := repo.DB().Exec(` + INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status) + VALUES(2, 'normal_user', 'pwd', 1, ?, 99999, 0, 0, 1, 10, ?, ?, 1) + `, now+86400000, now, now).Error; err != nil { + t.Fatalf("insert user: %v", err) + } + if err := repo.DB().Exec(` + INSERT INTO tunnel(id, name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx) + VALUES(12, 'ip-speed-permission-tunnel', 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 0) + `, now, now).Error; err != nil { + t.Fatalf("insert tunnel: %v", err) + } + if err := repo.DB().Exec(` + INSERT INTO node(id, 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) + VALUES(20, 'ip-speed-permission-node', 'ip-speed-permission-secret', '10.22.0.1', '10.22.0.1', '', '32200-32210', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0) + `, now, now).Error; err != nil { + t.Fatalf("insert node: %v", err) + } + if err := repo.DB().Exec(` + INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) + VALUES(12, 1, 20, 32201, 'round', 1, 'tls') + `).Error; err != nil { + t.Fatalf("insert chain_tunnel: %v", err) + } + if err := repo.DB().Exec(` + INSERT INTO speed_limit(id, name, speed, created_time, status) + VALUES(9, 'per-ip-10m', 10, ?, 1) + `, now).Error; err != nil { + t.Fatalf("insert speed limit: %v", err) + } + if err := repo.DB().Exec(` + INSERT INTO user_tunnel(user_id, tunnel_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) + VALUES(2, 12, 10, 99999, 0, 0, 1, ?, 1) + `, now+86400000).Error; err != nil { + t.Fatalf("insert user tunnel: %v", err) + } + + userToken, err := auth.GenerateToken(2, "normal_user", 1, secret) + if err != nil { + t.Fatalf("generate user token: %v", err) + } + body, err := json.Marshal(map[string]interface{}{ + "name": "blocked-ip-speed", + "tunnelId": 12, + "remoteAddr": "1.1.1.1:443", + "strategy": "fifo", + "ipSpeedId": 9, + }) + if err != nil { + t.Fatalf("marshal create payload: %v", err) + } + req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/create", bytes.NewReader(body)) + req.Header.Set("Authorization", userToken) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + router.ServeHTTP(res, req) + assertCodeMsg(t, res, -1, "普通用户无法设置每 IP 限速规则") +} + func TestNonAdminCannotSetSpeedIdOrPort(t *testing.T) { secret := "contract-jwt-secret-perm" router, repo := setupContractRouter(t, secret)