package webserver import ( "bufio" "context" "encoding/json" "errors" "fmt" "html" "io" "log" "net/http" "net/url" "os" "path" "path/filepath" "sort" "strings" "sync/atomic" "time" "github.com/gorilla/websocket" "sfu/diskwriter" "sfu/group" "sfu/rtpconn" "sfu/stats" ) var server atomic.Value var StaticRoot string func Serve(address string, dataDir string) error { http.Handle("/", &fileHandler{http.Dir(StaticRoot)}) http.HandleFunc("/group/", groupHandler) http.HandleFunc("/recordings", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/recordings/", http.StatusPermanentRedirect) }) http.HandleFunc("/recordings/", recordingsHandler) http.HandleFunc("/ws", wsHandler) http.HandleFunc("/ice-servers.json", func(w http.ResponseWriter, r *http.Request) { mungeHeader(w) serveFile(w, r, filepath.Join(dataDir, "ice-servers.json")) }) http.HandleFunc("/public-groups.json", publicHandler) http.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) { statsHandler(w, r, dataDir) }) s := &http.Server{ Addr: address, ReadHeaderTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, } s.RegisterOnShutdown(func() { group.Range(func(g *group.Group) bool { go g.Shutdown("server is shutting down") return true }) }) server.Store(s) err := s.ListenAndServeTLS( filepath.Join(dataDir, "cert.pem"), filepath.Join(dataDir, "key.pem"), ) if err == http.ErrServerClosed { return nil } return err } func mungeHeader(w http.ResponseWriter) { w.Header().Add("Content-Security-Policy", "connect-src ws: wss: 'self'; img-src data: 'self'; media-src blob: 'self'; default-src 'self'") } func notFound(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusNotFound) f, err := os.Open(path.Join(StaticRoot, "404.html")) if err != nil { fmt.Fprintln(w, "
Not found
") return } defer f.Close() io.Copy(w, f) } var ErrIsDirectory = errors.New("is a directory") func httpError(w http.ResponseWriter, err error) { if os.IsNotExist(err) { notFound(w) return } if os.IsPermission(err) { http.Error(w, "403 forbidden", http.StatusForbidden) return } http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) return } const ( normalCacheControl = "max-age=1800" veryCachableCacheControl = "max-age=86400" ) func makeCachable(w http.ResponseWriter, p string, fi os.FileInfo, cachable bool) { etag := fmt.Sprintf("\"%v-%v\"", fi.Size(), fi.ModTime().UnixNano()) w.Header().Set("ETag", etag) if !cachable { w.Header().Set("cache-control", "no-cache") return } cc := normalCacheControl if strings.HasPrefix(p, "/fonts/") || strings.HasPrefix(p, "/scripts/") || strings.HasPrefix(p, "/css/") { cc = veryCachableCacheControl } w.Header().Set("Cache-Control", cc) } // fileHandler is our custom reimplementation of http.FileServer type fileHandler struct { root http.FileSystem } func (fh *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { mungeHeader(w) p := r.URL.Path // this ensures any leading .. are removed by path.Clean below if !strings.HasPrefix(p, "/") { p = "/" + p r.URL.Path = p } p = path.Clean(p) f, err := fh.root.Open(p) if err != nil { httpError(w, err) return } defer f.Close() fi, err := f.Stat() if err != nil { httpError(w, err) return } if fi.IsDir() { u := r.URL.Path if u[len(u)-1] != '/' { http.Redirect(w, r, u+"/", http.StatusPermanentRedirect) return } index := path.Join(p, "index.html") ff, err := fh.root.Open(index) if err != nil { if os.IsNotExist(err) { err = os.ErrPermission } httpError(w, err) return } defer ff.Close() dd, err := ff.Stat() if err != nil { httpError(w, err) return } if dd.IsDir() { httpError(w, ErrIsDirectory) return } f, fi = ff, dd p = index } makeCachable(w, p, fi, true) http.ServeContent(w, r, fi.Name(), fi.ModTime(), f) } // serveFile is similar to http.ServeFile, except that it doesn't check // for .. and adds cachability headers. func serveFile(w http.ResponseWriter, r *http.Request, p string) { f, err := os.Open(p) if err != nil { httpError(w, err) return } defer f.Close() fi, err := f.Stat() if err != nil { httpError(w, err) return } if fi.IsDir() { httpError(w, ErrIsDirectory) return } makeCachable(w, p, fi, true) http.ServeContent(w, r, fi.Name(), fi.ModTime(), f) } func parseGroupName(path string) string { if !strings.HasPrefix(path, "/group/") { return "" } name := path[len("/group/"):] if name == "" { return "" } if name[len(name)-1] == '/' { name = name[:len(name)-1] } return name } func groupHandler(w http.ResponseWriter, r *http.Request) { mungeHeader(w) name := parseGroupName(r.URL.Path) if name == "" { notFound(w) return } if strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.Path[:len(r.URL.Path)-1], http.StatusPermanentRedirect) return } g, err := group.Add(name, nil) if err != nil { if os.IsNotExist(err) { notFound(w) } else { log.Printf("addGroup: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } return } if redirect := g.Redirect(); redirect != "" { http.Redirect(w, r, redirect, http.StatusPermanentRedirect) return } serveFile(w, r, filepath.Join(StaticRoot, "sfu.html")) } func publicHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/json") w.Header().Set("cache-control", "no-cache") if r.Method == "HEAD" { return } g := group.GetPublic() e := json.NewEncoder(w) e.Encode(g) return } func getPassword(dataDir string) (string, string, error) { f, err := os.Open(filepath.Join(dataDir, "passwd")) if err != nil { return "", "", err } defer f.Close() r := bufio.NewReader(f) s, err := r.ReadString('\n') if err != nil { return "", "", err } l := strings.SplitN(strings.TrimSpace(s), ":", 2) if len(l) != 2 { return "", "", errors.New("couldn't parse passwords") } return l[0], l[1], nil } func failAuthentication(w http.ResponseWriter, realm string) { w.Header().Set("www-authenticate", fmt.Sprintf("basic realm=\"%v\"", realm)) http.Error(w, "Haha!", http.StatusUnauthorized) } func statsHandler(w http.ResponseWriter, r *http.Request, dataDir string) { u, p, err := getPassword(dataDir) if err != nil { log.Printf("Passwd: %v", err) failAuthentication(w, "stats") return } username, password, ok := r.BasicAuth() if !ok || username != u || password != p { failAuthentication(w, "stats") return } w.Header().Set("content-type", "text/html; charset=utf-8") w.Header().Set("cache-control", "no-cache") if r.Method == "HEAD" { return } ss := stats.GetGroups() fmt.Fprintf(w, "\n\n") fmt.Fprintf(w, "%v
\n", html.EscapeString(gs.Name)) fmt.Fprintf(w, "%v | |||
Up | %v | ", up.Id) if up.MaxBitrate > 0 { fmt.Fprintf(w, "%v | ", up.MaxBitrate) } fmt.Fprintf(w, "|
Down | %v | ", down.Id) if down.MaxBitrate > 0 { fmt.Fprintf(w, "%v | ", down.MaxBitrate) } fmt.Fprintf(w, "
%v | %d | ", html.EscapeString(fi.Name()), html.EscapeString(fi.Name()), fi.Size(), ) fmt.Fprintf(w, "