feat: 初始化 meshgo MQTT 服务
- 支持 TCP / WebSocket 监听,配置热重载,systemd 集成 - meshAuthHook 实现用户名/密码认证与 ACL - meshLogHook 打印所有 MQTT 事件(CONNECT/PUBLISH/SUBSCRIBE 等) - meshDBHook 将 msh/# 主题 payload 异步写入数据库 - 数据库支持 SQLite(默认)和 MySQL,自动初始化并补充缺失配置 - payload_log 表字段:ID、client、topic、qos、payload、created_at、sender_ip - 自动补充 config.yaml 缺失字段(文件存在时写回) - .gitignore 屏蔽 data/ 和 .workbuddy/
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# data 目录(运行时数据,勿提交)
|
||||||
|
data/
|
||||||
|
|
||||||
|
# WorkBuddy 工作目录
|
||||||
|
.workbuddy/
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.o
|
||||||
|
|
||||||
|
meshgo
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
BINARY := meshgo
|
||||||
|
GOOS := linux
|
||||||
|
GOARCH := amd64
|
||||||
|
OUT := ./build/$(BINARY)
|
||||||
|
|
||||||
|
.PHONY: all build build-linux build-win install clean
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
## 本机编译(Windows CGO)
|
||||||
|
build:
|
||||||
|
@mkdir -p build
|
||||||
|
CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o $(OUT) .
|
||||||
|
@echo ">>> 已生成: $(OUT)"
|
||||||
|
|
||||||
|
## Linux 交叉编译(需要目标机器有 gcc,可选)
|
||||||
|
## 若遇 sqlite3 链接错误,请在 Linux 服务器上直接编译:
|
||||||
|
## make build-linux-native
|
||||||
|
build-linux:
|
||||||
|
@mkdir -p build
|
||||||
|
CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) CC=x86_64-linux-gnu-gcc \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o $(OUT) .
|
||||||
|
@echo ">>> 已生成 Linux $(GOARCH) 二进制: $(OUT)"
|
||||||
|
|
||||||
|
## Linux 服务器上原生编译(推荐生产环境使用)
|
||||||
|
build-linux-native:
|
||||||
|
@mkdir -p build
|
||||||
|
CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o $(OUT) .
|
||||||
|
@echo ">>> 已生成: $(OUT)"
|
||||||
|
|
||||||
|
## 部署到目标机器(需要 SSH_HOST 环境变量,如 make install SSH_HOST=root@192.168.1.10)
|
||||||
|
install: build
|
||||||
|
@test -n "$(SSH_HOST)" || (echo "请设置 SSH_HOST,例如: make install SSH_HOST=root@192.168.1.10" && exit 1)
|
||||||
|
scp $(OUT) $(SSH_HOST):/usr/local/bin/$(BINARY)
|
||||||
|
scp meshgo.service $(SSH_HOST):/etc/systemd/system/$(BINARY).service
|
||||||
|
ssh $(SSH_HOST) "systemctl daemon-reload && systemctl enable $(BINARY) && systemctl restart $(BINARY)"
|
||||||
|
@echo ">>> 部署完成"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build/
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultConfigPath = "/etc/meshgo/config.yaml"
|
||||||
|
|
||||||
|
// Config 主配置结构
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `yaml:"server"`
|
||||||
|
Auth AuthConfig `yaml:"auth"`
|
||||||
|
Logging LoggingConfig `yaml:"logging"`
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig MQTT 监听相关配置
|
||||||
|
type ServerConfig struct {
|
||||||
|
// TCP 监听地址,如 :1883
|
||||||
|
TCPAddr string `yaml:"tcp_addr"`
|
||||||
|
// WebSocket 监听地址,如 :8883(留空则不启动)
|
||||||
|
WSAddr string `yaml:"ws_addr"`
|
||||||
|
// 最大并发连接数,0 表示不限
|
||||||
|
MaxConnections int `yaml:"max_connections"`
|
||||||
|
// 客户端消息写入超时(秒)
|
||||||
|
WriteTimeout int `yaml:"write_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthConfig 认证配置
|
||||||
|
type AuthConfig struct {
|
||||||
|
// 是否开启用户名/密码认证
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
// 允许匿名连接
|
||||||
|
AllowAnonymous bool `yaml:"allow_anonymous"`
|
||||||
|
// 内置用户列表(简单场景使用)
|
||||||
|
Users []UserEntry `yaml:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserEntry 单条用户凭证
|
||||||
|
type UserEntry struct {
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggingConfig 日志配置
|
||||||
|
type LoggingConfig struct {
|
||||||
|
// debug / info / warn / error
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
// 日志文件路径,留空则输出到 stdout
|
||||||
|
File string `yaml:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseConfig 数据库配置
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
// 是否启用数据库,默认 false
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
// 数据库类型:sqlite3(默认) / mysql
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
// SQLite 数据库文件路径(相对于 dataDir)
|
||||||
|
// MySQL 留空,DSN 填 DSN 字段
|
||||||
|
File string `yaml:"file"`
|
||||||
|
// MySQL DSN,如 user:password@tcp(127.0.0.1:3306)/meshgo
|
||||||
|
// SQLite 模式下此字段被忽略
|
||||||
|
DSN string `yaml:"dsn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 全局单例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var (
|
||||||
|
current *Config
|
||||||
|
mu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get 返回当前配置的只读快照
|
||||||
|
func Get() *Config {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 从磁盘加载配置,替换全局单例
|
||||||
|
func Load(path string) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg := defaultConfig()
|
||||||
|
if err = yaml.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 补充缺失字段的默认值
|
||||||
|
applyDefaults(cfg)
|
||||||
|
mu.Lock()
|
||||||
|
current = cfg
|
||||||
|
mu.Unlock()
|
||||||
|
log.Printf("[config] 已加载配置文件: %s", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload 重新读取配置文件(SIGHUP 时调用)
|
||||||
|
func Reload(path string) {
|
||||||
|
log.Printf("[config] 收到重载信号,重新读取: %s", path)
|
||||||
|
if err := Load(path); err != nil {
|
||||||
|
log.Printf("[config] 重载失败,继续使用旧配置: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureConfigComplete 确保配置文件存在且字段完整
|
||||||
|
// - 文件不存在:创建带注释的默认配置
|
||||||
|
// - 文件存在但缺失字段:补充缺失字段并写回文件
|
||||||
|
func EnsureConfigComplete(path string) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
// 文件不存在,创建默认配置
|
||||||
|
cfg := defaultConfig()
|
||||||
|
return writeDefaultConfig(path, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件存在,解析并补充缺失字段
|
||||||
|
cfg := defaultConfig()
|
||||||
|
if err = yaml.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if changed := applyDefaults(cfg); changed {
|
||||||
|
log.Printf("[config] 检测到配置文件缺失字段,已自动补充: %s", path)
|
||||||
|
if err = writeDefaultConfig(path, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeDefaultConfig 写入带注释头的默认配置
|
||||||
|
func writeDefaultConfig(path string, cfg *Config) error {
|
||||||
|
data, err := yaml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
header := `# meshgo MQTT 服务配置文件
|
||||||
|
# 修改后执行: systemctl reload meshgo 即可热重载(无需重启服务)
|
||||||
|
#
|
||||||
|
# 热重载支持的字段:auth / logging
|
||||||
|
# 需要重启才能生效的字段:server.tcp_addr / server.ws_addr / database
|
||||||
|
|
||||||
|
`
|
||||||
|
return os.WriteFile(path, append([]byte(header), data...), 0640)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDefaults 补充零值字段的默认值,返回是否发生了修改
|
||||||
|
func applyDefaults(cfg *Config) bool {
|
||||||
|
changed := false
|
||||||
|
// Server
|
||||||
|
if cfg.Server.TCPAddr == "" {
|
||||||
|
cfg.Server.TCPAddr = ":1883"
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if cfg.Server.WriteTimeout == 0 {
|
||||||
|
cfg.Server.WriteTimeout = 3
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
// Auth
|
||||||
|
if !cfg.Auth.Enabled {
|
||||||
|
cfg.Auth.Enabled = false
|
||||||
|
cfg.Auth.AllowAnonymous = true
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
// Logging
|
||||||
|
if cfg.Logging.Level == "" {
|
||||||
|
cfg.Logging.Level = "info"
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
// Database
|
||||||
|
if cfg.Database.Type == "" {
|
||||||
|
cfg.Database.Type = "sqlite3"
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if cfg.Database.File == "" {
|
||||||
|
cfg.Database.File = "meshgo.db"
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultConfig 返回一份合理的默认配置
|
||||||
|
func defaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Server: ServerConfig{
|
||||||
|
TCPAddr: ":1883",
|
||||||
|
WSAddr: "",
|
||||||
|
MaxConnections: 0,
|
||||||
|
WriteTimeout: 3,
|
||||||
|
},
|
||||||
|
Auth: AuthConfig{
|
||||||
|
Enabled: false,
|
||||||
|
AllowAnonymous: true,
|
||||||
|
Users: []UserEntry{},
|
||||||
|
},
|
||||||
|
Logging: LoggingConfig{
|
||||||
|
Level: "info",
|
||||||
|
File: "",
|
||||||
|
},
|
||||||
|
Database: DatabaseConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Type: "sqlite3",
|
||||||
|
File: "meshgo.db",
|
||||||
|
DSN: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"meshgo/config"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PayloadLog — payload 日志表(仅记录 msh/# 主题)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type PayloadLog struct {
|
||||||
|
ID uint64 `gorm:"primaryKey;autoIncrement"` // 自增 ID
|
||||||
|
Client string `gorm:"type:varchar(255);index"` // 客户端 ID
|
||||||
|
Topic string `gorm:"type:varchar(512);index"` // 完整主题
|
||||||
|
Qos byte // QoS 等级
|
||||||
|
Payload []byte // 原始 payload
|
||||||
|
CreatedAt int64 `gorm:"index"` // 发送时间(Unix 秒)
|
||||||
|
SenderIP string `gorm:"type:varchar(64)"` // 发送者 IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (PayloadLog) TableName() string { return "payload_log" }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DB 全局单例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *gorm.DB
|
||||||
|
dbMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get 返回当前数据库实例(只读)
|
||||||
|
func Get() *gorm.DB {
|
||||||
|
dbMu.RLock()
|
||||||
|
defer dbMu.RUnlock()
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 设置数据库实例(仅供内部和测试使用)
|
||||||
|
func Set(d *gorm.DB) {
|
||||||
|
dbMu.Lock()
|
||||||
|
db = d
|
||||||
|
dbMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init 根据配置初始化数据库连接,完成自动迁移
|
||||||
|
// dbType: "sqlite3" | "mysql"
|
||||||
|
func Init(cfg *config.DatabaseConfig, dataDir string) error {
|
||||||
|
if !cfg.Enabled {
|
||||||
|
log.Println("[db] 数据库未启用,跳过初始化")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *gorm.DB
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
switch strings.ToLower(cfg.Type) {
|
||||||
|
case "mysql":
|
||||||
|
db, err = initMySQL(cfg.DSN)
|
||||||
|
case "sqlite3", "":
|
||||||
|
db, err = initSQLite(cfg, dataDir)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("[db] 不支持的数据库类型: %s(支持: sqlite3, mysql)", cfg.Type)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("[db] 初始化失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动迁移表结构(仅新增列,不会删列)
|
||||||
|
if err = db.AutoMigrate(&PayloadLog{}); err != nil {
|
||||||
|
return fmt.Errorf("[db] 表迁移失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Set(db)
|
||||||
|
log.Printf("[db] 已连接 %s", cfg.Type)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSQLite 构建 SQLite 连接,dbFile 相对于 dataDir
|
||||||
|
func initSQLite(cfg *config.DatabaseConfig, dataDir string) (*gorm.DB, error) {
|
||||||
|
path := filepath.Join(dataDir, cfg.File)
|
||||||
|
// 确保父目录存在
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("[db] SQLite 数据库路径: %s", path)
|
||||||
|
|
||||||
|
// 默认 foreign_keys=ON;sqlite3 driver 支持 _loc=Local
|
||||||
|
dsn := fmt.Sprintf("%s?_foreign_keys=on&_loc=Local", path)
|
||||||
|
|
||||||
|
gormDB, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复用底层 sql.DB 设置连接池
|
||||||
|
sqlDB, err := gormDB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxOpenConns(1) // SQLite 建议单连接
|
||||||
|
sqlDB.SetMaxIdleConns(1)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
|
return gormDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initMySQL 构建 MySQL 连接
|
||||||
|
func initMySQL(dsn string) (*gorm.DB, error) {
|
||||||
|
if dsn == "" {
|
||||||
|
return nil, fmt.Errorf("MySQL DSN 未配置(请填写 database.dsn 字段)")
|
||||||
|
}
|
||||||
|
gormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sqlDB, err := gormDB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxOpenConns(25)
|
||||||
|
sqlDB.SetMaxIdleConns(5)
|
||||||
|
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
||||||
|
return gormDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Close 关闭数据库连接
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func Close() error {
|
||||||
|
gormDB := Get()
|
||||||
|
if gormDB == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sqlDB, err := gormDB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WritePayloadLog 异步写入 payload 日志(丢到 channel 不阻塞主流程)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var WriteCh = make(chan *PayloadLog, 1000) // 有缓冲 channel,导出供 main.go 关闭
|
||||||
|
|
||||||
|
// StartWriter 启动异步写入 worker
|
||||||
|
func StartWriter() {
|
||||||
|
go func() {
|
||||||
|
for entry := range WriteCh {
|
||||||
|
if err := insertPayloadLog(entry); err != nil {
|
||||||
|
log.Printf("[db] 写入 payload_log 失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 将待写入对象推入队列(非阻塞)
|
||||||
|
// 数据库未初始化时静默跳过(由 main.go 保证 Serve 之前已 Init,此为安全兜底)
|
||||||
|
func Insert(entry *PayloadLog) {
|
||||||
|
// 安全兜底:若 WriteCh 未初始化(极端情况),直接丢弃
|
||||||
|
if WriteCh == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case WriteCh <- entry:
|
||||||
|
default:
|
||||||
|
log.Printf("[db] 写入队列已满,丢弃日志: topic=%s", entry.Topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertPayloadLog(entry *PayloadLog) error {
|
||||||
|
gormDB := Get()
|
||||||
|
if gormDB == nil {
|
||||||
|
return nil // Init 尚未完成,静默跳过
|
||||||
|
}
|
||||||
|
return gormDB.Create(entry).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
module meshgo
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/mochi-mqtt/server/v2 v2.7.9 // indirect
|
||||||
|
github.com/rs/xid v1.4.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/driver/mysql v1.6.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||||
|
gorm.io/gorm v1.31.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mochi-mqtt/server/v2 v2.7.9 h1:y0g4vrSLAag7T07l2oCzOa/+nKVLoazKEWAArwqBNYI=
|
||||||
|
github.com/mochi-mqtt/server/v2 v2.7.9/go.mod h1:lZD3j35AVNqJL5cezlnSkuG05c0FCHSsfAKSPBOSbqc=
|
||||||
|
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
||||||
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"meshgo/database"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mqtt "github.com/mochi-mqtt/server/v2"
|
||||||
|
"github.com/mochi-mqtt/server/v2/packets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// meshLogHook 实现所有可打印日志的 Hook 接口
|
||||||
|
type meshLogHook struct {
|
||||||
|
mqtt.HookBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *meshLogHook) ID() string { return "meshgo-log" }
|
||||||
|
|
||||||
|
func (h *meshLogHook) Provides(b byte) bool {
|
||||||
|
return bytes.Contains([]byte{
|
||||||
|
mqtt.OnStarted,
|
||||||
|
mqtt.OnStopped,
|
||||||
|
mqtt.OnConnect,
|
||||||
|
mqtt.OnDisconnect,
|
||||||
|
mqtt.OnSessionEstablished,
|
||||||
|
mqtt.OnPublish,
|
||||||
|
mqtt.OnPublished,
|
||||||
|
mqtt.OnSubscribe,
|
||||||
|
mqtt.OnSubscribed,
|
||||||
|
mqtt.OnUnsubscribe,
|
||||||
|
mqtt.OnUnsubscribed,
|
||||||
|
mqtt.OnWillSent,
|
||||||
|
mqtt.OnQosComplete,
|
||||||
|
mqtt.OnQosDropped,
|
||||||
|
mqtt.OnPublishDropped,
|
||||||
|
}, []byte{b})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnStarted 服务启动完成
|
||||||
|
func (h *meshLogHook) OnStarted() {
|
||||||
|
log.Println("[hook] ✓ 服务已启动")
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnStopped 服务停止
|
||||||
|
func (h *meshLogHook) OnStopped() {
|
||||||
|
log.Println("[hook] ✓ 服务已停止")
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnConnect 客户端请求连接(认证前)
|
||||||
|
func (h *meshLogHook) OnConnect(cl *mqtt.Client, pk packets.Packet) error {
|
||||||
|
log.Printf("[hook] CONNECT client=%s remote=%s",
|
||||||
|
safeClientID(cl), safeRemoteAddr(cl))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnSessionEstablished 客户端认证成功,session 建立
|
||||||
|
func (h *meshLogHook) OnSessionEstablished(cl *mqtt.Client, pk packets.Packet) {
|
||||||
|
user := string(pk.Connect.Username)
|
||||||
|
if user == "" {
|
||||||
|
user = "(anonymous)"
|
||||||
|
}
|
||||||
|
log.Printf("[hook] CONNECTED client=%s username=%s keepalive=%d",
|
||||||
|
safeClientID(cl), user, pk.Connect.Keepalive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnDisconnect 客户端断开(err=nil 表示主动断开,expire=true 表示 session 过期)
|
||||||
|
func (h *meshLogHook) OnDisconnect(cl *mqtt.Client, err error, expire bool) {
|
||||||
|
cause := "主动断开"
|
||||||
|
if expire {
|
||||||
|
cause = "session 过期"
|
||||||
|
} else if err != nil {
|
||||||
|
cause = fmt.Sprintf("异常: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("[hook] DISCONNECT client=%s cause=%s", safeClientID(cl), cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPublish 收到客户端发布的原始消息(可修改 packet 后继续)
|
||||||
|
func (h *meshLogHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (packets.Packet, error) {
|
||||||
|
// 打印消息内容,过长截断
|
||||||
|
body := string(pk.Payload)
|
||||||
|
if len(body) > 200 {
|
||||||
|
body = body[:200] + "...(truncated)"
|
||||||
|
}
|
||||||
|
log.Printf("[hook] PUBLISH client=%s topic=%s qos=%d retain=%t payload=%s",
|
||||||
|
safeClientID(cl), pk.TopicName, pk.FixedHeader.Qos, pk.FixedHeader.Retain, body)
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPublished 消息已投递给所有订阅者
|
||||||
|
func (h *meshLogHook) OnPublished(cl *mqtt.Client, pk packets.Packet) {
|
||||||
|
log.Printf("[hook] PUBLISHED client=%s topic=%s id=%d",
|
||||||
|
safeClientID(cl), pk.TopicName, pk.PacketID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnSubscribe 客户端订阅请求(可修改过滤条件)
|
||||||
|
func (h *meshLogHook) OnSubscribe(cl *mqtt.Client, pk packets.Packet) packets.Packet {
|
||||||
|
for _, sub := range pk.Filters {
|
||||||
|
log.Printf("[hook] SUBSCRIBE client=%s filter=%s qos=%d",
|
||||||
|
safeClientID(cl), sub.Filter, sub.Qos)
|
||||||
|
}
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnSubscribed 订阅成功
|
||||||
|
func (h *meshLogHook) OnSubscribed(cl *mqtt.Client, pk packets.Packet, reasonCodes []byte) {
|
||||||
|
for i, sub := range pk.Filters {
|
||||||
|
code := "?"
|
||||||
|
if i < len(reasonCodes) {
|
||||||
|
code = fmt.Sprintf("%d", reasonCodes[i])
|
||||||
|
}
|
||||||
|
log.Printf("[hook] SUBSCRIBED client=%s filter=%s reason=%s",
|
||||||
|
safeClientID(cl), sub.Filter, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnUnsubscribe 客户端取消订阅
|
||||||
|
func (h *meshLogHook) OnUnsubscribe(cl *mqtt.Client, pk packets.Packet) packets.Packet {
|
||||||
|
for _, sub := range pk.Filters {
|
||||||
|
log.Printf("[hook] UNSUBSCRIBE client=%s filter=%s",
|
||||||
|
safeClientID(cl), sub.Filter)
|
||||||
|
}
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnUnsubscribed 取消订阅完成
|
||||||
|
func (h *meshLogHook) OnUnsubscribed(cl *mqtt.Client, pk packets.Packet) {
|
||||||
|
log.Printf("[hook] UNSUBSCRIBED client=%s", safeClientID(cl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnWillSent 遗嘱消息已发送
|
||||||
|
func (h *meshLogHook) OnWillSent(cl *mqtt.Client, pk packets.Packet) {
|
||||||
|
log.Printf("[hook] LWT_SENT client=%s topic=%s", safeClientID(cl), pk.TopicName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnQosComplete QoS 交付完成
|
||||||
|
func (h *meshLogHook) OnQosComplete(cl *mqtt.Client, pk packets.Packet) {
|
||||||
|
log.Printf("[hook] QOS_COMPLETE client=%s topic=%s id=%d",
|
||||||
|
safeClientID(cl), pk.TopicName, pk.PacketID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnQosDropped QoS 消息超时丢弃
|
||||||
|
func (h *meshLogHook) OnQosDropped(cl *mqtt.Client, pk packets.Packet) {
|
||||||
|
log.Printf("[hook] QOS_DROPPED client=%s topic=%s id=%d",
|
||||||
|
safeClientID(cl), pk.TopicName, pk.PacketID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPublishDropped 消息因客户端慢被丢弃
|
||||||
|
func (h *meshLogHook) OnPublishDropped(cl *mqtt.Client, pk packets.Packet) {
|
||||||
|
log.Printf("[hook] PUBLISH_DROPPED client=%s topic=%s",
|
||||||
|
safeClientID(cl), pk.TopicName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 辅助函数
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func safeClientID(cl *mqtt.Client) string {
|
||||||
|
if cl == nil {
|
||||||
|
return "(nil)"
|
||||||
|
}
|
||||||
|
return cl.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeRemoteAddr(cl *mqtt.Client) string {
|
||||||
|
if cl == nil {
|
||||||
|
return "(unknown)"
|
||||||
|
}
|
||||||
|
return cl.Net.Remote
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译期接口检查
|
||||||
|
var _ mqtt.Hook = (*meshLogHook)(nil)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// meshDBHook — 将 msh/# 主题的 payload 写入数据库
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// meshDBHook 拦截 msh/# 主题消息并写入 payload_log 表
|
||||||
|
type meshDBHook struct {
|
||||||
|
mqtt.HookBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *meshDBHook) ID() string { return "meshgo-db" }
|
||||||
|
|
||||||
|
func (h *meshDBHook) Provides(b byte) bool {
|
||||||
|
return b == mqtt.OnPublish
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPublish 收到发布消息时,检查是否为 msh/# 并异步写库
|
||||||
|
func (h *meshDBHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (packets.Packet, error) {
|
||||||
|
// 仅记录 msh/ 开头的主题
|
||||||
|
if !strings.HasPrefix(pk.TopicName, "msh/") {
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &database.PayloadLog{
|
||||||
|
Client: safeClientID(cl),
|
||||||
|
Topic: pk.TopicName,
|
||||||
|
Qos: pk.FixedHeader.Qos,
|
||||||
|
Payload: pk.Payload,
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
SenderIP: safeRemoteAddr(cl),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步写入,不阻塞消息投递
|
||||||
|
database.Insert(entry)
|
||||||
|
log.Printf("[hook] [db] queued msh payload: client=%s topic=%s qos=%d size=%d bytes",
|
||||||
|
entry.Client, entry.Topic, entry.Qos, len(entry.Payload))
|
||||||
|
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译期接口检查
|
||||||
|
var _ mqtt.Hook = (*meshDBHook)(nil)
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"meshgo/config"
|
||||||
|
"meshgo/database"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
mqtt "github.com/mochi-mqtt/server/v2"
|
||||||
|
"github.com/mochi-mqtt/server/v2/listeners"
|
||||||
|
"github.com/mochi-mqtt/server/v2/packets"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 1. 确保运行时目录存在
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
ensureDir(dataDir)
|
||||||
|
ensureDir(configDir)
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 2. 初始化配置(如无配置文件则写入默认值)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if err := config.EnsureConfigComplete(configFile); err != nil {
|
||||||
|
log.Fatalf("[main] 无法写入默认配置文件: %v", err)
|
||||||
|
}
|
||||||
|
if err := config.Load(configFile); err != nil {
|
||||||
|
log.Fatalf("[main] 无法加载配置文件: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 3. 初始化数据库(在启动服务前完成,避免 hook 调用时未初始化)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if err := database.Init(&config.Get().Database, dataDir); err != nil {
|
||||||
|
log.Fatalf("[main] 数据库初始化失败: %v", err)
|
||||||
|
}
|
||||||
|
database.StartWriter() // 启动异步写入 worker
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 4. 构建 MQTT 服务器(所有初始化在 Serve 之前完成)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
cfg := config.Get()
|
||||||
|
server := mqtt.New(&mqtt.Options{
|
||||||
|
InlineClient: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册认证 hook
|
||||||
|
if err := server.AddHook(new(meshAuthHook), nil); err != nil {
|
||||||
|
log.Fatalf("[main] 注册认证 hook 失败: %v", err)
|
||||||
|
}
|
||||||
|
// 注册日志 hook
|
||||||
|
if err := server.AddHook(new(meshLogHook), nil); err != nil {
|
||||||
|
log.Fatalf("[main] 注册日志 hook 失败: %v", err)
|
||||||
|
}
|
||||||
|
// 注册 payload 数据库日志 hook
|
||||||
|
if err := server.AddHook(new(meshDBHook), nil); err != nil {
|
||||||
|
log.Fatalf("[main] 注册 payload 数据库 hook 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 TCP 监听
|
||||||
|
tcpListener := listeners.NewTCP(
|
||||||
|
listeners.Config{ID: "tcp1", Address: cfg.Server.TCPAddr},
|
||||||
|
)
|
||||||
|
if err := server.AddListener(tcpListener); err != nil {
|
||||||
|
log.Fatalf("[main] 添加 TCP 监听失败 (%s): %v", cfg.Server.TCPAddr, err)
|
||||||
|
}
|
||||||
|
log.Printf("[main] TCP 监听已绑定: %s", cfg.Server.TCPAddr)
|
||||||
|
|
||||||
|
// 可选:添加 WebSocket 监听
|
||||||
|
if cfg.Server.WSAddr != "" {
|
||||||
|
wsListener := listeners.NewWebsocket(
|
||||||
|
listeners.Config{ID: "ws1", Address: cfg.Server.WSAddr},
|
||||||
|
)
|
||||||
|
if err := server.AddListener(wsListener); err != nil {
|
||||||
|
log.Fatalf("[main] 添加 WebSocket 监听失败 (%s): %v", cfg.Server.WSAddr, err)
|
||||||
|
}
|
||||||
|
log.Printf("[main] WebSocket 监听已绑定: %s", cfg.Server.WSAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 5. 启动服务器(阻塞,直到收到退出信号)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
log.Println("[main] meshgo MQTT 服务已启动")
|
||||||
|
if err := server.Serve(); err != nil {
|
||||||
|
log.Fatalf("[main] MQTT 服务器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 5. 信号处理
|
||||||
|
// SIGHUP → 热重载配置
|
||||||
|
// SIGINT / SIGTERM → 优雅退出
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
for sig := range sigCh {
|
||||||
|
switch sig {
|
||||||
|
case syscall.SIGHUP:
|
||||||
|
log.Println("[main] 收到 SIGHUP,热重载配置…")
|
||||||
|
config.Reload(configFile)
|
||||||
|
// 注:监听地址变更需要重启服务,此处仅刷新认证/日志等运行时配置
|
||||||
|
case syscall.SIGINT, syscall.SIGTERM:
|
||||||
|
log.Println("[main] 正在优雅关闭服务…")
|
||||||
|
close(database.WriteCh) // 停止异步写入 worker
|
||||||
|
if err := database.Close(); err != nil {
|
||||||
|
log.Printf("[main] 关闭数据库时出错: %v", err)
|
||||||
|
}
|
||||||
|
if err := server.Close(); err != nil {
|
||||||
|
log.Printf("[main] 关闭服务器时出错: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("[main] meshgo 已停止")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDir 确保目录存在,不存在则创建(含父目录)
|
||||||
|
func ensureDir(path string) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
if err = os.MkdirAll(path, 0755); err != nil {
|
||||||
|
log.Fatalf("[main] 无法创建目录 %s: %v", path, err)
|
||||||
|
}
|
||||||
|
log.Printf("[main] 已创建目录: %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 认证 Hook
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// meshAuthHook 实现 mochi-mqtt Hook 接口,基于配置文件进行认证
|
||||||
|
type meshAuthHook struct {
|
||||||
|
mqtt.HookBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *meshAuthHook) ID() string { return "meshgo-auth" }
|
||||||
|
|
||||||
|
func (h *meshAuthHook) Provides(b byte) bool {
|
||||||
|
return bytes.Contains([]byte{
|
||||||
|
mqtt.OnConnectAuthenticate,
|
||||||
|
mqtt.OnACLCheck,
|
||||||
|
}, []byte{b})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnConnectAuthenticate 验证客户端连接凭证
|
||||||
|
func (h *meshAuthHook) OnConnectAuthenticate(cl *mqtt.Client, pk packets.Packet) bool {
|
||||||
|
cfg := config.Get().Auth
|
||||||
|
|
||||||
|
// 未开启认证,全部放行
|
||||||
|
if !cfg.Enabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
username := string(pk.Connect.Username)
|
||||||
|
password := string(pk.Connect.Password)
|
||||||
|
|
||||||
|
// 匿名连接处理
|
||||||
|
if username == "" {
|
||||||
|
return cfg.AllowAnonymous
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐条比对用户列表
|
||||||
|
for _, u := range cfg.Users {
|
||||||
|
if u.Username == username && u.Password == password {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnACLCheck 主题级别 ACL 检查(默认全放行,可按需扩展)
|
||||||
|
func (h *meshAuthHook) OnACLCheck(cl *mqtt.Client, topic string, write bool) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 meshAuthHook 实现了 mqtt.Hook 接口(编译期检查)
|
||||||
|
var _ mqtt.Hook = (*meshAuthHook)(nil)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=meshgo MQTT Broker
|
||||||
|
Documentation=https://github.com/mochi-mqtt/server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# 以 root 运行以便绑定 1883 端口;如已设置 CAP_NET_BIND_SERVICE 可改为普通用户
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
# 二进制路径,编译后 cp meshgo /usr/local/bin/meshgo
|
||||||
|
ExecStart=/usr/local/bin/meshgo
|
||||||
|
|
||||||
|
# systemctl reload meshgo → 发送 SIGHUP → 热重载配置
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
# 日志输出交由 journald 接管
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=meshgo
|
||||||
|
|
||||||
|
# 资源保护(可选)
|
||||||
|
PrivateTmp=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Linux/macOS:使用系统标准路径
|
||||||
|
const (
|
||||||
|
dataDir = "/var/lib/meshgo"
|
||||||
|
configDir = "/etc/meshgo"
|
||||||
|
configFile = configDir + "/config.yaml"
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Windows:映射到当前目录下的 data/ 子目录,方便本地测试
|
||||||
|
const (
|
||||||
|
dataDir = `./data/var/lib/meshgo`
|
||||||
|
configDir = `./data/etc/meshgo`
|
||||||
|
configFile = configDir + `/config.yaml`
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user