8 "golang.org/x/net/websocket"
20 const version
= "v1.0.0"
21 const formatString
= "2006-01-02T15:04"
23 //go:embed templates/*
26 //var content = os.DirFS("../broadcaster-server/")
28 var config ServerConfig
= NewServerConfig()
31 configFlag
:= flag
.String("c", "", "path to configuration file")
32 addUserFlag
:= flag
.Bool("a", false, "interactively add an admin user then exit")
33 versionFlag
:= flag
.Bool("v", false, "print version then exit")
37 fmt
.Println("Broadcaster Server", version
)
40 if *configFlag
== "" {
41 log
.Fatal("must specify a configuration file with -c")
43 config
.LoadFromFile(*configFlag
)
46 defer db
.CloseDatabase()
49 scanner
:= bufio
.NewScanner(os
.Stdin
)
50 fmt
.Println("Enter new admin username:")
54 username
:= scanner
.Text()
55 fmt
.Println("Enter new admin password (will be printed in the clear):")
59 password
:= scanner
.Text()
60 if username
== "" || password
== "" {
61 fmt
.Println("Both username and password must be specified")
64 if err
:= users
.CreateUser(username
, password
, true); err
!= nil {
70 log
.Println("Broadcaster Server", version
, "starting up")
73 InitAudioFiles(config
.AudioFilesPath
)
78 http
.HandleFunc("/login", logInPage
)
79 http
.Handle("/file-downloads/", http
.StripPrefix("/file-downloads/", http
.FileServer(http
.Dir(config
.AudioFilesPath
))))
81 // Authenticated routes
83 http
.Handle("/", requireUser(homePage
))
84 http
.Handle("/logout", requireUser(logOutPage
))
85 http
.Handle("/change-password", requireUser(changePasswordPage
))
87 http
.Handle("/playlists/", requireUser(playlistSection
))
88 http
.Handle("/files/", requireUser(fileSection
))
89 http
.Handle("/radios/", requireUser(radioSection
))
91 http
.Handle("/stop", requireUser(stopPage
))
95 // TODO: user management
97 // Websocket routes, which perform their own auth
99 http
.Handle("/radio-ws", websocket
.Handler(RadioSync
))
100 http
.Handle("/web-ws", websocket
.Handler(WebSync
))
102 err
:= http
.ListenAndServe(config
.BindAddress
+":"+strconv
.Itoa(config
.Port
), nil)
108 type authenticatedHandler
func(http
.ResponseWriter
, *http
.Request
, User
)
110 type AuthMiddleware
struct {
111 handler authenticatedHandler
115 func (m AuthMiddleware
) ServeHTTP(w http
.ResponseWriter
, r
*http
.Request
) {
116 user
, err
:= currentUser(w
, r
)
117 if err
!= nil ||
(m
.mustBeAdmin
&& !user
.IsAdmin
) {
118 http
.Redirect(w
, r
, "/login", http
.StatusFound
)
121 m
.handler(w
, r
, user
)
124 func requireUser(handler authenticatedHandler
) AuthMiddleware
{
125 return AuthMiddleware
{
131 func requireAdmin(handler authenticatedHandler
) AuthMiddleware
{
132 return AuthMiddleware
{
138 type HeaderData
struct {
143 func renderHeader(w http
.ResponseWriter
, selectedMenu
string) {
144 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/header.html"))
146 SelectedMenu
: selectedMenu
,
147 Username
: "username",
149 err
:= tmpl
.Execute(w
, data
)
155 func renderFooter(w http
.ResponseWriter
) {
156 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/footer.html"))
157 err
:= tmpl
.Execute(w
, nil)
163 type HomeData
struct {
168 func homePage(w http
.ResponseWriter
, r
*http
.Request
, user User
) {
169 renderHeader(w
, "status")
170 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/index.html"))
175 tmpl
.Execute(w
, data
)
179 type LogInData
struct {
183 func logInPage(w http
.ResponseWriter
, r
*http
.Request
) {
185 username
:= r
.Form
["username"]
186 password
:= r
.Form
["password"]
189 user
, err
:= users
.Authenticate(username
[0], password
[0])
191 errText
= "Incorrect login"
193 createSessionCookie(w
, user
.Username
)
194 http
.Redirect(w
, r
, "/", http
.StatusFound
)
203 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/login.html"))
204 tmpl
.Execute(w
, data
)
208 func playlistSection(w http
.ResponseWriter
, r
*http
.Request
, user User
) {
209 path
:= strings
.Split(r
.URL
.Path
, "/")
214 if path
[2] == "new" {
215 editPlaylistPage(w
, r
, 0)
216 } else if path
[2] == "submit" && r
.Method
== "POST" {
218 } else if path
[2] == "delete" && r
.Method
== "POST" {
220 } else if path
[2] == "" {
223 id
, err
:= strconv
.Atoi(path
[2])
228 editPlaylistPage(w
, r
, id
)
232 func fileSection(w http
.ResponseWriter
, r
*http
.Request
, user User
) {
233 path
:= strings
.Split(r
.URL
.Path
, "/")
238 if path
[2] == "upload" {
240 } else if path
[2] == "delete" && r
.Method
== "POST" {
242 } else if path
[2] == "" {
250 func radioSection(w http
.ResponseWriter
, r
*http
.Request
, user User
) {
251 path
:= strings
.Split(r
.URL
.Path
, "/")
256 if path
[2] == "new" {
257 editRadioPage(w
, r
, 0)
258 } else if path
[2] == "submit" && r
.Method
== "POST" {
260 } else if path
[2] == "delete" && r
.Method
== "POST" {
262 } else if path
[2] == "" {
265 id
, err
:= strconv
.Atoi(path
[2])
270 editRadioPage(w
, r
, id
)
274 type ChangePasswordPageData
struct {
279 func changePasswordPage(w http
.ResponseWriter
, r
*http
.Request
, user User
) {
280 var data ChangePasswordPageData
281 if r
.Method
== "POST" {
284 w
.WriteHeader(http
.StatusBadRequest
)
287 oldPassword
:= r
.Form
.Get("oldPassword")
288 newPassword
:= r
.Form
.Get("newPassword")
289 err
= users
.UpdatePassword(user
.Username
, oldPassword
, newPassword
)
291 data
.Message
= "Failed to change password: " + err
.Error()
294 data
.Message
= "Successfully changed password"
295 data
.ShowForm
= false
296 cookie
, err
:= r
.Cookie("broadcast_session")
298 log
.Println("clearing other sessions for username", user
.Username
, "token", cookie
.Value
)
299 db
.ClearOtherSessions(user
.Username
, cookie
.Value
)
306 renderHeader(w
, "change-password")
307 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/change_password.html"))
308 err
:= tmpl
.Execute(w
, data
)
315 type PlaylistsPageData
struct {
319 func playlistsPage(w http
.ResponseWriter
, _
*http
.Request
) {
320 renderHeader(w
, "playlists")
321 data
:= PlaylistsPageData
{
322 Playlists
: db
.GetPlaylists(),
324 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlists.html"))
325 err
:= tmpl
.Execute(w
, data
)
332 type RadiosPageData
struct {
336 func radiosPage(w http
.ResponseWriter
, _
*http
.Request
) {
337 renderHeader(w
, "radios")
338 data
:= RadiosPageData
{
339 Radios
: db
.GetRadios(),
341 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radios.html"))
342 err
:= tmpl
.Execute(w
, data
)
349 type EditPlaylistPageData
struct {
351 Entries
[]PlaylistEntry
355 func editPlaylistPage(w http
.ResponseWriter
, r
*http
.Request
, id
int) {
356 var data EditPlaylistPageData
357 for _
, f
:= range files
.Files() {
358 data
.Files
= append(data
.Files
, f
.Name
)
361 data
.Playlist
.Enabled
= true
362 data
.Playlist
.Name
= "New Playlist"
363 data
.Playlist
.StartTime
= time
.Now().Format(formatString
)
364 data
.Entries
= append(data
.Entries
, PlaylistEntry
{})
366 playlist
, err
:= db
.GetPlaylist(id
)
371 data
.Playlist
= playlist
372 data
.Entries
= db
.GetEntriesForPlaylist(id
)
374 renderHeader(w
, "radios")
375 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlist.html"))
376 tmpl
.Execute(w
, data
)
380 func submitPlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
384 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
388 _
, err
= time
.Parse(formatString
, r
.Form
.Get("playlistStartTime"))
393 p
.Enabled
= r
.Form
.Get("playlistEnabled") == "1"
394 p
.Name
= r
.Form
.Get("playlistName")
395 p
.StartTime
= r
.Form
.Get("playlistStartTime")
397 delays
:= r
.Form
["delaySeconds"]
398 filenames
:= r
.Form
["filename"]
399 isRelatives
:= r
.Form
["isRelative"]
401 entries
:= make([]PlaylistEntry
, 0)
402 for i
:= range delays
{
404 delay
, err
:= strconv
.Atoi(delays
[i
])
408 e
.DelaySeconds
= delay
410 e
.IsRelative
= isRelatives
[i
] == "1"
411 e
.Filename
= filenames
[i
]
412 entries
= append(entries
, e
)
414 cleanedEntries
:= make([]PlaylistEntry
, 0)
415 for _
, e
:= range entries
{
416 if e
.DelaySeconds
!= 0 || e
.Filename
!= "" {
417 cleanedEntries
= append(cleanedEntries
, e
)
424 id
= db
.CreatePlaylist(p
)
426 db
.SetEntriesForPlaylist(cleanedEntries
, id
)
427 // Notify connected radios
428 playlists
.NotifyChanges()
430 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
433 func deletePlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
436 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
440 db
.DeletePlaylist(id
)
441 playlists
.NotifyChanges()
443 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
446 type EditRadioPageData
struct {
450 func editRadioPage(w http
.ResponseWriter
, r
*http
.Request
, id
int) {
451 var data EditRadioPageData
453 data
.Radio
.Name
= "New Radio"
454 data
.Radio
.Token
= generateSession()
456 radio
, err
:= db
.GetRadio(id
)
463 renderHeader(w
, "radios")
464 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radio.html"))
465 tmpl
.Execute(w
, data
)
469 func submitRadio(w http
.ResponseWriter
, r
*http
.Request
) {
473 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
478 radio
.Name
= r
.Form
.Get("radioName")
479 radio
.Token
= r
.Form
.Get("radioToken")
481 db
.UpdateRadio(radio
)
483 db
.CreateRadio(radio
)
486 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
489 func deleteRadio(w http
.ResponseWriter
, r
*http
.Request
) {
492 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
498 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
501 type FilesPageData
struct {
505 func filesPage(w http
.ResponseWriter
, _
*http
.Request
) {
506 renderHeader(w
, "files")
507 data
:= FilesPageData
{
508 Files
: files
.Files(),
510 log
.Println("file page data", data
)
511 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/files.html"))
512 err
:= tmpl
.Execute(w
, data
)
519 func deleteFile(w http
.ResponseWriter
, r
*http
.Request
) {
522 filename
:= r
.Form
.Get("filename")
526 files
.Delete(filename
)
528 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
531 func uploadFile(w http
.ResponseWriter
, r
*http
.Request
) {
532 err
:= r
.ParseMultipartForm(100 << 20)
533 file
, handler
, err
:= r
.FormFile("file")
535 path
:= filepath
.Join(files
.Path(), filepath
.Base(handler
.Filename
))
536 f
, _
:= os
.Create(path
)
539 log
.Println("uploaded file to", path
)
542 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
545 func logOutPage(w http
.ResponseWriter
, r
*http
.Request
, user User
) {
546 clearSessionCookie(w
)
548 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/logout.html"))
553 func stopPage(w http
.ResponseWriter
, r
*http
.Request
, user User
) {
555 radioId
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
560 commandRouter
.Stop(radioId
)
561 http
.Redirect(w
, r
, "/", http
.StatusFound
)