]> Sergey Matveev's repositories - tofuproxy.git/blob - trip.go
Raise copyright years
[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.RoundNoHead,
69                 rounds.RoundGemini,
70                 rounds.RoundWARC,
71                 rounds.RoundDenySpy,
72                 rounds.RoundRedditOld,
73                 rounds.RoundHabrImage,
74         } {
75                 if cont, _ := round(host, nil, w, req); !cont {
76                         return
77                 }
78         }
79
80         reqFlags := []string{}
81         unauthorized := false
82
83         caches.HTTPAuthCacheM.RLock()
84         if creds, ok := caches.HTTPAuthCache[req.URL.Host]; ok {
85                 req.SetBasicAuth(creds[0], creds[1])
86                 unauthorized = true
87         }
88         caches.HTTPAuthCacheM.RUnlock()
89
90 Retry:
91         resp, err := transport.RoundTrip(req)
92         if err != nil {
93                 fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
94                 http.Error(w, err.Error(), http.StatusBadGateway)
95                 return
96         }
97
98         if resp.StatusCode == http.StatusUnauthorized {
99                 resp.Body.Close()
100                 caches.HTTPAuthCacheM.Lock()
101                 if unauthorized {
102                         delete(caches.HTTPAuthCache, req.URL.Host)
103                 } else {
104                         unauthorized = true
105                 }
106                 fifos.LogVarious <- fmt.Sprintf(
107                         "%s %s\tHTTP authorization required", req.Method, req.URL.Host,
108                 )
109                 user, pass, err := authDialog(host, resp.Header.Get("WWW-Authenticate"))
110                 if err != nil {
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)
114                         return
115                 }
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)
120                 goto Retry
121         }
122         if unauthorized {
123                 reqFlags = append(reqFlags, "auth")
124         }
125         if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" {
126                 reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol)
127         }
128
129         for k, vs := range resp.Header {
130                 if _, ok := proxyHeaders[k]; ok {
131                         continue
132                 }
133                 for _, v := range vs {
134                         w.Header().Add(k, v)
135                 }
136         }
137
138         for _, round := range []Round{
139                 rounds.RoundDenyFonts,
140                 rounds.RoundTranscodeWebP,
141                 rounds.RoundTranscodeJXL,
142                 rounds.RoundTranscodeAVIF,
143                 rounds.RoundRedirectHTML,
144         } {
145                 cont, err := round(host, resp, w, req)
146                 if err != nil {
147                         http.Error(w, err.Error(), http.StatusBadGateway)
148                         return
149                 }
150                 if !cont {
151                         return
152                 }
153         }
154
155         for h := range proxyHeaders {
156                 if v := resp.Header.Get(h); v != "" {
157                         w.Header().Add(h, v)
158                 }
159         }
160         w.WriteHeader(resp.StatusCode)
161         n, err := io.Copy(w, resp.Body)
162         if err != nil {
163                 log.Printf("Error during %s: %+v\n", req.URL, err)
164         }
165         resp.Body.Close()
166         msg := fmt.Sprintf(
167                 "%s %s\t%s\t%s\t%s\t%s",
168                 req.Method, req.URL,
169                 resp.Status,
170                 resp.Header.Get("Content-Type"),
171                 humanize.IBytes(uint64(n)),
172                 strings.Join(reqFlags, ","),
173         )
174         if resp.StatusCode == http.StatusOK {
175                 fifos.LogOK <- msg
176         } else {
177                 fifos.LogNonOK <- msg
178         }
179 }