// Package streaming serves media file bytes over HTTP: range requests, // correct MIME types, and no full-file buffering. Authorization (does // this user get to hear this song?) and path resolution happen in the // caller; ResolvePath here only guards against path traversal once a // caller has already decided which folder/file to serve. package streaming import ( "errors" "mime" "net/http" "os" "path/filepath" "strings" ) var ErrPathEscapesFolder = errors.New("resolved path escapes its music folder") // ResolvePath joins a music folder's root with a stored relative path and // verifies the result is still within that root, rejecting any ".." // traversal or symlink escape. Callers must always go through this // rather than filepath.Join'ing user- or tag-derived paths directly. func ResolvePath(folderRoot, relativePath string) (string, error) { root, err := filepath.Abs(folderRoot) if err != nil { return "", err } root, err = filepath.EvalSymlinks(root) if err != nil { return "", err } joined := filepath.Join(root, filepath.FromSlash(relativePath)) resolved, err := filepath.EvalSymlinks(joined) if err != nil { return "", err } if resolved != root && !strings.HasPrefix(resolved, root+string(filepath.Separator)) { return "", ErrPathEscapesFolder } return resolved, nil } // ServeFile streams a validated absolute path to the client, supporting // HTTP range requests and conditional GETs via http.ServeContent, which // reads directly from the *os.File rather than buffering it in memory. func ServeFile(w http.ResponseWriter, r *http.Request, absPath, displayName string) error { f, err := os.Open(absPath) if err != nil { return err } defer f.Close() info, err := f.Stat() if err != nil { return err } if info.IsDir() { return errors.New("refusing to stream a directory") } if ct := MimeType(absPath); ct != "" { w.Header().Set("Content-Type", ct) } w.Header().Set("Accept-Ranges", "bytes") http.ServeContent(w, r, displayName, info.ModTime(), f) return nil } // MimeType returns the media MIME type for a path's extension, falling // back to a generic octet-stream for anything unrecognized. func MimeType(path string) string { ext := strings.ToLower(filepath.Ext(path)) if ct, ok := extraMimeTypes[ext]; ok { return ct } if ct := mime.TypeByExtension(ext); ct != "" { return ct } return "application/octet-stream" } // extraMimeTypes fills in audio types that a bare OS mime.types file // often doesn't have registered. var extraMimeTypes = map[string]string{ ".mp3": "audio/mpeg", ".flac": "audio/flac", ".ogg": "audio/ogg", ".oga": "audio/ogg", ".opus": "audio/opus", ".aac": "audio/aac", ".m4a": "audio/mp4", ".m4b": "audio/mp4", ".wav": "audio/wav", ".aiff": "audio/aiff", ".aif": "audio/aiff", ".wma": "audio/x-ms-wma", }