aboutsummaryrefslogtreecommitdiffstats
path: root/internal/gopherproxy/gemini.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/gopherproxy/gemini.go')
-rw-r--r--internal/gopherproxy/gemini.go292
1 files changed, 292 insertions, 0 deletions
diff --git a/internal/gopherproxy/gemini.go b/internal/gopherproxy/gemini.go
new file mode 100644
index 0000000..89d820c
--- /dev/null
+++ b/internal/gopherproxy/gemini.go
@@ -0,0 +1,292 @@
1package gopherproxy
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "io"
8 "log"
9 "mime"
10 "net/http"
11 "net/url"
12 "strings"
13
14 "golang.org/x/net/html/charset"
15 "golang.org/x/text/transform"
16
17 "git.vulpes.one/gopherproxy/pkg/libgemini"
18
19 "github.com/temoto/robotstxt"
20)
21
22type GeminiTemplateVariables struct {
23 Title string
24 URL string
25 Assets AssetList
26 Sections []GeminiSection
27 Nav []GeminiNavItem
28 IsPlain bool
29}
30
31type GeminiNavItem struct {
32 Label string
33 URL string
34 Current bool
35}
36
37type GeminiSection struct {
38 Type string
39 Text string
40 URL template.URL
41 Items []string
42}
43
44func urlToGeminiNav(url string) (items []GeminiNavItem) {
45 partialURL := "/gemini"
46 parts := strings.Split(url, "/")
47
48 if len(parts) != 0 && parts[len(parts)-1] == "" {
49 parts = parts[:len(parts)-1]
50 }
51
52 for _, part := range parts {
53 partialURL = partialURL + "/" + part
54
55 items = append(items, GeminiNavItem{
56 Label: part,
57 URL: partialURL,
58 Current: false,
59 })
60 }
61
62 items[len(items)-1].Current = true
63
64 return
65}
66
67func resolveURL(uri string, baseURL *url.URL) (resolvedURL string) {
68 if strings.HasPrefix(uri, "//") {
69 resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "//")
70 } else if strings.HasPrefix(uri, "gemini://") {
71 resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "gemini://")
72 } else if strings.HasPrefix(uri, "gopher://") {
73 resolvedURL = "/gopher/" + strings.TrimPrefix(uri, "gopher://")
74 } else {
75 url, err := url.Parse(uri)
76 if err != nil {
77 return ""
78 }
79 adjustedURL := baseURL.ResolveReference(url)
80 path := adjustedURL.Path
81 if !strings.HasPrefix(path, "/") {
82 path = "/" + path
83 }
84 if adjustedURL.Scheme == "gemini" {
85 resolvedURL = "/gemini/" + adjustedURL.Host + path
86 } else if adjustedURL.Scheme == "gopher" {
87 resolvedURL = "/gopher/" + adjustedURL.Host + path
88 } else {
89 resolvedURL = adjustedURL.String()
90 }
91 }
92
93 return
94}
95
96func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []GeminiSection) {
97 baseURL, err := url.Parse(fmt.Sprintf(
98 "gemini://%s/%s",
99 hostport,
100 uri,
101 ))
102 if err != nil {
103 return
104 }
105
106 unpreppedSections := libgemini.ParseGeminiDocument(body)
107
108 for _, section := range unpreppedSections {
109 if section.Type != libgemini.LINK {
110 sections = append(sections, GeminiSection{
111 Type: section.Type.String(),
112 Text: section.Text,
113 URL: template.URL(section.URL),
114 Items: section.Items,
115 })
116 } else {
117 sections = append(sections, GeminiSection{
118 Type: section.Type.String(),
119 Text: section.Text,
120 URL: template.URL(resolveURL(section.URL, baseURL)),
121 Items: section.Items,
122 })
123 }
124 }
125
126 return
127}
128
129func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc {
130 return func(w http.ResponseWriter, req *http.Request) {
131 agent := req.UserAgent()
132 path := strings.TrimPrefix(req.URL.Path, "/gemini/")
133
134 if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) {
135 log.Printf("UserAgent %s ignored robots.txt", agent)
136 }
137
138 parts := strings.Split(path, "/")
139 hostport := parts[0]
140
141 if len(hostport) == 0 {
142 http.Redirect(w, req, "/", http.StatusFound)
143 return
144 }
145
146 title := hostport
147
148 var qs string
149
150 if req.URL.RawQuery != "" {
151 qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery))
152 }
153
154 uri, err := url.QueryUnescape(strings.Join(parts[1:], "/"))
155 if err != nil {
156 if e := tpl.Execute(w, GeminiTemplateVariables{
157 Title: title,
158 URL: hostport,
159 Assets: assetList,
160 Sections: []GeminiSection{{
161 Type: libgemini.RAW_TEXT.String(),
162 Text: fmt.Sprintf("Error: %s", err),
163 }},
164 Nav: urlToGeminiNav(hostport),
165 IsPlain: true,
166 }); e != nil {
167 log.Println("Template error: " + e.Error())
168 log.Println(err.Error())
169 }
170 return
171 }
172
173 if uri != "" {
174 title = fmt.Sprintf("%s/%s", hostport, uri)
175 }
176
177 res, err := libgemini.Get(
178 fmt.Sprintf(
179 "gemini://%s/%s%s",
180 hostport,
181 uri,
182 qs,
183 ),
184 )
185
186 if err != nil {
187 if e := tpl.Execute(w, GeminiTemplateVariables{
188 Title: title,
189 URL: fmt.Sprintf("%s/%s", hostport, uri),
190 Assets: assetList,
191 Sections: []GeminiSection{{
192 Type: libgemini.RAW_TEXT.String(),
193 Text: fmt.Sprintf("Error: %s", err),
194 }},
195 Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)),
196 IsPlain: true,
197 }); e != nil {
198 log.Println("Template error: " + e.Error())
199 log.Println(err.Error())
200 }
201 return
202 }
203
204 if int(res.Header.Status/10) == 3 {
205 baseURL, err := url.Parse(fmt.Sprintf(
206 "gemini://%s/%s",
207 hostport,
208 uri,
209 ))
210 if err != nil {
211 if e := tpl.Execute(w, GeminiTemplateVariables{
212 Title: title,
213 URL: fmt.Sprintf("%s/%s", hostport, uri),
214 Assets: assetList,
215 Sections: []GeminiSection{{
216 Type: libgemini.RAW_TEXT.String(),
217 Text: fmt.Sprintf("Error: %s", err),
218 }},
219 Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)),
220 IsPlain: true,
221 }); e != nil {
222 log.Println("Template error: " + e.Error())
223 log.Println(err.Error())
224 }
225 return
226 }
227
228 http.Redirect(w, req, resolveURL(res.Header.Meta, baseURL), http.StatusFound)
229 return
230 }
231
232 if int(res.Header.Status/10) != 2 {
233 if err := tpl.Execute(w, GeminiTemplateVariables{
234 Title: title,
235 URL: fmt.Sprintf("%s/%s", hostport, uri),
236 Assets: assetList,
237 Sections: []GeminiSection{{
238 Type: libgemini.RAW_TEXT.String(),
239 Text: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta),
240 }},
241 Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)),
242 IsPlain: true,
243 }); err != nil {
244 log.Println("Template error: " + err.Error())
245 }
246 return
247 }
248
249 if strings.HasPrefix(res.Header.Meta, "text/") && !strings.HasPrefix(res.Header.Meta, "text/html") && !strings.HasPrefix(res.Header.Meta, "text/css") {
250 buf := new(bytes.Buffer)
251
252 _, params, err := mime.ParseMediaType(res.Header.Meta)
253 if err != nil {
254 buf.ReadFrom(res.Body)
255 } else {
256 encoding, _ := charset.Lookup(params["charset"])
257 readbuf := new(bytes.Buffer)
258 readbuf.ReadFrom(res.Body)
259
260 writer := transform.NewWriter(buf, encoding.NewDecoder())
261 writer.Write(readbuf.Bytes())
262 writer.Close()
263 }
264
265 var sections []GeminiSection
266 isPlain := true
267
268 if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) {
269 sections = parseGeminiDocument(buf, uri, hostport)
270 isPlain = false
271 } else {
272 sections = append(sections, GeminiSection{
273 Type: libgemini.RAW_TEXT.String(),
274 Text: buf.String(),
275 })
276 }
277
278 if err := tpl.Execute(w, GeminiTemplateVariables{
279 Title: title,
280 URL: fmt.Sprintf("%s/%s", hostport, uri),
281 Assets: assetList,
282 Sections: sections,
283 Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)),
284 IsPlain: isPlain,
285 }); err != nil {
286 log.Println("Template error: " + err.Error())
287 }
288 } else {
289 io.Copy(w, res.Body)
290 }
291 }
292}