From 40ef449f6ed7cd2bf650af7051dfc04396276d9b Mon Sep 17 00:00:00 2001 From: rubenseyer Date: Mon, 12 Feb 2018 22:19:42 +0100 Subject: [PATCH 1/4] Config system (see also issue #21) Removes synchronizing set config keys to freezelog, since the system is the preferred way to persist configuration. --- cmd/grumble/args.go | 24 +++++----- cmd/grumble/freeze.go | 32 +------------- cmd/grumble/grumble.go | 65 ++++++++++++++++++++++++--- cmd/grumble/murmurdb.go | 2 +- cmd/grumble/server.go | 27 ++++-------- go.mod | 1 + go.sum | 2 + pkg/serverconf/config.go | 53 +++++++++++++++------- pkg/serverconf/config_test.go | 8 ++-- pkg/serverconf/file.go | 54 +++++++++++++++++++++++ pkg/serverconf/file_ini.go | 83 +++++++++++++++++++++++++++++++++++ 11 files changed, 263 insertions(+), 88 deletions(-) create mode 100644 pkg/serverconf/file.go create mode 100644 pkg/serverconf/file_ini.go diff --git a/cmd/grumble/args.go b/cmd/grumble/args.go index 4b2331c..816c196 100644 --- a/cmd/grumble/args.go +++ b/cmd/grumble/args.go @@ -30,6 +30,9 @@ var usageTmpl = `usage: grumble [options] --log (default: $DATADIR/grumble.log) Log file path. + --ini (default: $DATADIR/grumble.ini) + Config file path. + --regen-keys Force grumble to regenerate its global RSA keypair (and certificate). @@ -46,12 +49,13 @@ var usageTmpl = `usage: grumble [options] ` type args struct { - ShowHelp bool - DataDir string - LogPath string - RegenKeys bool - SQLiteDB string - CleanUp bool + ShowHelp bool + DataDir string + LogPath string + ConfigPath string + RegenKeys bool + SQLiteDB string + CleanUp bool } func defaultDataDir() string { @@ -63,10 +67,6 @@ func defaultDataDir() string { return filepath.Join(homedir, dirname) } -func defaultLogPath() string { - return filepath.Join(defaultDataDir(), "grumble.log") -} - func Usage() { t, err := template.New("usage").Parse(usageTmpl) if err != nil { @@ -92,7 +92,9 @@ func init() { flag.BoolVar(&Args.ShowHelp, "help", false, "") flag.StringVar(&Args.DataDir, "datadir", defaultDataDir(), "") - flag.StringVar(&Args.LogPath, "log", defaultLogPath(), "") + flag.StringVar(&Args.LogPath, "log", "", "") + flag.StringVar(&Args.ConfigPath, "ini", "", "") + flag.BoolVar(&Args.RegenKeys, "regen-keys", false, "") flag.StringVar(&Args.SQLiteDB, "import-murmurdb", "", "") diff --git a/cmd/grumble/freeze.go b/cmd/grumble/freeze.go index a8dbcfe..1729584 100644 --- a/cmd/grumble/freeze.go +++ b/cmd/grumble/freeze.go @@ -19,7 +19,6 @@ import ( "mumble.info/grumble/pkg/ban" "mumble.info/grumble/pkg/freezer" "mumble.info/grumble/pkg/mumbleproto" - "mumble.info/grumble/pkg/serverconf" ) // Freeze a server to disk and closes the log file. @@ -74,7 +73,7 @@ func (server *Server) Freeze() (fs *freezer.Server, err error) { fs = new(freezer.Server) // Freeze all config kv-pairs - allCfg := server.cfg.GetAll() + allCfg := server.cfg.GetAllPersistent() for k, v := range allCfg { fs.Config = append(fs.Config, &freezer.ConfigKeyValuePair{ Key: proto.String(k), @@ -420,11 +419,10 @@ func NewServerFromFrozen(name string) (s *Server, err error) { } } - s, err = NewServer(id) + s, err = NewServer(id, configFile.ServerConfig(id, cfgMap)) if err != nil { return nil, err } - s.cfg = serverconf.New(cfgMap) // Unfreeze the server's frozen bans. s.UnfreezeBanList(fs.BanList) @@ -835,29 +833,3 @@ func (server *Server) UpdateFrozenBans(bans []ban.Ban) { } server.numLogOps += 1 } - -// UpdateConfig writes an updated config value to the datastore. -func (server *Server) UpdateConfig(key, value string) { - fcfg := &freezer.ConfigKeyValuePair{ - Key: proto.String(key), - Value: proto.String(value), - } - err := server.freezelog.Put(fcfg) - if err != nil { - server.Fatal(err) - } - server.numLogOps += 1 -} - -// ResetConfig writes to the freezelog that the config with key -// has been reset to its default value. -func (server *Server) ResetConfig(key string) { - fcfg := &freezer.ConfigKeyValuePair{ - Key: proto.String(key), - } - err := server.freezelog.Put(fcfg) - if err != nil { - server.Fatal(err) - } - server.numLogOps += 1 -} diff --git a/cmd/grumble/grumble.go b/cmd/grumble/grumble.go index 139eadc..7ec0f65 100644 --- a/cmd/grumble/grumble.go +++ b/cmd/grumble/grumble.go @@ -14,10 +14,12 @@ import ( "mumble.info/grumble/pkg/blobstore" "mumble.info/grumble/pkg/logtarget" + "mumble.info/grumble/pkg/serverconf" ) var servers map[int64]*Server var blobStore blobstore.BlobStore +var configFile *serverconf.ConfigFile func main() { var err error @@ -36,10 +38,42 @@ func main() { } dataDir.Close() + // Open the config file + var configFn string + if Args.ConfigPath != "" { + configFn = Args.ConfigPath + } else { + configFn = filepath.Join(Args.DataDir, "grumble.ini") + } + if filepath.Ext(configFn) == ".ini" { + // Create it if it doesn't exist + configFd, err := os.OpenFile(configFn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700) + if err == nil { + configFd.WriteString(serverconf.DefaultConfigFile) + log.Fatalf("Default config written to %v\n", configFn) + configFd.Close() + } else if err != nil && !os.IsExist(err) { + log.Fatalf("Unable to open config file (%v): %v", configFn, err) + return + } + } + configFile, err = serverconf.NewConfigFile(configFn) + if err != nil { + log.Fatalf("Unable to open config file (%v): %v", configFn, err) + return + } + config := configFile.GlobalConfig() + // Set up logging + var logFn string + if Args.LogPath != "" { + logFn = Args.LogPath + } else { + logFn = config.PathValue("LogPath", Args.DataDir) + } logtarget.Default, err = logtarget.OpenFile(Args.LogPath, os.Stderr) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to open log file (%v): %v", Args.LogPath, err) + fmt.Fprintf(os.Stderr, "Unable to open log file (%v): %v", logFn, err) return } log.SetPrefix("[G] ") @@ -48,6 +82,23 @@ func main() { log.Printf("Grumble") log.Printf("Using data directory: %s", Args.DataDir) + // Warn on some unsupported configuration options for users migrating from Murmur + if config.StringValue("database") != "" { + log.Println("* Grumble does not yet support Murmur databases directly (see issue #21 on github).") + if driver := config.StringValue("dbDriver"); driver == "QSQLITE" { + log.Println(" To convert a previous SQLite database, use the --import-murmurdb flag.") + } + } + if config.StringValue("sslDHParams") != "" { + log.Println("* Go does not implement DHE modes in TLS, so the configured dhparams are ignored.") + } + if config.StringValue("ice") != "" { + log.Println("* Grumble does not support ZeroC ICE.") + } + if config.StringValue("grpc") != "" { + log.Println("* Grumble does not yet support gRPC (see issue #23 on github).") + } + // Open the blobstore. If the directory doesn't // already exist, create the directory and open // the blobstore. @@ -65,9 +116,9 @@ func main() { // and corresponding certificate. // These are used as the default certificate of all virtual servers // and the SSH admin console, but can be overridden using the "key" - // and "cert" arguments to Grumble. - certFn := filepath.Join(Args.DataDir, "cert.pem") - keyFn := filepath.Join(Args.DataDir, "key.pem") + // and "cert" arguments to Grumble. todo(rubenseyer) implement override by cli + certFn := config.PathValue("CertPath", Args.DataDir) + keyFn := config.PathValue("KeyPath", Args.DataDir) shouldRegen := false if Args.RegenKeys { shouldRegen = true @@ -164,10 +215,10 @@ func main() { if err != nil { log.Fatalf("Unable to read file from data directory: %v", err.Error()) } - // The data dir file descriptor. + // The servers dir file descriptor. err = serversDir.Close() if err != nil { - log.Fatalf("Unable to close data directory: %v", err.Error()) + log.Fatalf("Unable to close servers directory: %v", err.Error()) return } @@ -191,7 +242,7 @@ func main() { // If no servers were found, create the default virtual server. if len(servers) == 0 { - s, err := NewServer(1) + s, err := NewServer(1, configFile.ServerConfig(1, nil)) if err != nil { log.Fatalf("Couldn't start server: %s", err.Error()) } diff --git a/cmd/grumble/murmurdb.go b/cmd/grumble/murmurdb.go index 83cd2c9..61bcf5c 100644 --- a/cmd/grumble/murmurdb.go +++ b/cmd/grumble/murmurdb.go @@ -86,7 +86,7 @@ func MurmurImport(filename string) (err error) { // Create a new Server from a Murmur SQLite database func NewServerFromSQLite(id int64, db *sql.DB) (s *Server, err error) { - s, err = NewServer(id) + s, err = NewServer(id, nil) if err != nil { return nil, err } diff --git a/cmd/grumble/server.go b/cmd/grumble/server.go index d46552d..32cc31c 100644 --- a/cmd/grumble/server.go +++ b/cmd/grumble/server.go @@ -19,7 +19,6 @@ import ( "log" "net" "net/http" - "path/filepath" "strings" "sync" "time" @@ -138,12 +137,16 @@ func (lf clientLogForwarder) Write(incoming []byte) (int, error) { } // Allocate a new Murmur instance -func NewServer(id int64) (s *Server, err error) { +func NewServer(id int64, config *serverconf.Config) (s *Server, err error) { s = new(Server) s.Id = id - s.cfg = serverconf.New(nil) + if config == nil { + s.cfg = serverconf.New(nil, nil) + } else { + s.cfg = config + } s.Users = make(map[uint32]*User) s.UserCertMap = make(map[string]*User) @@ -191,10 +194,6 @@ func (server *Server) setConfigPassword(key, password string) { // Could be racy, but shouldn't really matter... val := "sha1$" + salt + "$" + digest server.cfg.Set(key, val) - - if server.cfgUpdate != nil { - server.cfgUpdate <- &KeyValuePair{Key: key, Value: val} - } } // SetSuperUserPassword sets password as the new SuperUser password @@ -436,14 +435,6 @@ func (server *Server) handlerLoop() { case client := <-server.clientAuthenticated: server.finishAuthenticate(client) - // Disk freeze config update - case kvp := <-server.cfgUpdate: - if !kvp.Reset { - server.UpdateConfig(kvp.Key, kvp.Value) - } else { - server.ResetConfig(kvp.Key) - } - // Server registration update // Tick every hour + a minute offset based on the server id. case <-regtick: @@ -1356,7 +1347,6 @@ func (server *Server) initPerLaunchData() { server.bye = make(chan bool) server.incoming = make(chan *Message) server.voicebroadcast = make(chan *VoiceBroadcast) - server.cfgUpdate = make(chan *KeyValuePair) server.tempRemove = make(chan *Channel, 1) server.clientAuthenticated = make(chan *Client) } @@ -1371,7 +1361,6 @@ func (server *Server) cleanPerLaunchData() { server.bye = nil server.incoming = nil server.voicebroadcast = nil - server.cfgUpdate = nil server.tempRemove = nil server.clientAuthenticated = nil } @@ -1460,8 +1449,8 @@ func (server *Server) Start() (err error) { */ // Wrap a TLS listener around the TCP connection - certFn := filepath.Join(Args.DataDir, "cert.pem") - keyFn := filepath.Join(Args.DataDir, "key.pem") + certFn := server.cfg.PathValue("CertPath", Args.DataDir) + keyFn := server.cfg.PathValue("KeyPath", Args.DataDir) cert, err := tls.LoadX509KeyPair(certFn, keyFn) if err != nil { return err diff --git a/go.mod b/go.mod index d675daa..e1c01ea 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/golang/protobuf v1.3.5 github.com/gorilla/websocket v1.4.2 golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 + gopkg.in/ini.v1 v1.55.0 ) diff --git a/go.sum b/go.sum index b388e29..95f6eeb 100644 --- a/go.sum +++ b/go.sum @@ -10,3 +10,5 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= +gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/pkg/serverconf/config.go b/pkg/serverconf/config.go index 0eb26d1..258b04f 100644 --- a/pkg/serverconf/config.go +++ b/pkg/serverconf/config.go @@ -5,6 +5,7 @@ package serverconf import ( + "path/filepath" "strconv" "sync" ) @@ -20,29 +21,34 @@ var defaultCfg = map[string]string{ "RememberChannel": "true", "WelcomeText": "Welcome to this server running Grumble.", "SendVersion": "true", + "LogPath": "grumble.log", + "CertPath": "cert.pem", + "KeyPath": "key.pem", } type Config struct { - cfgMap map[string]string - mutex sync.RWMutex + fallbackMap map[string]string + persistentMap map[string]string + mutex sync.RWMutex } -// Create a new Config using cfgMap as the intial internal config map. -// If cfgMap is nil, ConfigWithMap will create a new config map. -func New(cfgMap map[string]string) *Config { - if cfgMap == nil { - cfgMap = make(map[string]string) +// New returns a new Config using persistentMap as the initial internal config map. +// The map persistentMap may not be reused. If set to nil, a new map is created. +// Optionally, defaults may be passed in fallbackMap. This map is only read, not written. +func New(persistentMap, fallbackMap map[string]string) *Config { + if persistentMap == nil { + persistentMap = make(map[string]string) } - return &Config{cfgMap: cfgMap} + return &Config{persistentMap: persistentMap, fallbackMap: fallbackMap} } -// GetAll gets a copy of the Config's internal config map -func (cfg *Config) GetAll() (all map[string]string) { +// GetAllPersistent returns a copy of the internal persistent key-value map. +func (cfg *Config) GetAllPersistent() (all map[string]string) { cfg.mutex.RLock() defer cfg.mutex.RUnlock() all = make(map[string]string) - for k, v := range cfg.cfgMap { + for k, v := range cfg.persistentMap { all[k] = v } return @@ -52,14 +58,14 @@ func (cfg *Config) GetAll() (all map[string]string) { func (cfg *Config) Set(key string, value string) { cfg.mutex.Lock() defer cfg.mutex.Unlock() - cfg.cfgMap[key] = value + cfg.persistentMap[key] = value } // Reset the value of a config key func (cfg *Config) Reset(key string) { cfg.mutex.Lock() defer cfg.mutex.Unlock() - delete(cfg.cfgMap, key) + delete(cfg.persistentMap, key) } // StringValue gets the value of a specific config key encoded as a string @@ -67,7 +73,12 @@ func (cfg *Config) StringValue(key string) (value string) { cfg.mutex.RLock() defer cfg.mutex.RUnlock() - value, exists := cfg.cfgMap[key] + value, exists := cfg.persistentMap[key] + if exists { + return value + } + + value, exists = cfg.fallbackMap[key] if exists { return value } @@ -80,7 +91,7 @@ func (cfg *Config) StringValue(key string) (value string) { return "" } -// IntValue gets the value of a speific config key as an int +// Get the value of a specific config key as an int func (cfg *Config) IntValue(key string) (intval int) { str := cfg.StringValue(key) intval, _ = strconv.Atoi(str) @@ -94,9 +105,19 @@ func (cfg *Config) Uint32Value(key string) (uint32val uint32) { return uint32(uintval) } -// BoolValue gets the value fo a sepcific config key as a bool +// Get the value of a specific config key as a bool func (cfg *Config) BoolValue(key string) (boolval bool) { str := cfg.StringValue(key) boolval, _ = strconv.ParseBool(str) return } + +// Get the value of a specific config key as a path, +// joined with the path in rel if not absolute. +func (cfg *Config) PathValue(key string, rel string) (path string) { + str := cfg.StringValue(key) + if filepath.IsAbs(str) { + return filepath.Clean(str) + } + return filepath.Join(rel, str) +} diff --git a/pkg/serverconf/config_test.go b/pkg/serverconf/config_test.go index 04cadf4..17054cf 100644 --- a/pkg/serverconf/config_test.go +++ b/pkg/serverconf/config_test.go @@ -9,7 +9,7 @@ import ( ) func TestIntValue(t *testing.T) { - cfg := New(nil) + cfg := New(nil, nil) cfg.Set("Test", "13") if cfg.IntValue("Test") != 13 { t.Errorf("Expected 13") @@ -17,7 +17,7 @@ func TestIntValue(t *testing.T) { } func TestFloatAsInt(t *testing.T) { - cfg := New(nil) + cfg := New(nil, nil) cfg.Set("Test", "13.4") if cfg.IntValue("Test") != 0 { t.Errorf("Expected 0") @@ -25,14 +25,14 @@ func TestFloatAsInt(t *testing.T) { } func TestDefaultValue(t *testing.T) { - cfg := New(nil) + cfg := New(nil, nil) if cfg.IntValue("MaxBandwidth") != 72000 { t.Errorf("Expected 72000") } } func TestBoolValue(t *testing.T) { - cfg := New(nil) + cfg := New(nil, nil) cfg.Set("DoStuffOnStartup", "true") if cfg.BoolValue("DoStuffOnStartup") != true { t.Errorf("Expected true") diff --git a/pkg/serverconf/file.go b/pkg/serverconf/file.go new file mode 100644 index 0000000..2fc04eb --- /dev/null +++ b/pkg/serverconf/file.go @@ -0,0 +1,54 @@ +package serverconf + +import ( + "strconv" +) + +type cfg interface { + // GlobalMap returns a copy of the top-level (global) configuration map. + GlobalMap() map[string]string +} + +type ConfigFile struct { + cfg +} + +func NewConfigFile(path string) (*ConfigFile, error) { + var f cfg + f, err := newinicfg(path) + if err != nil { + return nil, err + } + return &ConfigFile{f}, nil +} + +// GlobalConfig returns a new *serverconf.Config representing the top-level +// (global) configuration. +func (c *ConfigFile) GlobalConfig() *Config { + return New(nil, c.GlobalMap()) +} + +// ServerConfig returns a new *serverconf.Config with the fallback representing +// the global configuration with server-specific values incremented by id. +// Optionally a persistent map which has priority may be passed. This map +// is consumed and cannot be reused. +func (c *ConfigFile) ServerConfig(id int64, persistentMap map[string]string) *Config { + m := c.GlobalMap() + + // Some server specific values from the global config must be offset. + // These are read differently by the server as well. + if v, ok := m["Port"]; ok { + i, err := strconv.ParseInt(v, 10, 64) + if err == nil { + m["Port"] = strconv.FormatInt(i+id-1, 10) + } + } + if v, ok := m["WebPort"]; ok { + i, err := strconv.ParseInt(v, 10, 64) + if err == nil { + m["WebPort"] = strconv.FormatInt(i+id-1, 10) + } + } + + return New(persistentMap, m) +} diff --git a/pkg/serverconf/file_ini.go b/pkg/serverconf/file_ini.go new file mode 100644 index 0000000..8709b30 --- /dev/null +++ b/pkg/serverconf/file_ini.go @@ -0,0 +1,83 @@ +package serverconf + +import ( + "gopkg.in/ini.v1" +) + +type inicfg struct { + file *ini.File +} + +func newinicfg(path string) (*inicfg, error) { + file, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true, UnescapeValueDoubleQuotes: true}, path) + if err != nil { + return nil, err + } + file.BlockMode = false // read only, avoid locking + return &inicfg{file}, nil +} + +func (f *inicfg) GlobalMap() map[string]string { + return f.file.Section("").KeysHash() +} + +// todo(rubenseyer): not all of these even work.. make sure to implement them +var DefaultConfigFile = `# Grumble configuration file. +# +# The commented out settings represent the defaults. +# Options here may be overridden by virtual server specific configuration. +# Make sure to enclose values containing # or ; in double quotes or backticks. + +# Address to bind the listeners to. +#Address = 0.0.0.0 + +# Port is the port to bind the native Mumble protocol to. +# WebPort is the port to bind the WebSocket Mumble protocol to. +# They are incremented for each virtual server. +#Port = 64738 +#WebPort = 443 + +# "Message of the day" HTML string sent to connecting clients. +#WelcomeText = "Welcome to this server running Grumble." + +# Maximum bandwidth (in bits per second) per client for voice. +# Grumble does not yet enforce this limit, but some clients nicely follow it. +#MaxBandwidth = 72000 + +# Maximum number of concurrent clients. +#MaxUsers = 1000 +#MaxUsersPerChannel = 0 + +#MaxTextMessageLength = 5000 +#MaxImageMessageLength = 131072 +#AllowHTML + +# DefaultChannel is the channel (by ID) new users join. +# The root channel is the default. +#DefaultChannel = 0 + +# Whether users will rejoin the last channel they were in. +#RememberChannel + +# Whether to include server version and server os in ping response. +#SendVersion +#SendOSInfo + +# Path to the log file (relative to the data directory). +#LogPath = grumble.log + +# Path to TLS certificate and key (relative to the data directory). +# The certificate needs to have the entire chain concatenated to be validate. +# If these paths do not exist, Grumble will autogenerate a certificate. +#CertPath = cert.pem +#KeyPath = key.pem + +# Options for public server registration. +# All of these have to be set to make the server public. +# RegisterName additionally sets the name of the root channel. +# RegisterPassword is a simple, arbitrary secret to guard your registration. Don't lose it. +#RegisterName = +#RegisterHost = +#RegisterPassword = +#RegisterWebUrl = +` From 693dd6f4e8b82c5c490c9d67bd5bba9f2509272e Mon Sep 17 00:00:00 2001 From: rubenseyer Date: Wed, 14 Feb 2018 15:29:16 +0100 Subject: [PATCH 2/4] SuperUser CLI flags, fix #20 Also adds some Murmur CLI flags, see #25 Since the only way right now to set the SuperUser pw is through the CLI on start, this commit also drops the config updates through the freezelog (probably a remnant of RPC), which the SetSuperUserPassword function was the last user of. --- cmd/grumble/args.go | 37 +++++++++++++++++++++++++++++-------- cmd/grumble/grumble.go | 40 +++++++++++++++++++++++++++++++++++++--- cmd/grumble/server.go | 2 +- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/cmd/grumble/args.go b/cmd/grumble/args.go index 816c196..e0cbd7d 100644 --- a/cmd/grumble/args.go +++ b/cmd/grumble/args.go @@ -21,7 +21,7 @@ var usageTmpl = `usage: grumble [options] grumble {{.Version}} ({{.BuildDate}}) target: {{.OS}}, {{.Arch}} - --help + --help, --version Shows this help listing. --datadir (default: {{.DefaultDataDir}}) @@ -33,6 +33,17 @@ var usageTmpl = `usage: grumble [options] --ini (default: $DATADIR/grumble.ini) Config file path. + --supw [server-id] + Set password for SuperUser account. Optionally takes + the virtual server to modify as the first positional argument. + + --readsupw [server-id] + Like --supw, but reads from stdin instead. + + --disablesu [server-id] + Disables the SuperUser account. Optionally takes + the virtual server to modify as the first positional argument. + --regen-keys Force grumble to regenerate its global RSA keypair (and certificate). @@ -49,13 +60,17 @@ var usageTmpl = `usage: grumble [options] ` type args struct { - ShowHelp bool - DataDir string - LogPath string - ConfigPath string - RegenKeys bool - SQLiteDB string - CleanUp bool + ShowHelp bool + DataDir string + LogPath string + ConfigPath string + SuperUserPW string + ReadPass bool + DisablePass bool + RegenKeys bool + ServerId int64 + SQLiteDB string + CleanUp bool } func defaultDataDir() string { @@ -90,11 +105,17 @@ var Args args func init() { flag.Usage = Usage + flag.BoolVar(&Args.ShowHelp, "version", false, "") flag.BoolVar(&Args.ShowHelp, "help", false, "") + flag.StringVar(&Args.DataDir, "datadir", defaultDataDir(), "") flag.StringVar(&Args.LogPath, "log", "", "") flag.StringVar(&Args.ConfigPath, "ini", "", "") + flag.StringVar(&Args.SuperUserPW, "supw", "", "") + flag.BoolVar(&Args.ReadPass, "readsupw", false, "") + flag.BoolVar(&Args.DisablePass, "disablesu", false, "") + flag.BoolVar(&Args.RegenKeys, "regen-keys", false, "") flag.StringVar(&Args.SQLiteDB, "import-murmurdb", "", "") diff --git a/cmd/grumble/grumble.go b/cmd/grumble/grumble.go index 7ec0f65..fcd5eb2 100644 --- a/cmd/grumble/grumble.go +++ b/cmd/grumble/grumble.go @@ -7,10 +7,12 @@ package main import ( "flag" "fmt" + "io/ioutil" "log" "os" "path/filepath" "regexp" + "strconv" "mumble.info/grumble/pkg/blobstore" "mumble.info/grumble/pkg/logtarget" @@ -29,6 +31,20 @@ func main() { Usage() return } + if Args.ReadPass { + data, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Fatalf("Failed to read password from stdin: %v", err) + } + Args.SuperUserPW = string(data) + } + if flag.NArg() > 0 && (Args.SuperUserPW != "" || Args.DisablePass) { + Args.ServerId, err = strconv.ParseInt(flag.Arg(0), 10, 64) + if err != nil { + log.Fatalf("Failed to parse server id %v: %v", flag.Arg(0), err) + return + } + } // Open the data dir to check whether it exists. dataDir, err := os.Open(Args.DataDir) @@ -114,9 +130,7 @@ func main() { // Check whether we should regenerate the default global keypair // and corresponding certificate. - // These are used as the default certificate of all virtual servers - // and the SSH admin console, but can be overridden using the "key" - // and "cert" arguments to Grumble. todo(rubenseyer) implement override by cli + // These are used as the default certificate of all virtual servers. certFn := config.PathValue("CertPath", Args.DataDir) keyFn := config.PathValue("KeyPath", Args.DataDir) shouldRegen := false @@ -232,6 +246,18 @@ func main() { if err != nil { log.Fatalf("Unable to load server: %v", err.Error()) } + + // Check if SuperUser password should be updated. + if Args.ServerId == 0 || Args.ServerId == s.Id { + if Args.DisablePass { + s.cfg.Reset("SuperUserPassword") + log.Printf("Disabled SuperUser for server %v", name) + } else if Args.SuperUserPW != "" { + s.SetSuperUserPassword(Args.SuperUserPW) + log.Printf("Set SuperUser password for server %v", name) + } + } + err = s.FreezeToFile() if err != nil { log.Fatalf("Unable to freeze server to disk: %v", err.Error()) @@ -240,6 +266,14 @@ func main() { } } + // If SuperUser password flags were passed, the servers should not start. + if Args.SuperUserPW != "" || Args.DisablePass { + if len(servers) == 0 { + log.Fatalf("No servers found to set password for") + } + return + } + // If no servers were found, create the default virtual server. if len(servers) == 0 { s, err := NewServer(1, configFile.ServerConfig(1, nil)) diff --git a/cmd/grumble/server.go b/cmd/grumble/server.go index 32cc31c..672ff4f 100644 --- a/cmd/grumble/server.go +++ b/cmd/grumble/server.go @@ -74,8 +74,8 @@ type Server struct { incoming chan *Message voicebroadcast chan *VoiceBroadcast - cfgUpdate chan *KeyValuePair tempRemove chan *Channel + //cfgUpdate chan *KeyValuePair // Signals to the server that a client has been successfully // authenticated. From ae41a612ba00d34d540867a5b672e561a1d0b66c Mon Sep 17 00:00:00 2001 From: rubenseyer Date: Wed, 14 Feb 2018 20:50:09 +0100 Subject: [PATCH 3/4] Minor features for parity with murmur.ini MaxUsers: modifies existing sessionpool similar to Murmur MaxUsersPerChannel: already implemented, inconsistent name AllowPing: affects registration, too DefaultChannel RememberChannel ServerPassword SendOSInfo: already implemented, inconsistent name Config keys are renamed to conform to murmur.ini --- cmd/grumble/client.go | 2 +- cmd/grumble/grumble.go | 6 +-- cmd/grumble/message.go | 4 +- cmd/grumble/register.go | 21 ++++---- cmd/grumble/server.go | 82 ++++++++++++++++++++--------- pkg/serverconf/cipherlist.go | 77 +++++++++++++++++++++++++++ pkg/serverconf/config.go | 27 +++++----- pkg/serverconf/config_test.go | 2 +- pkg/serverconf/file.go | 8 +-- pkg/serverconf/file_ini.go | 69 +++++++++++++----------- pkg/sessionpool/sessionpool.go | 20 ++++--- pkg/sessionpool/sessionpool_test.go | 32 ++++++----- 12 files changed, 241 insertions(+), 109 deletions(-) create mode 100644 pkg/serverconf/cipherlist.go diff --git a/cmd/grumble/client.go b/cmd/grumble/client.go index a2f535a..ca8f582 100644 --- a/cmd/grumble/client.go +++ b/cmd/grumble/client.go @@ -510,7 +510,7 @@ func (client *Client) tlsRecvLoop() { Release: proto.String("Grumble"), CryptoModes: cryptstate.SupportedModes(), } - if client.server.cfg.BoolValue("SendOSInfo") { + if client.server.cfg.BoolValue("sendversion") { version.Os = proto.String(runtime.GOOS) version.OsVersion = proto.String("(Unknown version)") } diff --git a/cmd/grumble/grumble.go b/cmd/grumble/grumble.go index fcd5eb2..af6fe03 100644 --- a/cmd/grumble/grumble.go +++ b/cmd/grumble/grumble.go @@ -85,7 +85,7 @@ func main() { if Args.LogPath != "" { logFn = Args.LogPath } else { - logFn = config.PathValue("LogPath", Args.DataDir) + logFn = config.PathValue("logfile", Args.DataDir) } logtarget.Default, err = logtarget.OpenFile(Args.LogPath, os.Stderr) if err != nil { @@ -131,8 +131,8 @@ func main() { // Check whether we should regenerate the default global keypair // and corresponding certificate. // These are used as the default certificate of all virtual servers. - certFn := config.PathValue("CertPath", Args.DataDir) - keyFn := config.PathValue("KeyPath", Args.DataDir) + certFn := config.PathValue("sslCert", Args.DataDir) + keyFn := config.PathValue("sslKey", Args.DataDir) shouldRegen := false if Args.RegenKeys { shouldRegen = true diff --git a/cmd/grumble/message.go b/cmd/grumble/message.go index 417faba..3b04b1c 100644 --- a/cmd/grumble/message.go +++ b/cmd/grumble/message.go @@ -593,7 +593,7 @@ func (server *Server) handleUserStateMessage(client *Client, msg *Message) { return } - maxChannelUsers := server.cfg.IntValue("MaxChannelUsers") + maxChannelUsers := server.cfg.IntValue("usersperchannel") if maxChannelUsers != 0 && len(dstChan.clients) >= maxChannelUsers { client.sendPermissionDeniedFallback(mumbleproto.PermissionDenied_ChannelFull, 0x010201, "Channel is full") @@ -653,7 +653,7 @@ func (server *Server) handleUserStateMessage(client *Client, msg *Message) { // Texture change if userstate.Texture != nil { - maximg := server.cfg.IntValue("MaxImageMessageLength") + maximg := server.cfg.IntValue("imagemessagelength") if maximg > 0 && len(userstate.Texture) > maximg { client.sendPermissionDeniedType(mumbleproto.PermissionDenied_TextTooLong) return diff --git a/cmd/grumble/register.go b/cmd/grumble/register.go index 50015a8..fd3479e 100644 --- a/cmd/grumble/register.go +++ b/cmd/grumble/register.go @@ -39,16 +39,19 @@ const registerUrl = "https://mumble.info/register.cgi" // This function is used to determine whether or not to periodically // contact the master server list and update this server's metadata. func (server *Server) IsPublic() bool { - if len(server.cfg.StringValue("RegisterName")) == 0 { + if len(server.cfg.StringValue("registerName")) == 0 { return false } - if len(server.cfg.StringValue("RegisterHost")) == 0 { + if len(server.cfg.StringValue("registerHostname")) == 0 { return false } - if len(server.cfg.StringValue("RegisterPassword")) == 0 { + if len(server.cfg.StringValue("registerPassword")) == 0 { return false } - if len(server.cfg.StringValue("RegisterWebUrl")) == 0 { + if len(server.cfg.StringValue("registerUrl")) == 0 { + return false + } + if !server.cfg.BoolValue("allowping") { return false } return true @@ -80,11 +83,11 @@ func (server *Server) RegisterPublicServer() { // Render registration XML template reg := Register{ - Name: server.cfg.StringValue("RegisterName"), - Host: server.cfg.StringValue("RegisterHost"), - Password: server.cfg.StringValue("RegisterPassword"), - Url: server.cfg.StringValue("RegisterWebUrl"), - Location: server.cfg.StringValue("RegisterLocation"), + Name: server.cfg.StringValue("registerName"), + Host: server.cfg.StringValue("registerHostname"), + Password: server.cfg.StringValue("registerPassword"), + Url: server.cfg.StringValue("registerUrl"), + Location: server.cfg.StringValue("registerLocation"), Port: server.CurrentPort(), Digest: digest, Users: len(server.clients), diff --git a/cmd/grumble/server.go b/cmd/grumble/server.go index 672ff4f..0a8c1a0 100644 --- a/cmd/grumble/server.go +++ b/cmd/grumble/server.go @@ -178,6 +178,16 @@ func (server *Server) RootChannel() *Channel { return root } +// Get a pointer to the default channel +func (server *Server) DefaultChannel() *Channel { + channel, exists := server.Channels[server.cfg.IntValue("defaultchannel")] + if !exists { + return server.RootChannel() + } + return channel +} + +// Set password as the new SuperUser password func (server *Server) setConfigPassword(key, password string) { saltBytes := make([]byte, 24) _, err := rand.Read(saltBytes) @@ -271,7 +281,14 @@ func (server *Server) handleIncomingClient(conn net.Conn) (err error) { client.lf = &clientLogForwarder{client, server.Logger} client.Logger = log.New(client.lf, "", 0) - client.session = server.pool.Get() + client.session, err = server.pool.Get() + if err != nil { + // Server is full. Murmur just closes the connection here anyway, + // so don't bother sending a Reject_ServerFull + client.Printf("Server is full, rejecting %v", conn.RemoteAddr()) + conn.Close() + return nil + } client.Printf("New connection: %v (%v)", conn.RemoteAddr(), client.Session()) client.tcpaddr = addr.(*net.TCPAddr) @@ -528,12 +545,13 @@ func (server *Server) handleAuthenticate(client *Client, msg *Message) { client.user = user } } - } - if client.user == nil && server.hasServerPassword() { - if auth.Password == nil || !server.CheckServerPassword(*auth.Password) { - client.RejectAuth(mumbleproto.Reject_WrongServerPW, "Invalid server password") - return + // Otherwise, the user is unregistered. If there is a server-wide password, they now need it. + if client.user == nil && server.hasServerPassword() { + if auth.Password == nil || !server.CheckServerPassword(*auth.Password) { + client.RejectAuth(mumbleproto.Reject_WrongServerPW, "Invalid server password") + return + } } } @@ -618,8 +636,8 @@ func (server *Server) finishAuthenticate(client *Client) { server.hclients[host] = append(server.hclients[host], client) server.hmutex.Unlock() - channel := server.RootChannel() - if client.IsRegistered() { + channel := server.DefaultChannel() + if server.cfg.BoolValue("rememberchannel") && client.IsRegistered() { lastChannel := server.Channels[client.user.LastChannelId] if lastChannel != nil { channel = lastChannel @@ -675,8 +693,8 @@ func (server *Server) finishAuthenticate(client *Client) { sync := &mumbleproto.ServerSync{} sync.Session = proto.Uint32(client.Session()) - sync.MaxBandwidth = proto.Uint32(server.cfg.Uint32Value("MaxBandwidth")) - sync.WelcomeText = proto.String(server.cfg.StringValue("WelcomeText")) + sync.MaxBandwidth = proto.Uint32(server.cfg.Uint32Value("bandwidth")) + sync.WelcomeText = proto.String(server.cfg.StringValue("welcometext")) if client.IsSuperUser() { sync.Permissions = proto.Uint64(uint64(acl.AllPermissions)) } else { @@ -693,9 +711,9 @@ func (server *Server) finishAuthenticate(client *Client) { } err := client.sendMessage(&mumbleproto.ServerConfig{ - AllowHtml: proto.Bool(server.cfg.BoolValue("AllowHTML")), - MessageLength: proto.Uint32(server.cfg.Uint32Value("MaxTextMessageLength")), - ImageMessageLength: proto.Uint32(server.cfg.Uint32Value("MaxImageMessageLength")), + AllowHtml: proto.Bool(server.cfg.BoolValue("allowhtml")), + MessageLength: proto.Uint32(server.cfg.Uint32Value("textmessagelength")), + ImageMessageLength: proto.Uint32(server.cfg.Uint32Value("imagemessagelength")), }) if err != nil { client.Panicf("%v", err) @@ -992,6 +1010,9 @@ func (server *Server) udpListenLoop() { // Length 12 is for ping datagrams from the ConnectDialog. if nread == 12 { + if !server.cfg.BoolValue("allowping") { + return + } readbuf := bytes.NewBuffer(buf) var ( tmp32 uint32 @@ -1001,11 +1022,11 @@ func (server *Server) udpListenLoop() { _ = binary.Read(readbuf, binary.BigEndian, &rand) buffer := bytes.NewBuffer(make([]byte, 0, 24)) - _ = binary.Write(buffer, binary.BigEndian, uint32((1<<16)|(2<<8)|2)) + _ = binary.Write(buffer, binary.BigEndian, uint32((1<<16)|(2<<8)|4)) _ = binary.Write(buffer, binary.BigEndian, rand) _ = binary.Write(buffer, binary.BigEndian, uint32(len(server.clients))) - _ = binary.Write(buffer, binary.BigEndian, server.cfg.Uint32Value("MaxUsers")) - _ = binary.Write(buffer, binary.BigEndian, server.cfg.Uint32Value("MaxBandwidth")) + _ = binary.Write(buffer, binary.BigEndian, server.cfg.Uint32Value("users")) + _ = binary.Write(buffer, binary.BigEndian, server.cfg.Uint32Value("bandwidth")) err = server.SendUDP(buffer.Bytes(), udpaddr) if err != nil { @@ -1283,9 +1304,9 @@ func (server *Server) IsCertHashBanned(hash string) bool { // Filter incoming text according to the server's current rules. func (server *Server) FilterText(text string) (filtered string, err error) { options := &htmlfilter.Options{ - StripHTML: !server.cfg.BoolValue("AllowHTML"), - MaxTextMessageLength: server.cfg.IntValue("MaxTextMessageLength"), - MaxImageMessageLength: server.cfg.IntValue("MaxImageMessageLength"), + StripHTML: !server.cfg.BoolValue("allowhtml"), + MaxTextMessageLength: server.cfg.IntValue("textmessagelength"), + MaxImageMessageLength: server.cfg.IntValue("imagemessagelength"), } return htmlfilter.Filter(text, options) } @@ -1339,7 +1360,7 @@ func isTimeout(err error) bool { // Initialize the per-launch data func (server *Server) initPerLaunchData() { - server.pool = sessionpool.New() + server.pool = sessionpool.New(server.cfg.Uint32Value("users")) server.clients = make(map[uint32]*Client) server.hclients = make(map[string][]*Client) server.hpclients = make(map[string]*Client) @@ -1368,7 +1389,7 @@ func (server *Server) cleanPerLaunchData() { // Port returns the port the native server will listen on when it is // started. func (server *Server) Port() int { - port := server.cfg.IntValue("Port") + port := server.cfg.IntValue("port") if port == 0 { return DefaultPort + int(server.Id) - 1 } @@ -1378,13 +1399,13 @@ func (server *Server) Port() int { // ListenWebPort returns true if we should listen to the // web port, otherwise false func (server *Server) ListenWebPort() bool { - return !server.cfg.BoolValue("NoWebServer") + return !server.cfg.BoolValue("nowebserver") } // WebPort returns the port the web server will listen on when it is // started. func (server *Server) WebPort() int { - port := server.cfg.IntValue("WebPort") + port := server.cfg.IntValue("webport") if port == 0 { return DefaultWebPort + int(server.Id) - 1 } @@ -1406,7 +1427,7 @@ func (server *Server) CurrentPort() int { // it is started. This must be an IP address, either IPv4 // or IPv6. func (server *Server) HostAddress() string { - host := server.cfg.StringValue("Address") + host := server.cfg.StringValue("host") if host == "" { return "0.0.0.0" } @@ -1449,8 +1470,8 @@ func (server *Server) Start() (err error) { */ // Wrap a TLS listener around the TCP connection - certFn := server.cfg.PathValue("CertPath", Args.DataDir) - keyFn := server.cfg.PathValue("KeyPath", Args.DataDir) + certFn := server.cfg.PathValue("sslCert", Args.DataDir) + keyFn := server.cfg.PathValue("sslKey", Args.DataDir) cert, err := tls.LoadX509KeyPair(certFn, keyFn) if err != nil { return err @@ -1459,6 +1480,15 @@ func (server *Server) Start() (err error) { Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequestClientCert, } + ciphersstr := server.cfg.StringValue("sslCiphers") + if ciphersstr != "" { + var invalid []string + server.tlscfg.CipherSuites, invalid = serverconf.ParseCipherlist(ciphersstr) + for _, cipher := range invalid { + log.Printf("Ignoring invalid or unsupported cipher \"%v\"", cipher) + } + server.tlscfg.PreferServerCipherSuites = true + } server.tlsl = tls.NewListener(server.tcpl, server.tlscfg) if shouldListenWeb { diff --git a/pkg/serverconf/cipherlist.go b/pkg/serverconf/cipherlist.go new file mode 100644 index 0000000..01c518d --- /dev/null +++ b/pkg/serverconf/cipherlist.go @@ -0,0 +1,77 @@ +package serverconf + +import ( + "crypto/tls" + "strings" +) + +var cipherLookup = map[string]uint16{ + // RFC + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + // These are the actual names per RFC 7905 + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + + // OpenSSL + "RC4-SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "DES-CBC3-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "AES128-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "AES256-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "AES128-SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "AES128-GCM-SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "AES256-GCM-SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "ECDHE-ECDSA-RC4-SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "ECDHE-ECDSA-AES128-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "ECDHE-ECDSA-AES256-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "ECDHE-RSA-RC4-SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "ECDHE-RSA-DES-CBC3-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "ECDHE-RSA-AES128-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "ECDHE-RSA-AES256-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "ECDHE-ECDSA-AES128-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "ECDHE-RSA-AES128-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "ECDHE-RSA-CHACHA20-POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "ECDHE-ECDSA-CHACHA20-POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, +} + +// ParseCipherlist parses a list of cipher suites separated by colons. +// It supports both RFC and OpenSSL names, but does not support OpenSSL +// cipher strings representing categories of cipher suites. +func ParseCipherlist(list string) (ciphers []uint16, invalid []string) { + strciphers := strings.Split(list, ":") + ciphers = make([]uint16, 0, len(strciphers)) + invalid = make([]string, 0) + for _, v := range strciphers { + c, ok := cipherLookup[v] + if ok { + ciphers = append(ciphers, c) + } else { + invalid = append(invalid, v) + } + } + return +} diff --git a/pkg/serverconf/config.go b/pkg/serverconf/config.go index 258b04f..78d9095 100644 --- a/pkg/serverconf/config.go +++ b/pkg/serverconf/config.go @@ -11,19 +11,20 @@ import ( ) var defaultCfg = map[string]string{ - "MaxBandwidth": "72000", - "MaxUsers": "1000", - "MaxUsersPerChannel": "0", - "MaxTextMessageLength": "5000", - "MaxImageMessageLength": "131072", - "AllowHTML": "true", - "DefaultChannel": "0", - "RememberChannel": "true", - "WelcomeText": "Welcome to this server running Grumble.", - "SendVersion": "true", - "LogPath": "grumble.log", - "CertPath": "cert.pem", - "KeyPath": "key.pem", + "bandwidth": "72000", + "users": "1000", + "usersperchannel": "0", + "textmessagelength": "5000", + "imagemessagelength": "131072", + "allowhtml": "true", + "defaultchannel": "0", + "rememberchannel": "true", + "welcometext": "Welcome to this server running Grumble.", + "sendversion": "true", + "allowping": "true", + "logfile": "grumble.log", + "sslCert": "cert.pem", + "sslKey": "key.pem", } type Config struct { diff --git a/pkg/serverconf/config_test.go b/pkg/serverconf/config_test.go index 17054cf..dcbf846 100644 --- a/pkg/serverconf/config_test.go +++ b/pkg/serverconf/config_test.go @@ -26,7 +26,7 @@ func TestFloatAsInt(t *testing.T) { func TestDefaultValue(t *testing.T) { cfg := New(nil, nil) - if cfg.IntValue("MaxBandwidth") != 72000 { + if cfg.IntValue("bandwidth") != 72000 { t.Errorf("Expected 72000") } } diff --git a/pkg/serverconf/file.go b/pkg/serverconf/file.go index 2fc04eb..49c3189 100644 --- a/pkg/serverconf/file.go +++ b/pkg/serverconf/file.go @@ -37,16 +37,16 @@ func (c *ConfigFile) ServerConfig(id int64, persistentMap map[string]string) *Co // Some server specific values from the global config must be offset. // These are read differently by the server as well. - if v, ok := m["Port"]; ok { + if v, ok := m["port"]; ok { i, err := strconv.ParseInt(v, 10, 64) if err == nil { - m["Port"] = strconv.FormatInt(i+id-1, 10) + m["port"] = strconv.FormatInt(i+id-1, 10) } } - if v, ok := m["WebPort"]; ok { + if v, ok := m["webport"]; ok { i, err := strconv.ParseInt(v, 10, 64) if err == nil { - m["WebPort"] = strconv.FormatInt(i+id-1, 10) + m["webport"] = strconv.FormatInt(i+id-1, 10) } } diff --git a/pkg/serverconf/file_ini.go b/pkg/serverconf/file_ini.go index 8709b30..e4db06e 100644 --- a/pkg/serverconf/file_ini.go +++ b/pkg/serverconf/file_ini.go @@ -21,7 +21,6 @@ func (f *inicfg) GlobalMap() map[string]string { return f.file.Section("").KeysHash() } -// todo(rubenseyer): not all of these even work.. make sure to implement them var DefaultConfigFile = `# Grumble configuration file. # # The commented out settings represent the defaults. @@ -29,55 +28,63 @@ var DefaultConfigFile = `# Grumble configuration file. # Make sure to enclose values containing # or ; in double quotes or backticks. # Address to bind the listeners to. -#Address = 0.0.0.0 +#host = 0.0.0.0 -# Port is the port to bind the native Mumble protocol to. -# WebPort is the port to bind the WebSocket Mumble protocol to. -# They are incremented for each virtual server. -#Port = 64738 -#WebPort = 443 +# port is the port to bind the native Mumble protocol to. +# webport is the port to bind the WebSocket Mumble protocol to. +# They are incremented for each virtual server (if set globally). +#port = 64738 +#webport = 443 + +# Whether to disable web server. +#nowebserver # "Message of the day" HTML string sent to connecting clients. -#WelcomeText = "Welcome to this server running Grumble." +#welcometext = "Welcome to this server running Grumble." + +# Password to join the server. +#serverpassword = # Maximum bandwidth (in bits per second) per client for voice. # Grumble does not yet enforce this limit, but some clients nicely follow it. -#MaxBandwidth = 72000 +#bandwidth = 72000 # Maximum number of concurrent clients. -#MaxUsers = 1000 -#MaxUsersPerChannel = 0 +#users = 1000 +#usersperchannel = 0 -#MaxTextMessageLength = 5000 -#MaxImageMessageLength = 131072 -#AllowHTML +#textmessagelength = 5000 +#imagemessagelength = 131072 +#allowhtml -# DefaultChannel is the channel (by ID) new users join. -# The root channel is the default. -#DefaultChannel = 0 +# The default channel is the channel (by ID) new users join. +# The root channel (ID = 0) is the default. +#defaultchannel = 0 # Whether users will rejoin the last channel they were in. -#RememberChannel +#rememberchannel -# Whether to include server version and server os in ping response. -#SendVersion -#SendOSInfo +# Whether to include server OS info in ping response. +#sendversion + +# Whether to respond to pings from the Connect dialog. +#allowping # Path to the log file (relative to the data directory). -#LogPath = grumble.log +#logfile = grumble.log # Path to TLS certificate and key (relative to the data directory). -# The certificate needs to have the entire chain concatenated to be validate. +# The certificate needs to have the entire chain concatenated to be valid. # If these paths do not exist, Grumble will autogenerate a certificate. -#CertPath = cert.pem -#KeyPath = key.pem +#sslCert = cert.pem +#sslKey = key.pem # Options for public server registration. # All of these have to be set to make the server public. -# RegisterName additionally sets the name of the root channel. -# RegisterPassword is a simple, arbitrary secret to guard your registration. Don't lose it. -#RegisterName = -#RegisterHost = -#RegisterPassword = -#RegisterWebUrl = +# registerName additionally sets the name of the root channel. +# registerPassword is a simple, arbitrary secret to guard your registration. Don't lose it. +#registerName = +#registerHostname = +#registerPassword = +#registerUrl = ` diff --git a/pkg/sessionpool/sessionpool.go b/pkg/sessionpool/sessionpool.go index 3e6c2ad..8bd524f 100644 --- a/pkg/sessionpool/sessionpool.go +++ b/pkg/sessionpool/sessionpool.go @@ -6,6 +6,7 @@ package sessionpool import ( + "errors" "math" "sync" ) @@ -17,11 +18,17 @@ type SessionPool struct { used map[uint32]bool unused []uint32 cur uint32 + max uint32 } // Create a new SessionPool container. -func New() (pool *SessionPool) { +func New(max uint32) (pool *SessionPool) { pool = new(SessionPool) + if max == 0 { + pool.max = math.MaxUint32 + } else { + pool.max = max + } return } @@ -41,7 +48,7 @@ func (pool *SessionPool) EnableUseTracking() { // Get a new session ID from the SessionPool. // Must be reclaimed using Reclaim() when done using it. -func (pool *SessionPool) Get() (id uint32) { +func (pool *SessionPool) Get() (id uint32, err error) { pool.mutex.Lock() defer pool.mutex.Unlock() @@ -60,11 +67,12 @@ func (pool *SessionPool) Get() (id uint32) { return } - // Check for depletion. If cur is MaxUint32, + // Check for depletion. If cur is max, // there aren't any session IDs left, since the - // increment below would overflow us back to 0. - if pool.cur == math.MaxUint32 { - panic("SessionPool depleted") + // increment below would return an out of range ID. + if pool.cur == pool.max { + err = errors.New("depleted session pool") + return } // Increment the next session id and return it. diff --git a/pkg/sessionpool/sessionpool_test.go b/pkg/sessionpool/sessionpool_test.go index 1f0b2c0..da9e8b1 100644 --- a/pkg/sessionpool/sessionpool_test.go +++ b/pkg/sessionpool/sessionpool_test.go @@ -6,35 +6,41 @@ import ( ) func TestReclaim(t *testing.T) { - pool := New() - id := pool.Get() + pool := New(2) + id, err := pool.Get() + if err != nil { + t.Errorf("Expected no error: %v", err) + } if id != 1 { t.Errorf("Got %v, expected 1 (first time)", id) } pool.Reclaim(1) - id = pool.Get() + id, err = pool.Get() + if err != nil { + t.Errorf("Expected no error: %v", err) + } if id != 1 { t.Errorf("Got %v, expected 1 (second time)", id) } - id = pool.Get() + id, err = pool.Get() + if err != nil { + t.Errorf("Expected no error: %v", err) + } if id != 2 { t.Errorf("Got %v, expected 2", id) } } func TestDepletion(t *testing.T) { - defer func() { - r := recover() - if r != "SessionPool depleted" { - t.Errorf("Expected depletion panic") - } - }() - pool := New() + pool := New(0) pool.cur = math.MaxUint32 - pool.Get() + _, err := pool.Get() + if err == nil { + t.Errorf("Expected depletion error") + } } func TestUseTracking(t *testing.T) { @@ -45,7 +51,7 @@ func TestUseTracking(t *testing.T) { } }() - pool := New() + pool := New(0) pool.EnableUseTracking() pool.Reclaim(42) } From 76ff3fb90756967a0cded45673259950e9bbec4f Mon Sep 17 00:00:00 2001 From: rubenseyer Date: Sat, 30 May 2020 19:02:50 +0200 Subject: [PATCH 4/4] grumble.go: bad merge in log handling --- cmd/grumble/grumble.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/grumble/grumble.go b/cmd/grumble/grumble.go index af6fe03..e3a0551 100644 --- a/cmd/grumble/grumble.go +++ b/cmd/grumble/grumble.go @@ -87,9 +87,9 @@ func main() { } else { logFn = config.PathValue("logfile", Args.DataDir) } - logtarget.Default, err = logtarget.OpenFile(Args.LogPath, os.Stderr) + logtarget.Default, err = logtarget.OpenFile(logFn, os.Stderr) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to open log file (%v): %v", logFn, err) + log.Fatalf("Unable to open log file (%v): %v", logFn, err) return } log.SetPrefix("[G] ")