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{
71 rounds.RoundRedditOld,
72 rounds.RoundHabrImage,
74 if cont, _ := round(host, nil, w, req); !cont {
79 reqFlags := []string{}
82 caches.HTTPAuthCacheM.RLock()
83 if creds, ok := caches.HTTPAuthCache[req.URL.Host]; ok {
84 req.SetBasicAuth(creds[0], creds[1])
87 caches.HTTPAuthCacheM.RUnlock()
90 resp, err := transport.RoundTrip(req)
92 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
93 http.Error(w, err.Error(), http.StatusBadGateway)
97 if resp.StatusCode == http.StatusUnauthorized {
99 caches.HTTPAuthCacheM.Lock()
101 delete(caches.HTTPAuthCache, req.URL.Host)
105 fifos.LogVarious <- fmt.Sprintf(
106 "%s %s\tHTTP authorization required", req.Method, req.URL.Host,
108 user, pass, err := authDialog(host, resp.Header.Get("WWW-Authenticate"))
110 caches.HTTPAuthCacheM.Unlock()
111 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
112 http.Error(w, err.Error(), http.StatusInternalServerError)
115 caches.HTTPAuthCache[req.URL.Host] = [2]string{user, pass}
116 caches.HTTPAuthCacheM.Unlock()
117 req.SetBasicAuth(user, pass)
118 fifos.LogHTTPAuth <- fmt.Sprintf("%s %s\t%s", req.Method, req.URL, user)
122 reqFlags = append(reqFlags, "auth")
124 if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" {
125 reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol)
128 for k, vs := range resp.Header {
129 if _, ok := proxyHeaders[k]; ok {
132 for _, v := range vs {
137 for _, round := range []Round{
138 rounds.RoundDenyFonts,
139 rounds.RoundTranscodeWebP,
140 rounds.RoundTranscodeJXL,
141 rounds.RoundTranscodeAVIF,
142 rounds.RoundRedirectHTML,
144 cont, err := round(host, resp, w, req)
146 http.Error(w, err.Error(), http.StatusBadGateway)
154 for h := range proxyHeaders {
155 if v := resp.Header.Get(h); v != "" {
159 w.WriteHeader(resp.StatusCode)
160 n, err := io.Copy(w, resp.Body)
162 log.Printf("Error during %s: %+v\n", req.URL, err)
166 "%s %s\t%s\t%s\t%s\t%s",
169 resp.Header.Get("Content-Type"),
170 humanize.IBytes(uint64(n)),
171 strings.Join(reqFlags, ","),
173 if resp.StatusCode == http.StatusOK {
176 fifos.LogNonOK <- msg