// tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU // manager, WARC/geminispace browser // Copyright (C) 2021-2024 Sergey Matveev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package tofuproxy import ( "fmt" "io" "log" "net" "net/http" "strings" "time" "github.com/dustin/go-humanize" "go.stargrave.org/tofuproxy/caches" "go.stargrave.org/tofuproxy/fifos" "go.stargrave.org/tofuproxy/rounds" ttls "go.stargrave.org/tofuproxy/tls" ) var ( transport = http.Transport{ DialContext: (&net.Dialer{ Timeout: time.Minute, KeepAlive: time.Minute, }).DialContext, MaxIdleConns: http.DefaultTransport.(*http.Transport).MaxIdleConns, IdleConnTimeout: http.DefaultTransport.(*http.Transport).IdleConnTimeout * 2, TLSHandshakeTimeout: time.Minute, DialTLSContext: ttls.DialTLS, ForceAttemptHTTP2: true, } proxyHeaders = map[string]struct{}{ "Location": {}, "Content-Type": {}, "Content-Length": {}, } ) type Round func( host string, resp *http.Response, w http.ResponseWriter, req *http.Request, ) (bool, error) func roundTrip(w http.ResponseWriter, req *http.Request) { defer req.Body.Close() fifos.LogReq <- fmt.Sprintf("%s %s", req.Method, req.URL) host := strings.TrimSuffix(req.URL.Host, ":443") for _, round := range []Round{ rounds.RoundGemini, rounds.RoundWARC, rounds.RoundDenySpy, rounds.RoundRedditOld, rounds.RoundHabrImage, } { if cont, _ := round(host, nil, w, req); !cont { return } } reqFlags := []string{} unauthorized := false caches.HTTPAuthCacheM.RLock() if creds, ok := caches.HTTPAuthCache[req.URL.Host]; ok { req.SetBasicAuth(creds[0], creds[1]) unauthorized = true } caches.HTTPAuthCacheM.RUnlock() Retry: resp, err := transport.RoundTrip(req) if err != nil { fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error()) http.Error(w, err.Error(), http.StatusBadGateway) return } if resp.StatusCode == http.StatusUnauthorized { resp.Body.Close() caches.HTTPAuthCacheM.Lock() if unauthorized { delete(caches.HTTPAuthCache, req.URL.Host) } else { unauthorized = true } fifos.LogVarious <- fmt.Sprintf( "%s %s\tHTTP authorization required", req.Method, req.URL.Host, ) user, pass, err := authDialog(host, resp.Header.Get("WWW-Authenticate")) if err != nil { caches.HTTPAuthCacheM.Unlock() fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } caches.HTTPAuthCache[req.URL.Host] = [2]string{user, pass} caches.HTTPAuthCacheM.Unlock() req.SetBasicAuth(user, pass) fifos.LogHTTPAuth <- fmt.Sprintf("%s %s\t%s", req.Method, req.URL, user) goto Retry } if unauthorized { reqFlags = append(reqFlags, "auth") } if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" { reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol) } for k, vs := range resp.Header { if _, ok := proxyHeaders[k]; ok { continue } for _, v := range vs { w.Header().Add(k, v) } } for _, round := range []Round{ rounds.RoundDenyFonts, rounds.RoundTranscodeWebP, rounds.RoundTranscodeJXL, rounds.RoundTranscodeAVIF, rounds.RoundRedirectHTML, } { cont, err := round(host, resp, w, req) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } if !cont { return } } for h := range proxyHeaders { if v := resp.Header.Get(h); v != "" { w.Header().Add(h, v) } } w.WriteHeader(resp.StatusCode) n, err := io.Copy(w, resp.Body) if err != nil { log.Printf("Error during %s: %+v\n", req.URL, err) } resp.Body.Close() msg := fmt.Sprintf( "%s %s\t%s\t%s\t%s\t%s", req.Method, req.URL, resp.Status, resp.Header.Get("Content-Type"), humanize.IBytes(uint64(n)), strings.Join(reqFlags, ","), ) if resp.StatusCode == http.StatusOK { fifos.LogOK <- msg } else { fifos.LogNonOK <- msg } }