2 tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU
3 manager, WARC/geminispace browser
4 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, version 3 of the License.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
33 "go.stargrave.org/tofuproxy/fifos"
34 ttls "go.stargrave.org/tofuproxy/tls"
38 ContentTypeGemini = "text/gemini"
39 SchemeGemini = "gemini://"
40 GeminiEntrypoint = "https://gemini"
44 var GemCodeName = map[int]string{
46 11: "SENSITIVE INPUT",
48 30: "REDIRECT - TEMPORARY",
49 31: "REDIRECT - PERMANENT",
50 40: "TEMPORARY FAILURE",
51 41: "SERVER UNAVAILABLE",
55 50: "PERMANENT FAILURE",
58 53: "PROXY REQUEST REFUSED",
60 60: "CLIENT CERTIFICATE REQUIRED",
61 61: "CERTIFICATE NOT AUTHORISED",
62 62: "CERTIFICATE NOT VALID",
65 func absolutizeURL(host, u string, paths ...string) string {
66 host = strings.TrimSuffix(host, GeminiPort)
67 if strings.Contains(u, "://") {
70 if strings.HasPrefix(u, "/") {
71 return GeminiEntrypoint + "/" + host + u
73 paths = append([]string{GeminiEntrypoint, host}, paths...)
74 paths = append(paths, u)
75 return strings.Join(paths, "/")
78 func geminifyURL(host, u string, paths ...string) string {
79 u = absolutizeURL(host, u, paths...)
80 if !strings.HasPrefix(u, SchemeGemini) {
83 return GeminiEntrypoint + "/" + strings.TrimPrefix(u, SchemeGemini)
89 w http.ResponseWriter,
95 paths := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/")
96 host, paths = paths[0], paths[1:]
97 if host == "gemini:" {
98 http.Redirect(w, req, strings.Join(
99 append([]string{GeminiEntrypoint}, paths[1:]...), "/",
100 ), http.StatusTemporaryRedirect)
104 if !strings.Contains(hostWithPort, ":") {
105 hostWithPort += GeminiPort
107 conn, err := ttls.DialTLS(context.TODO(), "tcp", hostWithPort)
109 log.Printf("%s: can not dial: %+v\n", req.URL, err)
112 query := fmt.Sprintf("%s%s/%s", SchemeGemini, host, strings.Join(paths, "/"))
113 if req.URL.RawQuery != "" {
114 query += "?" + req.URL.RawQuery
116 if _, err = conn.Write([]byte(query + "\r\n")); err != nil {
117 log.Printf("%s: can not send request: %+v\n", req.URL, err)
120 if len(paths) > 0 && paths[len(paths)-1] == "" {
121 paths = paths[:len(paths)-1]
123 br := bufio.NewReader(conn)
124 rawResp, err := br.ReadString('\n')
126 log.Printf("%s: can not read response: %+v\n", req.URL, err)
129 cols := strings.SplitN(rawResp, " ", 2)
131 err = fmt.Errorf("invalid response format: %s", rawResp)
132 log.Printf("%s: %s\n", req.URL, err)
135 code, err := strconv.Atoi(cols[0])
137 log.Printf("%s: can not parse response code: %+v\n", req.URL, err)
140 codeName := GemCodeName[code]
144 if 10 <= code && code <= 19 {
145 w.Header().Add("Content-Type", "text/plain")
146 w.WriteHeader(http.StatusBadRequest)
147 fmt.Fprintf(w, "%s\n%d (%s): INPUT is not supported\n", cols[1], code, codeName)
150 if 30 <= code && code <= 39 {
151 w.Header().Add("Content-Type", "text/html")
152 w.WriteHeader(http.StatusOK)
153 u := geminifyURL(host, cols[1], paths...)
157 <html><head><title>%d (%s) redirection</title></head>
158 <body><a href="%s">%s</a></body></html>`,
159 code, codeName, u, u,
161 fifos.LogRedir <- fmt.Sprintf(
162 "%s %s\t%d\t%s", req.Method, req.URL, code, cols[1],
167 "%s %s\t%d (%s)\t%s",
172 if 40 <= code && code <= 49 {
173 w.Header().Add("Content-Type", "text/plain")
174 w.WriteHeader(http.StatusBadGateway)
175 fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
176 fifos.LogNonOK <- msg
179 if 50 <= code && code <= 59 {
180 w.Header().Add("Content-Type", "text/plain")
181 w.WriteHeader(http.StatusBadGateway)
182 fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
183 fifos.LogNonOK <- msg
186 if 60 <= code && code <= 69 {
187 w.Header().Add("Content-Type", "text/plain")
188 w.WriteHeader(http.StatusUnauthorized)
189 fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
190 fifos.LogNonOK <- msg
193 if !(20 <= code && code <= 29) {
194 err = fmt.Errorf("unknown response code: %d", code)
195 log.Printf("%s: %s\n", req.URL, err)
196 fifos.LogNonOK <- msg
199 contentType := strings.Split(strings.TrimRight(cols[1], "\r\n"), ";")[0]
200 if contentType == ContentTypeGemini &&
201 !strings.Contains(req.Header.Get("Accept"), ContentTypeGemini) {
202 w.Header().Add("Content-Type", "text/html")
203 w.WriteHeader(http.StatusOK)
204 raw, err := io.ReadAll(br)
206 log.Printf("%s: can not read response body: %+v\n", req.URL, err)
210 fmt.Fprintf(&buf, `<!DOCTYPE html>
211 <html><head><title>%d (%s)</title></head><body>
214 for _, line := range strings.Split(string(raw), "\n") {
215 if strings.HasPrefix(line, "```") {
217 buf.WriteString("</pre>\n")
219 buf.WriteString("<pre>" + line[3:] + "\n")
225 fmt.Fprintf(&buf, "%s\n", line)
228 if strings.HasPrefix(line, "=> ") {
229 cols = strings.Fields(line)
230 u := geminifyURL(host, cols[1], paths...)
234 &buf, "<a href=\"%s\">%s</a><br/>\n",
235 u, html.EscapeString(cols[1]),
239 &buf, "<a href=\"%s\">%s</a> (<tt>%s</tt>)<br/>\n",
240 u, html.EscapeString(strings.Join(cols[2:], " ")), cols[1],
245 if strings.HasPrefix(line, "# ") {
246 fmt.Fprintf(&buf, "<h1>%s</h1>\n", html.EscapeString(line[2:]))
249 if strings.HasPrefix(line, "## ") {
250 fmt.Fprintf(&buf, "<h2>%s</h2>\n", html.EscapeString(line[3:]))
253 if strings.HasPrefix(line, "### ") {
254 fmt.Fprintf(&buf, "<h3>%s</h3>\n", html.EscapeString(line[4:]))
257 if strings.HasPrefix(line, "* ") {
258 fmt.Fprintf(&buf, "• %s\n", html.EscapeString(line[2:]))
261 if strings.HasPrefix(line, "> ") {
263 &buf, "<blockquote><tt>%s</tt></blockquote>\n",
264 html.EscapeString(line[2:]),
268 fmt.Fprintf(&buf, "%s<br/>\n", html.EscapeString(line))
270 buf.WriteString("</body></html>\n")
271 _, err = w.Write(buf.Bytes())
275 w.Header().Add("Content-Type", contentType)
276 w.WriteHeader(http.StatusOK)
277 _, err = io.Copy(w, br)
279 log.Printf("%s: can not read response body: %+v\n", req.URL, err)