package http import ( "encoding/json" "html/template" "log" "net/http" "os" "path/filepath" "strings" "time" fb "github.com/filebrowser/filebrowser" ) // Handler returns a function compatible with http.HandleFunc. func Handler(m *fb.FileBrowser) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { code, err := serve(&fb.Context{ FileBrowser: m, User: nil, File: nil, }, w, r) if code >= 400 { w.WriteHeader(code) txt := http.StatusText(code) log.Printf("%v: %v %v\n", r.URL.Path, code, txt) w.Write([]byte(txt + "\n")) } if err != nil { log.Print(err) } }) } // serve is the main entry point of this HTML application. func serve(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) { // Checks if the URL contains the baseURL and strips it. Otherwise, it just // returns a 404 fb.Error because we're not supposed to be here! p := strings.TrimPrefix(r.URL.Path, c.BaseURL) if len(p) >= len(r.URL.Path) && c.BaseURL != "" { return http.StatusNotFound, nil } r.URL.Path = p // Check if this request is made to the service worker. If so, // pass it through a template to add the needed variables. if r.URL.Path == "/sw.js" { return renderFile(c, w, "sw.js") } // Checks if this request is made to the static assets folder. If so, and // if it is a GET request, returns with the asset. Otherwise, returns // a status not implemented. if matchURL(r.URL.Path, "/static") { if r.Method != http.MethodGet { return http.StatusNotImplemented, nil } return staticHandler(c, w, r) } // Checks if this request is made to the API and directs to the // API handler if so. if matchURL(r.URL.Path, "/api") { r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") return apiHandler(c, w, r) } // If it is a request to the preview and a static website generator is // active, build the preview. if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil { r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview") return c.StaticGen.Preview(c, w, r) } if strings.HasPrefix(r.URL.Path, "/share/") { r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/") return sharePage(c, w, r) } // Any other request should show the index.html file. w.Header().Set("x-frame-options", "SAMEORIGIN") w.Header().Set("x-content-type-options", "nosniff") w.Header().Set("x-xss-protection", "1; mode=block") return renderFile(c, w, "index.html") } // staticHandler handles the static assets path. func staticHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) { if r.URL.Path != "/static/manifest.json" { http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r) return 0, nil } return renderFile(c, w, "static/manifest.json") } // apiHandler is the main entry point for the /api endpoint. func apiHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) { if r.URL.Path == "/auth/get" { return authHandler(c, w, r) } if r.URL.Path == "/auth/renew" { return renewAuthHandler(c, w, r) } valid, _ := validateAuth(c, r) if !valid { return http.StatusForbidden, nil } c.Router, r.URL.Path = splitURL(r.URL.Path) if !c.User.Allowed(r.URL.Path) { return http.StatusForbidden, nil } if c.StaticGen != nil { // If we are using the 'magic url' for the settings, // we should redirect the request for the acutual path. if r.URL.Path == "/settings" { r.URL.Path = c.StaticGen.SettingsPath() } // Executes the Static website generator hook. code, err := c.StaticGen.Hook(c, w, r) if code != 0 || err != nil { return code, err } } if c.Router == "checksum" || c.Router == "download" || c.Router == "subtitle" || c.Router == "subtitles" { var err error c.File, err = fb.GetInfo(r.URL, c.FileBrowser, c.User) if err != nil { return ErrorToHTTP(err, false), err } } var code int var err error switch c.Router { case "download": code, err = downloadHandler(c, w, r) case "checksum": code, err = checksumHandler(c, w, r) case "command": code, err = command(c, w, r) case "search": code, err = search(c, w, r) case "resource": code, err = resourceHandler(c, w, r) case "users": code, err = usersHandler(c, w, r) case "settings": code, err = settingsHandler(c, w, r) case "share": code, err = shareHandler(c, w, r) case "subtitles": code, err = subtitlesHandler(c, w, r) case "subtitle": code, err = subtitleHandler(c, w, r) default: code = http.StatusNotFound } return code, err } // serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. func checksumHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) { query := r.URL.Query().Get("algo") val, err := c.File.Checksum(query) if err == fb.ErrInvalidOption { return http.StatusBadRequest, err } else if err != nil { return http.StatusInternalServerError, err } w.Write([]byte(val)) return 0, nil } // splitURL splits the path and returns everything that stands // before the first slash and everything that goes after. func splitURL(path string) (string, string) { if path == "" { return "", "" } path = strings.TrimPrefix(path, "/") i := strings.Index(path, "/") if i == -1 { return "", path } return path[0:i], path[i:] } // renderFile renders a file using a template with some needed variables. func renderFile(c *fb.Context, w http.ResponseWriter, file string) (int, error) { tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file))) var contentType string switch filepath.Ext(file) { case ".html": contentType = "text/html" case ".js": contentType = "application/javascript" case ".json": contentType = "application/json" default: contentType = "text" } w.Header().Set("Content-Type", contentType+"; charset=utf-8") data := map[string]interface{}{ "BaseURL": c.RootURL(), "NoAuth": c.NoAuth, "Version": fb.Version, "CSS": template.CSS(c.CSS), "ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "", "ReCaptchaHost": c.ReCaptchaHost, "ReCaptchaKey": c.ReCaptchaKey, } if c.StaticGen != nil { data["StaticGen"] = c.StaticGen.Name() } err := tpl.Execute(w, data) if err != nil { return http.StatusInternalServerError, err } return 0, nil } // sharePage build the share page. func sharePage(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) { s, err := c.Store.Share.Get(r.URL.Path) if err == fb.ErrNotExist { w.WriteHeader(http.StatusNotFound) return renderFile(c, w, "static/share/404.html") } if err != nil { return http.StatusInternalServerError, err } if s.Expires && s.ExpireDate.Before(time.Now()) { c.Store.Share.Delete(s.Hash) w.WriteHeader(http.StatusNotFound) return renderFile(c, w, "static/share/404.html") } r.URL.Path = s.Path info, err := os.Stat(s.Path) if err != nil { c.Store.Share.Delete(s.Hash) return ErrorToHTTP(err, false), err } c.File = &fb.File{ Path: s.Path, Name: info.Name(), ModTime: info.ModTime(), Mode: info.Mode(), IsDir: info.IsDir(), Size: info.Size(), } dl := r.URL.Query().Get("dl") if dl == "" || dl == "0" { tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html"))) w.Header().Set("Content-Type", "text/html; charset=utf-8") err := tpl.Execute(w, map[string]interface{}{ "BaseURL": c.RootURL(), "File": c.File, }) if err != nil { return http.StatusInternalServerError, err } return 0, nil } return downloadHandler(c, w, r) } // renderJSON prints the JSON version of data to the browser. func renderJSON(w http.ResponseWriter, data interface{}) (int, error) { marsh, err := json.Marshal(data) if err != nil { return http.StatusInternalServerError, err } w.Header().Set("Content-Type", "application/json; charset=utf-8") if _, err := w.Write(marsh); err != nil { return http.StatusInternalServerError, err } return 0, nil } // matchURL checks if the first URL matches the second. func matchURL(first, second string) bool { first = strings.ToLower(first) second = strings.ToLower(second) return strings.HasPrefix(first, second) } // ErrorToHTTP converts errors to HTTP Status Code. func ErrorToHTTP(err error, gone bool) int { switch { case err == nil: return http.StatusOK case os.IsPermission(err): return http.StatusForbidden case os.IsNotExist(err): if !gone { return http.StatusNotFound } return http.StatusGone case os.IsExist(err): return http.StatusConflict default: return http.StatusInternalServerError } }