diff options
author | Feuerfuchs <git@feuerfuchs.dev> | 2019-11-26 13:13:02 +0100 |
---|---|---|
committer | Feuerfuchs <git@feuerfuchs.dev> | 2019-11-26 13:13:02 +0100 |
commit | 93115f804220c31c2aa10f123560fb11135f06d8 (patch) | |
tree | a3c12f68c4263f3e34b1e03f12e7962aab9cfbf5 /gopherproxy.go | |
parent | Fix title on startpage (diff) | |
download | gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.tar.gz gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.tar.bz2 gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.zip |
Add IPv6 support, general restructuring
Diffstat (limited to 'gopherproxy.go')
-rw-r--r-- | gopherproxy.go | 693 |
1 files changed, 0 insertions, 693 deletions
diff --git a/gopherproxy.go b/gopherproxy.go deleted file mode 100644 index 8c0bb89..0000000 --- a/gopherproxy.go +++ /dev/null | |||
@@ -1,693 +0,0 @@ | |||
1 | package gopherproxy | ||
2 | |||
3 | import ( | ||
4 | "bufio" | ||
5 | "bytes" | ||
6 | "crypto/md5" | ||
7 | "fmt" | ||
8 | "html" | ||
9 | "html/template" | ||
10 | "io" | ||
11 | "io/ioutil" | ||
12 | "log" | ||
13 | "mime" | ||
14 | "net/http" | ||
15 | "net/url" | ||
16 | "regexp" | ||
17 | "strings" | ||
18 | |||
19 | "golang.org/x/net/html/charset" | ||
20 | "golang.org/x/text/transform" | ||
21 | |||
22 | "github.com/temoto/robotstxt" | ||
23 | |||
24 | "github.com/prologic/go-gopher" | ||
25 | |||
26 | "github.com/gobuffalo/packr/v2" | ||
27 | |||
28 | "github.com/davidbyttow/govips/pkg/vips" | ||
29 | |||
30 | "github.com/NYTimes/gziphandler" | ||
31 | ) | ||
32 | |||
33 | const ( | ||
34 | ITEM_TYPE_GEMINI_LINE = "" | ||
35 | ITEM_TYPE_GEMINI_LINK = " =>" | ||
36 | ) | ||
37 | |||
38 | type Item struct { | ||
39 | Link template.URL | ||
40 | Type string | ||
41 | Text string | ||
42 | } | ||
43 | |||
44 | type AssetList struct { | ||
45 | Style string | ||
46 | JS string | ||
47 | FontW string | ||
48 | FontW2 string | ||
49 | PropFontW string | ||
50 | PropFontW2 string | ||
51 | } | ||
52 | |||
53 | func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { | ||
54 | if strings.HasPrefix(uri, "//") { | ||
55 | resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//") | ||
56 | } else if strings.HasPrefix(uri, "gemini://") { | ||
57 | resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://") | ||
58 | } else if strings.HasPrefix(uri, "gopher://") { | ||
59 | resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://") | ||
60 | } else { | ||
61 | url, err := url.Parse(uri) | ||
62 | if err != nil { | ||
63 | return "" | ||
64 | } | ||
65 | adjustedURI := baseURL.ResolveReference(url) | ||
66 | if adjustedURI.Scheme == "gemini" { | ||
67 | resolvedURI = "/gemini/" + adjustedURI.Host + adjustedURI.Path | ||
68 | } else if adjustedURI.Scheme == "gopher" { | ||
69 | resolvedURI = "/gopher/" + adjustedURI.Host + adjustedURI.Path | ||
70 | } else { | ||
71 | resolvedURI = adjustedURI.String() | ||
72 | } | ||
73 | } | ||
74 | |||
75 | return | ||
76 | } | ||
77 | |||
78 | func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { | ||
79 | var title string | ||
80 | |||
81 | out := make([]Item, len(d.Items)) | ||
82 | |||
83 | for i, x := range d.Items { | ||
84 | if x.Type == gopher.INFO && x.Selector == "TITLE" { | ||
85 | title = x.Description | ||
86 | continue | ||
87 | } | ||
88 | |||
89 | tr := Item{ | ||
90 | Text: x.Description, | ||
91 | Type: x.Type.String(), | ||
92 | } | ||
93 | |||
94 | if x.Type == gopher.INFO { | ||
95 | out[i] = tr | ||
96 | continue | ||
97 | } | ||
98 | |||
99 | if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") { | ||
100 | link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:") | ||
101 | if strings.HasPrefix(link, "gemini://") { | ||
102 | link = fmt.Sprintf( | ||
103 | "/gemini/%s", | ||
104 | strings.TrimPrefix(link, "gemini://"), | ||
105 | ) | ||
106 | } else if strings.HasPrefix(link, "gopher://") { | ||
107 | link = fmt.Sprintf( | ||
108 | "/gopher/%s", | ||
109 | strings.TrimPrefix(link, "gopher://"), | ||
110 | ) | ||
111 | } | ||
112 | tr.Link = template.URL(link) | ||
113 | } else { | ||
114 | var hostport string | ||
115 | if x.Port == 70 { | ||
116 | hostport = x.Host | ||
117 | } else { | ||
118 | hostport = fmt.Sprintf("%s:%d", x.Host, x.Port) | ||
119 | } | ||
120 | path := url.PathEscape(x.Selector) | ||
121 | path = strings.Replace(path, "%2F", "/", -1) | ||
122 | tr.Link = template.URL( | ||
123 | fmt.Sprintf( | ||
124 | "/gopher/%s/%s%s", | ||
125 | hostport, | ||
126 | string(byte(x.Type)), | ||
127 | path, | ||
128 | ), | ||
129 | ) | ||
130 | } | ||
131 | |||
132 | out[i] = tr | ||
133 | } | ||
134 | |||
135 | if title == "" { | ||
136 | if uri != "" { | ||
137 | title = fmt.Sprintf("%s/%s", hostport, uri) | ||
138 | } else { | ||
139 | title = hostport | ||
140 | } | ||
141 | } | ||
142 | |||
143 | return tpl.Execute(w, struct { | ||
144 | Title string | ||
145 | URI string | ||
146 | Assets AssetList | ||
147 | Lines []Item | ||
148 | RawText string | ||
149 | Error bool | ||
150 | Protocol string | ||
151 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false, "gopher"}) | ||
152 | } | ||
153 | |||
154 | func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (items []Item) { | ||
155 | baseURL, err := url.Parse(fmt.Sprintf( | ||
156 | "gemini://%s/%s", | ||
157 | hostport, | ||
158 | uri, | ||
159 | )) | ||
160 | if err != nil { | ||
161 | return []Item{} | ||
162 | } | ||
163 | |||
164 | scanner := bufio.NewScanner(body) | ||
165 | |||
166 | for scanner.Scan() { | ||
167 | line := strings.Trim(scanner.Text(), "\r\n") | ||
168 | |||
169 | item := Item{ | ||
170 | Type: ITEM_TYPE_GEMINI_LINE, | ||
171 | Text: line, | ||
172 | } | ||
173 | |||
174 | linkMatch := GeminiLinkPattern.FindStringSubmatch(line) | ||
175 | if len(linkMatch) != 0 && linkMatch[0] != "" { | ||
176 | item.Type = ITEM_TYPE_GEMINI_LINK | ||
177 | item.Link = template.URL(resolveURI(linkMatch[1], baseURL)) | ||
178 | item.Text = linkMatch[2] | ||
179 | if item.Text == "" { | ||
180 | item.Text = linkMatch[1] | ||
181 | } | ||
182 | } | ||
183 | |||
184 | items = append(items, item) | ||
185 | } | ||
186 | |||
187 | return | ||
188 | } | ||
189 | |||
190 | func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { | ||
191 | return func(w http.ResponseWriter, req *http.Request) { | ||
192 | if err := tpl.Execute(w, struct { | ||
193 | Title string | ||
194 | URI string | ||
195 | Assets AssetList | ||
196 | RawText string | ||
197 | Lines []Item | ||
198 | Error bool | ||
199 | Protocol string | ||
200 | }{"Gopher/Gemini proxy", "", assetList, startpagetext, nil, false, "startpage"}); err != nil { | ||
201 | log.Println("Template error: " + err.Error()) | ||
202 | } | ||
203 | } | ||
204 | } | ||
205 | |||
206 | // GopherHandler returns a Handler that proxies requests | ||
207 | // to the specified Gopher server as denoated by the first argument | ||
208 | // to the request path and renders the content using the provided template. | ||
209 | // The optional robots parameters points to a robotstxt.RobotsData struct | ||
210 | // to test user agents against a configurable robotst.txt file. | ||
211 | func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { | ||
212 | return func(w http.ResponseWriter, req *http.Request) { | ||
213 | agent := req.UserAgent() | ||
214 | path := strings.TrimPrefix(req.URL.Path, "/gopher/") | ||
215 | |||
216 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { | ||
217 | log.Printf("UserAgent %s ignored robots.txt", agent) | ||
218 | } | ||
219 | |||
220 | parts := strings.Split(path, "/") | ||
221 | hostport := parts[0] | ||
222 | |||
223 | if len(hostport) == 0 { | ||
224 | http.Redirect(w, req, "/", http.StatusFound) | ||
225 | return | ||
226 | } | ||
227 | |||
228 | title := hostport | ||
229 | |||
230 | var qs string | ||
231 | |||
232 | if req.URL.RawQuery != "" { | ||
233 | qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) | ||
234 | } | ||
235 | |||
236 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | ||
237 | if err != nil { | ||
238 | if e := tpl.Execute(w, struct { | ||
239 | Title string | ||
240 | URI string | ||
241 | Assets AssetList | ||
242 | RawText string | ||
243 | Lines []Item | ||
244 | Error bool | ||
245 | Protocol string | ||
246 | }{title, hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil { | ||
247 | log.Println("Template error: " + e.Error()) | ||
248 | log.Println(err.Error()) | ||
249 | } | ||
250 | return | ||
251 | } | ||
252 | |||
253 | if uri != "" { | ||
254 | title = fmt.Sprintf("%s/%s", hostport, uri) | ||
255 | } | ||
256 | |||
257 | res, err := gopher.Get( | ||
258 | fmt.Sprintf( | ||
259 | "gopher://%s/%s%s", | ||
260 | hostport, | ||
261 | uri, | ||
262 | qs, | ||
263 | ), | ||
264 | ) | ||
265 | |||
266 | if err != nil { | ||
267 | if e := tpl.Execute(w, struct { | ||
268 | Title string | ||
269 | URI string | ||
270 | Assets AssetList | ||
271 | RawText string | ||
272 | Lines []Item | ||
273 | Error bool | ||
274 | Protocol string | ||
275 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil { | ||
276 | log.Println("Template error: " + e.Error()) | ||
277 | } | ||
278 | return | ||
279 | } | ||
280 | |||
281 | if res.Body != nil { | ||
282 | if len(parts) < 2 { | ||
283 | io.Copy(w, res.Body) | ||
284 | } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") { | ||
285 | buf := new(bytes.Buffer) | ||
286 | buf.ReadFrom(res.Body) | ||
287 | if err := tpl.Execute(w, struct { | ||
288 | Title string | ||
289 | URI string | ||
290 | Assets AssetList | ||
291 | RawText string | ||
292 | Lines []Item | ||
293 | Error bool | ||
294 | Protocol string | ||
295 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gopher"}); err != nil { | ||
296 | log.Println("Template error: " + err.Error()) | ||
297 | } | ||
298 | } else if strings.HasPrefix(parts[1], "T") { | ||
299 | _, _, err = vips.NewTransform(). | ||
300 | Load(res.Body). | ||
301 | ResizeStrategy(vips.ResizeStrategyAuto). | ||
302 | ResizeWidth(160). | ||
303 | Quality(75). | ||
304 | Output(w). | ||
305 | Apply() | ||
306 | } else { | ||
307 | io.Copy(w, res.Body) | ||
308 | } | ||
309 | } else { | ||
310 | if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { | ||
311 | if e := tpl.Execute(w, struct { | ||
312 | Title string | ||
313 | URI string | ||
314 | Assets AssetList | ||
315 | RawText string | ||
316 | Lines []Item | ||
317 | Error bool | ||
318 | Protocol string | ||
319 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil { | ||
320 | log.Println("Template error: " + e.Error()) | ||
321 | log.Println(e.Error()) | ||
322 | } | ||
323 | } | ||
324 | } | ||
325 | } | ||
326 | } | ||
327 | |||
328 | func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { | ||
329 | return func(w http.ResponseWriter, req *http.Request) { | ||
330 | agent := req.UserAgent() | ||
331 | path := strings.TrimPrefix(req.URL.Path, "/gemini/") | ||
332 | |||
333 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { | ||
334 | log.Printf("UserAgent %s ignored robots.txt", agent) | ||
335 | } | ||
336 | |||
337 | parts := strings.Split(path, "/") | ||
338 | hostport := parts[0] | ||
339 | |||
340 | if len(hostport) == 0 { | ||
341 | http.Redirect(w, req, "/", http.StatusFound) | ||
342 | return | ||
343 | } | ||
344 | |||
345 | title := hostport | ||
346 | |||
347 | var qs string | ||
348 | |||
349 | if req.URL.RawQuery != "" { | ||
350 | qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) | ||
351 | } | ||
352 | |||
353 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | ||
354 | if err != nil { | ||
355 | if e := tpl.Execute(w, struct { | ||
356 | Title string | ||
357 | URI string | ||
358 | Assets AssetList | ||
359 | RawText string | ||
360 | Lines []Item | ||
361 | Error bool | ||
362 | Protocol string | ||
363 | }{title, hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil { | ||
364 | log.Println("Template error: " + e.Error()) | ||
365 | log.Println(err.Error()) | ||
366 | } | ||
367 | return | ||
368 | } | ||
369 | |||
370 | if uri != "" { | ||
371 | title = fmt.Sprintf("%s/%s", hostport, uri) | ||
372 | } | ||
373 | |||
374 | res, err := GeminiGet( | ||
375 | fmt.Sprintf( | ||
376 | "gemini://%s/%s%s", | ||
377 | hostport, | ||
378 | uri, | ||
379 | qs, | ||
380 | ), | ||
381 | ) | ||
382 | |||
383 | if err != nil { | ||
384 | if e := tpl.Execute(w, struct { | ||
385 | Title string | ||
386 | URI string | ||
387 | Assets AssetList | ||
388 | RawText string | ||
389 | Lines []Item | ||
390 | Error bool | ||
391 | Protocol string | ||
392 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil { | ||
393 | log.Println("Template error: " + e.Error()) | ||
394 | log.Println(err.Error()) | ||
395 | } | ||
396 | return | ||
397 | } | ||
398 | |||
399 | if int(res.Header.Status/10) == 3 { | ||
400 | baseURL, err := url.Parse(fmt.Sprintf( | ||
401 | "gemini://%s/%s", | ||
402 | hostport, | ||
403 | uri, | ||
404 | )) | ||
405 | if err != nil { | ||
406 | if e := tpl.Execute(w, struct { | ||
407 | Title string | ||
408 | URI string | ||
409 | Assets AssetList | ||
410 | RawText string | ||
411 | Lines []Item | ||
412 | Error bool | ||
413 | Protocol string | ||
414 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil { | ||
415 | log.Println("Template error: " + e.Error()) | ||
416 | log.Println(err.Error()) | ||
417 | } | ||
418 | return | ||
419 | } | ||
420 | |||
421 | http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound) | ||
422 | return | ||
423 | } | ||
424 | |||
425 | if int(res.Header.Status/10) != 2 { | ||
426 | if err := tpl.Execute(w, struct { | ||
427 | Title string | ||
428 | URI string | ||
429 | Assets AssetList | ||
430 | RawText string | ||
431 | Lines []Item | ||
432 | Error bool | ||
433 | Protocol string | ||
434 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), nil, true, "gemini"}); err != nil { | ||
435 | log.Println("Template error: " + err.Error()) | ||
436 | } | ||
437 | return | ||
438 | } | ||
439 | |||
440 | if strings.HasPrefix(res.Header.Meta, "text/") { | ||
441 | buf := new(bytes.Buffer) | ||
442 | |||
443 | _, params, err := mime.ParseMediaType(res.Header.Meta) | ||
444 | if err != nil { | ||
445 | buf.ReadFrom(res.Body) | ||
446 | } else { | ||
447 | encoding, _ := charset.Lookup(params["charset"]) | ||
448 | readbuf := new(bytes.Buffer) | ||
449 | readbuf.ReadFrom(res.Body) | ||
450 | |||
451 | writer := transform.NewWriter(buf, encoding.NewDecoder()) | ||
452 | writer.Write(readbuf.Bytes()) | ||
453 | writer.Close() | ||
454 | } | ||
455 | |||
456 | var ( | ||
457 | rawText string | ||
458 | items []Item | ||
459 | ) | ||
460 | |||
461 | if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) { | ||
462 | items = parseGeminiDocument(buf, uri, hostport) | ||
463 | } else { | ||
464 | rawText = buf.String() | ||
465 | } | ||
466 | |||
467 | if err := tpl.Execute(w, struct { | ||
468 | Title string | ||
469 | URI string | ||
470 | Assets AssetList | ||
471 | RawText string | ||
472 | Lines []Item | ||
473 | Error bool | ||
474 | Protocol string | ||
475 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, rawText, items, false, "gemini"}); err != nil { | ||
476 | log.Println("Template error: " + err.Error()) | ||
477 | } | ||
478 | } else { | ||
479 | io.Copy(w, res.Body) | ||
480 | } | ||
481 | } | ||
482 | } | ||
483 | |||
484 | // RobotsTxtHandler returns the contents of the robots.txt file | ||
485 | // if configured and valid. | ||
486 | func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { | ||
487 | return func(w http.ResponseWriter, req *http.Request) { | ||
488 | if robotstxtdata == nil { | ||
489 | http.Error(w, "Not Found", http.StatusNotFound) | ||
490 | return | ||
491 | } | ||
492 | |||
493 | w.Header().Set("Content-Type", "text/plain") | ||
494 | w.Write(robotstxtdata) | ||
495 | } | ||
496 | } | ||
497 | |||
498 | func FaviconHandler(favicondata []byte) http.HandlerFunc { | ||
499 | return func(w http.ResponseWriter, req *http.Request) { | ||
500 | if favicondata == nil { | ||
501 | http.Error(w, "Not Found", http.StatusNotFound) | ||
502 | return | ||
503 | } | ||
504 | |||
505 | w.Header().Set("Content-Type", "image/vnd.microsoft.icon") | ||
506 | w.Header().Set("Cache-Control", "max-age=2592000") | ||
507 | w.Write(favicondata) | ||
508 | } | ||
509 | } | ||
510 | |||
511 | func StyleHandler(styledata []byte) http.HandlerFunc { | ||
512 | return func(w http.ResponseWriter, req *http.Request) { | ||
513 | w.Header().Set("Content-Type", "text/css") | ||
514 | w.Header().Set("Cache-Control", "max-age=2592000") | ||
515 | w.Write(styledata) | ||
516 | } | ||
517 | } | ||
518 | |||
519 | func JavaScriptHandler(jsdata []byte) http.HandlerFunc { | ||
520 | return func(w http.ResponseWriter, req *http.Request) { | ||
521 | w.Header().Set("Content-Type", "text/javascript") | ||
522 | w.Header().Set("Cache-Control", "max-age=2592000") | ||
523 | w.Write(jsdata) | ||
524 | } | ||
525 | } | ||
526 | |||
527 | func FontHandler(woff2 bool, fontdata []byte) http.HandlerFunc { | ||
528 | return func(w http.ResponseWriter, req *http.Request) { | ||
529 | if fontdata == nil { | ||
530 | http.Error(w, "Not Found", http.StatusNotFound) | ||
531 | return | ||
532 | } | ||
533 | |||
534 | if woff2 { | ||
535 | w.Header().Set("Content-Type", "font/woff2") | ||
536 | } else { | ||
537 | w.Header().Set("Content-Type", "font/woff") | ||
538 | } | ||
539 | w.Header().Set("Cache-Control", "max-age=2592000") | ||
540 | |||
541 | w.Write(fontdata) | ||
542 | } | ||
543 | } | ||
544 | |||
545 | // ListenAndServe creates a listening HTTP server bound to | ||
546 | // the interface specified by bind and sets up a Gopher to HTTP | ||
547 | // proxy proxying requests as requested and by default will prozy | ||
548 | // to a Gopher server address specified by uri if no servers is | ||
549 | // specified by the request. The robots argument is a pointer to | ||
550 | // a robotstxt.RobotsData struct for testing user agents against | ||
551 | // a configurable robots.txt file. | ||
552 | func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error { | ||
553 | var ( | ||
554 | tpl *template.Template | ||
555 | robotsdata *robotstxt.RobotsData | ||
556 | ) | ||
557 | |||
558 | robotstxtdata, err := ioutil.ReadFile(robotsfile) | ||
559 | if err != nil { | ||
560 | log.Printf("error reading robots.txt: %s", err) | ||
561 | robotstxtdata = nil | ||
562 | } else { | ||
563 | robotsdata, err = robotstxt.FromBytes(robotstxtdata) | ||
564 | if err != nil { | ||
565 | log.Printf("error reading robots.txt: %s", err) | ||
566 | robotstxtdata = nil | ||
567 | } | ||
568 | } | ||
569 | |||
570 | box := packr.New("assets", "./assets") | ||
571 | |||
572 | fontdataw, err := box.Find("iosevka-term-ss03-regular.woff") | ||
573 | if err != nil { | ||
574 | fontdataw = []byte{} | ||
575 | } | ||
576 | fontwAsset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff", md5.Sum(fontdataw)) | ||
577 | |||
578 | fontdataw2, err := box.Find("iosevka-term-ss03-regular.woff2") | ||
579 | if err != nil { | ||
580 | fontdataw2 = []byte{} | ||
581 | } | ||
582 | fontw2Asset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff2", md5.Sum(fontdataw2)) | ||
583 | |||
584 | propfontdataw, err := box.Find("iosevka-aile-regular.woff") | ||
585 | if err != nil { | ||
586 | propfontdataw = []byte{} | ||
587 | } | ||
588 | propfontwAsset := fmt.Sprintf("/iosevka-aile-regular-%x.woff", md5.Sum(propfontdataw)) | ||
589 | |||
590 | propfontdataw2, err := box.Find("iosevka-aile-regular.woff2") | ||
591 | if err != nil { | ||
592 | propfontdataw2 = []byte{} | ||
593 | } | ||
594 | propfontw2Asset := fmt.Sprintf("/iosevka-aile-regular-%x.woff2", md5.Sum(propfontdataw2)) | ||
595 | |||
596 | styledata, err := box.Find("style.css") | ||
597 | if err != nil { | ||
598 | styledata = []byte{} | ||
599 | } | ||
600 | styleAsset := fmt.Sprintf("/style-%x.css", md5.Sum(styledata)) | ||
601 | |||
602 | jsdata, err := box.Find("main.js") | ||
603 | if err != nil { | ||
604 | jsdata = []byte{} | ||
605 | } | ||
606 | jsAsset := fmt.Sprintf("/main-%x.js", md5.Sum(jsdata)) | ||
607 | |||
608 | favicondata, err := box.Find("favicon.ico") | ||
609 | if err != nil { | ||
610 | favicondata = []byte{} | ||
611 | } | ||
612 | |||
613 | startpagedata, err := ioutil.ReadFile(startpagefile) | ||
614 | if err != nil { | ||
615 | startpagedata, err = box.Find("startpage.txt") | ||
616 | if err != nil { | ||
617 | startpagedata = []byte{} | ||
618 | } | ||
619 | } | ||
620 | startpagetext := string(startpagedata) | ||
621 | |||
622 | tpldata, err := ioutil.ReadFile(".template") | ||
623 | if err == nil { | ||
624 | tpltext = string(tpldata) | ||
625 | } | ||
626 | |||
627 | funcMap := template.FuncMap{ | ||
628 | "safeHtml": func(s string) template.HTML { | ||
629 | return template.HTML(s) | ||
630 | }, | ||
631 | "safeCss": func(s string) template.CSS { | ||
632 | return template.CSS(s) | ||
633 | }, | ||
634 | "safeJs": func(s string) template.JS { | ||
635 | return template.JS(s) | ||
636 | }, | ||
637 | "HTMLEscape": func(s string) string { | ||
638 | return html.EscapeString(s) | ||
639 | }, | ||
640 | "split": strings.Split, | ||
641 | "last": func(s []string) string { | ||
642 | return s[len(s)-1] | ||
643 | }, | ||
644 | "pop": func(s []string) []string { | ||
645 | return s[:len(s)-1] | ||
646 | }, | ||
647 | "replace": func(pattern, output string, input interface{}) string { | ||
648 | var re = regexp.MustCompile(pattern) | ||
649 | var inputStr = fmt.Sprintf("%v", input) | ||
650 | return re.ReplaceAllString(inputStr, output) | ||
651 | }, | ||
652 | "trimLeftChar": func(s string) string { | ||
653 | for i := range s { | ||
654 | if i > 0 { | ||
655 | return s[i:] | ||
656 | } | ||
657 | } | ||
658 | return s[:0] | ||
659 | }, | ||
660 | "hasPrefix": func(s string, prefix string) bool { | ||
661 | return strings.HasPrefix(s, prefix) | ||
662 | }, | ||
663 | "title": func(s string) string { | ||
664 | return strings.Title(s) | ||
665 | }, | ||
666 | } | ||
667 | |||
668 | tpl, err = template.New("gophermenu").Funcs(funcMap).Parse(tpltext) | ||
669 | if err != nil { | ||
670 | log.Fatal(err) | ||
671 | } | ||
672 | |||
673 | vips.Startup(&vips.Config{ | ||
674 | ConcurrencyLevel: vipsconcurrency, | ||
675 | }) | ||
676 | |||
677 | assets := AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset} | ||
678 | |||
679 | http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, startpagetext, assets))) | ||
680 | http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, assets, robotsdebug))) | ||
681 | http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, assets, robotsdebug))) | ||
682 | http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) | ||
683 | http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) | ||
684 | http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) | ||
685 | http.Handle(jsAsset, gziphandler.GzipHandler(JavaScriptHandler(jsdata))) | ||
686 | http.HandleFunc(fontwAsset, FontHandler(false, fontdataw)) | ||
687 | http.HandleFunc(fontw2Asset, FontHandler(true, fontdataw2)) | ||
688 | http.HandleFunc(propfontwAsset, FontHandler(false, propfontdataw)) | ||
689 | http.HandleFunc(propfontw2Asset, FontHandler(true, propfontdataw2)) | ||
690 | //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/")))) | ||
691 | |||
692 | return http.ListenAndServe(bind, nil) | ||
693 | } | ||