1 // tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU
2 // manager, WARC/geminispace browser
3 // Copyright (C) 2021-2024 Sergey Matveev <stargrave@stargrave.org>
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, version 3 of the License.
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
31 "go.stargrave.org/tofuproxy/fifos"
32 ttls "go.stargrave.org/tofuproxy/tls"
36 ContentTypeGemini = "text/gemini"
37 SchemeGemini = "gemini://"
38 GeminiEntrypoint = "https://gemini"
42 var GemCodeName = map[int]string{
44 11: "SENSITIVE INPUT",
46 30: "REDIRECT - TEMPORARY",
47 31: "REDIRECT - PERMANENT",
48 40: "TEMPORARY FAILURE",
49 41: "SERVER UNAVAILABLE",
53 50: "PERMANENT FAILURE",
56 53: "PROXY REQUEST REFUSED",
58 60: "CLIENT CERTIFICATE REQUIRED",
59 61: "CERTIFICATE NOT AUTHORISED",
60 62: "CERTIFICATE NOT VALID",
63 func absolutizeURL(host, u string, paths ...string) string {
64 host = strings.TrimSuffix(host, GeminiPort)
65 if strings.Contains(u, "://") {
68 if strings.HasPrefix(u, "/") {
69 return GeminiEntrypoint + "/" + host + u
71 paths = append([]string{GeminiEntrypoint, host}, paths...)
72 paths = append(paths, u)
73 return strings.Join(paths, "/")
76 func geminifyURL(host, u string, paths ...string) string {
77 u = absolutizeURL(host, u, paths...)
78 if !strings.HasPrefix(u, SchemeGemini) {
81 return GeminiEntrypoint + "/" + strings.TrimPrefix(u, SchemeGemini)
87 w http.ResponseWriter,
93 paths := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/")
94 host, paths = paths[0], paths[1:]
95 if host == "gemini:" {
96 http.Redirect(w, req, strings.Join(
97 append([]string{GeminiEntrypoint}, paths[1:]...), "/",
98 ), http.StatusTemporaryRedirect)
102 if !strings.Contains(hostWithPort, ":") {
103 hostWithPort += GeminiPort
105 conn, err := ttls.DialTLS(context.TODO(), "tcp", hostWithPort)
107 log.Printf("%s: can not dial: %+v\n", req.URL, err)
110 query := fmt.Sprintf("%s%s/%s", SchemeGemini, host, strings.Join(paths, "/"))
111 if req.URL.RawQuery != "" {
112 query += "?" + req.URL.RawQuery
114 if _, err = conn.Write([]byte(query + "\r\n")); err != nil {
115 log.Printf("%s: can not send request: %+v\n", req.URL, err)
118 if len(paths) > 0 && paths[len(paths)-1] == "" {
119 paths = paths[:len(paths)-1]
121 br := bufio.NewReader(conn)
122 rawResp, err := br.ReadString('\n')
124 log.Printf("%s: can not read response: %+v\n", req.URL, err)
127 cols := strings.SplitN(strings.TrimRight(rawResp, "\r\n"), " ", 2)
129 err = fmt.Errorf("invalid response format: %s", rawResp)
130 log.Printf("%s: %s\n", req.URL, err)
133 code, err := strconv.Atoi(cols[0])
135 log.Printf("%s: can not parse response code: %+v\n", req.URL, err)
138 codeName := GemCodeName[code]
142 if 10 <= code && code <= 19 {
143 w.Header().Add("Content-Type", "text/plain")
144 w.WriteHeader(http.StatusBadRequest)
145 fmt.Fprintf(w, "%s\n%d (%s): INPUT is not supported\n", cols[1], code, codeName)
148 if 30 <= code && code <= 39 {
149 w.Header().Add("Content-Type", "text/html")
150 w.WriteHeader(http.StatusOK)
151 u := geminifyURL(host, cols[1], paths...)
152 fmt.Fprintf(w, `<!DOCTYPE html>
153 <html><head><title>%d (%s) redirection</title></head>
154 <body><a href="%s">%s</a></body></html>`, code, codeName, u, u)
155 fifos.LogRedir <- fmt.Sprintf(
156 "%s %s\t%d\t%s", req.Method, req.URL, code, cols[1],
161 "%s %s\t%d (%s)\t%s",
166 if 40 <= code && code <= 49 {
167 w.Header().Add("Content-Type", "text/plain")
168 w.WriteHeader(http.StatusBadGateway)
169 fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
170 fifos.LogNonOK <- msg
173 if 50 <= code && code <= 59 {
174 w.Header().Add("Content-Type", "text/plain")
175 w.WriteHeader(http.StatusBadGateway)
176 fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
177 fifos.LogNonOK <- msg
180 if 60 <= code && code <= 69 {
181 w.Header().Add("Content-Type", "text/plain")
182 w.WriteHeader(http.StatusUnauthorized)
183 fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
184 fifos.LogNonOK <- msg
187 if !(20 <= code && code <= 29) {
188 err = fmt.Errorf("unknown response code: %d", code)
189 log.Printf("%s: %s\n", req.URL, err)
190 fifos.LogNonOK <- msg
193 contentType := strings.Split(strings.TrimRight(cols[1], "\r\n"), ";")[0]
194 if contentType == ContentTypeGemini &&
195 !strings.Contains(req.Header.Get("Accept"), ContentTypeGemini) {
196 w.Header().Add("Content-Type", "text/html")
197 w.WriteHeader(http.StatusOK)
198 raw, err := io.ReadAll(br)
200 log.Printf("%s: can not read response body: %+v\n", req.URL, err)
204 fmt.Fprintf(&buf, `<!DOCTYPE html>
205 <html><head><title>%d (%s)</title></head><body>
208 for _, line := range strings.Split(string(raw), "\n") {
209 if strings.HasPrefix(line, "```") {
211 buf.WriteString("</pre>\n")
213 buf.WriteString("<pre>" + line[3:] + "\n")
219 fmt.Fprintf(&buf, "%s\n", line)
222 if strings.HasPrefix(line, "=>") {
223 line = strings.TrimLeft(line[2:], " ")
224 cols = strings.Fields(line)
225 u1 := geminifyURL(host, cols[0], paths...)
226 u2 := geminifyURL(host, cols[0])
230 &buf, "<a href=\"%s\">%s</a> <a href=\"%s\">[2]</a><br/>\n",
231 u1, html.EscapeString(cols[0]), u2,
235 &buf, "<a href=\"%s\">%s</a> <a href=\"%s\">[2]</a>(<tt>%s</tt>)<br/>\n",
236 u1, html.EscapeString(strings.Join(cols[1:], " ")), u2, cols[0],
241 if strings.HasPrefix(line, "# ") {
242 fmt.Fprintf(&buf, "<h1>%s</h1>\n", html.EscapeString(line[2:]))
245 if strings.HasPrefix(line, "## ") {
246 fmt.Fprintf(&buf, "<h2>%s</h2>\n", html.EscapeString(line[3:]))
249 if strings.HasPrefix(line, "### ") {
250 fmt.Fprintf(&buf, "<h3>%s</h3>\n", html.EscapeString(line[4:]))
253 if strings.HasPrefix(line, "> ") {
255 &buf, "<blockquote><tt>%s</tt></blockquote>\n",
256 html.EscapeString(line[2:]),
260 fmt.Fprintf(&buf, "%s<br/>\n", html.EscapeString(line))
262 buf.WriteString("</body></html>\n")
263 _, err = w.Write(buf.Bytes())
267 w.Header().Add("Content-Type", contentType)
268 w.WriteHeader(http.StatusOK)
269 _, err = io.Copy(w, br)
271 log.Printf("%s: can not read response body: %+v\n", req.URL, err)