aboutsummaryrefslogtreecommitdiffstats
path: root/internal/gopherproxy/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/gopherproxy/main.go')
-rw-r--r--internal/gopherproxy/main.go300
1 files changed, 300 insertions, 0 deletions
diff --git a/internal/gopherproxy/main.go b/internal/gopherproxy/main.go
new file mode 100644
index 0000000..33230c1
--- /dev/null
+++ b/internal/gopherproxy/main.go
@@ -0,0 +1,300 @@
1package gopherproxy
2
3import (
4 "crypto/md5"
5 "fmt"
6 "html"
7 "html/template"
8 "io/ioutil"
9 "log"
10 "net/http"
11 "regexp"
12 "strings"
13
14 "github.com/NYTimes/gziphandler"
15 "github.com/davidbyttow/govips/pkg/vips"
16 "github.com/gobuffalo/packr/v2"
17 "github.com/temoto/robotstxt"
18)
19
20type AssetList struct {
21 Style string
22 JS string
23 FontRegularW string
24 FontRegularW2 string
25 FontBoldW string
26 FontBoldW2 string
27}
28
29type startTemplateVariables struct {
30 Title string
31 URL string
32 Assets AssetList
33 Content string
34}
35
36func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc {
37 return func(w http.ResponseWriter, req *http.Request) {
38 if err := tpl.Execute(w, startTemplateVariables{
39 Title: "Gopher/Gemini proxy",
40 Assets: assetList,
41 Content: startpagetext,
42 }); err != nil {
43 log.Println("Template error: " + err.Error())
44 }
45 }
46}
47
48// RobotsTxtHandler returns the contents of the robots.txt file
49// if configured and valid.
50func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc {
51 return func(w http.ResponseWriter, req *http.Request) {
52 if robotstxtdata == nil {
53 http.Error(w, "Not Found", http.StatusNotFound)
54 return
55 }
56
57 w.Header().Set("Content-Type", "text/plain")
58 w.Write(robotstxtdata)
59 }
60}
61
62func FaviconHandler(favicondata []byte) http.HandlerFunc {
63 return func(w http.ResponseWriter, req *http.Request) {
64 if favicondata == nil {
65 http.Error(w, "Not Found", http.StatusNotFound)
66 return
67 }
68
69 w.Header().Set("Content-Type", "image/vnd.microsoft.icon")
70 w.Header().Set("Cache-Control", "max-age=2592000")
71 w.Write(favicondata)
72 }
73}
74
75func StyleHandler(styledata []byte) http.HandlerFunc {
76 return func(w http.ResponseWriter, req *http.Request) {
77 w.Header().Set("Content-Type", "text/css")
78 w.Header().Set("Cache-Control", "max-age=2592000")
79 w.Write(styledata)
80 }
81}
82
83func JavaScriptHandler(jsdata []byte) http.HandlerFunc {
84 return func(w http.ResponseWriter, req *http.Request) {
85 w.Header().Set("Content-Type", "text/javascript")
86 w.Header().Set("Cache-Control", "max-age=2592000")
87 w.Write(jsdata)
88 }
89}
90
91func FontHandler(woff2 bool, fontdata []byte) http.HandlerFunc {
92 return func(w http.ResponseWriter, req *http.Request) {
93 if fontdata == nil {
94 http.Error(w, "Not Found", http.StatusNotFound)
95 return
96 }
97
98 if woff2 {
99 w.Header().Set("Content-Type", "font/woff2")
100 } else {
101 w.Header().Set("Content-Type", "font/woff")
102 }
103 w.Header().Set("Cache-Control", "max-age=2592000")
104
105 w.Write(fontdata)
106 }
107}
108
109// ListenAndServe creates a listening HTTP server bound to
110// the interface specified by bind and sets up a Gopher to HTTP
111// proxy proxying requests as requested and by default will prozy
112// to a Gopher server address specified by uri if no servers is
113// specified by the request. The robots argument is a pointer to
114// a robotstxt.RobotsData struct for testing user agents against
115// a configurable robots.txt file.
116func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error {
117 box := packr.New("assets", "../../assets")
118
119 //
120 // Robots
121
122 var robotsdata *robotstxt.RobotsData
123
124 robotstxtdata, err := ioutil.ReadFile(robotsfile)
125 if err != nil {
126 log.Printf("error reading robots.txt: %s", err)
127 robotstxtdata = nil
128 } else {
129 robotsdata, err = robotstxt.FromBytes(robotstxtdata)
130 if err != nil {
131 log.Printf("error reading robots.txt: %s", err)
132 robotstxtdata = nil
133 }
134 }
135
136 //
137 // Fonts
138
139 fontRegularWData, err := box.Find("iosevka-fixed-ss03-regular.woff")
140 if err != nil {
141 fontRegularWData = []byte{}
142 }
143 fontRegularWAsset := fmt.Sprintf("/iosevka-fixed-ss03-regular-%x.woff", md5.Sum(fontRegularWData))
144
145 fontRegularW2Data, err := box.Find("iosevka-fixed-ss03-regular.woff2")
146 if err != nil {
147 fontRegularW2Data = []byte{}
148 }
149 fontRegularW2Asset := fmt.Sprintf("/iosevka-fixed-ss03-regular-%x.woff2", md5.Sum(fontRegularW2Data))
150
151 fontBoldWData, err := box.Find("iosevka-fixed-ss03-bold.woff")
152 if err != nil {
153 fontBoldWData = []byte{}
154 }
155 fontBoldWAsset := fmt.Sprintf("/iosevka-fixed-ss03-bold-%x.woff", md5.Sum(fontBoldWData))
156
157 fontBoldW2Data, err := box.Find("iosevka-fixed-ss03-bold.woff2")
158 if err != nil {
159 fontBoldW2Data = []byte{}
160 }
161 fontBoldW2Asset := fmt.Sprintf("/iosevka-fixed-ss03-bold-%x.woff2", md5.Sum(fontBoldW2Data))
162
163 //
164 // Stylesheet
165
166 styledata, err := box.Find("style.css")
167 if err != nil {
168 styledata = []byte{}
169 }
170 styleAsset := fmt.Sprintf("/style-%x.css", md5.Sum(styledata))
171
172 //
173 // JavaScript
174
175 jsdata, err := box.Find("main.js")
176 if err != nil {
177 jsdata = []byte{}
178 }
179 jsAsset := fmt.Sprintf("/main-%x.js", md5.Sum(jsdata))
180
181 //
182 // Favicon
183
184 favicondata, err := box.Find("favicon.ico")
185 if err != nil {
186 favicondata = []byte{}
187 }
188
189 //
190 // Start page text
191
192 startpagedata, err := ioutil.ReadFile(startpagefile)
193 if err != nil {
194 startpagedata, err = box.Find("startpage.txt")
195 if err != nil {
196 startpagedata = []byte{}
197 }
198 }
199 startpagetext := string(startpagedata)
200
201 //
202 //
203
204 funcMap := template.FuncMap{
205 "safeHtml": func(s string) template.HTML {
206 return template.HTML(s)
207 },
208 "safeCss": func(s string) template.CSS {
209 return template.CSS(s)
210 },
211 "safeJs": func(s string) template.JS {
212 return template.JS(s)
213 },
214 "HTMLEscape": func(s string) string {
215 return html.EscapeString(s)
216 },
217 "split": strings.Split,
218 "last": func(s []string) string {
219 return s[len(s)-1]
220 },
221 "pop": func(s []string) []string {
222 return s[:len(s)-1]
223 },
224 "replace": func(pattern, output string, input interface{}) string {
225 var re = regexp.MustCompile(pattern)
226 var inputStr = fmt.Sprintf("%v", input)
227 return re.ReplaceAllString(inputStr, output)
228 },
229 "trimLeftChar": func(s string) string {
230 for i := range s {
231 if i > 0 {
232 return s[i:]
233 }
234 }
235 return s[:0]
236 },
237 "hasPrefix": func(s string, prefix string) bool {
238 return strings.HasPrefix(s, prefix)
239 },
240 "hasSuffix": func(s string, suffix string) bool {
241 return strings.HasSuffix(s, suffix)
242 },
243 "title": func(s string) string {
244 return strings.Title(s)
245 },
246 "string": func(s interface{}) string {
247 return fmt.Sprint(s)
248 },
249 }
250
251 //
252
253 tplBox := packr.New("templates", "./tpl")
254
255 templates := template.New("main.html").Funcs(funcMap)
256
257 for _, filename := range tplBox.List() {
258 if strings.HasSuffix(filename, ".html") {
259 tplStr, _ := tplBox.FindString(filename)
260 templates, _ = templates.New(filename).Parse(tplStr)
261 }
262 }
263
264 //
265
266 startpageTpl := templates.Lookup("startpage.html")
267 geminiTpl := templates.Lookup("gemini.html")
268 gopherTpl := templates.Lookup("gopher.html")
269
270 //
271 //
272
273 vips.Startup(&vips.Config{
274 ConcurrencyLevel: vipsconcurrency,
275 })
276
277 assets := AssetList{
278 Style: styleAsset,
279 JS: jsAsset,
280 FontRegularW: fontRegularWAsset,
281 FontRegularW2: fontRegularW2Asset,
282 FontBoldW: fontBoldWAsset,
283 FontBoldW2: fontBoldW2Asset,
284 }
285
286 http.Handle("/", gziphandler.GzipHandler(DefaultHandler(startpageTpl, startpagetext, assets)))
287 http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(gopherTpl, robotsdata, assets, robotsdebug)))
288 http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(geminiTpl, robotsdata, assets, robotsdebug)))
289 http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata)))
290 http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata)))
291 http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata)))
292 http.Handle(jsAsset, gziphandler.GzipHandler(JavaScriptHandler(jsdata)))
293 http.HandleFunc(fontRegularWAsset, FontHandler(false, fontRegularWData))
294 http.HandleFunc(fontRegularW2Asset, FontHandler(true, fontRegularW2Data))
295 http.HandleFunc(fontBoldWAsset, FontHandler(false, fontBoldWData))
296 http.HandleFunc(fontBoldW2Asset, FontHandler(true, fontBoldW2Data))
297 //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/"))))
298
299 return http.ListenAndServe(bind, nil)
300}