package web import ( "net/http" "git.noroutes.net/NoRoutes/vibetunes/internal/database" "git.noroutes.net/NoRoutes/vibetunes/internal/streaming" ) // handleSharePage serves a public, unauthenticated page for a share // token — the whole point of a share link is that the recipient doesn't // need a VibeSonic account. Expired or unknown tokens 404 rather than // leaking which is which. func (h *Web) handleSharePage(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") share, err := database.GetShareByToken(h.DB, token) if err != nil || share.IsExpired() { http.NotFound(w, r) return } _ = database.RecordShareVisit(h.DB, share.ID) songIDs, err := database.ShareSongIDs(h.DB, share.ID) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } var rows []SongRow for _, id := range songIDs { song, serr := database.GetSong(h.DB, id) if serr != nil { continue } rows = append(rows, toSongRow(h.DB, song, albumOf(h.DB, song), "")) } owner, _ := database.GetUserByID(h.DB, share.OwnerID) ownerName := "" if owner != nil { ownerName = owner.Username } render(w, "share", PageData{ Title: "Shared music", Data: struct { Description string OwnerName string Songs []SongRow Token string }{share.Description, ownerName, rows, token}, }) } // handleShareStream serves one song from a share, publicly and without // a session — but only a song that is actually part of that share, and // only through the same path-validated streaming.ServeFile used // everywhere else, so a share link can never be used to fetch arbitrary // filesystem paths. func (h *Web) handleShareStream(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") songID := r.PathValue("songID") share, err := database.GetShareByToken(h.DB, token) if err != nil || share.IsExpired() { http.NotFound(w, r) return } songIDs, err := database.ShareSongIDs(h.DB, share.ID) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } allowed := false for _, id := range songIDs { if id == songID { allowed = true break } } if !allowed { http.NotFound(w, r) return } song, err := database.GetSong(h.DB, songID) if err != nil { http.NotFound(w, r) return } mf, err := database.GetMediaFile(h.DB, song.MediaFileID) if err != nil { http.NotFound(w, r) return } folder, err := database.GetMusicFolder(h.DB, mf.MusicFolderID) if err != nil { http.NotFound(w, r) return } absPath, perr := streaming.ResolvePath(folder.Path, mf.RelativePath) if perr != nil { http.Error(w, "403 Forbidden", http.StatusForbidden) return } if err := streaming.ServeFile(w, r, absPath, mf.RelativePath); err != nil { http.Error(w, "could not read media file", http.StatusInternalServerError) } }