mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-03 19:17:29 -05:00 
			
		
		
		
	browse: Support absolute and recursive directory symlinks
This commit is contained in:
		
							parent
							
								
									4e52b3fe8a
								
							
						
					
					
						commit
						981f364845
					
				@ -246,7 +246,9 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if f.IsDir() {
 | 
							isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath, config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if isDir {
 | 
				
			||||||
			name += "/"
 | 
								name += "/"
 | 
				
			||||||
			dirCount++
 | 
								dirCount++
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
@ -260,7 +262,7 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
 | 
				
			|||||||
		url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
 | 
							url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fileinfos = append(fileinfos, FileInfo{
 | 
							fileinfos = append(fileinfos, FileInfo{
 | 
				
			||||||
			IsDir:     f.IsDir() || isSymlinkTargetDir(f, urlPath, config),
 | 
								IsDir:     isDir,
 | 
				
			||||||
			IsSymlink: isSymlink(f),
 | 
								IsSymlink: isSymlink(f),
 | 
				
			||||||
			Name:      f.Name(),
 | 
								Name:      f.Name(),
 | 
				
			||||||
			Size:      f.Size(),
 | 
								Size:      f.Size(),
 | 
				
			||||||
@ -291,19 +293,19 @@ func isSymlinkTargetDir(f os.FileInfo, urlPath string, config *Config) bool {
 | 
				
			|||||||
	if !isSymlink(f) {
 | 
						if !isSymlink(f) {
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fullPath := func(fileName string) string {
 | 
					
 | 
				
			||||||
		fullPath := filepath.Join(string(config.Fs.Root.(http.Dir)), urlPath, fileName)
 | 
						// a bit strange but we want Stat thru the jailed filesystem to be safe
 | 
				
			||||||
		return filepath.Clean(fullPath)
 | 
						target, err := config.Fs.Root.Open(filepath.Join(urlPath, f.Name()))
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	target, err := os.Readlink(fullPath(f.Name()))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	targetInfo, err := os.Lstat(fullPath(target))
 | 
						defer target.Close()
 | 
				
			||||||
 | 
						targetInto, err := target.Stat()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return targetInfo.IsDir()
 | 
					
 | 
				
			||||||
 | 
						return targetInto.IsDir()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
 | 
					// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
 | 
				
			||||||
 | 
				
			|||||||
@ -3,12 +3,14 @@ package browse
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/http/httptest"
 | 
						"net/http/httptest"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"text/template"
 | 
						"text/template"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@ -17,6 +19,8 @@ import (
 | 
				
			|||||||
	"github.com/mholt/caddy/caddyhttp/staticfiles"
 | 
						"github.com/mholt/caddy/caddyhttp/staticfiles"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const testDirPrefix = "caddy_browse_test"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestSort(t *testing.T) {
 | 
					func TestSort(t *testing.T) {
 | 
				
			||||||
	// making up []fileInfo with bogus values;
 | 
						// making up []fileInfo with bogus values;
 | 
				
			||||||
	// to be used to make up our "listing"
 | 
						// to be used to make up our "listing"
 | 
				
			||||||
@ -453,3 +457,146 @@ func TestBrowseRedirect(t *testing.T) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDirSymlink(t *testing.T) {
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							source       string
 | 
				
			||||||
 | 
							target       string
 | 
				
			||||||
 | 
							pathScope    string
 | 
				
			||||||
 | 
							url          string
 | 
				
			||||||
 | 
							expectedName string
 | 
				
			||||||
 | 
							expectedURL  string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							// test case can expect a directory "dir" and a symlink to it called "symlink"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"dir", "$TMP/rel_symlink_to_dir", "/", "/",
 | 
				
			||||||
 | 
								"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
 | 
				
			||||||
 | 
							{"$TMP/dir", "$TMP/abs_symlink_to_dir", "/", "/",
 | 
				
			||||||
 | 
								"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"../../dir", "$TMP/sub/dir/rel_symlink_to_dir", "/", "/sub/dir/",
 | 
				
			||||||
 | 
								"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
 | 
				
			||||||
 | 
							{"$TMP/dir", "$TMP/sub/dir/abs_symlink_to_dir", "/", "/sub/dir/",
 | 
				
			||||||
 | 
								"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"../../dir", "$TMP/with/scope/rel_symlink_to_dir", "/with/scope", "/with/scope/",
 | 
				
			||||||
 | 
								"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
 | 
				
			||||||
 | 
							{"$TMP/dir", "$TMP/with/scope/abs_symlink_to_dir", "/with/scope", "/with/scope/",
 | 
				
			||||||
 | 
								"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"../../../../dir", "$TMP/with/scope/sub/dir/rel_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
 | 
				
			||||||
 | 
								"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
 | 
				
			||||||
 | 
							{"$TMP/dir", "$TMP/with/scope/sub/dir/abs_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
 | 
				
			||||||
 | 
								"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"symlink", "$TMP/rel_symlink_to_symlink", "/", "/",
 | 
				
			||||||
 | 
								"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
 | 
				
			||||||
 | 
							{"$TMP/symlink", "$TMP/abs_symlink_to_symlink", "/", "/",
 | 
				
			||||||
 | 
								"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"../../symlink", "$TMP/sub/dir/rel_symlink_to_symlink", "/", "/sub/dir/",
 | 
				
			||||||
 | 
								"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
 | 
				
			||||||
 | 
							{"$TMP/symlink", "$TMP/sub/dir/abs_symlink_to_symlink", "/", "/sub/dir/",
 | 
				
			||||||
 | 
								"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"../../symlink", "$TMP/with/scope/rel_symlink_to_symlink", "/with/scope", "/with/scope/",
 | 
				
			||||||
 | 
								"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
 | 
				
			||||||
 | 
							{"$TMP/symlink", "$TMP/with/scope/abs_symlink_to_symlink", "/with/scope", "/with/scope/",
 | 
				
			||||||
 | 
								"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"../../../../symlink", "$TMP/with/scope/sub/dir/rel_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
 | 
				
			||||||
 | 
								"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
 | 
				
			||||||
 | 
							{"$TMP/symlink", "$TMP/with/scope/sub/dir/abs_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
 | 
				
			||||||
 | 
								"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, tc := range testCases {
 | 
				
			||||||
 | 
							func() {
 | 
				
			||||||
 | 
								tmpdir, err := ioutil.TempDir("", testDirPrefix)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("failed to create test directory: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								defer os.RemoveAll(tmpdir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := os.MkdirAll(filepath.Join(tmpdir, "dir"), 0755); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("failed to create test dir 'dir': %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := os.Symlink("dir", filepath.Join(tmpdir, "symlink")); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("failed to create test symlink 'symlink': %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								sourceResolved := strings.Replace(tc.source, "$TMP", tmpdir, -1)
 | 
				
			||||||
 | 
								targetResolved := strings.Replace(tc.target, "$TMP", tmpdir, -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := os.MkdirAll(filepath.Dir(sourceResolved), 0755); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("failed to create source symlink dir: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := os.MkdirAll(filepath.Dir(targetResolved), 0755); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("failed to create target symlink dir: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := os.Symlink(sourceResolved, targetResolved); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("failed to create test symlink: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								b := Browse{
 | 
				
			||||||
 | 
									Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
 | 
				
			||||||
 | 
										t.Fatalf("Test %d - Next shouldn't be called", i)
 | 
				
			||||||
 | 
										return 0, nil
 | 
				
			||||||
 | 
									}),
 | 
				
			||||||
 | 
									Configs: []Config{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											PathScope: tc.pathScope,
 | 
				
			||||||
 | 
											Fs: staticfiles.FileServer{
 | 
				
			||||||
 | 
												Root: http.Dir(tmpdir),
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req, err := http.NewRequest("GET", tc.url, nil)
 | 
				
			||||||
 | 
								req.Header.Add("Accept", "application/json")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								rec := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								returnCode, _ := b.ServeHTTP(rec, req)
 | 
				
			||||||
 | 
								if returnCode != http.StatusOK {
 | 
				
			||||||
 | 
									t.Fatalf("Test %d - wrong return code, expected %d, got %d",
 | 
				
			||||||
 | 
										i, http.StatusOK, returnCode)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								type jsonEntry struct {
 | 
				
			||||||
 | 
									Name      string
 | 
				
			||||||
 | 
									IsDir     bool
 | 
				
			||||||
 | 
									IsSymlink bool
 | 
				
			||||||
 | 
									URL       string
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								var entries []jsonEntry
 | 
				
			||||||
 | 
								if err := json.Unmarshal(rec.Body.Bytes(), &entries); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Test %d - failed to parse json: %v", i, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								found := false
 | 
				
			||||||
 | 
								for _, e := range entries {
 | 
				
			||||||
 | 
									if e.Name != tc.expectedName {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									found = true
 | 
				
			||||||
 | 
									if !e.IsDir {
 | 
				
			||||||
 | 
										t.Fatalf("Test %d - expected to be a dir, got %v", i, e.IsDir)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if !e.IsSymlink {
 | 
				
			||||||
 | 
										t.Fatalf("Test %d - expected to be a symlink, got %v", i, e.IsSymlink)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if e.URL != tc.expectedURL {
 | 
				
			||||||
 | 
										t.Fatalf("Test %d - wrong URL, expected %v, got %v", i, tc.expectedURL, e.URL)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !found {
 | 
				
			||||||
 | 
									t.Fatalf("Test %d - failed, could not find name %v", i, tc.expectedName)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user