This commit is contained in:
Ruben Seyer 2020-05-30 17:03:41 +00:00 committed by GitHub
commit 0cb35a7e20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 522 additions and 160 deletions

View file

@ -21,7 +21,7 @@ var usageTmpl = `usage: grumble [options]
grumble {{.Version}} ({{.BuildDate}})
target: {{.OS}}, {{.Arch}}
--help
--help, --version
Shows this help listing.
--datadir <data-dir> (default: {{.DefaultDataDir}})
@ -30,6 +30,20 @@ var usageTmpl = `usage: grumble [options]
--log <log-path> (default: $DATADIR/grumble.log)
Log file path.
--ini <config-path> (default: $DATADIR/grumble.ini)
Config file path.
--supw <password> [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).
@ -46,12 +60,17 @@ 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
SuperUserPW string
ReadPass bool
DisablePass bool
RegenKeys bool
ServerId int64
SQLiteDB string
CleanUp bool
}
func defaultDataDir() string {
@ -63,10 +82,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 {
@ -90,9 +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", defaultLogPath(), "")
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", "", "")

View file

@ -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)")
}

View file

@ -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
}

View file

@ -7,17 +7,21 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"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
@ -27,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)
@ -36,10 +54,42 @@ func main() {
}
dataDir.Close()
// Set up logging
logtarget.Default, err = logtarget.OpenFile(Args.LogPath, os.Stderr)
// 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 {
fmt.Fprintf(os.Stderr, "Unable to open log file (%v): %v", Args.LogPath, err)
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("logfile", Args.DataDir)
}
logtarget.Default, err = logtarget.OpenFile(logFn, os.Stderr)
if err != nil {
log.Fatalf("Unable to open log file (%v): %v", logFn, err)
return
}
log.SetPrefix("[G] ")
@ -48,6 +98,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.
@ -63,11 +130,9 @@ 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.
certFn := filepath.Join(Args.DataDir, "cert.pem")
keyFn := filepath.Join(Args.DataDir, "key.pem")
// These are used as the default certificate of all virtual servers.
certFn := config.PathValue("sslCert", Args.DataDir)
keyFn := config.PathValue("sslKey", Args.DataDir)
shouldRegen := false
if Args.RegenKeys {
shouldRegen = true
@ -164,10 +229,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
}
@ -181,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())
@ -189,9 +266,17 @@ 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)
s, err := NewServer(1, configFile.ServerConfig(1, nil))
if err != nil {
log.Fatalf("Couldn't start server: %s", err.Error())
}

View file

@ -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

View file

@ -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
}

View file

@ -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),

View file

@ -19,7 +19,6 @@ import (
"log"
"net"
"net/http"
"path/filepath"
"strings"
"sync"
"time"
@ -75,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.
@ -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)
@ -175,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)
@ -191,10 +204,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
@ -272,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)
@ -436,14 +452,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:
@ -537,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
}
}
}
@ -627,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
@ -684,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 {
@ -702,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)
@ -1001,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
@ -1010,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 {
@ -1292,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)
}
@ -1348,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)
@ -1356,7 +1368,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 +1382,6 @@ func (server *Server) cleanPerLaunchData() {
server.bye = nil
server.incoming = nil
server.voicebroadcast = nil
server.cfgUpdate = nil
server.tempRemove = nil
server.clientAuthenticated = nil
}
@ -1379,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
}
@ -1389,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
}
@ -1417,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"
}
@ -1460,8 +1470,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("sslCert", Args.DataDir)
keyFn := server.cfg.PathValue("sslKey", Args.DataDir)
cert, err := tls.LoadX509KeyPair(certFn, keyFn)
if err != nil {
return err
@ -1470,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 {

1
go.mod
View file

@ -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
)

2
go.sum
View file

@ -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=

View file

@ -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
}

View file

@ -5,44 +5,51 @@
package serverconf
import (
"path/filepath"
"strconv"
"sync"
)
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 <b>Grumble</b>.",
"SendVersion": "true",
"bandwidth": "72000",
"users": "1000",
"usersperchannel": "0",
"textmessagelength": "5000",
"imagemessagelength": "131072",
"allowhtml": "true",
"defaultchannel": "0",
"rememberchannel": "true",
"welcometext": "Welcome to this server running <b>Grumble</b>.",
"sendversion": "true",
"allowping": "true",
"logfile": "grumble.log",
"sslCert": "cert.pem",
"sslKey": "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 +59,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 +74,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 +92,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 +106,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)
}

View file

@ -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)
if cfg.IntValue("MaxBandwidth") != 72000 {
cfg := New(nil, nil)
if cfg.IntValue("bandwidth") != 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")

54
pkg/serverconf/file.go Normal file
View file

@ -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)
}

View file

@ -0,0 +1,90 @@
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()
}
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.
#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 (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 <b>Grumble</b>."
# 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.
#bandwidth = 72000
# Maximum number of concurrent clients.
#users = 1000
#usersperchannel = 0
#textmessagelength = 5000
#imagemessagelength = 131072
#allowhtml
# 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
# 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).
#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 valid.
# If these paths do not exist, Grumble will autogenerate a certificate.
#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 =
#registerHostname =
#registerPassword =
#registerUrl =
`

View file

@ -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.

View file

@ -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)
}