From 22995e5655b3d5117743d98bf5c7ba8ed335a2c5 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 20 May 2019 15:46:34 -0600 Subject: Implement most of browse; fix a couple obvious bugs; some cleanup --- modules/caddyhttp/staticfiles/browse.go | 308 ++++++++++++++------------------ 1 file changed, 130 insertions(+), 178 deletions(-) (limited to 'modules/caddyhttp/staticfiles/browse.go') diff --git a/modules/caddyhttp/staticfiles/browse.go b/modules/caddyhttp/staticfiles/browse.go index 15ff105..2bb130f 100644 --- a/modules/caddyhttp/staticfiles/browse.go +++ b/modules/caddyhttp/staticfiles/browse.go @@ -1,58 +1,65 @@ package staticfiles import ( + "bytes" + "encoding/json" + "html/template" "net/http" + "os" + "path" + "strings" + + "bitbucket.org/lightcodelabs/caddy2" + "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" ) // Browse configures directory browsing. type Browse struct { -} + TemplateFile string `json:"template_file"` -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -// If so, control is handed over to ServeListing. -func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) error { - // TODO: convert this handler - return nil + template *template.Template +} - // // Browse works on existing directories; delegate everything else - // requestedFilepath, err := bc.Fs.Root.Open(r.URL.Path) - // if err != nil { - // switch { - // case os.IsPermission(err): - // return http.StatusForbidden, err - // case os.IsExist(err): - // return http.StatusNotFound, err - // default: - // return b.Next.ServeHTTP(w, r) - // } - // } - // defer requestedFilepath.Close() +func (sf *StaticFiles) serveBrowse(dirPath string, w http.ResponseWriter, r *http.Request) error { + dir, err := sf.openFile(dirPath, w) + if err != nil { + return err + } + defer dir.Close() + + repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer) + + listing, err := sf.loadDirectoryContents(dir, r.URL.Path, repl) + switch { + case os.IsPermission(err): + return caddyhttp.Error(http.StatusForbidden, err) + case os.IsNotExist(err): + return caddyhttp.Error(http.StatusNotFound, err) + case err != nil: + return caddyhttp.Error(http.StatusInternalServerError, err) + } + + sf.browseApplyQueryParams(w, r, &listing) + + // write response as either JSON or HTML + var buf *bytes.Buffer + acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) + if strings.Contains(acceptHeader, "application/json") { + if buf, err = sf.browseWriteJSON(listing); err != nil { + return caddyhttp.Error(http.StatusInternalServerError, err) + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + } else { + if buf, err = sf.browseWriteHTML(listing); err != nil { + return caddyhttp.Error(http.StatusInternalServerError, err) + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + } + buf.WriteTo(w) - // info, err := requestedFilepath.Stat() - // if err != nil { - // switch { - // case os.IsPermission(err): - // return http.StatusForbidden, err - // case os.IsExist(err): - // return http.StatusGone, err - // default: - // return b.Next.ServeHTTP(w, r) - // } - // } - // if !info.IsDir() { - // return b.Next.ServeHTTP(w, r) - // } - - // // Do not reply to anything else because it might be nonsensical - // switch r.Method { - // case http.MethodGet, http.MethodHead: - // // proceed, noop - // case "PROPFIND", http.MethodOptions: - // return http.StatusNotImplemented, nil - // default: - // return b.Next.ServeHTTP(w, r) - // } + return nil + // TODO: Sigh... do we have to put this here? // // Browsing navigation gets messed up if browsing a directory // // that doesn't end in "/" (which it should, anyway) // u := *r.URL @@ -68,138 +75,83 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) error { // return b.ServeListing(w, r, requestedFilepath, bc) } -// func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string, config *Config) (*Listing, bool, error) { -// files, err := requestedFilepath.Readdir(-1) -// if err != nil { -// return nil, false, err -// } - -// // Determine if user can browse up another folder -// var canGoUp bool -// curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) -// for _, other := range b.Configs { -// if strings.HasPrefix(curPathDir, other.PathScope) { -// canGoUp = true -// break -// } -// } - -// // Assemble listing of directory contents -// listing, hasIndex := directoryListing(files, canGoUp, urlPath, config) - -// return &listing, hasIndex, nil -// } - -// // handleSortOrder gets and stores for a Listing the 'sort' and 'order', -// // and reads 'limit' if given. The latter is 0 if not given. -// // -// // This sets Cookies. -// func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) { -// sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit") - -// // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies -// switch sort { -// case "": -// sort = sortByNameDirFirst -// if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { -// sort = sortCookie.Value -// } -// case sortByName, sortByNameDirFirst, sortBySize, sortByTime: -// http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil}) -// } - -// switch order { -// case "": -// order = "asc" -// if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { -// order = orderCookie.Value -// } -// case "asc", "desc": -// http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil}) -// } - -// if limitQuery != "" { -// limit, err = strconv.Atoi(limitQuery) -// if err != nil { // if the 'limit' query can't be interpreted as a number, return err -// return -// } -// } - -// return -// } - -// // ServeListing returns a formatted view of 'requestedFilepath' contents'. -// func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) { -// listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path, bc) -// if err != nil { -// switch { -// case os.IsPermission(err): -// return http.StatusForbidden, err -// case os.IsExist(err): -// return http.StatusGone, err -// default: -// return http.StatusInternalServerError, err -// } -// } -// if containsIndex && !b.IgnoreIndexes { // directory isn't browsable -// return b.Next.ServeHTTP(w, r) -// } -// listing.Context = httpserver.Context{ -// Root: bc.Fs.Root, -// Req: r, -// URL: r.URL, -// } -// listing.User = bc.Variables - -// // Copy the query values into the Listing struct -// var limit int -// listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope) -// if err != nil { -// return http.StatusBadRequest, err -// } - -// listing.applySort() - -// if limit > 0 && limit <= len(listing.Items) { -// listing.Items = listing.Items[:limit] -// listing.ItemsLimitedTo = limit -// } - -// var buf *bytes.Buffer -// acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) -// switch { -// case strings.Contains(acceptHeader, "application/json"): -// if buf, err = b.formatAsJSON(listing, bc); err != nil { -// return http.StatusInternalServerError, err -// } -// w.Header().Set("Content-Type", "application/json; charset=utf-8") - -// default: // There's no 'application/json' in the 'Accept' header; browse normally -// if buf, err = b.formatAsHTML(listing, bc); err != nil { -// return http.StatusInternalServerError, err -// } -// w.Header().Set("Content-Type", "text/html; charset=utf-8") - -// } - -// _, _ = buf.WriteTo(w) - -// return http.StatusOK, nil -// } - -// func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) { -// marsh, err := json.Marshal(listing.Items) -// if err != nil { -// return nil, err -// } - -// buf := new(bytes.Buffer) -// _, err = buf.Write(marsh) -// return buf, err -// } - -// func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) { -// buf := new(bytes.Buffer) -// err := bc.Template.Execute(buf, listing) -// return buf, err -// } +func (sf *StaticFiles) loadDirectoryContents(dir *os.File, urlPath string, repl caddy2.Replacer) (browseListing, error) { + files, err := dir.Readdir(-1) + if err != nil { + return browseListing{}, err + } + + // determine if user can browse up another folder + curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) + canGoUp := strings.HasPrefix(curPathDir, sf.Root) + + return sf.directoryListing(files, canGoUp, urlPath, repl), nil +} + +// browseApplyQueryParams applies query parameters to the listing. +// It mutates the listing and may set cookies. +func (sf *StaticFiles) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseListing) { + sortParam := r.URL.Query().Get("sort") + orderParam := r.URL.Query().Get("order") + limitParam := r.URL.Query().Get("limit") + + // first figure out what to sort by + switch sortParam { + case "": + sortParam = sortByNameDirFirst + if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { + sortParam = sortCookie.Value + } + case sortByName, sortByNameDirFirst, sortBySize, sortByTime: + http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil}) + } + + // then figure out the order + switch orderParam { + case "": + orderParam = "asc" + if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { + orderParam = orderCookie.Value + } + case "asc", "desc": + http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) + } + + // finally, apply the sorting and limiting + listing.applySortAndLimit(sortParam, orderParam, limitParam) +} + +func (sf *StaticFiles) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) { + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(listing.Items) + return buf, err +} + +func (sf *StaticFiles) browseWriteHTML(listing browseListing) (*bytes.Buffer, error) { + buf := new(bytes.Buffer) + err := sf.Browse.template.Execute(buf, listing) + return buf, err +} + +// isSymlink return true if f is a symbolic link +func isSymlink(f os.FileInfo) bool { + return f.Mode()&os.ModeSymlink != 0 +} + +// isSymlinkTargetDir return true if f's symbolic link target +// is a directory. Return false if not a symbolic link. +// TODO: Re-implement +func isSymlinkTargetDir(f os.FileInfo, urlPath string) bool { + // if !isSymlink(f) { + // return false + // } + + // // TODO: Ensure path is sanitized + // target:= path.Join(root, urlPath, f.Name())) + // targetInfo, err := os.Stat(target) + // if err != nil { + // return false + // } + // return targetInfo.IsDir() + return false +} -- cgit v1.2.3