Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Response struct {

Error string
User string
IsAdmin bool
Section string

// Paging
Expand All @@ -54,16 +55,28 @@ type Response struct {
Youtubes []youtube.Video
}

func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}

func NewResponse(r *http.Request, ps httprouter.Params) *Response {
diskInfo, err := NewDiskInfo(datadir)
if err != nil {
panic(err)
}
user, _, _ := r.BasicAuth()
isAdmin := stringInSlice(user, httpAdminUsers)
return &Response{
Config: config.Get(),
Request: r,
Params: &ps,
User: ps.ByName("user"),
IsAdmin: isAdmin,
HTTPHost: httpHost,
Version: version,
Backlink: backlink,
Expand Down
77 changes: 46 additions & 31 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ var (
debug bool
httpAddr string
httpAdmins arrayFlags
httpAdminUsers []string
httpReadOnlys arrayFlags
httpReadOnlyUsers []string
httpHost string
httpPrefix string
letsencrypt bool
Expand Down Expand Up @@ -61,7 +64,8 @@ func init() {
cli.StringVar(&datadir, "data-dir", "/data", "data directory")
cli.BoolVar(&debug, "debug", false, "debug mode")
cli.StringVar(&httpAddr, "http-addr", ":80", "listen address")
cli.Var(&httpAdmins, "http-admin", "HTTP basic auth user/password for admin.")
cli.Var(&httpAdmins, "http-admin", "HTTP basic auth user/password for admins.")
cli.Var(&httpReadOnlys, "http-read-only", "HTTP basic auth user/password for read only users.")
cli.StringVar(&httpHost, "http-host", "", "HTTP host")
cli.StringVar(&httpPrefix, "http-prefix", "/streamlist", "HTTP URL prefix (not actually supported yet!)")
cli.BoolVar(&letsencrypt, "letsencrypt", false, "enable TLS using Let's Encrypt")
Expand All @@ -74,6 +78,17 @@ func main() {

cli.Parse(os.Args[1:])

for _, httpUser := range httpAdmins {
split := strings.Split(httpUser, ":")
httpUsername := split[0]
httpAdminUsers = append(httpAdminUsers, httpUsername)
}
for _, httpUser := range httpReadOnlys {
split := strings.Split(httpUser, ":")
httpUsername := split[0]
httpReadOnlyUsers = append(httpReadOnlyUsers, httpUsername)
}

// logtailer
logtail, err = logtailer.NewLogtailer(200 * 1024)
if err != nil {
Expand Down Expand Up @@ -159,55 +174,55 @@ func main() {
r.HandleMethodNotAllowed = false

// Handlers
r.GET("/", Log(Auth(index, false)))
r.GET(Prefix("/logs"), Log(Auth(logs, false)))
r.GET(Prefix("/"), Log(Auth(home, false)))
r.GET("/", Log(auth(index, "readonly")))
r.GET(Prefix("/logs"), Log(auth(logs, "admin")))
r.GET(Prefix("/"), Log(auth(home, "readonly")))

// Library
r.GET(Prefix("/library"), Log(Auth(library, false)))
r.GET(Prefix("/library"), Log(auth(library, "readonly")))

// Media
r.GET(Prefix("/media/thumbnail/:media"), Log(Auth(thumbnailMedia, false)))
r.GET(Prefix("/media/view/:media"), Log(Auth(viewMedia, false)))
r.GET(Prefix("/media/delete/:media"), Log(Auth(deleteMedia, false)))
r.GET(Prefix("/media/access/:filename"), Auth(streamMedia, false))
r.GET(Prefix("/media/download/:filename"), Auth(downloadMedia, false))
r.GET(Prefix("/media/thumbnail/:media"), Log(auth(thumbnailMedia, "readonly")))
r.GET(Prefix("/media/view/:media"), Log(auth(viewMedia, "readonly")))
r.GET(Prefix("/media/delete/:media"), Log(auth(deleteMedia, "admin")))
r.GET(Prefix("/media/access/:filename"), auth(streamMedia, "readonly"))
r.GET(Prefix("/media/download/:filename"), auth(downloadMedia, "readonly"))

// Publicly accessible streaming (using playlist id as "auth")
r.GET(Prefix("/stream/:list/:filename"), Auth(streamMedia, true))
r.GET(Prefix("/stream/:list/:filename"), auth(streamMedia, "none"))

// Import
r.GET(Prefix("/import"), Log(Auth(importHandler, false)))
r.GET(Prefix("/import"), Log(auth(importHandler, "admin")))

// Archiver
r.GET(Prefix("/archiver/jobs"), Auth(archiverJobs, false))
r.POST(Prefix("/archiver/save/:id"), Log(Auth(archiverSave, false)))
r.GET(Prefix("/archiver/cancel/:id"), Log(Auth(archiverCancel, false)))
r.GET(Prefix("/archiver/jobs"), auth(archiverJobs, "admin"))
r.POST(Prefix("/archiver/save/:id"), Log(auth(archiverSave, "admin")))
r.GET(Prefix("/archiver/cancel/:id"), Log(auth(archiverCancel, "admin")))

// List
r.GET(Prefix("/create"), Log(Auth(createList, false)))
r.POST(Prefix("/create"), Log(Auth(createList, false)))
r.POST(Prefix("/add/:list/:media"), Log(Auth(addMediaList, false)))
r.POST(Prefix("/remove/:list/:media"), Log(Auth(removeMediaList, false)))
r.GET(Prefix("/remove/:list/:media"), Log(Auth(removeMediaList, false)))
r.GET(Prefix("/create"), Log(auth(createList, "admin")))
r.POST(Prefix("/create"), Log(auth(createList, "admin")))
r.POST(Prefix("/add/:list/:media"), Log(auth(addMediaList, "admin")))
r.POST(Prefix("/remove/:list/:media"), Log(auth(removeMediaList, "admin")))
r.GET(Prefix("/remove/:list/:media"), Log(auth(removeMediaList, "admin")))

r.GET(Prefix("/edit/:id"), Log(Auth(editList, false)))
r.POST(Prefix("/edit/:id"), Log(Auth(editList, false)))
r.GET(Prefix("/shuffle/:id"), Log(Auth(shuffleList, false)))
r.GET(Prefix("/play/:id"), Log(Auth(playList, true)))
r.GET(Prefix("/m3u/:id"), Log(Auth(m3uList, true)))
r.GET(Prefix("/podcast/:id"), Log(Auth(podcastList, true)))
r.GET(Prefix("/edit/:id"), Log(auth(editList, "admin")))
r.POST(Prefix("/edit/:id"), Log(auth(editList, "admin")))
r.GET(Prefix("/shuffle/:id"), Log(auth(shuffleList, "admin")))
r.GET(Prefix("/play/:id"), Log(auth(playList, "none")))
r.GET(Prefix("/m3u/:id"), Log(auth(m3uList, "none")))
r.GET(Prefix("/podcast/:id"), Log(auth(podcastList, "none")))

r.POST(Prefix("/config"), Log(Auth(configHandler, false)))
r.POST(Prefix("/config"), Log(auth(configHandler, "admin")))

r.GET(Prefix("/delete/:id"), Log(Auth(deleteList, false)))
r.GET(Prefix("/delete/:id"), Log(auth(deleteList, "admin")))

// API
r.GET(Prefix("/v1/status"), Log(Auth(v1status, true)))
r.GET(Prefix("/v1/status"), Log(auth(v1status, "none")))

// Assets
r.GET(Prefix("/static/*path"), Auth(staticAsset, true)) // TODO: Auth() but by checking Origin/Referer for a valid playlist ID?
r.GET(Prefix("/logo.png"), Log(Auth(logo, true)))
r.GET(Prefix("/static/*path"), auth(staticAsset, "none"))
r.GET(Prefix("/logo.png"), Log(auth(logo, "none")))

//
// Server
Expand Down
9 changes: 5 additions & 4 deletions templates/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<script src="/streamlist/static/semantic/semantic.min.js"></script>
</head>
<body>

{{if $.User}}
<div class="navmenu ui secondary menu">
{{if $.Backlink}}
Expand All @@ -39,9 +38,11 @@
<a class="item {{if eq $.Section "library"}}active{{end}}" href="/streamlist/library">
Library
</a>
<a class="item {{if eq $.Section "import"}}active{{end}}" href="/streamlist/import">
Import
</a>
{{if $.IsAdmin}}
<a class="item {{if eq $.Section "import"}}active{{end}}" href="/streamlist/import">
Import
</a>
{{end}}
</div>
{{end}}

Expand Down
10 changes: 7 additions & 3 deletions templates/home.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{{template "header.html" .}}

<div class="ui container">
<a class="ui right floated blue labeled icon button" href="/streamlist/create"><i class="plus icon"></i>New Playlist</a>
{{if $.IsAdmin}}
<a class="ui right floated blue labeled icon button" href="/streamlist/create"><i class="plus icon"></i>New Playlist</a>
{{end}}
<div class="ui hidden clearing divider"></div>
<h2 class="ui header">Playlists</h2>

Expand All @@ -22,8 +24,10 @@ <h2 class="ui header">Playlists</h2>
{{with $tl := $list.TotalLength}}
{{duration $tl}}
{{end}}
&nbsp;&nbsp;
<a href="/streamlist/edit/{{$list.ID}}"><i class="setting icon"></i></a>
{{if $.IsAdmin}}
&nbsp;&nbsp;
<a href="/streamlist/edit/{{$list.ID}}"><i class="setting icon"></i></a>
{{end}}
</td>
</tr>
{{end}}
Expand Down
10 changes: 6 additions & 4 deletions templates/library.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@ <h2 class="ui header">Library</h2>
{{range $list := $.Lists}}
<div class="listbox">
{{$hasmedia := $list.HasMedia $media}}
<button {{if $hasmedia}}style="display: none;"{{end}} class="toggler ui mini basic button" data-url="/streamlist/add/{{$list.ID}}/{{$media.ID}}"><i class="checkmark icon"></i> {{$list.Title}}</button>
<button {{if not $hasmedia}}style="display: none;"{{end}} class="toggler ui mini green button" data-url="/streamlist/remove/{{$list.ID}}/{{$media.ID}}"><i class="checkmark icon"></i> {{$list.Title}}</button>
<button {{if $hasmedia}}style="display: none;"{{end}} class="{{if $.IsAdmin}}toggler {{end}}ui mini basic button" {{if $.IsAdmin}}data-url="/streamlist/add/{{$list.ID}}/{{$media.ID}}"{{end}}><i class="checkmark icon"></i> {{$list.Title}}</button>
<button {{if not $hasmedia}}style="display: none;"{{end}} class="{{if $.IsAdmin}}toggler {{end}}ui mini green button" {{if $.IsAdmin}}data-url="/streamlist/remove/{{$list.ID}}/{{$media.ID}}"{{end}}><i class="checkmark icon"></i> {{$list.Title}}</button>
</div>
{{end}}
</td>
<td class="right aligned four wide">
{{duration $media.Length}}
&nbsp;&nbsp;
<a href="/streamlist/media/delete/{{$media.ID}}?p={{$.Page}}&q={{$.Query}}" data-prompt="Delete {{$media.Title}}?" class="confirm"><i class="red trash icon"></i></a>
{{if $.IsAdmin}}
&nbsp;&nbsp;
<a href="/streamlist/media/delete/{{$media.ID}}?p={{$.Page}}&q={{$.Query}}" data-prompt="Delete {{$media.Title}}?" class="confirm"><i class="red trash icon"></i></a>
{{end}}
</td>
</tr>
{{end}}
Expand Down
26 changes: 18 additions & 8 deletions web.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,27 @@ func Log(h httprouter.Handle) httprouter.Handle {
}
}

func Auth(h httprouter.Handle, optional bool) httprouter.Handle {
func auth(h httprouter.Handle, role string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user := ""

// none role is no auth required
if role == "none" {
h(w, r, ps)
return
}

// Method: Basic Auth (if we're not behind a reverse proxy, use basic auth)
if httpAdmins != nil {
for _, httpAdmin := range httpAdmins {
split := strings.Split(httpAdmin, ":")
var userList []string
// Admin are always OK
userList = append(userList, httpAdmins...)
// If role readonly, we add readonly users
if role == "readonly" {
userList = append(userList, httpReadOnlys...)
}
for _, httpUser := range userList {
split := strings.Split(httpUser, ":")
httpUsername := split[0]
httpPassword := split[1]
user, password, _ := r.BasicAuth()
Expand All @@ -134,10 +147,6 @@ func Auth(h httprouter.Handle, optional bool) httprouter.Handle {
return
}
}
if optional {
h(w, r, ps)
return
}
w.Header().Set("WWW-Authenticate", `Basic realm="Sign-in Required"`)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
Expand All @@ -155,7 +164,8 @@ func Auth(h httprouter.Handle, optional bool) httprouter.Handle {
user = r.Header.Get(reverseProxyAuthHeader)
}

if user == "" && !optional {
//if user == "" && !optional {
if user == "" {
logger.Errorf("auth failed: client %q", clientIP)
if backlink != "" {
http.Redirect(w, r, backlink, http.StatusFound)
Expand Down