diff --git a/Makefile b/Makefile index 2de4069..4b21fee 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ PACKAGES = \ pkg/cryptstate \ pkg/mumbleproto \ pkg/blobstore \ + pkg/serverconf \ pkg/sqlite GCFLAGS = \ @@ -18,6 +19,7 @@ GCFLAGS = \ -Ipkg/packetdatastream/_obj \ -Ipkg/mumbleproto/_obj \ -Ipkg/blobstore/_obj \ + -Ipkg/serverconf/_obj \ -Ipkg/sqlite/_obj LDFLAGS = \ @@ -25,6 +27,7 @@ LDFLAGS = \ -Lpkg/packetdatastream/_obj \ -Lpkg/mumbleproto/_obj \ -Lpkg/blobstore/_obj \ + -Lpkg/serverconf/_obj \ -Lpkg/sqlite/_obj GOFILES = \ @@ -39,7 +42,9 @@ GOFILES = \ murmurdb.go \ freeze.go \ gencert.go \ - register.go + register.go \ + ctlrpc.go \ + ctl.go .PHONY: grumble grumble: pkg diff --git a/ctl.go b/ctl.go new file mode 100644 index 0000000..01289d1 --- /dev/null +++ b/ctl.go @@ -0,0 +1,82 @@ +// Copyright (c) 2011 The Grumble Authors +// The use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE-file. + +package main + +import ( + "log" + "os" + "path/filepath" + "rpc" + "strconv" +) + +var CtlUsage = `grumble ctl + + help + Show this help + + start [id] + Start a server + + stop [id] + Stop a server + + setconf [id] [key] [value] + Set a config value for server with id + + getconf [id] [key] [value] + Get a config value for server with id +` + +func GrumbleCtl(args []string) { + log.SetFlags(0) + + if len(args) <= 1 || args[0] == "help" { + log.Printf(CtlUsage) + return + } + + sid, _ := strconv.Atoi64(args[1]) + + client, err := rpc.Dial("unix", filepath.Join(os.Getenv("HOME"), ".grumble", "ctl")) + if err != nil { + log.Fatalf("Could not connect to control socket: %v", err) + } + + switch args[0] { + case "start": + err := client.Call("ctl.Start", sid, nil) + if err != nil { + log.Fatalf("Unable to start: %v", err) + } + log.Printf("[%v] Started") + case "stop": + err := client.Call("ctl.Stop", sid, nil) + if err != nil { + log.Fatalf("Unable to stop: %v", err) + } + log.Printf("[%v] Stopped", sid) + case "setconf": + if len(args) < 4 { + return + } + result := &ConfigValue{} + err := client.Call("ctl.SetConfig", &ConfigValue{sid, args[2], args[3]}, result) + if err != nil { + log.Fatalf("Unable to set config: %v", err) + } + log.Printf("[%v] %v=%v", result.Id, result.Key, result.Value) + case "getconf": + if len(args) < 3 { + return + } + result := &ConfigValue{} + err := client.Call("ctl.GetConfig", &ConfigValue{sid, args[2], ""}, result) + if err != nil { + log.Fatalf("Unable to get config: %v", err) + } + log.Printf("[%v] %v=%v", result.Id, result.Key, result.Value) + } +} diff --git a/ctlrpc.go b/ctlrpc.go new file mode 100644 index 0000000..90d6f76 --- /dev/null +++ b/ctlrpc.go @@ -0,0 +1,46 @@ +// Copyright (c) 2011 The Grumble Authors +// The use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE-file. + +package main + +import ( + "os" +) + +type ControlRPC struct { + +} + +// Start a server +func (c *ControlRPC) Start(in *int, out *int) os.Error { + return nil +} + +// Stop a server +func (c *ControlRPC) Stop(in *int, out *int) os.Error { + return nil +} + +type ConfigValue struct { + Id int64 + Key string + Value string +} + +// Set a config value +func (c *ControlRPC) SetConfig(in *ConfigValue, out *ConfigValue) os.Error { + servers[in.Id].SetConfig(in.Key, in.Value) + out.Id = in.Id + out.Key = in.Key + out.Value = in.Value + return nil +} + +// Get a config value +func (c *ControlRPC) GetConfig(in *ConfigValue, out *ConfigValue) os.Error { + out.Id = in.Id + out.Key = in.Key + out.Value = servers[in.Id].GetConfig(in.Key) + return nil +} diff --git a/freeze.go b/freeze.go index a0b7afb..07a8e8d 100644 --- a/freeze.go +++ b/freeze.go @@ -14,10 +14,10 @@ import ( ) type frozenServer struct { - Id int "id" - MaxUsers int "max_user" - Channels []frozenChannel "channels" - Users []frozenUser "users" + Id int "id" + Config map[string]string "config" + Channels []frozenChannel "channels" + Users []frozenUser "users" } type frozenUser struct { @@ -98,7 +98,7 @@ func (server *Server) FreezeToFile(filename string) (err os.Error) { // Freeze a server func (server *Server) Freeze() (fs frozenServer, err os.Error) { fs.Id = int(server.Id) - fs.MaxUsers = server.MaxUsers + fs.Config = server.cfg channels := []frozenChannel{} for _, c := range server.Channels { @@ -224,6 +224,10 @@ func NewServerFromFrozen(filename string) (s *Server, err os.Error) { return nil, err } + if fs.Config != nil { + s.cfg = fs.Config + } + // Add all channels, but don't hook up parent/child relationships // until all of them are loaded. for _, fc := range fs.Channels { diff --git a/grumble.go b/grumble.go index 476d816..3fafa6b 100644 --- a/grumble.go +++ b/grumble.go @@ -11,20 +11,24 @@ import ( "os" "os/signal" "log" + "net" "sqlite" "path/filepath" "regexp" + "rpc" "time" ) var help *bool = flag.Bool("help", false, "Show this help") var datadir *string = flag.String("datadir", "", "Directory to use for server storage") var blobdir *string = flag.String("blobdir", "", "Directory to use for blob storage") +var ctlpath *string = flag.String("ctlpath", "", "File to use for ctl socket") var sqlitedb *string = flag.String("murmurdb", "", "Path to murmur.sqlite to import server structure from") var cleanup *bool = flag.Bool("clean", false, "Clean up existing data dir content before importing Murmur data") var gencert *bool = flag.Bool("gencert", false, "Generate a self-signed certificate for use with Grumble") var globalBlobstore *blobstore.BlobStore +var servers map[int64]*Server func Usage() { fmt.Fprintf(os.Stderr, "usage: grumble [options]\n") @@ -42,16 +46,16 @@ func MurmurImport(filename string) (err os.Error) { panic(err.String()) } - var servers []int64 + var serverids []int64 var sid int64 for stmt.Next() { stmt.Scan(&sid) - servers = append(servers, sid) + serverids = append(serverids, sid) } - log.Printf("Found servers: %v (%v servers)", servers, len(servers)) + log.Printf("Found servers: %v (%v servers)", serverids, len(serverids)) - for _, sid := range servers { + for _, sid := range serverids { m, err := NewServerFromSQLite(sid, db) if err != nil { return err @@ -70,6 +74,12 @@ func MurmurImport(filename string) (err os.Error) { func main() { var err os.Error + + if len(os.Args) >= 2 && os.Args[1] == "ctl" { + GrumbleCtl(os.Args[2:]) + return + } + flag.Parse() if *help == true { Usage() @@ -88,6 +98,10 @@ func main() { *blobdir = filepath.Join(os.Getenv("HOME"), ".grumble", "blob") } + if len(*ctlpath) == 0 { + *ctlpath = filepath.Join(os.Getenv("HOME"), ".grumble", "ctl") + } + log.Printf("Using blob directory: %s", *blobdir) globalBlobstore, err = blobstore.NewBlobStore(*blobdir, true) if err != nil { @@ -161,7 +175,7 @@ func main() { log.Fatalf("Murmur import failed: %s", err.String()) } - servers := make(map[int64]*Server) + servers = make(map[int64]*Server) for _, name := range names { if matched, _ := regexp.MatchString("^[0-9]+$", name); matched { log.Printf("Loading server %v", name) @@ -184,6 +198,22 @@ func main() { go s.ListenAndMurmur() } + os.Remove(*ctlpath) + + addr, err := net.ResolveUnixAddr("unix", *ctlpath) + if err != nil { + log.Panicf("Unable to resolve ctl addr: %v", err) + } + + lis, err := net.ListenUnix("unix", addr) + if err != nil { + log.Panicf("Unable to listen on ctl socket: %v", err) + } + + ctl := &ControlRPC{} + rpc.RegisterName("ctl", ctl) + go rpc.Accept(lis) + if len(servers) > 0 { ticker := time.NewTicker(10e9) // 10 secs for { diff --git a/pkg/serverconf/Makefile b/pkg/serverconf/Makefile new file mode 100644 index 0000000..f46cdca --- /dev/null +++ b/pkg/serverconf/Makefile @@ -0,0 +1,6 @@ +include $(GOROOT)/src/Make.inc + +TARG = grumble/serverconf +GOFILES = config.go + +include $(GOROOT)/src/Make.pkg diff --git a/pkg/serverconf/config.go b/pkg/serverconf/config.go new file mode 100644 index 0000000..18a5ea0 --- /dev/null +++ b/pkg/serverconf/config.go @@ -0,0 +1,64 @@ +// Copyright (c) 2011 The Grumble Authors +// The use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE-file. + +package serverconf + +import ( + "strconv" +) + +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", +} + +type Config map[string]string + +func (cfg Config) Set(key string, value string) { + cfg[key] = value +} + +func (cfg Config) StringValue(key string) (value string) { + value, exists := cfg[key] + if exists { + return value + } + + value, exists = defaultCfg[key] + if exists { + return value + } + + return "" +} + +func (cfg Config) IntValue(key string) (intval int) { + str := cfg.StringValue(key) + intval, _ = strconv.Atoi(str) + return +} + +func (cfg Config) Uint32Value(key string) (uint32val uint32) { + str := cfg.StringValue(key) + uintval, _ := strconv.Atoui(str) + return uint32(uintval) +} + +func (cfg Config) BoolValue(key string) (boolval bool) { + str := cfg.StringValue(key) + boolval, _ = strconv.Atob(str) + return +} + +func (cfg Config) Reset(key string) { + cfg[key] = "", false +} diff --git a/pkg/serverconf/config_test.go b/pkg/serverconf/config_test.go new file mode 100644 index 0000000..6e4fae6 --- /dev/null +++ b/pkg/serverconf/config_test.go @@ -0,0 +1,40 @@ +// Copyright (c) 2011 The Grumble Authors +// The use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE-file. + +package serverconf + +import ( + "testing" +) + +func TestIntValue(t *testing.T) { + cfg := make(Config) + cfg.Set("Test", "13") + if cfg.IntValue("Test") != 13 { + t.Errorf("Expected 13") + } +} + +func TestFloatAsInt(t *testing.T) { + cfg := make(Config) + cfg.Set("Test", "13.4") + if cfg.IntValue("Test") != 0 { + t.Errorf("Expected 0") + } +} + +func TestDefaultValue(t *testing.T) { + cfg := make(Config) + if cfg.IntValue("MaxBandwidth") != 72000 { + t.Errorf("Expected 72000") + } +} + +func TestBoolValue(t *testing.T) { + cfg := make(Config) + cfg.Set("DoStuffOnStartup", "true") + if cfg.BoolValue("DoStuffOnStartup") != true { + t.Errorf("Expected true") + } +} diff --git a/register.go b/register.go index d7e303b..1e0df5c 100644 --- a/register.go +++ b/register.go @@ -74,16 +74,16 @@ func newTLSClientAuthConn(addr string, cfg *tls.Config) (c *http.ClientConn, err // 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.RegisterName) == 0 { + if len(server.cfg.StringValue("RegisterName")) == 0 { return false } - if len(server.RegisterHost) == 0 { + if len(server.cfg.StringValue("RegisterHost")) == 0 { return false } - if len(server.RegisterPassword) == 0 { + if len(server.cfg.StringValue("RegisterPassword")) == 0 { return false } - if len(server.RegisterWebUrl) == 0 { + if len(server.cfg.StringValue("RegisterWebUrl")) == 0 { return false } return true @@ -121,11 +121,11 @@ func (server *Server) RegisterPublicServer() { return } err = t.Execute(buf, map[string]string{ - "name": server.RegisterName, - "host": server.RegisterHost, - "password": server.RegisterPassword, - "url": server.RegisterWebUrl, - "location": server.RegisterLocation, + "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"), "port": strconv.Itoa(server.port), "digest": digest, "users": strconv.Itoa(len(server.clients)), diff --git a/server.go b/server.go index 4955392..e473291 100644 --- a/server.go +++ b/server.go @@ -21,6 +21,7 @@ import ( "cryptstate" "fmt" "gob" + "grumble/serverconf" "hash" "io" "path/filepath" @@ -57,19 +58,14 @@ type Server struct { udpsend chan *Message voicebroadcast chan *VoiceBroadcast freezeRequest chan *freezeRequest + configRequest chan *configRequest // Signals to the server that a client has been successfully // authenticated. clientAuthenticated chan *Client - // Config-related - MaxUsers int - MaxBandwidth uint32 - RegisterName string - RegisterHost string - RegisterPassword string - RegisterWebUrl string - RegisterLocation string + // Server configuration + cfg serverconf.Config // Clients clients map[uint32]*Client @@ -128,6 +124,13 @@ type freezeRequest struct { readCloser io.ReadCloser } +type configRequest struct { + done chan bool + set bool + key string + value string +} + // Allocate a new Murmur instance func NewServer(id int64, addr string, port int) (s *Server, err os.Error) { s = new(Server) @@ -137,6 +140,8 @@ func NewServer(id int64, addr string, port int) (s *Server, err os.Error) { s.port = port s.running = false + s.cfg = make(map[string]string) + s.sessions = make(map[uint32]bool) s.clients = make(map[uint32]*Client) s.Users = make(map[uint32]*User) @@ -150,11 +155,9 @@ func NewServer(id int64, addr string, port int) (s *Server, err os.Error) { s.udpsend = make(chan *Message) s.voicebroadcast = make(chan *VoiceBroadcast) s.freezeRequest = make(chan *freezeRequest) + s.configRequest = make(chan *configRequest) s.clientAuthenticated = make(chan *Client) - s.MaxBandwidth = 300000 - s.MaxUsers = 10 - s.Channels = make(map[int]*Channel) s.root = s.NewChannel(0, "Root") s.aclcache = NewACLCache() @@ -391,6 +394,10 @@ func (server *Server) handler() { } go server.handleFreezeRequest(req, &fs) + // Synchronzied config get/set + case req := <-server.configRequest: + server.handleConfigRequest(req) + // Server registration update // Tick every hour + a minute offset based on the server id. case <-time.Tick((3600 + ((server.Id * 60) % 600)) * 1e9): @@ -426,6 +433,15 @@ func (server *Server) handleFreezeRequest(freq *freezeRequest, fs *frozenServer) } } +func (server *Server) handleConfigRequest(cfgReq *configRequest) { + if cfgReq.set { + server.cfg.Set(cfgReq.key, cfgReq.value) + } else { + cfgReq.value = server.cfg.StringValue(cfgReq.key) + } + cfgReq.done <- true +} + // Handle an Authenticate protobuf message. This is handled in a separate // goroutine to allow for remote authenticators that are slow to respond. // @@ -639,7 +655,7 @@ func (server *Server) finishAuthenticate(client *Client) { sync := &mumbleproto.ServerSync{} sync.Session = proto.Uint32(client.Session) - sync.MaxBandwidth = proto.Uint32(server.MaxBandwidth) + sync.MaxBandwidth = proto.Uint32(server.cfg.Uint32Value("MaxBandwidth")) if client.IsSuperUser() { sync.Permissions = proto.Uint64(uint64(AllPermissions)) } else { @@ -951,8 +967,8 @@ func (server *Server) ListenUDP() { _ = binary.Write(buffer, binary.BigEndian, uint32((1<<16)|(2<<8)|2)) _ = binary.Write(buffer, binary.BigEndian, rand) _ = binary.Write(buffer, binary.BigEndian, uint32(len(server.clients))) - _ = binary.Write(buffer, binary.BigEndian, uint32(server.MaxUsers)) - _ = binary.Write(buffer, binary.BigEndian, uint32(server.MaxBandwidth)) + _ = binary.Write(buffer, binary.BigEndian, server.cfg.Uint32Value("MaxUsers")) + _ = binary.Write(buffer, binary.BigEndian, server.cfg.Uint32Value("MaxBandwidth")) server.udpsend <- &Message{ buf: buffer.Bytes(), @@ -1059,6 +1075,22 @@ func (s *Server) FreezeServer() io.ReadCloser { return fr.readCloser } +// Set the value of a config key +func (s *Server) SetConfig(key string, value string) { + cfgReq := &configRequest{make(chan bool), true, key, value} + s.configRequest <- cfgReq + <-cfgReq.done + return +} + +// Get the value of a config key +func (s *Server) GetConfig(key string) (value string) { + cfgReq := &configRequest{make(chan bool), false, key, ""} + s.configRequest <- cfgReq + <-cfgReq.done + return cfgReq.value +} + // Register a client on the server. func (s *Server) RegisterClient(client *Client) (uid uint32) { // Increment nextUserId only if registration succeeded.