summaryrefslogtreecommitdiff
path: root/contrib/fs/gemini.go
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/fs/gemini.go')
-rw-r--r--contrib/fs/gemini.go138
1 files changed, 81 insertions, 57 deletions
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
-}