summaryrefslogtreecommitdiff
path: root/contrib/fs/gemini.go
blob: 6f9c75d381176fcda9205ca253f3b4283c0e4923 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package fs

import (
	"context"
	"crypto/tls"
	"io"
	"net/url"
	"os"
	"path"
	"strings"
	"text/template"

	sr "tildegit.org/tjp/sliderule"
	"tildegit.org/tjp/sliderule/contrib/tlsauth"
	"tildegit.org/tjp/sliderule/gemini"
)

// TitanUpload decorates a handler to implement uploads via the titan protocol.
//
// 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(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_")
			if err != nil {
				return gemini.Failure(err)
			}

			if err := os.Chmod(tmpf.Name(), 0644); err != nil {
				return gemini.Failure(err)
			}

			if _, err := io.Copy(tmpf, body); err != nil {
				_ = os.Remove(tmpf.Name())
				return gemini.Failure(err)
			}

			request = cloneRequest(request)
			request.Path = strings.SplitN(request.Path, ";", 2)[0]

			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.Failure(err)
			}

			return responder.Handle(ctx, request)
		})

		handler = tlsauth.GeminiAuth(approver)(handler)

		handler = sr.Filter(func(_ context.Context, r *sr.Request) bool {
			return gemini.GetTitanRequestBody(r) != nil
		}, nil)(handler)

		return handler
	}
}

func cloneRequest(start *sr.Request) *sr.Request {
	next := &sr.Request{}
	*next = *start

	next.URL = &url.URL{}
	*next.URL = *start.URL

	if start.TLSState != nil {
		next.TLSState = &tls.ConnectionState{}
		*next.TLSState = *start.TLSState
	}

	return next
}

// 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(fsroot, urlroot string) sr.Handler {
	return fileHandler(gemini.ServerProtocol, fsroot, urlroot)
}

// GeminiDirectoryDefault serves up default files for directory path requests.
//
// If any of the supported filenames are found, the contents of the file is returned
// as the gemini response.
//
// It returns nil for any paths which don't correspond to a directory.
//
// 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 in the directory's contents to function properly.
func GeminiDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Handler {
	return directoryDefault(gemini.ServerProtocol, fsroot, urlroot, true, filenames...)
}

// GeminiDirectoryListing produces a listing of the contents of any requested directories.
//
// It returns "51 Not Found" for any paths which don't correspond to a filesystem directory.
//
// 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.
//
// The template may be nil, in which case DefaultGeminiDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
func GeminiDirectoryListing(fsroot, urlroot string, tmpl *template.Template) sr.Handler {
	if tmpl == nil {
		tmpl = DefaultGeminiDirectoryList
	}
	return directoryListing(gemini.ServerProtocol, fsroot, urlroot, "file.gmi", true, tmpl)
}

// DefaultGeminiDirectoryList is a template which renders a reasonable gemtext dir list.
var DefaultGeminiDirectoryList = template.Must(template.New("gemini_dirlist").Parse(`
# {{ .DirName }}
{{ range .Entries }}
=> {{ .Name }}{{ if .IsDir }}/{{ end -}}
{{ end }}
=> ../
`[1:]))