package api import ( "encoding/json" "fmt" "net/http" "opal/internal/config" "os" "strings" "github.com/gabriel-vasile/mimetype" ) func RegisterLibraryEndpoints() { ProbeLibraries() http.HandleFunc("/libraries", EndpointLibraries) http.HandleFunc("/libraries/", EndpointLibraryList) } type Library struct { Name string `json:"Name"` PathName string `json:"PathName"` Path string `json:"-"` //when marshalled, this will be excluded } var Libraries []Library type endpointLibraries struct { AvailableLibraries []Library } type LibraryListEntry struct { Name string Type string //file, movie, show, directory, may add more filetypes in future } type endpointLibraryList struct { LibraryInfo Library Listing []LibraryListEntry } func EndpointLibraries(w http.ResponseWriter, r *http.Request) { var ret endpointLibraries ret.AvailableLibraries = Libraries retJson, err := json.Marshal(ret) if err != nil { fmt.Println("API warning: failed to marshal /libraries response") http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(retJson) } // TODO/NON-CRITICAL BUG FIX: web client sends /libraries//{path}/ that then redirects to /libraries/{path}/, should put this comment in the web client code but im too lazy func EndpointLibraryList(w http.ResponseWriter, r *http.Request) { url_split := strings.Split(r.URL.Path, "/") if url_split[0] != "" || url_split[1] != "libraries" { //impossible for this to fail unless there is something really wrong http.Error(w, "Internal server error", http.StatusInternalServerError) return } if len(url_split) < 4 { http.Error(w, "Malformed request", http.StatusBadRequest) return } library_string := url_split[2] path := strings.Join(url_split[3:], "/") var ret endpointLibraryList //TODO: use a map instead for _, lib := range Libraries { if lib.PathName != library_string { continue } ret.LibraryInfo = lib } if ret.LibraryInfo.Name == "" { //not found in Libraries because string un-initialised http.Error(w, "Malformed request", http.StatusBadRequest) //TODO: library not found (should return 404?) return } //TODO: this is probably a vulnerability directoryListing, err := os.ReadDir(ret.LibraryInfo.Path + "/" + path) if err != nil { fmt.Printf("API warning: potential server error or malicious client: %s\n", err.Error()) http.Error(w, "Malformed request", http.StatusBadRequest) return } for _, directoryEntry := range directoryListing { var entry LibraryListEntry if directoryEntry.IsDir() { entry.Name = directoryEntry.Name() entry.Type = "directory" } //TODO: update to use opal.cfg files to determine if movie or show if directoryEntry.Type().IsRegular() { //TODO: formatted string? filePath := ret.LibraryInfo.Path + "/" + path + "/" + directoryEntry.Name() mtype, err := mimetype.DetectFile(filePath) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } entry.Name = directoryEntry.Name() if strings.HasPrefix(mtype.String(), "video/") { entry.Type = "video" } else { entry.Type = "file" } } ret.Listing = append(ret.Listing, entry) } retJson, err := json.Marshal(ret) if err != nil { fmt.Printf("API warning: failed to marshal library listing response for %s\n", library_string) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(retJson) } func ProbeLibraries() { libraries_config := config.FindNode("/libraries", &config.RootConfigNode) if libraries_config == nil { fmt.Println("Error: unable to find config node for libraries") os.Exit(1) } for _, child := range libraries_config.Children { var lib Library lib.Name = config.FetchValue(child.Name, "name", false) lib.PathName = config.FetchValue(child.Name, "path_name", false) lib.Path = config.FetchValue(child.Name, "path", false) if strings.HasPrefix("ERROR", lib.Name) || strings.HasPrefix("ERROR", lib.PathName) { fmt.Printf("Error: %s: bad config\n", child.Name) os.Exit(1) } Libraries = append(Libraries, lib) } }