diff --git a/INSTALL b/INSTALL index 3a69561..f3cc297 100644 --- a/INSTALL +++ b/INSTALL @@ -12,15 +12,6 @@ On Windows, do go build -ldflags="-s -w" -## Set the server administrator credentials - -This step is optional, it is currently only relevant for access to the -`/stats.html` and `/stats.json` locations. - - mkdir data - echo 'god:topsecret' > data/passwd - - ## Set up a group Set up a group called *test* by creating a file `groups/test.json`: diff --git a/README b/README index 0270c03..66a5d4f 100644 --- a/README +++ b/README @@ -37,6 +37,21 @@ available commands; the output depends on whether you are an operator or not. +# The global configuration file + +The server may be configured in the JSON file `data/config.json`. This +file may look as follows: + + { + "admin":[{"username":"root","password":"secret"}] + } + +The fields are as follows: + +- `admin` defines the users allowed to look at the `/stats.html` file; it + has the same syntax as user definitions in groups (see below). + + # Group definitions Groups are defined by files in the `./groups` directory (this may be diff --git a/galene.go b/galene.go index c88481c..033bf72 100644 --- a/galene.go +++ b/galene.go @@ -21,7 +21,7 @@ import ( ) func main() { - var cpuprofile, memprofile, mutexprofile, httpAddr, dataDir string + var cpuprofile, memprofile, mutexprofile, httpAddr string var udpRange string flag.StringVar(&httpAddr, "http", ":8443", "web server `address`") @@ -31,7 +31,7 @@ func main() { "redirect to canonical `host`") flag.BoolVar(&webserver.Insecure, "insecure", false, "act as an HTTP server rather than HTTPS") - flag.StringVar(&dataDir, "data", "./data/", + flag.StringVar(&group.DataDirectory, "data", "./data/", "data `directory`") flag.StringVar(&group.Directory, "groups", "./groups/", "group description `directory`") @@ -112,7 +112,7 @@ func main() { log.Printf("File descriptor limit is %v, please increase it!", n) } - ice.ICEFilename = filepath.Join(dataDir, "ice-servers.json") + ice.ICEFilename = filepath.Join(group.DataDirectory, "ice-servers.json") // make sure the list of public groups is updated early go group.Update() @@ -123,7 +123,7 @@ func main() { serverDone := make(chan struct{}) go func() { - err := webserver.Serve(httpAddr, dataDir) + err := webserver.Serve(httpAddr, group.DataDirectory) if err != nil { log.Printf("Server: %v", err) } diff --git a/group/group.go b/group/group.go index 69e0c4f..1c5800b 100644 --- a/group/group.go +++ b/group/group.go @@ -17,7 +17,7 @@ import ( "github.com/pion/webrtc/v3" ) -var Directory string +var Directory, DataDirectory string var UseMDNS bool var UDPMin, UDPMax uint16 @@ -802,8 +802,67 @@ func matchClient(group string, creds ClientCredentials, users []ClientPattern) ( return false, false } -// Type Description represents a group description together with some -// metadata about the JSON file it was deserialised from. +// Configuration represents the contents of the data/config.json file. +type Configuration struct { + // The modtime and size of the file. These are used to detect + // when a file has changed on disk. + modTime time.Time `json:"-"` + fileSize int64 `json:"-"` + + Admin []ClientPattern `json:"admin"` +} + +var configuration struct { + mu sync.Mutex + configuration *Configuration +} + +func GetConfiguration() (*Configuration, error) { + configuration.mu.Lock() + defer configuration.mu.Unlock() + + if configuration.configuration == nil { + configuration.configuration = &Configuration{} + } + + filename := filepath.Join(DataDirectory, "config.json") + fi, err := os.Stat(filename) + if err != nil { + if os.IsNotExist(err) { + if !configuration.configuration.modTime.Equal( + time.Time{}, + ) { + configuration.configuration = &Configuration{} + return configuration.configuration, nil + } + } + return nil, err + } + + if configuration.configuration.modTime.Equal(fi.ModTime()) && + configuration.configuration.fileSize == fi.Size() { + return configuration.configuration, nil + } + + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + d := json.NewDecoder(f) + d.DisallowUnknownFields() + var conf Configuration + err = d.Decode(&conf) + if err != nil { + return nil, err + } + configuration.configuration = &conf + return configuration.configuration, nil +} + +// Description represents a group description together with some metadata +// about the JSON file it was deserialised from. type Description struct { // The file this was deserialised from. This is not necessarily // the name of the group, for example in case of a subgroup. diff --git a/webserver/webserver.go b/webserver/webserver.go index 187fc03..7ed637c 100644 --- a/webserver/webserver.go +++ b/webserver/webserver.go @@ -1,7 +1,6 @@ package webserver import ( - "bufio" "context" "crypto/tls" "encoding/json" @@ -327,26 +326,20 @@ func publicHandler(w http.ResponseWriter, r *http.Request) { return } -func getPassword(dataDir string) (string, string, error) { - f, err := os.Open(filepath.Join(dataDir, "passwd")) +func adminMatch(username, password string) (bool, error) { + conf, err := group.GetConfiguration() if err != nil { - return "", "", err - } - defer f.Close() - - r := bufio.NewReader(f) - - s, err := r.ReadString('\n') - if err != nil { - return "", "", err + return false, err } - l := strings.SplitN(strings.TrimSpace(s), ":", 2) - if len(l) != 2 { - return "", "", errors.New("couldn't parse passwords") + for _, cred := range conf.Admin { + if cred.Username == "" || cred.Username == username { + if ok, _ := cred.Password.Match(password); ok { + return true, nil + } + } } - - return l[0], l[1], nil + return false, nil } func failAuthentication(w http.ResponseWriter, realm string) { @@ -356,15 +349,16 @@ func failAuthentication(w http.ResponseWriter, realm string) { } func statsHandler(w http.ResponseWriter, r *http.Request, dataDir string) { - u, p, err := getPassword(dataDir) - if err != nil { - log.Printf("Passwd: %v", err) + username, password, ok := r.BasicAuth() + if !ok { failAuthentication(w, "stats") return } - username, password, ok := r.BasicAuth() - if !ok || username != u || password != p { + if ok, err := adminMatch(username, password); !ok { + if err != nil { + log.Printf("Administrator password: %v", err) + } failAuthentication(w, "stats") return } @@ -377,7 +371,7 @@ func statsHandler(w http.ResponseWriter, r *http.Request, dataDir string) { ss := stats.GetGroups() e := json.NewEncoder(w) - err = e.Encode(ss) + err := e.Encode(ss) if err != nil { log.Printf("stats.json: %v", err) }