diff options
Diffstat (limited to 'internal/port/gemini.go')
-rw-r--r-- | internal/port/gemini.go | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/internal/port/gemini.go b/internal/port/gemini.go new file mode 100644 index 0000000..f9b0b97 --- /dev/null +++ b/internal/port/gemini.go | |||
@@ -0,0 +1,205 @@ | |||
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 | "regexp" | ||
13 | "strings" | ||
14 | |||
15 | "golang.org/x/net/html/charset" | ||
16 | "golang.org/x/text/transform" | ||
17 | |||
18 | "git.vulpes.one/Feuerfuchs/port/port/libgemini" | ||
19 | |||
20 | "github.com/temoto/robotstxt" | ||
21 | ) | ||
22 | |||
23 | var ( | ||
24 | TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") | ||
25 | ) | ||
26 | |||
27 | func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { | ||
28 | if strings.HasPrefix(uri, "//") { | ||
29 | resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//") | ||
30 | } else if strings.HasPrefix(uri, "gemini://") { | ||
31 | resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://") | ||
32 | } else if strings.HasPrefix(uri, "gopher://") { | ||
33 | resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://") | ||
34 | } else { | ||
35 | url, err := url.Parse(uri) | ||
36 | if err != nil { | ||
37 | return "" | ||
38 | } | ||
39 | adjustedURI := baseURL.ResolveReference(url) | ||
40 | path := adjustedURI.Path | ||
41 | if !strings.HasPrefix(path, "/") { | ||
42 | path = "/" + path | ||
43 | } | ||
44 | if adjustedURI.Scheme == "gemini" { | ||
45 | resolvedURI = "/gemini/" + adjustedURI.Host + path | ||
46 | } else if adjustedURI.Scheme == "gopher" { | ||
47 | resolvedURI = "/gopher/" + adjustedURI.Host + path | ||
48 | } else { | ||
49 | resolvedURI = adjustedURI.String() | ||
50 | } | ||
51 | } | ||
52 | |||
53 | return | ||
54 | } | ||
55 | |||
56 | func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { | ||
57 | return func(w http.ResponseWriter, req *http.Request) { | ||
58 | agent := req.UserAgent() | ||
59 | path := strings.TrimPrefix(req.URL.Path, "/gemini/") | ||
60 | |||
61 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { | ||
62 | log.Printf("UserAgent %s ignored robots.txt", agent) | ||
63 | } | ||
64 | |||
65 | parts := strings.Split(path, "/") | ||
66 | hostport := parts[0] | ||
67 | |||
68 | if len(hostport) == 0 { | ||
69 | http.Redirect(w, req, "/", http.StatusFound) | ||
70 | return | ||
71 | } | ||
72 | |||
73 | title := hostport | ||
74 | |||
75 | var qs string | ||
76 | |||
77 | if req.URL.RawQuery != "" { | ||
78 | qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) | ||
79 | } | ||
80 | |||
81 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | ||
82 | if err != nil { | ||
83 | if e := tpl.Execute(w, TemplateVariables{ | ||
84 | Title: title, | ||
85 | URI: hostport, | ||
86 | Assets: assetList, | ||
87 | RawText: fmt.Sprintf("Error: %s", err), | ||
88 | Error: true, | ||
89 | Protocol: "gemini", | ||
90 | }); e != nil { | ||
91 | log.Println("Template error: " + e.Error()) | ||
92 | log.Println(err.Error()) | ||
93 | } | ||
94 | return | ||
95 | } | ||
96 | |||
97 | if uri != "" { | ||
98 | title = fmt.Sprintf("%s/%s", hostport, uri) | ||
99 | } | ||
100 | |||
101 | res, err := libgemini.Get( | ||
102 | fmt.Sprintf( | ||
103 | "gemini://%s/%s%s", | ||
104 | hostport, | ||
105 | uri, | ||
106 | qs, | ||
107 | ), | ||
108 | ) | ||
109 | |||
110 | if err != nil { | ||
111 | if e := tpl.Execute(w, TemplateVariables{ | ||
112 | Title: title, | ||
113 | URI: fmt.Sprintf("%s/%s", hostport, uri), | ||
114 | Assets: assetList, | ||
115 | RawText: fmt.Sprintf("Error: %s", err), | ||
116 | Error: true, | ||
117 | Protocol: "gemini", | ||
118 | }); e != nil { | ||
119 | log.Println("Template error: " + e.Error()) | ||
120 | log.Println(err.Error()) | ||
121 | } | ||
122 | return | ||
123 | } | ||
124 | |||
125 | if int(res.Header.Status/10) == 3 { | ||
126 | baseURL, err := url.Parse(fmt.Sprintf( | ||
127 | "gemini://%s/%s", | ||
128 | hostport, | ||
129 | uri, | ||
130 | )) | ||
131 | if err != nil { | ||
132 | if e := tpl.Execute(w, TemplateVariables{ | ||
133 | Title: title, | ||
134 | URI: fmt.Sprintf("%s/%s", hostport, uri), | ||
135 | Assets: assetList, | ||
136 | RawText: fmt.Sprintf("Error: %s", err), | ||
137 | Error: true, | ||
138 | Protocol: "gemini", | ||
139 | }); e != nil { | ||
140 | log.Println("Template error: " + e.Error()) | ||
141 | log.Println(err.Error()) | ||
142 | } | ||
143 | return | ||
144 | } | ||
145 | |||
146 | http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound) | ||
147 | return | ||
148 | } | ||
149 | |||
150 | if int(res.Header.Status/10) != 2 { | ||
151 | if err := tpl.Execute(w, TemplateVariables{ | ||
152 | Title: title, | ||
153 | URI: fmt.Sprintf("%s/%s", hostport, uri), | ||
154 | Assets: assetList, | ||
155 | RawText: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), | ||
156 | Error: true, | ||
157 | Protocol: "gemini", | ||
158 | }); err != nil { | ||
159 | log.Println("Template error: " + err.Error()) | ||
160 | } | ||
161 | return | ||
162 | } | ||
163 | |||
164 | if strings.HasPrefix(res.Header.Meta, "text/") { | ||
165 | buf := new(bytes.Buffer) | ||
166 | |||
167 | _, params, err := mime.ParseMediaType(res.Header.Meta) | ||
168 | if err != nil { | ||
169 | buf.ReadFrom(res.Body) | ||
170 | } else { | ||
171 | encoding, _ := charset.Lookup(params["charset"]) | ||
172 | readbuf := new(bytes.Buffer) | ||
173 | readbuf.ReadFrom(res.Body) | ||
174 | |||
175 | writer := transform.NewWriter(buf, encoding.NewDecoder()) | ||
176 | writer.Write(readbuf.Bytes()) | ||
177 | writer.Close() | ||
178 | } | ||
179 | |||
180 | var ( | ||
181 | rawText string | ||
182 | items []Item | ||
183 | ) | ||
184 | |||
185 | if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { | ||
186 | items = parseGeminiDocument(buf, uri, hostport) | ||
187 | } else { | ||
188 | rawText = buf.String() | ||
189 | } | ||
190 | |||
191 | if err := tpl.Execute(w, TemplateVariables{ | ||
192 | Title: title, | ||
193 | URI: fmt.Sprintf("%s/%s", hostport, uri), | ||
194 | Assets: assetList, | ||
195 | Lines: items, | ||
196 | RawText: rawText, | ||
197 | Protocol: "gemini", | ||
198 | }); err != nil { | ||
199 | log.Println("Template error: " + err.Error()) | ||
200 | } | ||
201 | } else { | ||
202 | io.Copy(w, res.Body) | ||
203 | } | ||
204 | } | ||
205 | } | ||