X-Git-Url: https://code.octet-stream.net/broadcaster/blobdiff_plain/7b615b3c71825b5b229b78509a16db37e1d3f38d..db43bd0e4ff4a2aee5f6a6610a4ad78c357f96b0:/server/main.go diff --git a/server/main.go b/server/main.go index 8f76cbf..d1d47d3 100644 --- a/server/main.go +++ b/server/main.go @@ -5,6 +5,7 @@ import ( "embed" "flag" "fmt" + "golang.org/x/crypto/bcrypt" "golang.org/x/net/websocket" "html/template" "io" @@ -22,6 +23,7 @@ const formatString = "2006-01-02T15:04" //go:embed templates/* var content embed.FS + //var content = os.DirFS("../broadcaster-server/") var config ServerConfig = NewServerConfig() @@ -75,40 +77,95 @@ func main() { // Public routes http.HandleFunc("/login", logInPage) - http.Handle("/file-downloads/", http.StripPrefix("/file-downloads/", http.FileServer(http.Dir(config.AudioFilesPath)))) + http.Handle("/file-downloads/", applyDisposition(http.StripPrefix("/file-downloads/", http.FileServer(http.Dir(config.AudioFilesPath))))) // Authenticated routes - http.HandleFunc("/", homePage) - http.HandleFunc("/logout", logOutPage) - http.HandleFunc("/change-password", changePasswordPage) + http.Handle("/", requireUser(homePage)) + http.Handle("/logout", requireUser(logOutPage)) + http.Handle("/change-password", requireUser(changePasswordPage)) - http.HandleFunc("/playlists/", playlistSection) - http.HandleFunc("/files/", fileSection) - http.HandleFunc("/radios/", radioSection) + http.Handle("/playlists/", requireUser(playlistSection)) + http.Handle("/files/", requireUser(fileSection)) + http.Handle("/radios/", requireUser(radioSection)) - http.Handle("/radio-ws", websocket.Handler(RadioSync)) - http.Handle("/web-ws", websocket.Handler(WebSync)) - http.HandleFunc("/stop", stopPage) + http.Handle("/stop", requireUser(stopPage)) // Admin routes + http.Handle("/users/", requireAdmin(userSection)) + + // Websocket routes, which perform their own auth + + http.Handle("/radio-ws", websocket.Handler(RadioSync)) + http.Handle("/web-ws", websocket.Handler(WebSync)) + err := http.ListenAndServe(config.BindAddress+":"+strconv.Itoa(config.Port), nil) if err != nil { log.Fatal(err) } } +type DispositionMiddleware struct { + handler http.Handler +} + +func (m DispositionMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("path", r.URL.Path) + if r.URL.Path != "/file-downloads/" { + w.Header().Add("Content-Disposition", "attachment") + } + m.handler.ServeHTTP(w, r) +} + +func applyDisposition(handler http.Handler) DispositionMiddleware { + return DispositionMiddleware{ + handler: handler, + } +} + +type authenticatedHandler func(http.ResponseWriter, *http.Request, User) + +type AuthMiddleware struct { + handler authenticatedHandler + mustBeAdmin bool +} + +func (m AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + user, err := currentUser(w, r) + if err != nil || (m.mustBeAdmin && !user.IsAdmin) { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + m.handler(w, r, user) +} + +func requireUser(handler authenticatedHandler) AuthMiddleware { + return AuthMiddleware{ + handler: handler, + mustBeAdmin: false, + } +} + +func requireAdmin(handler authenticatedHandler) AuthMiddleware { + return AuthMiddleware{ + handler: handler, + mustBeAdmin: true, + } +} + type HeaderData struct { SelectedMenu string - Username string + User User + Version string } -func renderHeader(w http.ResponseWriter, selectedMenu string) { +func renderHeader(w http.ResponseWriter, selectedMenu string, user User) { tmpl := template.Must(template.ParseFS(content, "templates/header.html")) data := HeaderData{ SelectedMenu: selectedMenu, - Username: "username", + User: user, + Version: version, } err := tmpl.Execute(w, data) if err != nil { @@ -129,8 +186,8 @@ type HomeData struct { Username string } -func homePage(w http.ResponseWriter, r *http.Request) { - renderHeader(w, "status") +func homePage(w http.ResponseWriter, r *http.Request, user User) { + renderHeader(w, "status", user) tmpl := template.Must(template.ParseFS(content, "templates/index.html")) data := HomeData{ LoggedIn: true, @@ -145,7 +202,6 @@ type LogInData struct { } func logInPage(w http.ResponseWriter, r *http.Request) { - log.Println("Log in page!") r.ParseForm() username := r.Form["username"] password := r.Form["password"] @@ -164,37 +220,37 @@ func logInPage(w http.ResponseWriter, r *http.Request) { data := LogInData{ Error: errText, } - renderHeader(w, "") + renderHeader(w, "", User{}) tmpl := template.Must(template.ParseFS(content, "templates/login.html")) tmpl.Execute(w, data) renderFooter(w) } -func playlistSection(w http.ResponseWriter, r *http.Request) { +func playlistSection(w http.ResponseWriter, r *http.Request, user User) { path := strings.Split(r.URL.Path, "/") if len(path) != 3 { http.NotFound(w, r) return } if path[2] == "new" { - editPlaylistPage(w, r, 0) + editPlaylistPage(w, r, 0, user) } else if path[2] == "submit" && r.Method == "POST" { submitPlaylist(w, r) } else if path[2] == "delete" && r.Method == "POST" { deletePlaylist(w, r) } else if path[2] == "" { - playlistsPage(w, r) + playlistsPage(w, r, user) } else { id, err := strconv.Atoi(path[2]) if err != nil { http.NotFound(w, r) return } - editPlaylistPage(w, r, id) + editPlaylistPage(w, r, id, user) } } -func fileSection(w http.ResponseWriter, r *http.Request) { +func fileSection(w http.ResponseWriter, r *http.Request, user User) { path := strings.Split(r.URL.Path, "/") if len(path) != 3 { http.NotFound(w, r) @@ -205,48 +261,160 @@ func fileSection(w http.ResponseWriter, r *http.Request) { } else if path[2] == "delete" && r.Method == "POST" { deleteFile(w, r) } else if path[2] == "" { - filesPage(w, r) + filesPage(w, r, user) } else { http.NotFound(w, r) return } } -func radioSection(w http.ResponseWriter, r *http.Request) { +func radioSection(w http.ResponseWriter, r *http.Request, user User) { path := strings.Split(r.URL.Path, "/") if len(path) != 3 { http.NotFound(w, r) return } if path[2] == "new" { - editRadioPage(w, r, 0) + editRadioPage(w, r, 0, user) } else if path[2] == "submit" && r.Method == "POST" { submitRadio(w, r) } else if path[2] == "delete" && r.Method == "POST" { deleteRadio(w, r) } else if path[2] == "" { - radiosPage(w, r) + radiosPage(w, r, user) } else { id, err := strconv.Atoi(path[2]) if err != nil { http.NotFound(w, r) return } - editRadioPage(w, r, id) + editRadioPage(w, r, id, user) } } +func userSection(w http.ResponseWriter, r *http.Request, user User) { + path := strings.Split(r.URL.Path, "/") + if len(path) != 3 { + http.NotFound(w, r) + return + } + if path[2] == "new" { + editUserPage(w, r, 0, user) + } else if path[2] == "submit" && r.Method == "POST" { + submitUser(w, r) + } else if path[2] == "delete" && r.Method == "POST" { + deleteUser(w, r) + } else if path[2] == "reset-password" && r.Method == "POST" { + resetUserPassword(w, r) + } else if path[2] == "" { + usersPage(w, r, user) + } else { + id, err := strconv.Atoi(path[2]) + if err != nil { + http.NotFound(w, r) + return + } + editUserPage(w, r, id, user) + } +} + +type EditUserPageData struct { + User User +} + +func editUserPage(w http.ResponseWriter, r *http.Request, id int, user User) { + var data EditUserPageData + if id != 0 { + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + data.User = user + } + renderHeader(w, "users", user) + tmpl := template.Must(template.ParseFS(content, "templates/user.html")) + tmpl.Execute(w, data) + renderFooter(w) +} + +func submitUser(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err == nil { + id, err := strconv.Atoi(r.Form.Get("userId")) + if err != nil { + return + } + if id == 0 { + newPassword := r.Form.Get("password") + hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return + } + user := User{ + Id: 0, + Username: r.Form.Get("username"), + IsAdmin: r.Form.Get("isAdmin") == "1", + PasswordHash: string(hashed), + } + db.CreateUser(user) + } else { + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + db.SetUserIsAdmin(user.Username, r.Form.Get("isAdmin") == "1") + } + } + http.Redirect(w, r, "/users/", http.StatusFound) +} + +func deleteUser(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err == nil { + id, err := strconv.Atoi(r.Form.Get("userId")) + if err != nil { + return + } + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + db.DeleteUser(user.Username) + } + http.Redirect(w, r, "/users/", http.StatusFound) +} + +func resetUserPassword(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err == nil { + id, err := strconv.Atoi(r.Form.Get("userId")) + if err != nil { + return + } + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + newPassword := r.Form.Get("newPassword") + hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return + } + db.SetUserPassword(user.Username, string(hashed)) + } + http.Redirect(w, r, "/users/", http.StatusFound) +} + type ChangePasswordPageData struct { Message string ShowForm bool } -func changePasswordPage(w http.ResponseWriter, r *http.Request) { - user, err := currentUser(w, r) - if err != nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } +func changePasswordPage(w http.ResponseWriter, r *http.Request, user User) { var data ChangePasswordPageData if r.Method == "POST" { err := r.ParseForm() @@ -265,7 +433,7 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request) { data.ShowForm = false cookie, err := r.Cookie("broadcast_session") if err == nil { - log.Println("clearing other sessions for username", user.Username, "token", cookie.Value) + log.Println("Clearing other sessions for username", user.Username, "token", cookie.Value) db.ClearOtherSessions(user.Username, cookie.Value) } } @@ -273,9 +441,26 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request) { data.Message = "" data.ShowForm = true } - renderHeader(w, "change-password") + renderHeader(w, "change-password", user) tmpl := template.Must(template.ParseFS(content, "templates/change_password.html")) - err = tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + log.Fatal(err) + } + renderFooter(w) +} + +type UsersPageData struct { + Users []User +} + +func usersPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "users", user) + data := UsersPageData{ + Users: db.GetUsers(), + } + tmpl := template.Must(template.ParseFS(content, "templates/users.html")) + err := tmpl.Execute(w, data) if err != nil { log.Fatal(err) } @@ -286,11 +471,14 @@ type PlaylistsPageData struct { Playlists []Playlist } -func playlistsPage(w http.ResponseWriter, _ *http.Request) { - renderHeader(w, "playlists") +func playlistsPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "playlists", user) data := PlaylistsPageData{ Playlists: db.GetPlaylists(), } + for i := range data.Playlists { + data.Playlists[i].StartTime = strings.Replace(data.Playlists[i].StartTime, "T", " ", -1) + } tmpl := template.Must(template.ParseFS(content, "templates/playlists.html")) err := tmpl.Execute(w, data) if err != nil { @@ -303,8 +491,8 @@ type RadiosPageData struct { Radios []Radio } -func radiosPage(w http.ResponseWriter, _ *http.Request) { - renderHeader(w, "radios") +func radiosPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "radios", user) data := RadiosPageData{ Radios: db.GetRadios(), } @@ -322,7 +510,7 @@ type EditPlaylistPageData struct { Files []string } -func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) { +func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int, user User) { var data EditPlaylistPageData for _, f := range files.Files() { data.Files = append(data.Files, f.Name) @@ -341,7 +529,7 @@ func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) { data.Playlist = playlist data.Entries = db.GetEntriesForPlaylist(id) } - renderHeader(w, "radios") + renderHeader(w, "playlists", user) tmpl := template.Must(template.ParseFS(content, "templates/playlist.html")) tmpl.Execute(w, data) renderFooter(w) @@ -417,7 +605,7 @@ type EditRadioPageData struct { Radio Radio } -func editRadioPage(w http.ResponseWriter, r *http.Request, id int) { +func editRadioPage(w http.ResponseWriter, r *http.Request, id int, user User) { var data EditRadioPageData if id == 0 { data.Radio.Name = "New Radio" @@ -430,7 +618,7 @@ func editRadioPage(w http.ResponseWriter, r *http.Request, id int) { } data.Radio = radio } - renderHeader(w, "radios") + renderHeader(w, "radios", user) tmpl := template.Must(template.ParseFS(content, "templates/radio.html")) tmpl.Execute(w, data) renderFooter(w) @@ -472,12 +660,11 @@ type FilesPageData struct { Files []FileSpec } -func filesPage(w http.ResponseWriter, _ *http.Request) { - renderHeader(w, "files") +func filesPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "files", user) data := FilesPageData{ Files: files.Files(), } - log.Println("file page data", data) tmpl := template.Must(template.ParseFS(content, "templates/files.html")) err := tmpl.Execute(w, data) if err != nil { @@ -506,26 +693,25 @@ func uploadFile(w http.ResponseWriter, r *http.Request) { f, _ := os.Create(path) defer f.Close() io.Copy(f, file) - log.Println("uploaded file to", path) + log.Println("Uploaded file to", path) files.Refresh() } http.Redirect(w, r, "/files/", http.StatusFound) } -func logOutPage(w http.ResponseWriter, r *http.Request) { +func logOutPage(w http.ResponseWriter, r *http.Request, user User) { + cookie, err := r.Cookie("broadcast_session") + if err == nil { + db.ClearSession(user.Username, cookie.Value) + } clearSessionCookie(w) - renderHeader(w, "") + renderHeader(w, "", user) tmpl := template.Must(template.ParseFS(content, "templates/logout.html")) tmpl.Execute(w, nil) renderFooter(w) } -func stopPage(w http.ResponseWriter, r *http.Request) { - _, err := currentUser(w, r) - if err != nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } +func stopPage(w http.ResponseWriter, r *http.Request, user User) { r.ParseForm() radioId, err := strconv.Atoi(r.Form.Get("radioId")) if err != nil {