| 
									
										
										
										
											2019-06-30 16:07:58 -06:00
										 |  |  | // Copyright 2015 Matthew Holt and The Caddy Authors | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  | // you may not use this file except in compliance with the License. | 
					
						
							|  |  |  | // You may obtain a copy of the License at | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | //     http://www.apache.org/licenses/LICENSE-2.0 | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  | // distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  | // See the License for the specific language governing permissions and | 
					
						
							|  |  |  | // limitations under the License. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | package fileserver | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	"bytes" | 
					
						
							|  |  |  | 	"encoding/json" | 
					
						
							|  |  |  | 	"html/template" | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 	"net/http" | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	"os" | 
					
						
							|  |  |  | 	"path" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-02 12:37:06 -06:00
										 |  |  | 	"github.com/caddyserver/caddy/v2" | 
					
						
							|  |  |  | 	"github.com/caddyserver/caddy/v2/modules/caddyhttp" | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Browse configures directory browsing. | 
					
						
							|  |  |  | type Browse struct { | 
					
						
							| 
									
										
										
										
											2019-05-22 12:32:36 -06:00
										 |  |  | 	TemplateFile string `json:"template_file,omitempty"` | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	template *template.Template | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | func (fsrv *FileServer) serveBrowse(dirPath string, w http.ResponseWriter, r *http.Request) error { | 
					
						
							| 
									
										
										
										
											2019-05-21 13:03:52 -06:00
										 |  |  | 	// navigation on the client-side gets messed up if the | 
					
						
							|  |  |  | 	// URL doesn't end in a trailing slash because hrefs like | 
					
						
							|  |  |  | 	// "/b/c" on a path like "/a" end up going to "/b/c" instead | 
					
						
							|  |  |  | 	// of "/a/b/c" - so we have to redirect in this case | 
					
						
							|  |  |  | 	if !strings.HasSuffix(r.URL.Path, "/") { | 
					
						
							|  |  |  | 		r.URL.Path += "/" | 
					
						
							|  |  |  | 		http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | 	dir, err := fsrv.openFile(dirPath, w) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	defer dir.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-06-14 11:58:28 -06:00
										 |  |  | 	repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-21 13:03:52 -06:00
										 |  |  | 	// calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f | 
					
						
							|  |  |  | 	listing, err := fsrv.loadDirectoryContents(dir, path.Clean(r.URL.Path), repl) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	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) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | 	fsrv.browseApplyQueryParams(w, r, &listing) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// 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") { | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | 		if buf, err = fsrv.browseWriteJSON(listing); err != nil { | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 			return caddyhttp.Error(http.StatusInternalServerError, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		w.Header().Set("Content-Type", "application/json; charset=utf-8") | 
					
						
							|  |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | 		if buf, err = fsrv.browseWriteHTML(listing); err != nil { | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 			return caddyhttp.Error(http.StatusInternalServerError, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		w.Header().Set("Content-Type", "text/html; charset=utf-8") | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-06-21 14:36:26 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	buf.WriteTo(w) | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	return nil | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-06-14 11:58:28 -06:00
										 |  |  | func (fsrv *FileServer) loadDirectoryContents(dir *os.File, urlPath string, repl caddy.Replacer) (browseListing, error) { | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	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, "/")) | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | 	canGoUp := strings.HasPrefix(curPathDir, fsrv.Root) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | 	return fsrv.directoryListing(files, canGoUp, urlPath, repl), nil | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // browseApplyQueryParams applies query parameters to the listing. | 
					
						
							|  |  |  | // It mutates the listing and may set cookies. | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseListing) { | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	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) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | func (fsrv *FileServer) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) { | 
					
						
							| 
									
										
										
										
											2019-08-08 07:59:02 +02:00
										 |  |  | 	buf := bufPool.Get().(*bytes.Buffer) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	err := json.NewEncoder(buf).Encode(listing.Items) | 
					
						
							| 
									
										
										
										
											2019-08-08 07:59:02 +02:00
										 |  |  | 	bufPool.Put(buf) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	return buf, err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | func (fsrv *FileServer) browseWriteHTML(listing browseListing) (*bytes.Buffer, error) { | 
					
						
							| 
									
										
										
										
											2019-08-08 07:59:02 +02:00
										 |  |  | 	buf := bufPool.Get().(*bytes.Buffer) | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | 	err := fsrv.Browse.template.Execute(buf, listing) | 
					
						
							| 
									
										
										
										
											2019-08-08 07:59:02 +02:00
										 |  |  | 	bufPool.Put(buf) | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | 	return buf, err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // isSymlink return true if f is a symbolic link | 
					
						
							|  |  |  | func isSymlink(f os.FileInfo) bool { | 
					
						
							|  |  |  | 	return f.Mode()&os.ModeSymlink != 0 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-21 13:03:52 -06:00
										 |  |  | // isSymlinkTargetDir returns true if f's symbolic link target | 
					
						
							|  |  |  | // is a directory. | 
					
						
							|  |  |  | func isSymlinkTargetDir(f os.FileInfo, root, urlPath string) bool { | 
					
						
							|  |  |  | 	if !isSymlink(f) { | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	target := sanitizedPathJoin(root, path.Join(urlPath, f.Name())) | 
					
						
							|  |  |  | 	targetInfo, err := os.Stat(target) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return targetInfo.IsDir() | 
					
						
							| 
									
										
										
										
											2019-05-20 15:46:34 -06:00
										 |  |  | } |