第二批:把根目录中纯逻辑领域文件(cache、service、admin route)按业务边界
迁到 internal/ 下的子包。各子包暴露 RegisterRoutes 给 web 包调用,根目录
只留下一行 bridge 文件保留旧名字别名。
新增包
- internal/auth/ SessionClaims / Manager / RequireAdmin / HashPassword /
VerifyPassword / AdminUserResponse 等。原 auth.go 中
被两个 admin route 依赖的 sessionClaims 现在以 auth.
SessionClaims 形式被它们 import;不再被锁在 main 包。
- internal/blocking/ Cache + RegisterRoutes,以前散在 blocking_cache.go
和 admin_blocking_routes.go 里。
- internal/runtimesettings/ Cache + RegisterRoutes。
- internal/help/ RenderMarkdown / RegisterPublicRoutes /
RegisterAdminRoutes(拆分原来的 registerHelpRoutes
和 registerAdminHelpRoutes 两条入口)。
- internal/mqttforward/ Manager / Reloader / Stats / RegisterRoutes。
forwarder runner、循环抑制 cache 等运行时逻辑随之迁入。
- internal/webutil/ ParseListOptions / WriteListResponse[WithTotal] /
ParseMapReportListOptions / ParseMapReportViewportOptions
以及 PtrString/PtrInt64/... 等指针解引用 helper。
以前散在 web.go 中,现在被各 admin route 子包共享,
避免 internal/blocking → internal/web → internal/blocking
的循环依赖。
- internal/store/testutil/ OpenStore(t) helper,让其它包测试零样板拿到 store。
根目录新增 bridge 文件
- blocking_bridge.go / runtime_settings_bridge.go / help_bridge.go /
mqttforward_bridge.go:用 type alias + thin wrapper 把上述子包的导出
名映射到旧的小写名(blockingCache、registerAdminBlockingRoutes 等),
让 main.go / web.go 等仍未迁出的文件无须改动。
修改
- auth.go 改为对 internal/auth 的 bridge;web.go 中 sessions.newCookie /
clearCookie 改为 NewCookie / ClearCookie。
- main_test.go 中 BlockingViolationForRecord* 测试不再直接构造未导出字段,
改成走 store.CreateNodeBlocking → newBlockingCache 的真实路径。
- internal/mqttforward 把以前 *_store.go 中没有方法依赖的运行时类型
(forwarder runner、loop cache)和 admin route 一并归位;mqtt_status.go
暂时仍留在根目录(依赖 main 中的 mqttClientInfoFromClient)。
go build ./... / go test ./... 全部通过;测试数量未变。
Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
4.1 KiB
Go
215 lines
4.1 KiB
Go
package blocking
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
storepkg "meshtastic_mqtt_server/internal/store"
|
|
)
|
|
|
|
type Cache struct {
|
|
mu sync.RWMutex
|
|
nodes map[string]struct{}
|
|
nodeNums map[int64]struct{}
|
|
ips map[string]struct{}
|
|
cidrs []*net.IPNet
|
|
words []forbiddenWordRule
|
|
}
|
|
|
|
type forbiddenWordRule struct {
|
|
word string
|
|
foldedWord string
|
|
matchType string
|
|
caseSensitive bool
|
|
}
|
|
|
|
func New(store *storepkg.Store) (*Cache, error) {
|
|
cache := &Cache{}
|
|
if err := cache.Reload(store); err != nil {
|
|
return nil, err
|
|
}
|
|
return cache, nil
|
|
}
|
|
|
|
func (c *Cache) Reload(store *storepkg.Store) error {
|
|
if store == nil {
|
|
return fmt.Errorf("store is required")
|
|
}
|
|
|
|
nodeRows, err := store.ListEnabledNodeBlocking()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ipRows, err := store.ListEnabledIPBlocking()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wordRows, err := store.ListEnabledForbiddenWordBlocking()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nodes := make(map[string]struct{}, len(nodeRows))
|
|
nodeNums := make(map[int64]struct{}, len(nodeRows))
|
|
for _, row := range nodeRows {
|
|
nodeID := strings.TrimSpace(row.NodeID)
|
|
if nodeID != "" {
|
|
nodes[nodeID] = struct{}{}
|
|
}
|
|
if row.NodeNum != nil {
|
|
nodeNums[*row.NodeNum] = struct{}{}
|
|
}
|
|
}
|
|
|
|
ips := make(map[string]struct{}, len(ipRows))
|
|
cidrs := make([]*net.IPNet, 0, len(ipRows))
|
|
for _, row := range ipRows {
|
|
value := strings.TrimSpace(row.IPValue)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if ip := net.ParseIP(value); ip != nil {
|
|
ips[ip.String()] = struct{}{}
|
|
continue
|
|
}
|
|
if _, ipNet, err := net.ParseCIDR(value); err == nil {
|
|
cidrs = append(cidrs, ipNet)
|
|
}
|
|
}
|
|
|
|
words := make([]forbiddenWordRule, 0, len(wordRows))
|
|
for _, row := range wordRows {
|
|
word := strings.TrimSpace(row.Word)
|
|
if word == "" || row.MatchType != storepkg.ForbiddenWordMatchContains {
|
|
continue
|
|
}
|
|
words = append(words, forbiddenWordRule{word: word, foldedWord: strings.ToLower(word), matchType: row.MatchType, caseSensitive: row.CaseSensitive})
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.nodes = nodes
|
|
c.nodeNums = nodeNums
|
|
c.ips = ips
|
|
c.cidrs = cidrs
|
|
c.words = words
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) IsNodeBlocked(nodeID any, nodeNum any) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
id, _ := nodeID.(string)
|
|
num, hasNum := blockingInt64FromAny(nodeNum)
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if id != "" {
|
|
if _, ok := c.nodes[id]; ok {
|
|
return true
|
|
}
|
|
}
|
|
if hasNum {
|
|
_, ok := c.nodeNums[num]
|
|
return ok
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Cache) IsIPBlocked(host string) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
host = strings.TrimSpace(host)
|
|
if host == "" {
|
|
return false
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if _, ok := c.ips[ip.String()]; ok {
|
|
return true
|
|
}
|
|
for _, ipNet := range c.cidrs {
|
|
if ipNet.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Cache) FindForbiddenWord(text any) (string, bool) {
|
|
if c == nil {
|
|
return "", false
|
|
}
|
|
value, ok := text.(string)
|
|
if !ok || value == "" {
|
|
return "", false
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
foldedText := ""
|
|
for _, rule := range c.words {
|
|
if rule.matchType != storepkg.ForbiddenWordMatchContains {
|
|
continue
|
|
}
|
|
if rule.caseSensitive {
|
|
if strings.Contains(value, rule.word) {
|
|
return rule.word, true
|
|
}
|
|
continue
|
|
}
|
|
if foldedText == "" {
|
|
foldedText = strings.ToLower(value)
|
|
}
|
|
if strings.Contains(foldedText, rule.foldedWord) {
|
|
return rule.word, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func blockingInt64FromAny(value any) (int64, bool) {
|
|
switch v := value.(type) {
|
|
case int:
|
|
return int64(v), true
|
|
case int8:
|
|
return int64(v), true
|
|
case int16:
|
|
return int64(v), true
|
|
case int32:
|
|
return int64(v), true
|
|
case int64:
|
|
return v, true
|
|
case uint:
|
|
return int64(v), true
|
|
case uint8:
|
|
return int64(v), true
|
|
case uint16:
|
|
return int64(v), true
|
|
case uint32:
|
|
return int64(v), true
|
|
case uint64:
|
|
if v > uint64(^uint64(0)>>1) {
|
|
return 0, false
|
|
}
|
|
return int64(v), true
|
|
case float64:
|
|
return int64(v), v == float64(int64(v))
|
|
case string:
|
|
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
|
return n, err == nil
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|