diff --git a/README.md b/README.md index 5fad301..ae32a60 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 每条传入的 `PUBLISH` 都会先进入: ```go -valid, _, record := mqtpp.MQTTPP(topic, payload, key) +valid, _, record := mqtpp.MQTTPP(topic, payload, key, mqtpp.Options{}) ``` - `valid == true`:保留原始 topic、payload、QoS、retain 等字段,正常转发给订阅匹配 topic 的客户端 diff --git a/admin_runtime_settings_routes.go b/admin_runtime_settings_routes.go new file mode 100644 index 0000000..efa1c47 --- /dev/null +++ b/admin_runtime_settings_routes.go @@ -0,0 +1,52 @@ +package main + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +const allowEncryptedForwardingLabel = "Allow encrypted MQTT packets to be forwarded when they cannot be decrypted" + +type runtimeSettingsRequest struct { + AllowEncryptedForwarding bool `json:"allow_encrypted_forwarding"` +} + +func registerAdminRuntimeSettingsRoutes(r gin.IRouter, store *store, settings *runtimeSettingsCache) { + r.GET("/runtime-settings", func(c *gin.Context) { + snapshot, err := store.GetRuntimeSettings() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"item": runtimeSettingsDTO(snapshot)}) + }) + + r.PUT("/runtime-settings", func(c *gin.Context) { + var req runtimeSettingsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid runtime settings request"}) + return + } + if _, err := store.SetBoolRuntimeSetting(runtimeSettingAllowEncryptedForwarding, req.AllowEncryptedForwarding, allowEncryptedForwardingLabel); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if settings != nil { + if err := settings.Reload(store); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + snapshot, err := store.GetRuntimeSettings() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"item": runtimeSettingsDTO(snapshot)}) + }) +} + +func runtimeSettingsDTO(settings runtimeSettingsSnapshot) gin.H { + return gin.H{"allow_encrypted_forwarding": settings.AllowEncryptedForwarding} +} diff --git a/db.go b/db.go index 561561a..705e9d5 100644 --- a/db.go +++ b/db.go @@ -102,6 +102,19 @@ func (helpContentRecord) TableName() string { return "help_content" } +type runtimeSettingRecord struct { + Key string `gorm:"column:key;primaryKey;size:128;not null"` + Value string `gorm:"column:value;type:text;not null"` + ValueType string `gorm:"column:value_type;size:32;not null;index"` + Label string `gorm:"column:label"` + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index"` +} + +func (runtimeSettingRecord) TableName() string { + return "runtime_settings" +} + type discardDetailsRecord struct { ID uint64 `gorm:"column:id;primaryKey;autoIncrement"` Topic string `gorm:"column:topic"` @@ -401,6 +414,7 @@ func (s *store) migrate() error { {label: "users", model: &userRecord{}}, {label: "login_log", model: &loginLogRecord{}}, {label: "help_content", model: &helpContentRecord{}}, + {label: "runtime_settings", model: &runtimeSettingRecord{}}, {label: "discard_details", model: &discardDetailsRecord{}}, {label: "node_blocking", model: &nodeBlockingRecord{}}, {label: "ip_blocking", model: &ipBlockingRecord{}}, diff --git a/db_test.go b/db_test.go index 6184211..531925e 100644 --- a/db_test.go +++ b/db_test.go @@ -15,7 +15,7 @@ func TestOpenStoreCreatesTables(t *testing.T) { st := openTestStore(t) defer st.Close() - for _, table := range []string{"users", "login_log", "discard_details", "node_blocking", "ip_blocking", "forbidden_word_blocking", "nodeinfo", "map_report", "text_message", "position", "telemetry", "routing", "traceroute"} { + for _, table := range []string{"users", "login_log", "runtime_settings", "discard_details", "node_blocking", "ip_blocking", "forbidden_word_blocking", "nodeinfo", "map_report", "text_message", "position", "telemetry", "routing", "traceroute"} { var name string if err := rawTestDB(t, st).QueryRow("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table).Scan(&name); err != nil { t.Fatalf("%s table missing: %v", table, err) diff --git a/main.go b/main.go index c093c52..9ce5b51 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,7 @@ type meshtasticFilterHook struct { dbQueue *dbWriteQueue stats *meshtasticMessageStats blocking *blockingCache + settings *runtimeSettingsCache } // ID 返回用于识别 Meshtastic payload 过滤器的 hook 名称。 @@ -63,7 +64,7 @@ func (h *meshtasticFilterHook) OnConnect(cl *mqtt.Client, pk packets.Packet) err // OnPublish 在 broker 转发消息前校验 payload;无效消息会被拒绝并丢弃。 func (h *meshtasticFilterHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (packets.Packet, error) { - valid, _, record := mqtpp.MQTTPP(pk.TopicName, pk.Payload, h.key) + valid, _, record := mqtpp.MQTTPP(pk.TopicName, pk.Payload, h.key, mqtpp.Options{AllowEncryptedForwarding: h.settings.AllowEncryptedForwarding()}) if !valid { h.rejectPublish(cl, pk, record) return pk, packets.ErrRejectPacket @@ -213,9 +214,13 @@ func run(cfg *config) error { if err != nil { return err } + settings, err := newRuntimeSettingsCache(store) + if err != nil { + return err + } messageStats := &meshtasticMessageStats{} - server, mqttAddr, err := startMQTTServer(cfg, dbQueue, messageStats, blocking) + server, mqttAddr, err := startMQTTServer(cfg, dbQueue, messageStats, blocking, settings) if err != nil { return err } @@ -234,7 +239,7 @@ func run(cfg *config) error { return err } mqttStatus := mqttRuntimeStatus{server: server, address: mqttAddr, tls: cfg.MQTT.TLS.Enabled, stats: messageStats, dbQueue: dbQueue} - httpServer = newHTTPServer(cfg.Web, store, sessions, mqttStatus, blocking, forwardManager) + httpServer = newHTTPServer(cfg.Web, store, sessions, mqttStatus, blocking, forwardManager, settings) webAddress := httpServer.Addr go func() { if cfg.Web.SocketPath != "" { @@ -275,12 +280,12 @@ func run(cfg *config) error { return runErr } -func startMQTTServer(cfg *config, dbQueue *dbWriteQueue, stats *meshtasticMessageStats, blocking *blockingCache) (*mqtt.Server, string, error) { +func startMQTTServer(cfg *config, dbQueue *dbWriteQueue, stats *meshtasticMessageStats, blocking *blockingCache, settings *runtimeSettingsCache) (*mqtt.Server, string, error) { server := mqtt.New(nil) if err := server.AddHook(new(auth.AllowHook), nil); err != nil { return nil, "", err } - if err := server.AddHook(&meshtasticFilterHook{key: cfg.key, dbQueue: dbQueue, stats: stats, blocking: blocking}, nil); err != nil { + if err := server.AddHook(&meshtasticFilterHook{key: cfg.key, dbQueue: dbQueue, stats: stats, blocking: blocking, settings: settings}, nil); err != nil { return nil, "", err } diff --git a/meshmap_frontend/src/api.ts b/meshmap_frontend/src/api.ts index 0119889..cc4f425 100644 --- a/meshmap_frontend/src/api.ts +++ b/meshmap_frontend/src/api.ts @@ -3,6 +3,8 @@ import type { AdminLoginResponse, AdminManagedUserResponse, AdminMqttStatus, + AdminRuntimeSettingsPayload, + AdminRuntimeSettingsResponse, AdminUsersResponse, BlockingRuleResponse, DiscardDetails, @@ -163,6 +165,14 @@ export function getAdminMqttStatus(): Promise { return getJSON('/api/admin/mqtt/status') } +export function getAdminRuntimeSettings(): Promise { + return getJSON('/api/admin/runtime-settings') +} + +export function updateAdminRuntimeSettings(payload: AdminRuntimeSettingsPayload): Promise { + return putJSON('/api/admin/runtime-settings', payload) +} + export function getAdminHelpContent(): Promise { return getJSON('/api/admin/help') } diff --git a/meshmap_frontend/src/components/AdminDashboard.vue b/meshmap_frontend/src/components/AdminDashboard.vue index 46c3f12..2053404 100644 --- a/meshmap_frontend/src/components/AdminDashboard.vue +++ b/meshmap_frontend/src/components/AdminDashboard.vue @@ -1,11 +1,15 @@