diff --git a/static/common.css b/static/common.css
index 5222644..64cc4cd 100644
--- a/static/common.css
+++ b/static/common.css
@@ -6,6 +6,10 @@ h1 {
font-size: 160%;
}
+.inline {
+ display: inline;
+}
+
.signature {
border-top: solid;
margin-top: 2em;
diff --git a/webserver.go b/webserver.go
index 99463d8..872ccee 100644
--- a/webserver.go
+++ b/webserver.go
@@ -9,7 +9,9 @@ import (
"io"
"log"
"net/http"
+ "net/url"
"os"
+ "path"
"path/filepath"
"strings"
"time"
@@ -24,6 +26,12 @@ func webserver() {
mungeHeader(w)
http.ServeFile(w, r, staticRoot+"/sfu.html")
})
+ 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("/public-groups.json", publicHandler)
http.HandleFunc("/stats", statsHandler)
@@ -95,22 +103,23 @@ func getPassword() (string, string, error) {
return l[0], l[1], nil
}
-func statsHandler(w http.ResponseWriter, r *http.Request) {
- bail := func() {
- w.Header().Set("www-authenticate", "basic realm=\"stats\"")
- http.Error(w, "Haha!", http.StatusUnauthorized)
- }
+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) {
u, p, err := getPassword()
if err != nil {
log.Printf("Passwd: %v", err)
- bail()
+ failAuthentication(w, "stats")
return
}
username, password, ok := r.BasicAuth()
if !ok || username != u || password != p {
- bail()
+ failAuthentication(w, "stats")
return
}
@@ -124,6 +133,7 @@ func statsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "\n
\n")
fmt.Fprintf(w, "Stats\n")
+ fmt.Fprintf(w, "")
fmt.Fprintf(w, "\n")
printBitrate := func(w io.Writer, rate, maxRate uint64) error {
@@ -192,3 +202,161 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
}
}()
}
+
+func recordingsHandler(w http.ResponseWriter, r *http.Request) {
+ if len(r.URL.Path) < 12 || r.URL.Path[:12] != "/recordings/" {
+ http.Error(w, "server error", http.StatusInternalServerError)
+ return
+ }
+
+ pth := r.URL.Path[12:]
+
+ if pth == "" {
+ http.Error(w, "nothing to see", http.StatusNotImplemented)
+ return
+ }
+
+ f, err := os.Open(filepath.Join(recordingsDir, pth))
+ if err != nil {
+ if os.IsNotExist(err) {
+ http.NotFound(w, r)
+ } else {
+ http.Error(w, "server error",
+ http.StatusInternalServerError)
+ }
+ return
+ }
+ defer f.Close()
+
+ fi, err := f.Stat()
+ if err != nil {
+ http.Error(w, "server error", http.StatusInternalServerError)
+ return
+ }
+
+ if fi.IsDir() {
+ if pth[len(pth)-1] != '/' {
+ http.Redirect(w, r,
+ r.URL.Path+"/", http.StatusPermanentRedirect)
+ return
+ }
+ ok := checkGroupPermissions(w, r, path.Dir(pth))
+ if !ok {
+ failAuthentication(w, "recordings/"+path.Dir(pth))
+ return
+ }
+ if r.Method == "POST" {
+ handleGroupAction(w, r, path.Dir(pth))
+ } else {
+ serveGroupRecordings(w, r, f, path.Dir(pth))
+ }
+ } else {
+ ok := checkGroupPermissions(w, r, path.Dir(pth))
+ if !ok {
+ failAuthentication(w, "recordings/"+path.Dir(pth))
+ return
+ }
+ http.ServeContent(w, r, r.URL.Path, fi.ModTime(), f)
+ }
+}
+
+func handleGroupAction(w http.ResponseWriter, r *http.Request, group string) {
+ if r.Method != "POST" {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ err := r.ParseForm()
+ if err != nil {
+ http.Error(w, "couldn't parse request", http.StatusBadRequest)
+ return
+ }
+
+ q := r.Form.Get("q")
+
+ switch q {
+ case "delete":
+ filename := r.Form.Get("filename")
+ if group == "" || filename == "" {
+ http.Error(w, "no filename provided",
+ http.StatusBadRequest)
+ return
+ }
+ err := os.Remove(
+ filepath.Join(recordingsDir, group+"/"+filename),
+ )
+ if err != nil {
+ if os.IsPermission(err) {
+ http.Error(w, "unauthorized",
+ http.StatusForbidden)
+ } else {
+ http.Error(w, "server error",
+ http.StatusInternalServerError)
+ }
+ return
+ }
+ http.Redirect(w, r, "/recordings/"+group+"/",
+ http.StatusSeeOther)
+ return
+ default:
+ http.Error(w, "unknown query", http.StatusBadRequest)
+ }
+}
+
+func checkGroupPermissions(w http.ResponseWriter, r *http.Request, group string) bool {
+ desc, err := getDescription(group)
+ if err != nil {
+ return false
+ }
+
+ user, pass, ok := r.BasicAuth()
+ if !ok {
+ return false
+ }
+
+ p, err := getPermission(desc, user, pass)
+ if err != nil || !p.Record {
+ return false
+ }
+
+ return true
+}
+
+func serveGroupRecordings(w http.ResponseWriter, r *http.Request, f *os.File, group string) {
+ fis, err := f.Readdir(-1)
+ if err != nil {
+ http.Error(w, "server error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("content-type", "text/html; charset=utf-8")
+ w.Header().Set("cache-control", "no-cache")
+
+ if r.Method == "HEAD" {
+ return
+ }
+
+ fmt.Fprintf(w, "\n\n")
+ fmt.Fprintf(w, "Recordings for group %v\n", group)
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "\n")
+
+ fmt.Fprintf(w, "\n")
+ for _, fi := range fis {
+ if fi.IsDir() {
+ continue
+ }
+ fmt.Fprintf(w, "%v | %d | ",
+ html.EscapeString(fi.Name()),
+ html.EscapeString(fi.Name()),
+ fi.Size(),
+ )
+ fmt.Fprintf(w,
+ " |
\n",
+ url.PathEscape(group), fi.Name())
+ }
+ fmt.Fprintf(w, "
\n")
+ fmt.Fprintf(w, "\n")
+}