From 775c0c1040e6a6622fec39d49b354bfa194a6998 Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 30 Sep 2023 20:08:33 -0600 Subject: file serving refactor * do away with fs.FS usage in gemini, like the previous refactor in gopher * remove spartan code in contrib * standardize fsroot/urlroot string arguments to file serving handlers --- contrib/fs/gemini.go | 138 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 57 deletions(-) (limited to 'contrib/fs/gemini.go') diff --git a/contrib/fs/gemini.go b/contrib/fs/gemini.go index 7549ce6..d0ad2d8 100644 --- a/contrib/fs/gemini.go +++ b/contrib/fs/gemini.go @@ -4,10 +4,10 @@ import ( "context" "crypto/tls" "io" - "io/fs" "net/url" "os" "path" + "path/filepath" "strings" "text/template" @@ -20,11 +20,15 @@ import ( // // It is a middleware rather than a handler because after the upload is processed, // the server is still responsible for generating a response. -func TitanUpload(approver tlsauth.Approver, rootdir string) sr.Middleware { - rootdir = strings.TrimSuffix(rootdir, "/") +func TitanUpload(fsroot, urlroot string, approver tlsauth.Approver) sr.Middleware { + fsroot = strings.TrimSuffix(fsroot, "/") return func(responder sr.Handler) sr.Handler { handler := sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { + if !strings.HasPrefix(request.Path, urlroot) { + return nil + } + body := gemini.GetTitanRequestBody(request) tmpf, err := os.CreateTemp("", "titan_upload_") @@ -40,8 +44,8 @@ func TitanUpload(approver tlsauth.Approver, rootdir string) sr.Middleware { request = cloneRequest(request) request.Path = strings.SplitN(request.Path, ";", 2)[0] - filepath := strings.TrimPrefix(request.Path, "/") - filepath = path.Join(rootdir, filepath) + filepath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") + filepath = path.Join(fsroot, filepath) if err := os.Rename(tmpf.Name(), filepath); err != nil { _ = os.Remove(tmpf.Name()) return gemini.PermanentFailure(err) @@ -75,21 +79,33 @@ func cloneRequest(start *sr.Request) *sr.Request { return next } -// GeminiFileHandler builds a handler which serves up files from a file system. +// GeminiFileHandler builds a handler which serves up files from the file system. // // It only serves responses for paths which do not correspond to directories on disk. -func GeminiFileHandler(fileSystem fs.FS) sr.Handler { +func GeminiFileHandler(fsroot, urlroot string) sr.Handler { + fsroot = strings.TrimRight(fsroot, "/") + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - filepath, file, err := ResolveFile(request, fileSystem) - if err != nil { - return gemini.Failure(err) + if !strings.HasPrefix(request.Path, urlroot) { + return nil } + requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - if file == nil { + fpath := filepath.Join(fsroot, requestpath) + if isPrivate(fpath) { + return nil + } + if isf, err := isFile(fpath); err != nil { + return gemini.Failure(err) + } else if !isf { return nil } - return gemini.Success(mediaType(filepath), file) + file, err := os.Open(fpath) + if err != nil { + return gemini.Failure(err) + } + return gemini.Success(mediaType(fpath), file) }) } @@ -102,30 +118,48 @@ func GeminiFileHandler(fileSystem fs.FS) sr.Handler { // // When it encounters a directory path which doesn't end in a trailing slash (/) it // redirects to a URL with the trailing slash appended. This is necessary for relative -// links not the directory's contents to function properly. -// -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they -// don't, it will produce nil responses for any directory paths. -func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) sr.Handler { +// links in the directory's contents to function properly. +func GeminiDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Handler { + fsroot = strings.TrimRight(fsroot, "/") + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - dirpath, dir, response := handleDirGemini(request, fileSystem) - if response != nil { - return response + if !strings.HasSuffix(request.Path, "/") { + u := *request.URL + u.Path += "/" + return gemini.PermanentRedirect(u.String()) } - if dir == nil { + + if !strings.HasPrefix(request.Path, urlroot) { return nil } - defer func() { _ = dir.Close() }() + requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - filepath, file, err := ResolveDirectoryDefault(fileSystem, dirpath, dir, filenames) - if err != nil { - return gemini.Failure(err) + fpath := filepath.Join(fsroot, requestpath) + if isPrivate(fpath) { + return nil } - if file == nil { + if isd, err := isDir(fpath); err != nil { + return gemini.Failure(err) + } else if !isd { return nil } - return gemini.Success(mediaType(filepath), file) + for _, fname := range filenames { + candidatepath := filepath.Join(fpath, fname) + if isf, err := isFile(candidatepath); err != nil { + return gemini.Failure(err) + } else if !isf { + continue + } + + file, err := os.Open(candidatepath) + if err != nil { + return gemini.Failure(err) + } + return gemini.Success(mediaType(candidatepath), file) + } + + return nil }) } @@ -137,26 +171,36 @@ func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) sr.Handler { // redirects to a URL with the trailing slash appended. This is necessary for relative // links not the directory's contents to function properly. // -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they -// don't, it will produce "51 Not Found" responses for any directory paths. -// // The template may be nil, in which case DefaultGeminiDirectoryList is used instead. The // template is then processed with RenderDirectoryListing. -func GeminiDirectoryListing(fileSystem fs.FS, template *template.Template) sr.Handler { +func GeminiDirectoryListing(fsroot, urlroot string, template *template.Template) sr.Handler { + fsroot = strings.TrimRight(fsroot, "/") + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - dirpath, dir, response := handleDirGemini(request, fileSystem) - if response != nil { - return response + if !strings.HasSuffix(request.Path, "/") { + u := *request.URL + u.Path += "/" + return gemini.PermanentRedirect(u.String()) } - if dir == nil { + if !strings.HasPrefix(request.Path, urlroot) { + return nil + } + requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") + + fpath := filepath.Join(fsroot, requestpath) + if isPrivate(fpath) { + return nil + } + if isd, err := isDir(fpath); err != nil { + return gemini.Failure(err) + } else if !isd { return nil } - defer func() { _ = dir.Close() }() if template == nil { template = DefaultGeminiDirectoryList } - body, err := RenderDirectoryListing(dirpath, dir, template, request.Server) + body, err := RenderDirectoryListing(fpath, requestpath, template, request.Server) if err != nil { return gemini.Failure(err) } @@ -173,23 +217,3 @@ var DefaultGeminiDirectoryList = template.Must(template.New("gemini_dirlist").Pa {{ end }} => ../ `[1:])) - -func handleDirGemini(request *sr.Request, fileSystem fs.FS) (string, fs.ReadDirFile, *sr.Response) { - path, dir, err := ResolveDirectory(request, fileSystem) - if err != nil { - return "", nil, gemini.Failure(err) - } - - if dir == nil { - return "", nil, nil - } - - if !strings.HasSuffix(request.Path, "/") { - _ = dir.Close() - url := *request.URL - url.Path += "/" - return "", nil, gemini.PermanentRedirect(url.String()) - } - - return path, dir, nil -} -- cgit v1.2.3