From 33a19d553807d171f6ba9f4dafe30f43bc4bab5e Mon Sep 17 00:00:00 2001 From: Thomas Karpiniec Date: Tue, 22 Oct 2024 19:36:37 +1100 Subject: [PATCH 1/1] Add licence, etc. --- .gitignore | 8 +- LICENSE | 9 + {radio => broadcaster-radio}/config.go | 2 +- {radio => broadcaster-radio}/files_machine.go | 4 +- {radio => broadcaster-radio}/gpio.go | 0 radio/radio.go => broadcaster-radio/main.go | 13 +- {radio => broadcaster-radio}/status.go | 2 +- {server => broadcaster-server}/command.go | 2 +- {server => broadcaster-server}/config.go | 0 {server => broadcaster-server}/database.go | 51 +++++- {server => broadcaster-server}/files.go | 0 .../main.go | 164 +++++++++++++----- {server => broadcaster-server}/model.go | 5 +- {server => broadcaster-server}/playlist.go | 0 {server => broadcaster-server}/radio_sync.go | 2 +- {server => broadcaster-server}/session.go | 13 +- {server => broadcaster-server}/status.go | 2 +- .../templates/change_password.html | 25 +++ .../templates}/files.html | 5 +- .../templates}/index.html | 8 +- .../templates}/login.html | 0 .../templates}/logout.html | 0 .../templates}/playlist.html | 4 +- .../templates}/playlists.html | 4 +- .../templates}/radio.html | 4 +- .../templates}/radios.html | 4 +- .../templates}/radios.partial.html | 0 broadcaster-server/user.go | 82 +++++++++ {server => broadcaster-server}/web_sync.go | 12 +- go.mod | 1 + go.sum | 2 + {protocol => internal/protocol}/protocol.go | 0 32 files changed, 334 insertions(+), 94 deletions(-) create mode 100644 LICENSE rename {radio => broadcaster-radio}/config.go (97%) rename {radio => broadcaster-radio}/files_machine.go (95%) rename {radio => broadcaster-radio}/gpio.go (100%) rename radio/radio.go => broadcaster-radio/main.go (96%) rename {radio => broadcaster-radio}/status.go (98%) rename {server => broadcaster-server}/command.go (94%) rename {server => broadcaster-server}/config.go (100%) rename {server => broadcaster-server}/database.go (75%) rename {server => broadcaster-server}/files.go (100%) rename server/broadcaster.go => broadcaster-server/main.go (68%) rename {server => broadcaster-server}/model.go (79%) rename {server => broadcaster-server}/playlist.go (100%) rename {server => broadcaster-server}/radio_sync.go (98%) rename {server => broadcaster-server}/session.go (69%) rename {server => broadcaster-server}/status.go (95%) create mode 100644 broadcaster-server/templates/change_password.html rename {templates => broadcaster-server/templates}/files.html (56%) rename {templates => broadcaster-server/templates}/index.html (90%) rename {templates => broadcaster-server/templates}/login.html (100%) rename {templates => broadcaster-server/templates}/logout.html (100%) rename {templates => broadcaster-server/templates}/playlist.html (96%) rename {templates => broadcaster-server/templates}/playlists.html (68%) rename {templates => broadcaster-server/templates}/radio.html (91%) rename {templates => broadcaster-server/templates}/radios.html (69%) rename {templates => broadcaster-server/templates}/radios.partial.html (100%) create mode 100644 broadcaster-server/user.go rename {server => broadcaster-server}/web_sync.go (91%) rename {protocol => internal/protocol}/protocol.go (100%) diff --git a/.gitignore b/.gitignore index c2f7731..b657038 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ -audio -broadcaster-server -broadcaster-radio -test-server.conf -test-radio.conf -test.db* +build +*.kate-swp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5dacd4d --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024 Thomas Karpiniec + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/radio/config.go b/broadcaster-radio/config.go similarity index 97% rename from radio/config.go rename to broadcaster-radio/config.go index 3152653..6837b7d 100644 --- a/radio/config.go +++ b/broadcaster-radio/config.go @@ -66,5 +66,5 @@ func (c *RadioConfig) ApplyDefaults() { func (c *RadioConfig) WebsocketURL() string { addr := strings.Replace(c.ServerURL, "https://", "wss://", -1) addr = strings.Replace(addr, "http://", "ws://", -1) - return addr + "/radiosync" + return addr + "/radio-ws" } diff --git a/radio/files_machine.go b/broadcaster-radio/files_machine.go similarity index 95% rename from radio/files_machine.go rename to broadcaster-radio/files_machine.go index ea2f775..143397a 100644 --- a/radio/files_machine.go +++ b/broadcaster-radio/files_machine.go @@ -1,7 +1,7 @@ package main import ( - "code.octet-stream.net/broadcaster/protocol" + "code.octet-stream.net/broadcaster/internal/protocol" "crypto/sha256" "encoding/hex" "io" @@ -106,7 +106,7 @@ func (m *FilesMachine) DownloadSingle(filename string, downloadResult chan<- err return } defer out.Close() - resp, err := http.Get(config.ServerURL + "/audio-files/" + filename) + resp, err := http.Get(config.ServerURL + "/file-downloads/" + filename) if err != nil { downloadResult <- err return diff --git a/radio/gpio.go b/broadcaster-radio/gpio.go similarity index 100% rename from radio/gpio.go rename to broadcaster-radio/gpio.go diff --git a/radio/radio.go b/broadcaster-radio/main.go similarity index 96% rename from radio/radio.go rename to broadcaster-radio/main.go index bcf5e38..6e31772 100644 --- a/radio/radio.go +++ b/broadcaster-radio/main.go @@ -1,9 +1,10 @@ package main import ( - "code.octet-stream.net/broadcaster/protocol" + "code.octet-stream.net/broadcaster/internal/protocol" "encoding/json" "flag" + "fmt" "github.com/gopxl/beep/v2" "github.com/gopxl/beep/v2/mp3" "github.com/gopxl/beep/v2/speaker" @@ -18,19 +19,25 @@ import ( "time" ) +const version = "v1.0.0" const sampleRate = 44100 var config RadioConfig = NewRadioConfig() func main() { configFlag := flag.String("c", "", "path to configuration file") - // TODO: support this - //generateFlag := flag.String("g", "", "create a template config file with specified name then exit") + versionFlag := flag.Bool("v", false, "print version and exit") flag.Parse() + if *versionFlag { + fmt.Println("Broadcaster Radio", version) + os.Exit(0) + } if *configFlag == "" { log.Fatal("must specify a configuration file with -c") } + + log.Println("Broadcaster Radio", version, "starting up") config.LoadFromFile(*configFlag) statusCollector.Config <- config diff --git a/radio/status.go b/broadcaster-radio/status.go similarity index 98% rename from radio/status.go rename to broadcaster-radio/status.go index 797a28f..7836815 100644 --- a/radio/status.go +++ b/broadcaster-radio/status.go @@ -1,7 +1,7 @@ package main import ( - "code.octet-stream.net/broadcaster/protocol" + "code.octet-stream.net/broadcaster/internal/protocol" "encoding/json" "golang.org/x/net/websocket" "time" diff --git a/server/command.go b/broadcaster-server/command.go similarity index 94% rename from server/command.go rename to broadcaster-server/command.go index cd7410b..346e842 100644 --- a/server/command.go +++ b/broadcaster-server/command.go @@ -1,7 +1,7 @@ package main import ( - "code.octet-stream.net/broadcaster/protocol" + "code.octet-stream.net/broadcaster/internal/protocol" "encoding/json" "golang.org/x/net/websocket" "sync" diff --git a/server/config.go b/broadcaster-server/config.go similarity index 100% rename from server/config.go rename to broadcaster-server/config.go diff --git a/server/database.go b/broadcaster-server/database.go similarity index 75% rename from server/database.go rename to broadcaster-server/database.go index d6bbf92..1312208 100644 --- a/server/database.go +++ b/broadcaster-server/database.go @@ -41,6 +41,7 @@ func InitDatabase() { CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, enabled INTEGER, name TEXT, start_time TEXT); CREATE TABLE IF NOT EXISTS playlist_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id INTEGER, position INTEGER, filename TEXT, delay_seconds INTEGER, is_relative INTEGER, CONSTRAINT fk_playlists FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS radios (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, token TEXT); + CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, is_admin INTEGER); ` _, err = db.sqldb.Exec(sqlStmt) if err != nil { @@ -60,7 +61,7 @@ func (d *Database) InsertSession(user string, token string, expiry time.Time) { } } -func (d *Database) GetUserForSession(token string) (string, error) { +func (d *Database) GetUserNameForSession(token string) (string, error) { var username string err := d.sqldb.QueryRow("SELECT username FROM sessions WHERE token = ? AND expiry > CURRENT_TIMESTAMP", token).Scan(&username) if err != nil { @@ -69,6 +70,54 @@ func (d *Database) GetUserForSession(token string) (string, error) { return username, nil } +func (d *Database) GetUser(username string) (User, error) { + var user User + err := d.sqldb.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE username = ?", username).Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin) + if err != nil { + return User{}, errors.New("no user with that username") + } + return user, nil +} + +func (d *Database) GetUsers() []User { + ret := make([]User, 0) + rows, err := d.sqldb.Query("SELECT id, username, password_hash, is_admin FROM users ORDER BY username ASC") + if err != nil { + return ret + } + defer rows.Close() + for rows.Next() { + var u User + if err := rows.Scan(&u.Id, &u.Username, &u.PasswordHash, &u.IsAdmin); err != nil { + return ret + } + ret = append(ret, u) + } + return ret +} + +func (d *Database) SetUserPassword(username string, passwordHash string) { + d.sqldb.Exec("UPDATE users SET password_hash = ? WHERE username = ?", passwordHash, username) +} + +func (d *Database) ClearOtherSessions(username string, token string) { + d.sqldb.Exec("DELETE FROM sessions WHERE username = ? AND token != ?", username, token) +} + +func (d *Database) SetUserIsAdmin(username string, isAdmin bool) { + d.sqldb.Exec("UPDATE users SET is_admin = ? WHERE username = ?", isAdmin, username) +} + +func (d *Database) CreateUser(user User) error { + _, err := d.sqldb.Exec("INSERT INTO users (username, password_hash, is_admin) values (?, ?, ?)", user.Username, user.PasswordHash, user.IsAdmin) + return err +} + +func (d *Database) DeleteUser(username string) error { + _, err := d.sqldb.Exec("DELETE FROM users WHERE username = ?", username) + return err +} + func (d *Database) CreatePlaylist(playlist Playlist) int { var id int tx, _ := d.sqldb.Begin() diff --git a/server/files.go b/broadcaster-server/files.go similarity index 100% rename from server/files.go rename to broadcaster-server/files.go diff --git a/server/broadcaster.go b/broadcaster-server/main.go similarity index 68% rename from server/broadcaster.go rename to broadcaster-server/main.go index e5f9922..079f977 100644 --- a/server/broadcaster.go +++ b/broadcaster-server/main.go @@ -1,7 +1,10 @@ package main import ( + "bufio" + "embed" "flag" + "fmt" "golang.org/x/net/websocket" "html/template" "io" @@ -14,43 +17,80 @@ import ( "time" ) +const version = "v1.0.0" const formatString = "2006-01-02T15:04" +//go:embed templates/* +var content embed.FS + var config ServerConfig = NewServerConfig() func main() { configFlag := flag.String("c", "", "path to configuration file") - // TODO: support this - //generateFlag := flag.String("g", "", "create a template config file with specified name then exit") + addUserFlag := flag.Bool("a", false, "interactively add an admin user then exit") + versionFlag := flag.Bool("v", false, "print version then exit") flag.Parse() + if *versionFlag { + fmt.Println("Broadcaster Server", version) + os.Exit(0) + } if *configFlag == "" { log.Fatal("must specify a configuration file with -c") } config.LoadFromFile(*configFlag) - log.Println("Hello, World! Woo broadcast time") InitDatabase() defer db.CloseDatabase() + if *addUserFlag { + scanner := bufio.NewScanner(os.Stdin) + fmt.Println("Enter new admin username:") + if !scanner.Scan() { + os.Exit(1) + } + username := scanner.Text() + fmt.Println("Enter new admin password (will be printed in the clear):") + if !scanner.Scan() { + os.Exit(1) + } + password := scanner.Text() + if username == "" || password == "" { + fmt.Println("Both username and password must be specified") + os.Exit(1) + } + if err := users.CreateUser(username, password, true); err != nil { + log.Fatal(err) + } + os.Exit(0) + } + + log.Println("Broadcaster Server", version, "starting up") InitCommandRouter() InitPlaylists() InitAudioFiles(config.AudioFilesPath) InitServerStatus() - http.HandleFunc("/", homePage) + // Public routes + http.HandleFunc("/login", logInPage) + http.Handle("/file-downloads/", http.StripPrefix("/file-downloads/", http.FileServer(http.Dir(config.AudioFilesPath)))) + + // Authenticated routes + + http.HandleFunc("/", homePage) http.HandleFunc("/logout", logOutPage) - http.HandleFunc("/secret", secretPage) - http.HandleFunc("/stop", stopPage) + http.HandleFunc("/change-password", changePasswordPage) + + http.HandleFunc("/playlists/", playlistSection) + http.HandleFunc("/files/", fileSection) + http.HandleFunc("/radios/", radioSection) - http.HandleFunc("/playlist/", playlistSection) - http.HandleFunc("/file/", fileSection) - http.HandleFunc("/radio/", radioSection) + http.Handle("/radio-ws", websocket.Handler(RadioSync)) + http.Handle("/web-ws", websocket.Handler(WebSync)) + http.HandleFunc("/stop", stopPage) - http.Handle("/radiosync", websocket.Handler(RadioSync)) - http.Handle("/websync", websocket.Handler(WebSync)) - http.Handle("/audio-files/", http.StripPrefix("/audio-files/", http.FileServer(http.Dir(config.AudioFilesPath)))) + // Admin routes err := http.ListenAndServe(config.BindAddress+":"+strconv.Itoa(config.Port), nil) if err != nil { @@ -64,7 +104,7 @@ type HomeData struct { } func homePage(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/index.html")) + tmpl := template.Must(template.ParseFS(content, "templates/index.html")) data := HomeData{ LoggedIn: true, Username: "Bob", @@ -72,20 +112,6 @@ func homePage(w http.ResponseWriter, r *http.Request) { tmpl.Execute(w, data) } -func secretPage(w http.ResponseWriter, r *http.Request) { - user, err := currentUser(w, r) - if err != nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } - tmpl := template.Must(template.ParseFiles("templates/index.html")) - data := HomeData{ - LoggedIn: true, - Username: user.username + ", you are special", - } - tmpl.Execute(w, data) -} - type LogInData struct { Error string } @@ -95,23 +121,23 @@ func logInPage(w http.ResponseWriter, r *http.Request) { r.ParseForm() username := r.Form["username"] password := r.Form["password"] - err := "" + errText := "" if username != nil { - log.Println("Looks like we have a username", username[0]) - if username[0] == "admin" && password[0] == "test" { - createSessionCookie(w) + user, err := users.Authenticate(username[0], password[0]) + if err != nil { + errText = "Incorrect login" + } else { + createSessionCookie(w, user.Username) http.Redirect(w, r, "/", http.StatusFound) return - } else { - err = "Incorrect login" } } data := LogInData{ - Error: err, + Error: errText, } - tmpl := template.Must(template.ParseFiles("templates/login.html")) + tmpl := template.Must(template.ParseFS(content, "templates/login.html")) tmpl.Execute(w, data) } @@ -181,6 +207,50 @@ func radioSection(w http.ResponseWriter, r *http.Request) { } } +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 + } + var data ChangePasswordPageData + if r.Method == "POST" { + err := r.ParseForm() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + oldPassword := r.Form.Get("oldPassword") + newPassword := r.Form.Get("newPassword") + err = users.UpdatePassword(user.Username, oldPassword, newPassword) + if err != nil { + data.Message = "Failed to change password: " + err.Error() + data.ShowForm = true + } else { + data.Message = "Successfully changed password" + data.ShowForm = false + cookie, err := r.Cookie("broadcast_session") + if err == nil { + log.Println("clearing other sessions for username", user.Username, "token", cookie.Value) + db.ClearOtherSessions(user.Username, cookie.Value) + } + } + } else { + data.Message = "" + data.ShowForm = true + } + tmpl := template.Must(template.ParseFS(content, "templates/change_password.html")) + err = tmpl.Execute(w, data) + if err != nil { + log.Fatal(err) + } +} + type PlaylistsPageData struct { Playlists []Playlist } @@ -189,7 +259,7 @@ func playlistsPage(w http.ResponseWriter, _ *http.Request) { data := PlaylistsPageData{ Playlists: db.GetPlaylists(), } - tmpl := template.Must(template.ParseFiles("templates/playlists.html")) + tmpl := template.Must(template.ParseFS(content, "templates/playlists.html")) err := tmpl.Execute(w, data) if err != nil { log.Fatal(err) @@ -204,7 +274,7 @@ func radiosPage(w http.ResponseWriter, _ *http.Request) { data := RadiosPageData{ Radios: db.GetRadios(), } - tmpl := template.Must(template.ParseFiles("templates/radios.html")) + tmpl := template.Must(template.ParseFS(content, "templates/radios.html")) err := tmpl.Execute(w, data) if err != nil { log.Fatal(err) @@ -236,7 +306,7 @@ func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) { data.Playlist = playlist data.Entries = db.GetEntriesForPlaylist(id) } - tmpl := template.Must(template.ParseFiles("templates/playlist.html")) + tmpl := template.Must(template.ParseFS(content, "templates/playlist.html")) tmpl.Execute(w, data) } @@ -290,7 +360,7 @@ func submitPlaylist(w http.ResponseWriter, r *http.Request) { // Notify connected radios playlists.NotifyChanges() } - http.Redirect(w, r, "/playlist/", http.StatusFound) + http.Redirect(w, r, "/playlists/", http.StatusFound) } func deletePlaylist(w http.ResponseWriter, r *http.Request) { @@ -303,7 +373,7 @@ func deletePlaylist(w http.ResponseWriter, r *http.Request) { db.DeletePlaylist(id) playlists.NotifyChanges() } - http.Redirect(w, r, "/playlist/", http.StatusFound) + http.Redirect(w, r, "/playlists/", http.StatusFound) } type EditRadioPageData struct { @@ -323,7 +393,7 @@ func editRadioPage(w http.ResponseWriter, r *http.Request, id int) { } data.Radio = radio } - tmpl := template.Must(template.ParseFiles("templates/radio.html")) + tmpl := template.Must(template.ParseFS(content, "templates/radio.html")) tmpl.Execute(w, data) } @@ -344,7 +414,7 @@ func submitRadio(w http.ResponseWriter, r *http.Request) { db.CreateRadio(radio) } } - http.Redirect(w, r, "/radio/", http.StatusFound) + http.Redirect(w, r, "/radios/", http.StatusFound) } func deleteRadio(w http.ResponseWriter, r *http.Request) { @@ -356,7 +426,7 @@ func deleteRadio(w http.ResponseWriter, r *http.Request) { } db.DeleteRadio(id) } - http.Redirect(w, r, "/radio/", http.StatusFound) + http.Redirect(w, r, "/radios/", http.StatusFound) } type FilesPageData struct { @@ -368,7 +438,7 @@ func filesPage(w http.ResponseWriter, _ *http.Request) { Files: files.Files(), } log.Println("file page data", data) - tmpl := template.Must(template.ParseFiles("templates/files.html")) + tmpl := template.Must(template.ParseFS(content, "templates/files.html")) err := tmpl.Execute(w, data) if err != nil { log.Fatal(err) @@ -384,7 +454,7 @@ func deleteFile(w http.ResponseWriter, r *http.Request) { } files.Delete(filename) } - http.Redirect(w, r, "/file/", http.StatusFound) + http.Redirect(w, r, "/files/", http.StatusFound) } func uploadFile(w http.ResponseWriter, r *http.Request) { @@ -398,12 +468,12 @@ func uploadFile(w http.ResponseWriter, r *http.Request) { log.Println("uploaded file to", path) files.Refresh() } - http.Redirect(w, r, "/file/", http.StatusFound) + http.Redirect(w, r, "/files/", http.StatusFound) } func logOutPage(w http.ResponseWriter, r *http.Request) { clearSessionCookie(w) - tmpl := template.Must(template.ParseFiles("templates/logout.html")) + tmpl := template.Must(template.ParseFS(content, "templates/logout.html")) tmpl.Execute(w, nil) } diff --git a/server/model.go b/broadcaster-server/model.go similarity index 79% rename from server/model.go rename to broadcaster-server/model.go index 94a6ab4..11f94e7 100644 --- a/server/model.go +++ b/broadcaster-server/model.go @@ -9,7 +9,10 @@ type PlaylistEntry struct { } type User struct { - username string + Id int + Username string + PasswordHash string + IsAdmin bool } type Playlist struct { diff --git a/server/playlist.go b/broadcaster-server/playlist.go similarity index 100% rename from server/playlist.go rename to broadcaster-server/playlist.go diff --git a/server/radio_sync.go b/broadcaster-server/radio_sync.go similarity index 98% rename from server/radio_sync.go rename to broadcaster-server/radio_sync.go index 2eaef34..66521f8 100644 --- a/server/radio_sync.go +++ b/broadcaster-server/radio_sync.go @@ -1,7 +1,7 @@ package main import ( - "code.octet-stream.net/broadcaster/protocol" + "code.octet-stream.net/broadcaster/internal/protocol" "encoding/json" "golang.org/x/net/websocket" "log" diff --git a/server/session.go b/broadcaster-server/session.go similarity index 69% rename from server/session.go rename to broadcaster-server/session.go index 4b4c445..a097989 100644 --- a/server/session.go +++ b/broadcaster-server/session.go @@ -17,26 +17,21 @@ func generateSession() string { return hex.EncodeToString(b) } -func currentUser(w http.ResponseWriter, r *http.Request) (User, error) { - // todo: check if user actually exists and is allowed to log in +func currentUser(_ http.ResponseWriter, r *http.Request) (User, error) { cookie, e := r.Cookie("broadcast_session") if e != nil { return User{}, e } - username, e := db.GetUserForSession(cookie.Value) - if e != nil { - return User{}, e - } - return User{username: username}, nil + return users.GetUserForSession(cookie.Value) } -func createSessionCookie(w http.ResponseWriter) { +func createSessionCookie(w http.ResponseWriter, username string) { sess := generateSession() log.Println("Generated a random session", sess) expiration := time.Now().Add(365 * 24 * time.Hour) cookie := http.Cookie{Name: "broadcast_session", Value: sess, Expires: expiration, SameSite: http.SameSiteLaxMode} - db.InsertSession("admin", sess, expiration) + db.InsertSession(username, sess, expiration) http.SetCookie(w, &cookie) } diff --git a/server/status.go b/broadcaster-server/status.go similarity index 95% rename from server/status.go rename to broadcaster-server/status.go index 260a0ce..b2edd49 100644 --- a/server/status.go +++ b/broadcaster-server/status.go @@ -1,7 +1,7 @@ package main import ( - "code.octet-stream.net/broadcaster/protocol" + "code.octet-stream.net/broadcaster/internal/protocol" "sync" ) diff --git a/broadcaster-server/templates/change_password.html b/broadcaster-server/templates/change_password.html new file mode 100644 index 0000000..431e2a4 --- /dev/null +++ b/broadcaster-server/templates/change_password.html @@ -0,0 +1,25 @@ + + + + + + Broadcaster + + +
+

Change Password

+ {{if ne .Message ""}} +

{{.Message}}

+ {{end}} + {{if .ShowForm}} +
+
+
+
+
+ +
+ {{end}} +
+ + diff --git a/templates/files.html b/broadcaster-server/templates/files.html similarity index 56% rename from templates/files.html rename to broadcaster-server/templates/files.html index 70d666f..fa2614f 100644 --- a/templates/files.html +++ b/broadcaster-server/templates/files.html @@ -8,14 +8,15 @@

Files! List

+

All files can be downloaded from the public file listing.

Upload New File

-

+
diff --git a/templates/index.html b/broadcaster-server/templates/index.html similarity index 90% rename from templates/index.html rename to broadcaster-server/templates/index.html index 5f4d747..367d48c 100644 --- a/templates/index.html +++ b/broadcaster-server/templates/index.html @@ -42,7 +42,7 @@ .split("; ") .find((row) => row.startsWith("broadcast_session=")) ?.split("=")[1]; - const socket = new WebSocket("/websync"); + const socket = new WebSocket("/web-ws"); socket.addEventListener("open", (event) => { socket.send(cookieValue); }); @@ -69,9 +69,9 @@ {{else}}

Log In

{{end}} -

File Management

-

Playlist Management

-

Radio Management

+

File Management

+

Playlist Management

+

Radio Management

Connected Radios

Loading... diff --git a/templates/login.html b/broadcaster-server/templates/login.html similarity index 100% rename from templates/login.html rename to broadcaster-server/templates/login.html diff --git a/templates/logout.html b/broadcaster-server/templates/logout.html similarity index 100% rename from templates/logout.html rename to broadcaster-server/templates/logout.html diff --git a/templates/playlist.html b/broadcaster-server/templates/playlist.html similarity index 96% rename from templates/playlist.html rename to broadcaster-server/templates/playlist.html index 1e550ca..e609712 100644 --- a/templates/playlist.html +++ b/broadcaster-server/templates/playlist.html @@ -28,7 +28,7 @@ Create New Playlist {{end}} -
+

@@ -71,7 +71,7 @@

{{if .Playlist.Id}}

Delete

-
+

diff --git a/templates/playlists.html b/broadcaster-server/templates/playlists.html similarity index 68% rename from templates/playlists.html rename to broadcaster-server/templates/playlists.html index 207d020..8b83d3d 100644 --- a/templates/playlists.html +++ b/broadcaster-server/templates/playlists.html @@ -10,10 +10,10 @@

Playlists!

    {{range .Playlists}} -
  • {{.Name}} {{.StartTime}} (Edit)
  • +
  • {{.Name}} {{.StartTime}} (Edit)
  • {{end}}
-

Add New Playlist

+

Add New Playlist

diff --git a/templates/radio.html b/broadcaster-server/templates/radio.html similarity index 91% rename from templates/radio.html rename to broadcaster-server/templates/radio.html index 1c3dde0..01e4f0c 100644 --- a/templates/radio.html +++ b/broadcaster-server/templates/radio.html @@ -15,7 +15,7 @@ Register New Radio {{end}} - +

@@ -31,7 +31,7 @@ {{if .Radio.Id}}

Delete

-
+

diff --git a/templates/radios.html b/broadcaster-server/templates/radios.html similarity index 69% rename from templates/radios.html rename to broadcaster-server/templates/radios.html index 255fbe2..d55ea4a 100644 --- a/templates/radios.html +++ b/broadcaster-server/templates/radios.html @@ -10,10 +10,10 @@

Radios

-

Register New Radio

+

Register New Radio

diff --git a/templates/radios.partial.html b/broadcaster-server/templates/radios.partial.html similarity index 100% rename from templates/radios.partial.html rename to broadcaster-server/templates/radios.partial.html diff --git a/broadcaster-server/user.go b/broadcaster-server/user.go new file mode 100644 index 0000000..275f0ec --- /dev/null +++ b/broadcaster-server/user.go @@ -0,0 +1,82 @@ +package main + +import ( + "errors" + "golang.org/x/crypto/bcrypt" +) + +var users Users + +type Users struct{} + +func (u *Users) GetUserForSession(token string) (User, error) { + username, err := db.GetUserNameForSession(token) + if err != nil { + return User{}, err + } + user, err := db.GetUser(username) + if err != nil { + return User{}, err + } + return user, nil +} + +func (u *Users) Authenticate(username string, clearPassword string) (User, error) { + user, err := db.GetUser(username) + if err != nil { + return User{}, err + } + err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(clearPassword)) + if err != nil { + return User{}, err + } + return user, nil +} + +func (u *Users) CreateUser(username string, clearPassword string, isAdmin bool) error { + if clearPassword == "" { + return errors.New("password cannot be empty") + } + hashed, err := bcrypt.GenerateFromPassword([]byte(clearPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + return db.CreateUser(User{ + Id: 0, + Username: username, + PasswordHash: string(hashed), + IsAdmin: isAdmin, + }) +} + +func (u *Users) DeleteUser(username string) { + db.DeleteUser(username) +} + +func (u *Users) UpdatePassword(username string, oldClearPassword string, newClearPassword string) error { + user, err := db.GetUser(username) + if err != nil { + return err + } + err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(oldClearPassword)) + if err != nil { + return errors.New("old password is incorrect") + } + if newClearPassword == "" { + return errors.New("password cannot be empty") + } + hashed, err := bcrypt.GenerateFromPassword([]byte(newClearPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + db.SetUserPassword(username, string(hashed)) + return nil +} + +func (u *Users) UpdateIsAdmin(username string, isAdmin bool) { + db.SetUserIsAdmin(username, isAdmin) +} + +func (u *Users) Users() []User { + return db.GetUsers() +} diff --git a/server/web_sync.go b/broadcaster-server/web_sync.go similarity index 91% rename from server/web_sync.go rename to broadcaster-server/web_sync.go index e59403d..a6b1a6e 100644 --- a/server/web_sync.go +++ b/broadcaster-server/web_sync.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "code.octet-stream.net/broadcaster/protocol" + "code.octet-stream.net/broadcaster/internal/protocol" "golang.org/x/net/websocket" ) @@ -18,12 +18,12 @@ func WebSync(ws *websocket.Conn) { badRead := false isAuthenticated := false - var user string + var user User for { // Ignore any massively oversize messages n, err := ws.Read(buf) if err != nil { - if user != "" { + if user.Username != "" { log.Println("Lost websocket to user:", user) } else { log.Println("Lost unauthenticated website websocket") @@ -40,9 +40,9 @@ func WebSync(ws *websocket.Conn) { if !isAuthenticated { token := string(buf[:n]) - u, err := db.GetUserForSession(token) + u, err := users.GetUserForSession(token) if err != nil { - log.Println("Could not find user for offered token", token) + log.Println("Could not find user for offered token", token, err) ws.Close() return } @@ -145,7 +145,7 @@ func sendRadioStatusToWeb(ws *websocket.Conn) error { Radios: webStatuses, } buf := new(strings.Builder) - tmpl := template.Must(template.ParseFiles("templates/radios.partial.html")) + tmpl := template.Must(template.ParseFS(content, "templates/radios.partial.html")) tmpl.Execute(buf, data) _, err := ws.Write([]byte(buf.String())) return err diff --git a/go.mod b/go.mod index c8401c7..7012bcb 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/BurntSushi/toml v1.4.0 github.com/gopxl/beep/v2 v2.1.0 github.com/warthog618/go-gpiocdev v0.9.0 + golang.org/x/crypto v0.28.0 golang.org/x/net v0.30.0 modernc.org/sqlite v1.33.1 ) diff --git a/go.sum b/go.sum index 3184d32..ab7d07f 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/warthog618/go-gpiocdev v0.9.0 h1:AZWUq1WObgKCO9cJCACFpwWQw6yu8vJbIE6f github.com/warthog618/go-gpiocdev v0.9.0/go.mod h1:GV4NZC82fWJERqk7Gu0+KfLSDIBEDNm6aPGiHlmT5fY= github.com/warthog618/go-gpiosim v0.1.0 h1:2rTMTcKUVZxpUuvRKsagnKAbKpd3Bwffp87xywEDVGI= github.com/warthog618/go-gpiosim v0.1.0/go.mod h1:Ngx/LYI5toxHr4E+Vm6vTgCnt0of0tktsSuMUEJ2wCI= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= diff --git a/protocol/protocol.go b/internal/protocol/protocol.go similarity index 100% rename from protocol/protocol.go rename to internal/protocol/protocol.go -- 2.39.5