forked from kevin/meshtastic_mqtt_server
安全加固:console_log.meshtastic 默认关闭(不打印解密明文),session_secure 默认 true,config.yaml 回写权限改 0600,公开接口错误信息脱敏(/api/health、/api/channels、列表接口,text-messages 移除 mqtt_remote_host),bot PSK 不回显(psk_set+更新保持原值),前端 help 页 DOMPurify 客户端消毒,后端 v1.6.0
This commit is contained in:
@@ -305,7 +305,7 @@ func writeBotNodeMutationResponse(c *gin.Context, status int, row *storepkg.BotN
|
||||
}
|
||||
|
||||
func botNodeDTO(row storepkg.BotNodeRecord) gin.H {
|
||||
return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": row.NodeNum, "long_name": row.LongName, "short_name": row.ShortName, "enabled": row.Enabled, "default_channel_id": row.DefaultChannelID, "topic_prefix": row.TopicPrefix, "psk": row.PSK, "public_key": row.PublicKey, "private_key_set": row.PrivateKey != "", "nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled, "nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds, "last_nodeinfo_broadcast_at": row.LastNodeInfoBroadcastAt, "llm_queue_enabled": row.LLMQueueEnabled, "llm_include_channel_messages": row.LLMIncludeChannelMessages, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": row.NodeNum, "long_name": row.LongName, "short_name": row.ShortName, "enabled": row.Enabled, "default_channel_id": row.DefaultChannelID, "topic_prefix": row.TopicPrefix, "psk_set": row.PSK != "", "public_key": row.PublicKey, "private_key_set": row.PrivateKey != "", "nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled, "nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds, "last_nodeinfo_broadcast_at": row.LastNodeInfoBroadcastAt, "llm_queue_enabled": row.LLMQueueEnabled, "llm_include_channel_messages": row.LLMIncludeChannelMessages, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
|
||||
}
|
||||
|
||||
func botMessageDTO(row storepkg.BotMessageRecord) gin.H {
|
||||
|
||||
@@ -233,7 +233,7 @@ func Default() *Config {
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
SessionSecret: "",
|
||||
SessionSecure: false,
|
||||
SessionSecure: true,
|
||||
},
|
||||
},
|
||||
AI: AIConfig{
|
||||
@@ -245,7 +245,9 @@ func Default() *Config {
|
||||
MQTT: true,
|
||||
LLM: true,
|
||||
SQL: true,
|
||||
Meshtastic: true,
|
||||
// 默认不打印解码后的 Meshtastic 数据包:其中包含私聊明文。
|
||||
// 需要调试时可显式开启 console_log.meshtastic。
|
||||
Meshtastic: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -713,7 +715,8 @@ func Write(path string, cfg *Config) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode config file %s: %w", path, err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
// 0600:配置文件含明文口令/密钥,只允许属主读写。
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return fmt.Errorf("write config file %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -116,6 +116,11 @@ func (s *Store) UpdateBotNode(id uint64, input BotNodeInput) (*BotNodeRecord, er
|
||||
preserved := existing.NodeNum
|
||||
input.NodeNum = &preserved
|
||||
}
|
||||
// PSK 不回显给前端后,更新表单不会携带原值;为空时保持现有 PSK,
|
||||
// 避免用户只是改个名字就把频道密钥重置为默认 AQ==。
|
||||
if strings.TrimSpace(input.PSK) == "" {
|
||||
input.PSK = existing.PSK
|
||||
}
|
||||
row, err := s.normalizedBotNodeRecord(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+8
-4
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -85,7 +86,7 @@ func NewRouter(cfg configpkg.WebConfig, consoleLog bool, store *storepkg.Store,
|
||||
return r
|
||||
}
|
||||
|
||||
const BackendVersion = "1.5.0"
|
||||
const BackendVersion = "1.6.0"
|
||||
|
||||
var CommitVersion = "dev"
|
||||
|
||||
@@ -93,8 +94,9 @@ func registerAPIRoutes(r gin.IRouter, store *storepkg.Store, mapTileCacheDir str
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
status := gin.H{"status": "ok", "database": "ok"}
|
||||
if err := store.Ping(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[web] health check database error: %v\n", err)
|
||||
status["status"] = "error"
|
||||
status["database"] = err.Error()
|
||||
status["database"] = "unavailable"
|
||||
c.JSON(http.StatusServiceUnavailable, status)
|
||||
return
|
||||
}
|
||||
@@ -153,7 +155,8 @@ func registerAPIRoutes(r gin.IRouter, store *storepkg.Store, mapTileCacheDir str
|
||||
r.GET("/channels", func(c *gin.Context) {
|
||||
rows, err := store.ListChannels()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
fmt.Fprintf(os.Stderr, "[web] list channels error: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
@@ -684,7 +687,8 @@ func mapReportClusterDTO(row storepkg.MapReportClusterRecord) gin.H {
|
||||
}
|
||||
|
||||
func textMessageDTO(row storepkg.TextMessageRecord) gin.H {
|
||||
return gin.H{"id": row.ID, "from_id": row.FromID, "from_num": row.FromNum, "packet_id": ptrInt64(row.PacketID), "text": ptrString(row.Text), "topic": row.Topic, "channel_id": ptrString(row.ChannelID), "created_at": row.CreatedAt, "mqtt_remote_host": ptrString(row.MQTTRemoteHost), "content_json": row.ContentJSON}
|
||||
// 不含 mqtt_remote_host:公开接口不暴露发布者 IP(见安全修复 T6 同类处理)。
|
||||
return gin.H{"id": row.ID, "from_id": row.FromID, "from_num": row.FromNum, "packet_id": ptrInt64(row.PacketID), "text": ptrString(row.Text), "topic": row.Topic, "channel_id": ptrString(row.ChannelID), "created_at": row.CreatedAt, "content_json": row.ContentJSON}
|
||||
}
|
||||
|
||||
func discardDetailsDTO(row storepkg.DiscardDetailsRecord) gin.H {
|
||||
|
||||
@@ -7,13 +7,15 @@
|
||||
package webutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"meshtastic_mqtt_server/internal/store"
|
||||
store "meshtastic_mqtt_server/internal/store"
|
||||
)
|
||||
|
||||
// ParseListOptions 从请求中读取 limit / offset / since / until / node_id /
|
||||
@@ -175,7 +177,8 @@ func ParseOptionalFloatQuery(c *gin.Context, name string) (float64, bool, bool)
|
||||
// WriteListResponse 把 rows 通过 convert 转成 gin.H 后包装成 {items, limit, offset}。
|
||||
func WriteListResponse[T any](c *gin.Context, rows []T, opts store.ListOptions, err error, convert func(T) gin.H) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
logQueryError(c, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
@@ -188,7 +191,8 @@ func WriteListResponse[T any](c *gin.Context, rows []T, opts store.ListOptions,
|
||||
// WriteListResponseWithTotal 在 WriteListResponse 基础上额外携带 total 字段。
|
||||
func WriteListResponseWithTotal[T any](c *gin.Context, rows []T, opts store.ListOptions, total int64, err error, convert func(T) gin.H) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
logQueryError(c, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
@@ -198,6 +202,11 @@ func WriteListResponseWithTotal[T any](c *gin.Context, rows []T, opts store.List
|
||||
c.JSON(http.StatusOK, gin.H{"items": items, "limit": opts.Limit, "offset": opts.Offset, "total": total})
|
||||
}
|
||||
|
||||
// logQueryError 把查询错误详情打到 stderr,响应体只回固定文案,避免泄露库表结构。
|
||||
func logQueryError(c *gin.Context, err error) {
|
||||
fmt.Fprintf(os.Stderr, "[web] %s %s query error: %v\n", c.Request.Method, c.Request.URL.Path, err)
|
||||
}
|
||||
|
||||
// PtrString / PtrInt64 / PtrUint64 / PtrFloat64 / PtrBool 把指针解引用成 any,
|
||||
// 用于把数据库可空字段转换成 JSON 时让 nil 序列化为 null。
|
||||
func PtrString(value *string) any {
|
||||
|
||||
Reference in New Issue
Block a user