]> Sergey Matveev's repositories - tofuproxy.git/blob - trip.go
External netrc module
[tofuproxy.git] / trip.go
1 /*
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>
5
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.
9
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.
14
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/>.
17 */
18
19 package tofuproxy
20
21 import (
22         "fmt"
23         "io"
24         "log"
25         "net"
26         "net/http"
27         "strings"
28         "time"
29
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"
35 )
36
37 var (
38         transport = http.Transport{
39                 DialContext: (&net.Dialer{
40                         Timeout:   time.Minute,
41                         KeepAlive: time.Minute,
42                 }).DialContext,
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,
48         }
49         proxyHeaders = map[string]struct{}{
50                 "Location":       {},
51                 "Content-Type":   {},
52                 "Content-Length": {},
53         }
54 )
55
56 type Round func(
57         host string,
58         resp *http.Response,
59         w http.ResponseWriter,
60         req *http.Request,
61 ) (bool, error)
62
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{
68                 rounds.RoundGemini,
69                 rounds.RoundWARC,
70                 rounds.RoundDenySpy,
71                 rounds.RoundRedditOld,
72                 rounds.RoundHabrImage,
73         } {
74                 if cont, _ := round(host, nil, w, req); !cont {
75                         return
76                 }
77         }
78
79         reqFlags := []string{}
80         unauthorized := false
81
82         caches.HTTPAuthCacheM.RLock()
83         if creds, ok := caches.HTTPAuthCache[req.URL.Host]; ok {
84                 req.SetBasicAuth(creds[0], creds[1])
85                 unauthorized = true
86         }
87         caches.HTTPAuthCacheM.RUnlock()
88
89 Retry:
90         resp, err := transport.RoundTrip(req)
91         if err != nil {
92                 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
93                 http.Error(w, err.Error(), http.StatusBadGateway)
94                 return
95         }
96
97         if resp.StatusCode == http.StatusUnauthorized {
98                 resp.Body.Close()
99                 caches.HTTPAuthCacheM.Lock()
100                 if unauthorized {
101                         delete(caches.HTTPAuthCache, req.URL.Host)
102                 } else {
103                         unauthorized = true
104                 }
105                 fifos.LogVarious <- fmt.Sprintf(
106                         "%s %s\tHTTP authorization required", req.Method, req.URL.Host,
107                 )
108                 user, pass, err := authDialog(host, resp.Header.Get("WWW-Authenticate"))
109                 if err != nil {
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)
113                         return
114                 }
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)
119                 goto Retry
120         }
121         if unauthorized {
122                 reqFlags = append(reqFlags, "auth")
123         }
124         if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" {
125                 reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol)
126         }
127
128         for k, vs := range resp.Header {
129                 if _, ok := proxyHeaders[k]; ok {
130                         continue
131                 }
132                 for _, v := range vs {
133                         w.Header().Add(k, v)
134                 }
135         }
136
137         for _, round := range []Round{
138                 rounds.RoundDenyFonts,
139                 rounds.RoundTranscodeWebP,
140                 rounds.RoundTranscodeJXL,
141                 rounds.RoundTranscodeAVIF,
142                 rounds.RoundRedirectHTML,
143         } {
144                 cont, err := round(host, resp, w, req)
145                 if err != nil {
146                         http.Error(w, err.Error(), http.StatusBadGateway)
147                         return
148                 }
149                 if !cont {
150                         return
151                 }
152         }
153
154         for h := range proxyHeaders {
155                 if v := resp.Header.Get(h); v != "" {
156                         w.Header().Add(h, v)
157                 }
158         }
159         w.WriteHeader(resp.StatusCode)
160         n, err := io.Copy(w, resp.Body)
161         if err != nil {
162                 log.Printf("Error during %s: %+v\n", req.URL, err)
163         }
164         resp.Body.Close()
165         msg := fmt.Sprintf(
166                 "%s %s\t%s\t%s\t%s\t%s",
167                 req.Method, req.URL,
168                 resp.Status,
169                 resp.Header.Get("Content-Type"),
170                 humanize.IBytes(uint64(n)),
171                 strings.Join(reqFlags, ","),
172         )
173         if resp.StatusCode == http.StatusOK {
174                 fifos.LogOK <- msg
175         } else {
176                 fifos.LogNonOK <- msg
177         }
178 }