diff options
Diffstat (limited to 'internal/port/gemini.go')
-rw-r--r-- | internal/port/gemini.go | 292 |
1 files changed, 0 insertions, 292 deletions
diff --git a/internal/port/gemini.go b/internal/port/gemini.go deleted file mode 100644 index f574816..0000000 --- a/internal/port/gemini.go +++ /dev/null | |||
@@ -1,292 +0,0 @@ | |||
1 | package port | ||
2 | |||
3 | import ( | ||
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/Feuerfuchs/port/pkg/libgemini" | ||
18 | |||
19 | "github.com/temoto/robotstxt" | ||
20 | ) | ||
21 | |||
22 | type GeminiTemplateVariables struct { | ||
23 | Title string | ||
24 | URL string | ||
25 | Assets AssetList | ||
26 | Sections []GeminiSection | ||
27 | Nav []GeminiNavItem | ||
28 | IsPlain bool | ||
29 | } | ||
30 | |||
31 | type GeminiNavItem struct { | ||
32 | Label string | ||
33 | URL string | ||
34 | Current bool | ||
35 | } | ||
36 | |||
37 | type GeminiSection struct { | ||
38 | Type string | ||
39 | Text string | ||
40 | URL template.URL | ||
41 | Items []string | ||
42 | } | ||
43 | |||
44 | func 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 | |||
67 | func 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 | |||
96 | func 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 | |||
129 | func 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 | } | ||