aboutsummaryrefslogtreecommitdiffstats
path: root/internal/gopherproxy
diff options
context:
space:
mode:
Diffstat (limited to 'internal/gopherproxy')
-rw-r--r--internal/gopherproxy/gemini.go292
-rw-r--r--internal/gopherproxy/gopher.go284
-rw-r--r--internal/gopherproxy/main.go300
-rw-r--r--internal/gopherproxy/tpl/_fonts.html16
-rw-r--r--internal/gopherproxy/tpl/_modals.html24
-rw-r--r--internal/gopherproxy/tpl/gemini.html65
-rw-r--r--internal/gopherproxy/tpl/gopher.html50
-rw-r--r--internal/gopherproxy/tpl/startpage.html30
8 files changed, 1061 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}
diff --git a/internal/gopherproxy/gopher.go b/internal/gopherproxy/gopher.go
new file mode 100644
index 0000000..5f4b39f
--- /dev/null
+++ b/internal/gopherproxy/gopher.go
@@ -0,0 +1,284 @@
1package gopherproxy
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "io"
8 "log"
9 "net"
10 "net/http"
11 "net/url"
12 "strings"
13
14 "git.vulpes.one/gopherproxy/pkg/libgopher"
15
16 "github.com/davidbyttow/govips/pkg/vips"
17 "github.com/temoto/robotstxt"
18)
19
20type gopherTemplateVariables struct {
21 Title string
22 URL string
23 Assets AssetList
24 Lines []GopherItem
25 Nav []GopherNavItem
26 IsPlain bool
27}
28
29type GopherNavItem struct {
30 Label string
31 URL string
32 Current bool
33}
34
35type GopherItem struct {
36 Link template.URL
37 Type string
38 Text string
39}
40
41func trimLeftChars(s string, n int) string {
42 m := 0
43 for i := range s {
44 if m >= n {
45 return s[i:]
46 }
47 m++
48 }
49 return s[:0]
50}
51
52func urlToGopherNav(url string) (items []GopherNavItem) {
53 partialURL := "/gopher"
54 parts := strings.Split(url, "/")
55
56 if len(parts) != 0 && parts[len(parts)-1] == "" {
57 parts = parts[:len(parts)-1]
58 }
59
60 for i, part := range parts {
61 if i == 1 {
62 partialURL = partialURL + "/1"
63 part = trimLeftChars(part, 1)
64
65 if part == "" {
66 continue
67 }
68 } else {
69 partialURL = partialURL + "/" + part
70 }
71
72 items = append(items, GopherNavItem{
73 Label: part,
74 URL: partialURL,
75 Current: false,
76 })
77 }
78
79 items[len(items)-1].Current = true
80
81 return
82}
83
84func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error {
85 var title string
86
87 out := make([]GopherItem, len(d.Items))
88
89 for i, x := range d.Items {
90 if x.Type == libgopher.INFO && x.Selector == "TITLE" {
91 title = x.Description
92 continue
93 }
94
95 tr := GopherItem{
96 Text: x.Description,
97 Type: x.Type.String(),
98 }
99
100 if x.Type == libgopher.INFO {
101 out[i] = tr
102 continue
103 }
104
105 if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") {
106 link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:")
107 if strings.HasPrefix(link, "gemini://") {
108 link = fmt.Sprintf(
109 "/gemini/%s",
110 strings.TrimPrefix(link, "gemini://"),
111 )
112 } else if strings.HasPrefix(link, "gopher://") {
113 link = fmt.Sprintf(
114 "/gopher/%s",
115 strings.TrimPrefix(link, "gopher://"),
116 )
117 }
118 tr.Link = template.URL(link)
119 } else {
120 var linkHostport string
121 if x.Port != "70" {
122 linkHostport = net.JoinHostPort(x.Host, x.Port)
123 } else {
124 linkHostport = x.Host
125 }
126
127 path := url.PathEscape(x.Selector)
128 path = strings.Replace(path, "%2F", "/", -1)
129 tr.Link = template.URL(
130 fmt.Sprintf(
131 "/gopher/%s/%s%s",
132 linkHostport,
133 string(byte(x.Type)),
134 path,
135 ),
136 )
137 }
138
139 out[i] = tr
140 }
141
142 if title == "" {
143 if uri != "" {
144 title = fmt.Sprintf("%s/%s", hostport, uri)
145 } else {
146 title = hostport
147 }
148 }
149
150 return tpl.Execute(w, gopherTemplateVariables{
151 Title: title,
152 URL: fmt.Sprintf("%s/%s", hostport, uri),
153 Assets: assetList,
154 Lines: out,
155 Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)),
156 })
157}
158
159// GopherHandler returns a Handler that proxies requests
160// to the specified Gopher server as denoated by the first argument
161// to the request path and renders the content using the provided template.
162// The optional robots parameters points to a robotstxt.RobotsData struct
163// to test user agents against a configurable robotst.txt file.
164func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc {
165 return func(w http.ResponseWriter, req *http.Request) {
166 agent := req.UserAgent()
167 path := strings.TrimPrefix(req.URL.Path, "/gopher/")
168
169 if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) {
170 log.Printf("UserAgent %s ignored robots.txt", agent)
171 }
172
173 parts := strings.Split(path, "/")
174 hostport := parts[0]
175
176 if len(hostport) == 0 {
177 http.Redirect(w, req, "/", http.StatusFound)
178 return
179 }
180
181 title := hostport
182
183 var qs string
184
185 if req.URL.RawQuery != "" {
186 qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery))
187 }
188
189 uri, err := url.QueryUnescape(strings.Join(parts[1:], "/"))
190 if err != nil {
191 if e := tpl.Execute(w, gopherTemplateVariables{
192 Title: title,
193 URL: hostport,
194 Assets: assetList,
195 Lines: []GopherItem{{
196 Text: fmt.Sprintf("Error: %s", err),
197 }},
198 Nav: urlToGopherNav(hostport),
199 IsPlain: true,
200 }); e != nil {
201 log.Println("Template error: " + e.Error())
202 log.Println(err.Error())
203 }
204 return
205 }
206
207 if uri != "" {
208 title = fmt.Sprintf("%s/%s", hostport, uri)
209 }
210
211 res, err := libgopher.Get(
212 fmt.Sprintf(
213 "gopher://%s/%s%s",
214 hostport,
215 uri,
216 qs,
217 ),
218 )
219
220 if err != nil {
221 if e := tpl.Execute(w, gopherTemplateVariables{
222 Title: title,
223 URL: fmt.Sprintf("%s/%s", hostport, uri),
224 Assets: assetList,
225 Lines: []GopherItem{{
226 Text: fmt.Sprintf("Error: %s", err),
227 }},
228 Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)),
229 IsPlain: true,
230 }); e != nil {
231 log.Println("Template error: " + e.Error())
232 }
233 return
234 }
235
236 if res.Body != nil {
237 if len(parts) < 2 {
238 io.Copy(w, res.Body)
239 } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") {
240 buf := new(bytes.Buffer)
241 buf.ReadFrom(res.Body)
242
243 if err := tpl.Execute(w, gopherTemplateVariables{
244 Title: title,
245 URL: fmt.Sprintf("%s/%s", hostport, uri),
246 Assets: assetList,
247 Lines: []GopherItem{{
248 Text: buf.String(),
249 }},
250 Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)),
251 IsPlain: true,
252 }); err != nil {
253 log.Println("Template error: " + err.Error())
254 }
255 } else if strings.HasPrefix(parts[1], "T") {
256 _, _, err = vips.NewTransform().
257 Load(res.Body).
258 ResizeStrategy(vips.ResizeStrategyAuto).
259 ResizeWidth(160).
260 Quality(75).
261 Output(w).
262 Apply()
263 } else {
264 io.Copy(w, res.Body)
265 }
266 } else {
267 if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil {
268 if e := tpl.Execute(w, gopherTemplateVariables{
269 Title: title,
270 URL: fmt.Sprintf("%s/%s", hostport, uri),
271 Assets: assetList,
272 Lines: []GopherItem{{
273 Text: fmt.Sprintf("Error: %s", err),
274 }},
275 Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)),
276 IsPlain: false,
277 }); e != nil {
278 log.Println("Template error: " + e.Error())
279 log.Println(e.Error())
280 }
281 }
282 }
283 }
284}
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}
diff --git a/internal/gopherproxy/tpl/_fonts.html b/internal/gopherproxy/tpl/_fonts.html
new file mode 100644
index 0000000..a947222
--- /dev/null
+++ b/internal/gopherproxy/tpl/_fonts.html
@@ -0,0 +1,16 @@
1<style>
2 @font-face {
3 font-family: 'Iosevka Term SS03';
4 font-style: normal;
5 font-weight: normal;
6 src: url('{{ .Assets.FontRegularW2 }}') format('woff2'),
7 url('{{ .Assets.FontRegularW }}') format('woff');
8 }
9 @font-face {
10 font-family: 'Iosevka Term SS03';
11 font-style: normal;
12 font-weight: bold;
13 src: url('{{ .Assets.FontBoldW2 }}') format('woff2'),
14 url('{{ .Assets.FontBoldW }}') format('woff');
15 }
16</style>
diff --git a/internal/gopherproxy/tpl/_modals.html b/internal/gopherproxy/tpl/_modals.html
new file mode 100644
index 0000000..3bbdef2
--- /dev/null
+++ b/internal/gopherproxy/tpl/_modals.html
@@ -0,0 +1,24 @@
1<aside class="modal modal--settings">
2 <div class="modal__content">
3 <header class="modal__head header-base">
4 <h1 class="modal__title">Settings</h1>
5 <button class="modal__close-btn">Close</button>
6 </header>
7 <div class="setting setting--word-wrap">
8 <strong class="setting__label">Wrap wide content</strong>
9 <button class="setting__value">[N/A]</button>
10 </div>
11 <div class="setting setting--monospace-font">
12 <strong class="setting__label">Gemini: Monospace font</strong>
13 <button class="setting__value">[N/A]</button>
14 </div>
15 <div class="setting setting--image-previews">
16 <strong class="setting__label">Image thumbnails</strong>
17 <button class="setting__value">[N/A]</button>
18 </div>
19 <div class="setting setting--clickable-plain-links">
20 <strong class="setting__label">Clickable links in text files</strong>
21 <button class="setting__value">[N/A]</button>
22 </div>
23 </div>
24</aside>
diff --git a/internal/gopherproxy/tpl/gemini.html b/internal/gopherproxy/tpl/gemini.html
new file mode 100644
index 0000000..8d20da1
--- /dev/null
+++ b/internal/gopherproxy/tpl/gemini.html
@@ -0,0 +1,65 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title>{{ .Title }} - Gemini proxy</title>
7 <link rel="stylesheet" href="{{ .Assets.Style }}" />
8 {{- template "_fonts.html" . -}}
9 </head>
10 <body class="{{ if .IsPlain }}is-plain{{ end }}">
11 <header class="header header-base">
12 <div class="location">
13 <a class="location__prefix">gemini://</a><a class="location__prefix location__prefix--mobile">://</a>
14 {{- range $i, $item := .Nav -}}
15 {{- if ne $i 0 -}}
16 <span class="location__slash">/</span>
17 {{- end -}}
18 {{- if .Current -}}
19 <span class="location__uripart">{{ .Label }}</span>
20 {{- else -}}
21 <a href="{{ .URL }}/" class="location__uripart">{{ .Label }}</a>
22 {{- end -}}
23 {{- end -}}
24 </div>
25 <div class="actions">
26 <div class="action"><button class="settings-btn">Settings</button></div>
27 </div>
28 </header>
29
30 <main class="wrap">
31 <div class="content{{ if not .IsPlain }} content--has-type-annotations{{ end }}">
32 {{- range .Sections -}}
33 {{- if eq .Type "RAW_TEXT" -}}
34 <div class="section"><span class="section__type">```</span><pre class="section__content">{{- .Text -}}</pre></div>
35 {{- else if eq .Type "REFLOW_TEXT" -}}
36 <div class="section"><p class="section__content">{{- .Text -}}</p></div>
37 {{- else if eq .Type "LINK" -}}
38 {{- $linkCls := "link" -}}
39 {{- $url := string .URL -}}
40 {{- if or (hasSuffix $url ".jpg") (hasSuffix $url ".jpeg") (hasSuffix $url ".png") (hasSuffix $url ".gif") -}}
41 {{- $linkCls = "link--IMG" -}}
42 {{- end -}}
43 <div class="section"><span class="section__type"> =></span><a class="section__content {{ $linkCls }}" href="{{ .URL }}">{{- .Text -}}</a></div>
44 {{- else if eq .Type "HEADING_1" -}}
45 <div class="section"><span class="section__type"> #</span><h1 class="section__content">{{- .Text -}}</h1></div>
46 {{- else if eq .Type "HEADING_2" -}}
47 <div class="section"><span class="section__type"> ##</span><h2 class="section__content">{{- .Text -}}</h2></div>
48 {{- else if eq .Type "HEADING_3" -}}
49 <div class="section"><span class="section__type">###</span><h3 class="section__content">{{- .Text -}}</h3></div>
50 {{- else if eq .Type "LIST" -}}
51 <div class="section"><ul class="section__content">
52 {{- range .Items -}}
53 <li>{{- . -}}</li>
54 {{- end -}}
55 </ul></div>
56 {{- end -}}
57 {{- end -}}
58 </div>
59 </main>
60
61 {{- template "_modals.html" . -}}
62
63 <script src="{{ .Assets.JS }}"></script>
64 </body>
65</html>
diff --git a/internal/gopherproxy/tpl/gopher.html b/internal/gopherproxy/tpl/gopher.html
new file mode 100644
index 0000000..6eb607c
--- /dev/null
+++ b/internal/gopherproxy/tpl/gopher.html
@@ -0,0 +1,50 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title>{{ .Title }} - Gopher proxy</title>
7 <link rel="stylesheet" href="{{ .Assets.Style }}" />
8 {{- template "_fonts.html" . -}}
9 </head>
10 <body class="{{ if .IsPlain }}is-plain{{ end }}">
11 <header class="header header-base">
12 <div class="location">
13 <a class="location__prefix">gopher://</a><a class="location__prefix location__prefix--mobile">://</a>
14 {{- range $i, $item := .Nav -}}
15 {{- if ne $i 0 -}}
16 <span class="location__slash">/</span>
17 {{- end -}}
18 {{- if .Current -}}
19 <span class="location__uripart">{{ .Label }}</span>
20 {{- else -}}
21 <a href="{{ .URL }}/" class="location__uripart">{{ .Label }}</a>
22 {{- end -}}
23 {{- end -}}
24 </div>
25 <div class="actions">
26 {{- if .IsPlain -}}
27 <div class="action"><a href="/gopher/{{ .URL | replace "^([^/]*)/0" "$1/9" }}">View raw</a></div>
28 {{- end -}}
29 <div class="action"><button class="settings-btn">Settings</button></div>
30 </div>
31 </header>
32
33 <main class="wrap">
34 <div class="content content--monospace{{ if not .IsPlain }} content--has-type-annotations{{ end }}">
35 {{- $page := . -}}
36 {{- range .Lines -}}
37 {{- if .Link -}}
38 <div class="section"><span class="section__type">{{- .Type -}}</span><a class="section__content link link--{{ .Type }}" href="{{ .Link }}">{{- .Text -}}</a></div>
39 {{- else -}}
40 <div class="section"><span class="section__type"></span><pre class="section__content">{{- .Text -}}</pre></div>
41 {{- end -}}
42 {{- end -}}
43 </div>
44 </main>
45
46 {{- template "_modals.html" . -}}
47
48 <script src="{{ .Assets.JS }}"></script>
49 </body>
50</html>
diff --git a/internal/gopherproxy/tpl/startpage.html b/internal/gopherproxy/tpl/startpage.html
new file mode 100644
index 0000000..772ac90
--- /dev/null
+++ b/internal/gopherproxy/tpl/startpage.html
@@ -0,0 +1,30 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title>{{ .Title }}</title>
7 <link rel="stylesheet" href="{{ .Assets.Style }}" />
8 {{- template "_fonts.html" . -}}
9 </head>
10 <body class="is-plain">
11 <header class="header header-base">
12 <div class="location">
13 <a class="location__prefix">start://</a><a class="location__prefix location__prefix--mobile">://</a>
14 </div>
15 <div class="actions">
16 <div class="action"><button class="settings-btn">Settings</button></div>
17 </div>
18 </header>
19
20 <main class="wrap">
21 <pre class="content content--monospace">
22 {{- .Content -}}
23 </pre>
24 </main>
25
26 {{- template "_modals.html" . -}}
27
28 <script src="{{ .Assets.JS }}"></script>
29 </body>
30</html>