aboutsummaryrefslogtreecommitdiffstats
path: root/internal/gopherproxy/gopher.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/gopherproxy/gopher.go')
-rw-r--r--internal/gopherproxy/gopher.go284
1 files changed, 284 insertions, 0 deletions
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}