2 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, version 3 of the License.
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 GNU General Public License for more details.
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>.
37 "github.com/dustin/go-humanize"
38 "go.cypherpunks.ru/ucspi"
42 tlsNextProtoS = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
43 caCert *x509.Certificate
44 caPrv crypto.PrivateKey
45 transport = http.Transport{
46 DialTLSContext: dialTLS,
47 ForceAttemptHTTP2: true,
49 sessionCache = tls.NewLRUClientSessionCache(1024)
54 imageExts = map[string]struct{}{
72 func dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
73 host := strings.TrimSuffix(addr, ":443")
75 VerifyPeerCertificate: func(
77 verifiedChains [][]*x509.Certificate,
79 return verifyCert(host, nil, rawCerts, verifiedChains)
81 ClientSessionCache: sessionCache,
82 NextProtos: []string{"h2", "http/1.1"},
84 conn, dialErr := tls.Dial(network, addr, &cfg)
86 if _, ok := dialErr.(ErrRejected); ok {
89 cfg.InsecureSkipVerify = true
90 cfg.VerifyPeerCertificate = func(
92 verifiedChains [][]*x509.Certificate,
94 return verifyCert(host, dialErr, rawCerts, verifiedChains)
97 conn, err = tls.Dial(network, addr, &cfg)
99 sinkErr <- fmt.Sprintf("%s\t%s", addr, dialErr.Error())
103 connState := conn.ConnectionState()
104 if connState.DidResume {
105 sinkTLS <- fmt.Sprintf(
107 strings.TrimSuffix(addr, ":443"),
108 ucspi.TLSVersion(connState.Version),
109 tls.CipherSuiteName(connState.CipherSuite),
110 spkiHash(connState.PeerCertificates[0]),
111 connState.NegotiatedProtocol,
117 func roundTrip(w http.ResponseWriter, req *http.Request) {
118 if req.Method == http.MethodHead {
119 http.Error(w, "go away", http.StatusMethodNotAllowed)
122 sinkReq <- fmt.Sprintf("%s %s", req.Method, req.URL.String())
123 host := strings.TrimSuffix(req.URL.Host, ":443")
125 for _, spy := range SpyDomains {
126 if strings.HasSuffix(host, spy) {
127 http.NotFound(w, req)
128 sinkOther <- fmt.Sprintf(
129 "%s %s\t%d\tspy one",
138 if host == "www.reddit.com" {
139 req.URL.Host = "old.reddit.com"
140 http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently)
144 if host == "habrastorage.org" &&
145 strings.Contains(req.URL.Path, "/webt/") &&
146 !strings.HasPrefix(req.URL.Path, "/webt/") {
147 req.URL.Path = req.URL.Path[strings.Index(req.URL.Path, "/webt/"):]
148 http.Redirect(w, req, req.URL.String(), http.StatusFound)
152 resp, err := transport.RoundTrip(req)
154 sinkErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
155 w.WriteHeader(http.StatusBadGateway)
156 w.Write([]byte(err.Error()))
160 for k, vs := range resp.Header {
161 if k == "Location" || k == "Content-Type" || k == "Content-Length" {
164 for _, v := range vs {
169 switch resp.Header.Get("Content-Type") {
170 case "application/font-woff", "application/font-sfnt":
171 // Those are deprecated types
173 case "font/otf", "font/ttf", "font/woff", "font/woff2":
174 http.NotFound(w, req)
175 sinkOther <- fmt.Sprintf(
176 "%s %s\t%d\tfonts are not allowed",
184 if strings.Contains(req.Header.Get("User-Agent"), "AppleWebKit/538.15") {
188 tmpFd, err := ioutil.TempFile("", "tofuproxy.*.webp")
193 defer os.Remove(tmpFd.Name())
194 defer resp.Body.Close()
195 if _, err = io.Copy(tmpFd, resp.Body); err != nil {
196 log.Printf("Error during %s: %+v\n", req.URL, err)
197 http.Error(w, err.Error(), http.StatusBadGateway)
201 cmd := exec.Command(CmdDWebP, tmpFd.Name(), "-o", "-")
202 data, err := cmd.Output()
204 http.Error(w, err.Error(), http.StatusBadGateway)
207 w.Header().Add("Content-Type", "image/png")
208 w.WriteHeader(http.StatusOK)
210 sinkOther <- fmt.Sprintf(
211 "%s %s\t%d\tWebP transcoded to PNG",
218 tmpFd, err := ioutil.TempFile("", "tofuproxy.*.jxl")
223 defer os.Remove(tmpFd.Name())
224 defer resp.Body.Close()
225 if _, err = io.Copy(tmpFd, resp.Body); err != nil {
226 log.Printf("Error during %s: %+v\n", req.URL, err)
227 http.Error(w, err.Error(), http.StatusBadGateway)
231 dstFn := tmpFd.Name() + ".png"
232 cmd := exec.Command(CmdDJXL, tmpFd.Name(), dstFn)
234 defer os.Remove(dstFn)
236 http.Error(w, err.Error(), http.StatusBadGateway)
239 data, err := ioutil.ReadFile(dstFn)
241 http.Error(w, err.Error(), http.StatusBadGateway)
244 w.Header().Add("Content-Type", "image/png")
245 w.WriteHeader(http.StatusOK)
247 sinkOther <- fmt.Sprintf(
248 "%s %s\t%d\tJPEG XL transcoded to PNG",
256 if req.Method == http.MethodGet {
258 switch resp.StatusCode {
259 case http.StatusMovedPermanently, http.StatusPermanentRedirect:
260 redirType = "permanent"
262 case http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect:
263 if strings.Contains(req.Header.Get("User-Agent"), "newsboat/") {
266 if _, ok := imageExts[filepath.Ext(req.URL.Path)]; ok {
269 redirType = "temporary"
275 w.Header().Add("Content-Type", "text/html")
276 w.WriteHeader(http.StatusOK)
277 location := resp.Header.Get("Location")
280 `<html><head><title>%d %s: %s redirection</title></head>
281 <body>Redirection to <a href="%s">%s</a></body></html>`,
282 resp.StatusCode, http.StatusText(resp.StatusCode),
283 redirType, location, location,
285 sinkRedir <- fmt.Sprintf(
286 "%s %s\t%s\t%s", req.Method, resp.Status, req.URL.String(), location,
292 for _, h := range []string{"Location", "Content-Type", "Content-Length"} {
293 if v := resp.Header.Get(h); v != "" {
297 w.WriteHeader(resp.StatusCode)
298 n, err := io.Copy(w, resp.Body)
300 log.Printf("Error during %s: %+v\n", req.URL, err)
308 resp.Header.Get("Content-Type"),
309 humanize.IBytes(uint64(n)),
311 if resp.StatusCode == http.StatusOK {
318 type Handler struct{}
320 func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
321 if req.Method != http.MethodConnect {
325 hj, ok := w.(http.Hijacker)
327 log.Fatalln("no hijacking")
329 conn, _, err := hj.Hijack()
334 conn.Write([]byte(fmt.Sprintf(
337 http.StatusOK, http.StatusText(http.StatusOK),
339 host := strings.Split(req.Host, ":")[0]
341 keypair, ok := hostCerts[host]
342 if !ok || !keypair.cert.NotAfter.After(time.Now().Add(time.Hour)) {
343 keypair = newKeypair(host, caCert, caPrv)
344 hostCerts[host] = keypair
347 tlsConn := tls.Server(conn, &tls.Config{
348 Certificates: []tls.Certificate{{
349 Certificate: [][]byte{keypair.cert.Raw},
350 PrivateKey: keypair.prv,
353 if err = tlsConn.Handshake(); err != nil {
354 log.Printf("TLS error %s: %+v\n", host, err)
358 Handler: &HTTPSHandler{host: req.Host},
359 TLSNextProto: tlsNextProtoS,
361 err = srv.Serve(&SingleListener{conn: tlsConn})
363 if _, ok := err.(AlreadyAccepted); !ok {
364 log.Printf("TLS serve error %s: %+v\n", host, err)
370 type HTTPSHandler struct {
374 func (h *HTTPSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
375 req.URL.Scheme = "https"
376 req.URL.Host = h.host
381 crtPath := flag.String("cert", "cert.pem", "Path to server X.509 certificate")
382 prvPath := flag.String("key", "prv.pem", "Path to server PKCS#8 private key")
383 bind := flag.String("bind", "[::1]:8080", "Bind address")
384 certs = flag.String("certs", "certs", "Directory with pinned certificates")
385 dnsSrv = flag.String("dns", "[::1]:53", "DNS server")
386 fifos = flag.String("fifos", "fifos", "Directory with FIFOs")
387 notai = flag.Bool("notai", false, "Do not prepend TAI64N to logs")
389 log.SetFlags(log.Lshortfile)
393 _, caCert, err = ucspi.CertificateFromFile(*crtPath)
397 caPrv, err = ucspi.PrivateKeyFromFile(*prvPath)
402 ln, err := net.Listen("tcp", *bind)
408 TLSNextProto: tlsNextProtoS,
410 log.Println("listening:", *bind)
411 if err := srv.Serve(ln); err != nil {