2 tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU
3 manager, WARC/geminispace browser
4 Copyright (C) 2021-2023 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/>.
30 "github.com/dustin/go-humanize"
31 "go.stargrave.org/tofuproxy/caches"
32 "go.stargrave.org/tofuproxy/fifos"
33 "go.stargrave.org/tofuproxy/rounds"
34 ttls "go.stargrave.org/tofuproxy/tls"
38 transport = http.Transport{
39 DialContext: (&net.Dialer{
41 KeepAlive: time.Minute,
43 MaxIdleConns: http.DefaultTransport.(*http.Transport).MaxIdleConns,
44 IdleConnTimeout: http.DefaultTransport.(*http.Transport).IdleConnTimeout * 2,
45 TLSHandshakeTimeout: time.Minute,
46 DialTLSContext: ttls.DialTLS,
47 ForceAttemptHTTP2: true,
49 proxyHeaders = map[string]struct{}{
59 w http.ResponseWriter,
63 func roundTrip(w http.ResponseWriter, req *http.Request) {
64 defer req.Body.Close()
65 fifos.LogReq <- fmt.Sprintf("%s %s", req.Method, req.URL)
66 host := strings.TrimSuffix(req.URL.Host, ":443")
67 for _, round := range []Round{
72 rounds.RoundRedditOld,
73 rounds.RoundHabrImage,
75 if cont, _ := round(host, nil, w, req); !cont {
80 reqFlags := []string{}
83 caches.HTTPAuthCacheM.RLock()
84 if creds, ok := caches.HTTPAuthCache[req.URL.Host]; ok {
85 req.SetBasicAuth(creds[0], creds[1])
88 caches.HTTPAuthCacheM.RUnlock()
91 resp, err := transport.RoundTrip(req)
93 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
94 http.Error(w, err.Error(), http.StatusBadGateway)
98 if resp.StatusCode == http.StatusUnauthorized {
100 caches.HTTPAuthCacheM.Lock()
102 delete(caches.HTTPAuthCache, req.URL.Host)
106 fifos.LogVarious <- fmt.Sprintf(
107 "%s %s\tHTTP authorization required", req.Method, req.URL.Host,
109 user, pass, err := authDialog(host, resp.Header.Get("WWW-Authenticate"))
111 caches.HTTPAuthCacheM.Unlock()
112 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
113 http.Error(w, err.Error(), http.StatusInternalServerError)
116 caches.HTTPAuthCache[req.URL.Host] = [2]string{user, pass}
117 caches.HTTPAuthCacheM.Unlock()
118 req.SetBasicAuth(user, pass)
119 fifos.LogHTTPAuth <- fmt.Sprintf("%s %s\t%s", req.Method, req.URL, user)
123 reqFlags = append(reqFlags, "auth")
125 if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" {
126 reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol)
129 for k, vs := range resp.Header {
130 if _, ok := proxyHeaders[k]; ok {
133 for _, v := range vs {
138 for _, round := range []Round{
139 rounds.RoundDenyFonts,
140 rounds.RoundTranscodeWebP,
141 rounds.RoundTranscodeJXL,
142 rounds.RoundTranscodeAVIF,
143 rounds.RoundRedirectHTML,
145 cont, err := round(host, resp, w, req)
147 http.Error(w, err.Error(), http.StatusBadGateway)
155 for h := range proxyHeaders {
156 if v := resp.Header.Get(h); v != "" {
160 w.WriteHeader(resp.StatusCode)
161 n, err := io.Copy(w, resp.Body)
163 log.Printf("Error during %s: %+v\n", req.URL, err)
167 "%s %s\t%s\t%s\t%s\t%s",
170 resp.Header.Get("Content-Type"),
171 humanize.IBytes(uint64(n)),
172 strings.Join(reqFlags, ","),
174 if resp.StatusCode == http.StatusOK {
177 fifos.LogNonOK <- msg