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/>.
28 "github.com/dustin/go-humanize"
29 "go.stargrave.org/tofuproxy/caches"
30 "go.stargrave.org/tofuproxy/fifos"
31 "go.stargrave.org/tofuproxy/rounds"
32 ttls "go.stargrave.org/tofuproxy/tls"
36 transport = http.Transport{
37 DialContext: (&net.Dialer{
39 KeepAlive: time.Minute,
41 MaxIdleConns: http.DefaultTransport.(*http.Transport).MaxIdleConns,
42 IdleConnTimeout: http.DefaultTransport.(*http.Transport).IdleConnTimeout * 2,
43 TLSHandshakeTimeout: time.Minute,
44 DialTLSContext: ttls.DialTLS,
45 ForceAttemptHTTP2: true,
47 proxyHeaders = map[string]struct{}{
57 w http.ResponseWriter,
61 func roundTrip(w http.ResponseWriter, req *http.Request) {
62 defer req.Body.Close()
63 fifos.LogReq <- fmt.Sprintf("%s %s", req.Method, req.URL)
64 host := strings.TrimSuffix(req.URL.Host, ":443")
65 for _, round := range []Round{
69 rounds.RoundRedditOld,
70 rounds.RoundHabrImage,
72 if cont, _ := round(host, nil, w, req); !cont {
77 reqFlags := []string{}
80 caches.HTTPAuthCacheM.RLock()
81 if creds, ok := caches.HTTPAuthCache[req.URL.Host]; ok {
82 req.SetBasicAuth(creds[0], creds[1])
85 caches.HTTPAuthCacheM.RUnlock()
88 resp, err := transport.RoundTrip(req)
90 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
91 http.Error(w, err.Error(), http.StatusBadGateway)
95 if resp.StatusCode == http.StatusUnauthorized {
97 caches.HTTPAuthCacheM.Lock()
99 delete(caches.HTTPAuthCache, req.URL.Host)
103 fifos.LogVarious <- fmt.Sprintf(
104 "%s %s\tHTTP authorization required", req.Method, req.URL.Host,
106 user, pass, err := authDialog(host, resp.Header.Get("WWW-Authenticate"))
108 caches.HTTPAuthCacheM.Unlock()
109 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
110 http.Error(w, err.Error(), http.StatusInternalServerError)
113 caches.HTTPAuthCache[req.URL.Host] = [2]string{user, pass}
114 caches.HTTPAuthCacheM.Unlock()
115 req.SetBasicAuth(user, pass)
116 fifos.LogHTTPAuth <- fmt.Sprintf("%s %s\t%s", req.Method, req.URL, user)
120 reqFlags = append(reqFlags, "auth")
122 if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" {
123 reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol)
126 for k, vs := range resp.Header {
127 if _, ok := proxyHeaders[k]; ok {
130 for _, v := range vs {
135 for _, round := range []Round{
136 rounds.RoundDenyFonts,
137 rounds.RoundTranscodeWebP,
138 rounds.RoundTranscodeJXL,
139 rounds.RoundTranscodeAVIF,
140 rounds.RoundRedirectHTML,
142 cont, err := round(host, resp, w, req)
144 http.Error(w, err.Error(), http.StatusBadGateway)
152 for h := range proxyHeaders {
153 if v := resp.Header.Get(h); v != "" {
157 w.WriteHeader(resp.StatusCode)
158 n, err := io.Copy(w, resp.Body)
160 log.Printf("Error during %s: %+v\n", req.URL, err)
164 "%s %s\t%s\t%s\t%s\t%s",
167 resp.Header.Get("Content-Type"),
168 humanize.IBytes(uint64(n)),
169 strings.Join(reqFlags, ","),
171 if resp.StatusCode == http.StatusOK {
174 fifos.LogNonOK <- msg