批量删除节点,一键屏蔽ip
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
mqtt "github.com/mochi-mqtt/server/v2"
|
||||
"github.com/mochi-mqtt/server/v2/packets"
|
||||
|
||||
mqttforwardpkg "meshtastic_mqtt_server/internal/mqttforward"
|
||||
storepkg "meshtastic_mqtt_server/internal/store"
|
||||
@@ -11,6 +14,11 @@ import (
|
||||
// 实现一般由 main 包传入(持有真正的 mqtt.Server / 写队列 / 统计器)。
|
||||
type MQTTStatusProvider interface {
|
||||
Status() AdminMQTTStatus
|
||||
// DisconnectClient 立即踢掉指定的 MQTT 客户端。clientID 不存在时返回 false。
|
||||
DisconnectClient(clientID string) bool
|
||||
// LookupClientRemoteHost 根据 clientID 查询当前连接的远端主机(不带端口),
|
||||
// 用于把该 IP 加入屏蔽表。clientID 不存在时返回空字符串与 false。
|
||||
LookupClientRemoteHost(clientID string) (string, bool)
|
||||
}
|
||||
|
||||
// MQTTRuntimeStatus 把 mqtt.Server / 写队列 / 转发统计三个上下文打包成
|
||||
@@ -127,3 +135,37 @@ func mqttClientInfo(c *mqtt.Client) mqttClientInfoView {
|
||||
RemoteAddr: c.Net.Remote,
|
||||
}
|
||||
}
|
||||
|
||||
// DisconnectClient 实现 MQTTStatusProvider:发送 Disconnect 报文并关闭连接。
|
||||
// 使用 ErrAdministrativeAction 作为断开理由,便于日志区分。
|
||||
func (m MQTTRuntimeStatus) DisconnectClient(clientID string) bool {
|
||||
if m.Server == nil || clientID == "" {
|
||||
return false
|
||||
}
|
||||
client, ok := m.Server.Clients.Get(clientID)
|
||||
if !ok || client == nil {
|
||||
return false
|
||||
}
|
||||
_ = m.Server.DisconnectClient(client, packets.ErrAdministrativeAction)
|
||||
return true
|
||||
}
|
||||
|
||||
// LookupClientRemoteHost 实现 MQTTStatusProvider:根据 clientID 查询当前连接的远端 IP。
|
||||
// Net.Remote 通常是 "host:port",这里只返回主机部分;解析失败时回退为整个值。
|
||||
func (m MQTTRuntimeStatus) LookupClientRemoteHost(clientID string) (string, bool) {
|
||||
if m.Server == nil || clientID == "" {
|
||||
return "", false
|
||||
}
|
||||
client, ok := m.Server.Clients.Get(clientID)
|
||||
if !ok || client == nil {
|
||||
return "", false
|
||||
}
|
||||
remote := client.Net.Remote
|
||||
if remote == "" {
|
||||
return "", false
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(remote); err == nil && host != "" {
|
||||
return host, true
|
||||
}
|
||||
return remote, true
|
||||
}
|
||||
|
||||
@@ -249,6 +249,67 @@ func registerAdminRoutes(r gin.IRouter, store *storepkg.Store, sessions *auth.Ma
|
||||
status.MessagesDropped = discardCount
|
||||
c.JSON(http.StatusOK, status)
|
||||
})
|
||||
// 一键断开 MQTT 客户端并把它的远端 IP 加入屏蔽表。reason 由前端传入;写库前先查 IP 避免连接断开后查不到。
|
||||
protected.POST("/mqtt/clients/:client_id/disconnect-and-block", func(c *gin.Context) {
|
||||
if mqttStatus == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "mqtt server not available"})
|
||||
return
|
||||
}
|
||||
clientID := c.Param("client_id")
|
||||
if clientID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid client id"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
// 请求体允许为空——若没传 reason 就走默认占位。
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
reason := strings.TrimSpace(req.Reason)
|
||||
if reason == "" {
|
||||
reason = "manual disconnect from admin dashboard"
|
||||
}
|
||||
|
||||
host, ok := mqttStatus.LookupClientRemoteHost(clientID)
|
||||
if !ok || host == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "mqtt client not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 先写屏蔽规则,再断开连接:万一断开后客户端立刻重连,新规则已经生效。
|
||||
var ipRule *storepkg.IPBlockingRecord
|
||||
row, err := store.CreateIPBlocking(host, reason, true)
|
||||
switch {
|
||||
case err == nil:
|
||||
ipRule = row
|
||||
case errors.Is(err, storepkg.ErrBlockingAlreadyExists):
|
||||
// 已经存在则忽略——只确保是启用状态。
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if blocking != nil {
|
||||
if err := blocking.Reload(store); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
disconnected := mqttStatus.DisconnectClient(clientID)
|
||||
resp := gin.H{
|
||||
"status": "ok",
|
||||
"client_id": clientID,
|
||||
"ip_value": host,
|
||||
"disconnected": disconnected,
|
||||
}
|
||||
if ipRule != nil {
|
||||
resp["ip_rule_id"] = ipRule.ID
|
||||
resp["ip_rule_created"] = true
|
||||
} else {
|
||||
resp["ip_rule_created"] = false
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
protected.GET("/users", func(c *gin.Context) {
|
||||
users, err := store.ListUsers()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user