package main import ( cryptotls "crypto/tls" "fmt" "os" "path/filepath" "runtime" "gopkg.in/yaml.v3" ) const configFileName = "config.yaml" type config struct { MQTT mqttConfig `yaml:"mqtt"` Meshtastic meshtasticConfig `yaml:"meshtastic"` Database databaseConfig `yaml:"database"` Web webConfig `yaml:"web"` key []byte } type mqttConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` TLS tlsConfig `yaml:"tls"` } type tlsConfig struct { Enabled bool `yaml:"enabled"` CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` } type meshtasticConfig struct { PSK string `yaml:"psk"` } type databaseConfig struct { Driver string `yaml:"driver"` SQLite sqliteConfig `yaml:"sqlite"` MySQL mysqlConfig `yaml:"mysql"` } type sqliteConfig struct { Path string `yaml:"path"` } type mysqlConfig struct { DSN string `yaml:"dsn"` } type webConfig struct { Enabled bool `yaml:"enabled"` Host string `yaml:"host"` Port int `yaml:"port"` SocketPath string `yaml:"socket_path"` StaticDir string `yaml:"static_dir"` Admin webAdminConfig `yaml:"admin"` } type webAdminConfig struct { Username string `yaml:"username"` Password string `yaml:"password"` SessionSecret string `yaml:"session_secret"` SessionSecure bool `yaml:"session_secure"` } type rawConfig struct { MQTT *rawMQTTConfig `yaml:"mqtt"` Meshtastic *rawMeshtasticConfig `yaml:"meshtastic"` Database *rawDatabaseConfig `yaml:"database"` Web *rawWebConfig `yaml:"web"` } type rawMQTTConfig struct { Host *string `yaml:"host"` Port *int `yaml:"port"` TLS *rawTLSConfig `yaml:"tls"` } type rawTLSConfig struct { Enabled *bool `yaml:"enabled"` CertFile *string `yaml:"cert_file"` KeyFile *string `yaml:"key_file"` } type rawMeshtasticConfig struct { PSK *string `yaml:"psk"` } type rawDatabaseConfig struct { Driver *string `yaml:"driver"` SQLite *rawSQLiteConfig `yaml:"sqlite"` MySQL *rawMySQLConfig `yaml:"mysql"` } type rawSQLiteConfig struct { Path *string `yaml:"path"` } type rawMySQLConfig struct { DSN *string `yaml:"dsn"` } type rawWebConfig struct { Enabled *bool `yaml:"enabled"` Host *string `yaml:"host"` Port *int `yaml:"port"` SocketPath *string `yaml:"socket_path"` StaticDir *string `yaml:"static_dir"` Admin *rawWebAdminConfig `yaml:"admin"` } type rawWebAdminConfig struct { Username *string `yaml:"username"` Password *string `yaml:"password"` SessionSecret *string `yaml:"session_secret"` SessionSecure *bool `yaml:"session_secure"` } // defaultConfig 返回内置默认配置。 func defaultConfig() *config { return &config{ MQTT: mqttConfig{ Host: "0.0.0.0", Port: 1883, TLS: tlsConfig{ Enabled: false, CertFile: "", KeyFile: "", }, }, Meshtastic: meshtasticConfig{ PSK: "AQ==", }, Database: databaseConfig{ Driver: "sqlite", SQLite: sqliteConfig{Path: defaultSQLitePath()}, MySQL: mysqlConfig{DSN: ""}, }, Web: webConfig{ Enabled: true, Host: "0.0.0.0", Port: 8080, SocketPath: defaultWebSocketPath(), StaticDir: "./dist", Admin: webAdminConfig{ Username: "admin", Password: "admin", SessionSecret: "", SessionSecure: false, }, }, } } // defaultConfigDir 根据操作系统返回配置目录。 func defaultConfigDir() string { if runtime.GOOS == "windows" { return filepath.Join(".", "win", "etc", "mesh_mqtt_go") } return filepath.Join(string(filepath.Separator), "etc", "mesh_mqtt_go") } // defaultConfigPath 返回默认配置文件路径。 func defaultConfigPath() string { return filepath.Join(defaultConfigDir(), configFileName) } func defaultSQLitePath() string { return defaultSQLitePathForGOOS(runtime.GOOS) } func defaultWebSocketPath() string { return defaultWebSocketPathForGOOS(runtime.GOOS) } func defaultWebSocketPathForGOOS(goos string) string { if goos == "windows" { return "" } return filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock") } func clearWebSocketPathOnUnsupportedGOOS(cfg *config, goos string) bool { if goos != "windows" || cfg.Web.SocketPath == "" { return false } cfg.Web.SocketPath = "" return true } func defaultSQLitePathForGOOS(goos string) string { if goos == "windows" { return filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db") } return filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go", "mesh_mqtt_go.db") } // loadConfig 加载配置文件;文件不存在时生成,字段缺失时自动补全并写回。 func loadConfig(path string) (*config, error) { if path == "" { path = defaultConfigPath() } if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return nil, fmt.Errorf("create config directory %s: %w", filepath.Dir(path), err) } if _, err := os.Stat(path); err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("stat config file %s: %w", path, err) } cfg := defaultConfig() if err := writeConfig(path, cfg); err != nil { return nil, err } return cfg, nil } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config file %s: %w", path, err) } var raw rawConfig if err := yaml.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("parse config file %s: %w", path, err) } cfg, changed := normalizeConfig(raw) if clearWebSocketPathOnUnsupportedGOOS(cfg, runtime.GOOS) { changed = true } if err := validateConfig(cfg); err != nil { return nil, err } if changed { if err := writeConfig(path, cfg); err != nil { return nil, err } } return cfg, nil } // normalizeConfig 将原始配置合并到默认配置,并标记是否补齐了缺失项。 func normalizeConfig(raw rawConfig) (*config, bool) { cfg := defaultConfig() changed := false if raw.MQTT == nil { changed = true } else { if raw.MQTT.Host == nil { changed = true } else { cfg.MQTT.Host = *raw.MQTT.Host } if raw.MQTT.Port == nil { changed = true } else { cfg.MQTT.Port = *raw.MQTT.Port } if raw.MQTT.TLS == nil { changed = true } else { if raw.MQTT.TLS.Enabled == nil { changed = true } else { cfg.MQTT.TLS.Enabled = *raw.MQTT.TLS.Enabled } if raw.MQTT.TLS.CertFile == nil { changed = true } else { cfg.MQTT.TLS.CertFile = *raw.MQTT.TLS.CertFile } if raw.MQTT.TLS.KeyFile == nil { changed = true } else { cfg.MQTT.TLS.KeyFile = *raw.MQTT.TLS.KeyFile } } } if raw.Meshtastic == nil { changed = true } else if raw.Meshtastic.PSK == nil { changed = true } else { cfg.Meshtastic.PSK = *raw.Meshtastic.PSK } if raw.Database == nil { changed = true } else { if raw.Database.Driver == nil { changed = true } else { cfg.Database.Driver = *raw.Database.Driver } if raw.Database.SQLite == nil { changed = true } else if raw.Database.SQLite.Path == nil { changed = true } else { cfg.Database.SQLite.Path = *raw.Database.SQLite.Path } if raw.Database.MySQL == nil { changed = true } else if raw.Database.MySQL.DSN == nil { changed = true } else { cfg.Database.MySQL.DSN = *raw.Database.MySQL.DSN } } if raw.Web == nil { changed = true } else { if raw.Web.Enabled == nil { changed = true } else { cfg.Web.Enabled = *raw.Web.Enabled } if raw.Web.Host == nil { changed = true } else { cfg.Web.Host = *raw.Web.Host } if raw.Web.Port == nil { changed = true } else { cfg.Web.Port = *raw.Web.Port } if raw.Web.SocketPath == nil { changed = true } else { cfg.Web.SocketPath = *raw.Web.SocketPath } if raw.Web.StaticDir == nil { changed = true } else { cfg.Web.StaticDir = *raw.Web.StaticDir } if raw.Web.Admin == nil { changed = true } else { if raw.Web.Admin.Username == nil { changed = true } else { cfg.Web.Admin.Username = *raw.Web.Admin.Username } if raw.Web.Admin.Password == nil { changed = true } else { cfg.Web.Admin.Password = *raw.Web.Admin.Password } if raw.Web.Admin.SessionSecret == nil { changed = true } else { cfg.Web.Admin.SessionSecret = *raw.Web.Admin.SessionSecret } if raw.Web.Admin.SessionSecure == nil { changed = true } else { cfg.Web.Admin.SessionSecure = *raw.Web.Admin.SessionSecure } } } return cfg, changed } func validateConfig(cfg *config) error { if cfg.MQTT.Port <= 0 || cfg.MQTT.Port > 65535 { return fmt.Errorf("invalid mqtt port %d: must be 1-65535", cfg.MQTT.Port) } switch cfg.Database.Driver { case "sqlite": if cfg.Database.SQLite.Path == "" { return fmt.Errorf("database.sqlite.path is required when database.driver is sqlite") } case "mysql": if cfg.Database.MySQL.DSN == "" { return fmt.Errorf("database.mysql.dsn is required when database.driver is mysql") } default: return fmt.Errorf("invalid database.driver %q: must be sqlite or mysql", cfg.Database.Driver) } if cfg.Web.Enabled { if cfg.Web.SocketPath == "" && (cfg.Web.Port <= 0 || cfg.Web.Port > 65535) { return fmt.Errorf("invalid web port %d: must be 1-65535", cfg.Web.Port) } if cfg.Web.StaticDir == "" { return fmt.Errorf("web.static_dir is required when web is enabled") } if cfg.Web.Admin.Username == "" { return fmt.Errorf("web.admin.username is required when web is enabled") } if cfg.Web.Admin.Password == "" { return fmt.Errorf("web.admin.password is required when web is enabled") } } return nil } func writeConfig(path string, cfg *config) error { data, err := yaml.Marshal(cfg) if err != nil { return fmt.Errorf("encode config file %s: %w", path, err) } if err := os.WriteFile(path, data, 0644); err != nil { return fmt.Errorf("write config file %s: %w", path, err) } return nil } // buildTLSConfig 根据配置构造 mochi listener 使用的 TLS 设置。 func buildTLSConfig(cfg tlsConfig) (*cryptotls.Config, error) { if !cfg.Enabled { return nil, nil } if cfg.CertFile == "" { return nil, fmt.Errorf("mqtt tls cert_file is required when tls is enabled") } if cfg.KeyFile == "" { return nil, fmt.Errorf("mqtt tls key_file is required when tls is enabled") } cert, err := cryptotls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) if err != nil { return nil, fmt.Errorf("load mqtt tls certificate: %w", err) } return &cryptotls.Config{ MinVersion: cryptotls.VersionTLS12, Certificates: []cryptotls.Certificate{cert}, }, nil }