From 4473383b88399bbc5433a9292847d954087c8d61 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Tue, 7 Sep 2021 16:49:23 +0300 Subject: [PATCH] Refactoring --- .gitignore | 4 + README | 8 +- all.do | 2 + cmd/tofuproxy/main.go | 72 +++++ conn.go | 3 +- dane.go | 11 +- doc/index.texi | 88 +----- doc/usage.texi | 66 +++++ fifo.go | 70 ----- mkfifos.sh => fifos/ensure.do | 5 +- fifos/fifos.go | 72 +++++ multitail.sh => fifos/multitail.sh | 14 +- main.go | 412 ----------------------------- prv.pem.do | 3 +- rounds/05noHead.go | 40 +++ rounds/10log.go | 35 +++ rounds/15spy.go | 69 +++++ rounds/20reddit.go | 36 +++ rounds/25habrImage.go | 37 +++ rounds/35denyFonts.go | 49 ++++ rounds/40transcodeWebP.go | 70 +++++ rounds/45transcodeJXL.go | 76 ++++++ rounds/50redirectHTML.go | 89 +++++++ spy.go | 23 -- tls.go | 148 +++++++++++ tofuproxy.do | 3 + trip.go | 130 +++++++++ verify.go | 28 +- x509.go | 3 +- 29 files changed, 1052 insertions(+), 614 deletions(-) create mode 100644 .gitignore create mode 100644 all.do create mode 100644 cmd/tofuproxy/main.go create mode 100644 doc/usage.texi delete mode 100644 fifo.go rename mkfifos.sh => fifos/ensure.do (54%) mode change 100755 => 100644 create mode 100644 fifos/fifos.go rename multitail.sh => fifos/multitail.sh (55%) delete mode 100644 main.go create mode 100644 rounds/05noHead.go create mode 100644 rounds/10log.go create mode 100644 rounds/15spy.go create mode 100644 rounds/20reddit.go create mode 100644 rounds/25habrImage.go create mode 100644 rounds/35denyFonts.go create mode 100644 rounds/40transcodeWebP.go create mode 100644 rounds/45transcodeJXL.go create mode 100644 rounds/50redirectHTML.go delete mode 100644 spy.go create mode 100644 tls.go create mode 100644 tofuproxy.do create mode 100644 trip.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7902b80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/cert.pem +/certs +/prv.pem +/tofuproxy diff --git a/README b/README index c62d94d..7f9ebc5 100644 --- a/README +++ b/README @@ -1,2 +1,6 @@ -tofuproxy -- HTTP proxy, MitMing all HTTPS connections, taking all -TLS-related certificates trust management. +tofuproxy -- HTTP proxy with TLS certificates management + +Home page: http://www.tofuproxy.stargrave.org/ + +NNCP is copylefted free software: see the file COPYING for copying +conditions. It should work on all POSIX-compatible systems. diff --git a/all.do b/all.do new file mode 100644 index 0000000..7288dc0 --- /dev/null +++ b/all.do @@ -0,0 +1,2 @@ +redo-ifchange cert.pem tofuproxy fifos/ensure +mkdir -p certs diff --git a/cmd/tofuproxy/main.go b/cmd/tofuproxy/main.go new file mode 100644 index 0000000..5ad6e62 --- /dev/null +++ b/cmd/tofuproxy/main.go @@ -0,0 +1,72 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 main + +import ( + "flag" + "log" + "net" + "net/http" + + "go.cypherpunks.ru/ucspi" + "go.stargrave.org/tofuproxy" + "go.stargrave.org/tofuproxy/fifos" +) + +func main() { + crtPath := flag.String("cert", "cert.pem", "Path to server X.509 certificate") + prvPath := flag.String("key", "prv.pem", "Path to server PKCS#8 private key") + bind := flag.String("bind", "[::1]:8080", "Bind address") + certs := flag.String("certs", "certs", "Directory with pinned certificates") + dnsSrv := flag.String("dns", "[::1]:53", "DNS server") + fifosDir := flag.String("fifos", "fifos", "Directory with FIFOs") + notai := flag.Bool("notai", false, "Do not prepend TAI64N to logs") + flag.Parse() + log.SetFlags(log.Lshortfile) + + var err error + _, caCert, err := ucspi.CertificateFromFile(*crtPath) + if err != nil { + log.Fatalln(err) + } + caPrv, err := ucspi.PrivateKeyFromFile(*prvPath) + if err != nil { + log.Fatalln(err) + } + + fifos.NoTAI = *notai + fifos.FIFOs = *fifosDir + fifos.Init() + tofuproxy.Certs = *certs + tofuproxy.DNSSrv = *dnsSrv + tofuproxy.CACert = caCert + tofuproxy.CAPrv = caPrv + + ln, err := net.Listen("tcp", *bind) + if err != nil { + log.Fatalln(err) + } + srv := http.Server{ + Handler: &tofuproxy.Handler{}, + TLSNextProto: tofuproxy.TLSNextProtoS, + } + log.Println("listening:", *bind, "certs:", *certs) + if err := srv.Serve(ln); err != nil { + log.Fatalln(err) + } +} diff --git a/conn.go b/conn.go index fbb8a5c..baa7c8d 100644 --- a/conn.go +++ b/conn.go @@ -1,4 +1,5 @@ /* +tofuproxy -- HTTP proxy with TLS certificates management Copyright (C) 2021 Sergey Matveev This program is free software: you can redistribute it and/or modify @@ -14,7 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package main +package tofuproxy import ( "net" diff --git a/dane.go b/dane.go index f660f02..cd97220 100644 --- a/dane.go +++ b/dane.go @@ -1,4 +1,5 @@ /* +tofuproxy -- HTTP proxy with TLS certificates management Copyright (C) 2021 Sergey Matveev This program is free software: you can redistribute it and/or modify @@ -14,7 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package main +package tofuproxy import ( "crypto/sha256" @@ -28,12 +29,10 @@ import ( "github.com/miekg/dns" ) -var ( - dnsSrv *string -) +var DNSSrv string func dane(addr string, cert *x509.Certificate) (bool, bool) { - if *dnsSrv == "" { + if DNSSrv == "" { return false, false } host := addr @@ -45,7 +44,7 @@ func dane(addr string, cert *x509.Certificate) (bool, bool) { } m := new(dns.Msg) m.SetQuestion(dns.Fqdn(fmt.Sprintf("_%s._tcp.%s", port, host)), dns.TypeTLSA) - msg, err := dns.Exchange(m, *dnsSrv) + msg, err := dns.Exchange(m, DNSSrv) if err != nil { log.Printf("DNS: %+v\n", err) return false, false diff --git a/doc/index.texi b/doc/index.texi index 353d2d5..5839a6b 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -9,8 +9,6 @@ Copyright @copyright{} 2021 @email{stargrave@@stargrave.org, Sergey Matveev} @node Top @top tofuproxy -@image{logs,,,Example logs,.webp} - @itemize @item I am tired that various HTTPS clients (like browsers and feed @@ -44,7 +42,8 @@ checks. Why the hell people just do not send PostScript documents instead!? @item And wonderful @url{http://jpegxl.info/, JPEG XL} image format is -not supported by most browsers. Even pretty old WebP is not supported +not supported by most browsers. Even pretty old +@url{https://developers.google.com/speed/webp, WebP} is not supported everywhere. @end itemize @@ -61,25 +60,25 @@ creating some kind of complex configuration framework. @item TLS connection between client and @command{tofuproxy} has the proper hostname set in ephemeral on-the-fly generated certificate. -@item @code{HEAD} method is forbidden, because of damned Xombrero loving - making it so much. Can live without it. +@item @code{HEAD} method for Xombrero is forbidden, as it loves it too much. @item @code{www.reddit.com} is redirected to @code{old.reddit.com}. @item @url{https://habr.com/ru/all/, Хабр}'s resolution reduced images are redirected to their full size variants. -@item Various spying domains (advertisement, tracking counters) are - responded with 404 error. +@item Various spying domains (advertisement, tracking counters) are denied. + +@item Web fonts downloads are forbidden. + +@item Permanent HTTP redirects are replaced with HTML page with the link. -@item Web fonts downloads are replaced with 404 errors. +@item Temporary HTTP redirects are replaced with HTML too, if it is + neither @url{https://newsboat.org/, Newsboat} nor image paths. -@item All HTTP redirects are replaced with HTML page with the link. - However temporary redirects are passed as is for @code{newsboat} - User-Agent. +@item WebP images, if it is not Xombrero, is transcoded to PNG. -@item WebP (except if User-Agent is Xombrero browser) and JPEG XL images - are transparently transcoded to PNG. +@item JPEG XL images are transparently transcoded to PNG too. @item Default Go's checks are applied to all certificates. If they pass, then certificate chain is saved on the disk. Future connections are @@ -92,73 +91,14 @@ creating some kind of complex configuration framework. @item Optionally DANE-EE check is also made for each domain you visit. -@item TLS session resumption is also supported. +@item TLS session resumption and keep-alives are also supported. @item And Go itself tries also to act as a @url{https://http2.github.io/, HTTP/2} client too. @end itemize -@image{dialog,,,Example dialog,.webp} - -@node Usage -@unnumbered Usage - -@itemize - -@item Build @command{tofuproxy}: - -@example -$ git clone git://git.stargrave.org/tofuproxy.git -$ cd tofuproxy -$ go build -@end example - -@item -Generate CA-capable certificate for the proxy, that will issue ephemeral -certificate to proxied domains: - -@example -$ redo cert.pem -@end example - -@item -Create directory with output FIFOs and directory for stored certificate chains: - -@example -$ ./mkfifos.sh -$ mkdir certs -@end example - -@item -Run @command{tofuproxy} itself. By default it will bind to -@code{[::1]:8080}, use @code{[::1]:53} DNS server for DANE requests -(set to an empty string to disable DANE lookups): - -@example -$ ./tofuproxy -main.go:316: listening: [::1]:8080 -@end example - -@item Trust your newly generated CA: - -@example -# cat /path/to/tofuproxy/cert.pem >> /etc/ssl/cert.pem -@end example - -@item Point you HTTP/HTTPS clients to @code{http://localhost:8080}. - -@item Watch logs with @url{https://github.com/halturin/multitail, multitail}: - -@example -$ ./multitail.sh -@end example - -@end itemize - -When you encounter something requiring your attention and decision, you -will be shown Tk-dialog through the @command{wish} invocation. GnuTLS'es -@command{certtool} is used for certificate information printing. +@include usage.texi @node TODO @unnumbered TODO diff --git a/doc/usage.texi b/doc/usage.texi new file mode 100644 index 0000000..2b606e2 --- /dev/null +++ b/doc/usage.texi @@ -0,0 +1,66 @@ +@node Usage +@unnumbered Usage + +@itemize + +@item Currently @command{tofuproxy} uses: + GnuTLS'es @url{https://www.gnutls.org/manual/html_node/certtool-Invocation.html, certtool}, + @url{http://cr.yp.to/redo.html, redo} build system, + @url{https://www.tcl.tk/, Tcl/Tk}'s @command{wish} shell for GUI dialogues, + @command{dwebp}, @command{djxl} for images transcoding, + @url{https://github.com/halturin/multitail, multitail} for logs viewing. + +@item Download and build @command{tofuproxy}: + +@example +$ git clone git://git.stargrave.org/tofuproxy.git +$ cd tofuproxy +$ redo all +@end example + +@item +If build fails because of untrusted @code{ca.cypherpunks.ru} certificate, then: + +@example +$ [fetch|wget] http://www.ca.cypherpunks.ru/cert.pem +$ [fetch|wget] http://www.ca.cypherpunks.ru/cert.pem.asc +$ gpg --auto-key-locate dane --locate-keys stargrave at stargrave dot org +$ gpg --auto-key-locate wkd --locate-keys stargrave at gnupg dot net +$ gpg --verify cert.pem.asc +$ SSL_CERT_FILE=cert.pem GIT_SSL_CAINFO=cert.pem redo all +@end example + +@item +Run @command{tofuproxy} itself. By default it will bind to +@code{[::1]:8080}, use @code{[::1]:53} DNS server for DANE requests +(set to an empty string to disable DANE lookups): + +@example +$ ./tofuproxy +main.go:316: listening: [::1]:8080 +@end example + +@item Trust your newly generated CA: + +@example +# cat cert.pem >> /etc/ssl/cert.pem +@end example + +@item Point you HTTP/HTTPS clients to @code{http://localhost:8080}. + +@item Watch logs: + +@example +$ ( cd fifos ; ./multitail.sh ) +@end example + +@image{logs,,,Example logs,.webp} + +@item +When you encounter something requiring your attention and decision, you +will be shown Tk-dialog through the @command{wish} invocation. GnuTLS'es +@command{certtool} is used for certificate information printing. + +@image{dialog,,,Example dialog,.webp} + +@end itemize diff --git a/fifo.go b/fifo.go deleted file mode 100644 index 948db85..0000000 --- a/fifo.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright (C) 2021 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 main - -import ( - "log" - "os" - "path/filepath" - "time" - - "go.cypherpunks.ru/tai64n/v2" -) - -var ( - notai *bool - sinkCert = make(chan string) - sinkErr = make(chan string) - sinkOK = make(chan string) - sinkOther = make(chan string) - sinkRedir = make(chan string) - sinkReq = make(chan string) - sinkTLS = make(chan string) - - fifos *string -) - -func sinker(c chan string, p string) { - tai := new(tai64n.TAI64N) - fd, err := os.OpenFile(p, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) - if err != nil { - log.Fatalln(err) - } - for s := range c { - if *notai { - fd.WriteString(s + "\n") - } else { - tai.FromTime(time.Now()) - fd.WriteString(tai64n.Encode(tai[:]) + " " + s + "\n") - } - fd.Close() - fd, err = os.OpenFile(p, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) - if err != nil { - log.Printf("Sink error: %+v\n", err) - } - } -} - -func fifoInit() { - go sinker(sinkCert, filepath.Join(*fifos, "cert")) - go sinker(sinkErr, filepath.Join(*fifos, "err")) - go sinker(sinkOK, filepath.Join(*fifos, "ok")) - go sinker(sinkOther, filepath.Join(*fifos, "other")) - go sinker(sinkRedir, filepath.Join(*fifos, "redir")) - go sinker(sinkReq, filepath.Join(*fifos, "req")) - go sinker(sinkTLS, filepath.Join(*fifos, "tls")) -} diff --git a/mkfifos.sh b/fifos/ensure.do old mode 100755 new mode 100644 similarity index 54% rename from mkfifos.sh rename to fifos/ensure.do index c822253..682d687 --- a/mkfifos.sh +++ b/fifos/ensure.do @@ -1,6 +1,3 @@ -#!/bin/sh - -mkdir fifos for f in cert err ok other redir req tls ; do - mkfifo fifos/$f + [ -p $f ] || mkfifo $f done diff --git a/fifos/fifos.go b/fifos/fifos.go new file mode 100644 index 0000000..6e78096 --- /dev/null +++ b/fifos/fifos.go @@ -0,0 +1,72 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 fifos + +import ( + "log" + "os" + "path/filepath" + "time" + + "go.cypherpunks.ru/tai64n/v2" +) + +var ( + NoTAI bool + FIFOs string + SinkCert = make(chan string) + SinkErr = make(chan string) + SinkOK = make(chan string) + SinkOther = make(chan string) + SinkRedir = make(chan string) + SinkReq = make(chan string) + SinkTLS = make(chan string) + +) + +func sinker(c chan string, p string) { + tai := new(tai64n.TAI64N) + for { + fd, err := os.OpenFile(p, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) + if err != nil { + log.Fatalln(err) + } + for s := range c { + if NoTAI { + _, err = fd.WriteString(s + "\n") + } else { + tai.FromTime(time.Now()) + _, err = fd.WriteString(tai64n.Encode(tai[:]) + " " + s + "\n") + } + if err != nil { + break + } + } + fd.Close() + } +} + +func Init() { + go sinker(SinkCert, filepath.Join(FIFOs, "cert")) + go sinker(SinkErr, filepath.Join(FIFOs, "err")) + go sinker(SinkOK, filepath.Join(FIFOs, "ok")) + go sinker(SinkOther, filepath.Join(FIFOs, "other")) + go sinker(SinkRedir, filepath.Join(FIFOs, "redir")) + go sinker(SinkReq, filepath.Join(FIFOs, "req")) + go sinker(SinkTLS, filepath.Join(FIFOs, "tls")) +} diff --git a/multitail.sh b/fifos/multitail.sh similarity index 55% rename from multitail.sh rename to fifos/multitail.sh index f52773c..e6f29fb 100755 --- a/multitail.sh +++ b/fifos/multitail.sh @@ -2,10 +2,10 @@ multitail \ -wh 10 \ - -t "Certificates" -ci magenta -l "while :; do tai64nlocal < fifos/cert ; done" \ - -t "Errors" -ci red -L "while :; do tai64nlocal < fifos/err ; done" \ - -t "Responses" -ci green --label "< " -l "while :; do tai64nlocal < fifos/ok ; done" \ - -t "Others" -ci white -L "while :; do tai64nlocal < fifos/other ; done" \ - -t "Redirections" -ci cyan --label "R " -L "while :; do tai64nlocal < fifos/redir ; done" \ - -t "Requests" -ci blue --label "> " -L "while :; do tai64nlocal < fifos/req ; done" \ - -t "TLS connections" -ci yellow --label "S " -L "while :; do tai64nlocal < fifos/tls ; done" + -t "Certificates" -ci magenta -l "while :; do tai64nlocal < cert ; done" \ + -t "Errors" -ci red -L "while :; do tai64nlocal < err ; done" \ + -t "Responses" -ci green --label "< " -l "while :; do tai64nlocal < ok ; done" \ + -t "Others" -ci white -L "while :; do tai64nlocal < other ; done" \ + -t "Redirections" -ci cyan --label "R " -L "while :; do tai64nlocal < redir ; done" \ + -t "Requests" -ci blue --label "> " -L "while :; do tai64nlocal < req ; done" \ + -t "TLS connections" -ci yellow --label "S " -L "while :; do tai64nlocal < tls ; done" diff --git a/main.go b/main.go deleted file mode 100644 index a48ec24..0000000 --- a/main.go +++ /dev/null @@ -1,412 +0,0 @@ -/* -Copyright (C) 2021 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 main - -import ( - "context" - "crypto" - "crypto/tls" - "crypto/x509" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/dustin/go-humanize" - "go.cypherpunks.ru/ucspi" -) - -var ( - tlsNextProtoS = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) - caCert *x509.Certificate - caPrv crypto.PrivateKey - transport = http.Transport{ - DialTLSContext: dialTLS, - ForceAttemptHTTP2: true, - } - sessionCache = tls.NewLRUClientSessionCache(1024) - - CmdDWebP = "dwebp" - CmdDJXL = "djxl" - - imageExts = map[string]struct{}{ - ".apng": {}, - ".avif": {}, - ".gif": {}, - ".heic": {}, - ".jp2": {}, - ".jpeg": {}, - ".jpg": {}, - ".jxl": {}, - ".mng": {}, - ".png": {}, - ".svg": {}, - ".tif": {}, - ".tiff": {}, - ".webp": {}, - } -) - -func dialTLS(ctx context.Context, network, addr string) (net.Conn, error) { - host := strings.TrimSuffix(addr, ":443") - cfg := tls.Config{ - VerifyPeerCertificate: func( - rawCerts [][]byte, - verifiedChains [][]*x509.Certificate, - ) error { - return verifyCert(host, nil, rawCerts, verifiedChains) - }, - ClientSessionCache: sessionCache, - NextProtos: []string{"h2", "http/1.1"}, - } - conn, dialErr := tls.Dial(network, addr, &cfg) - if dialErr != nil { - if _, ok := dialErr.(ErrRejected); ok { - return nil, dialErr - } - cfg.InsecureSkipVerify = true - cfg.VerifyPeerCertificate = func( - rawCerts [][]byte, - verifiedChains [][]*x509.Certificate, - ) error { - return verifyCert(host, dialErr, rawCerts, verifiedChains) - } - var err error - conn, err = tls.Dial(network, addr, &cfg) - if err != nil { - sinkErr <- fmt.Sprintf("%s\t%s", addr, dialErr.Error()) - return nil, err - } - } - connState := conn.ConnectionState() - if connState.DidResume { - sinkTLS <- fmt.Sprintf( - "%s\t%s %s\t%s\t%s", - strings.TrimSuffix(addr, ":443"), - ucspi.TLSVersion(connState.Version), - tls.CipherSuiteName(connState.CipherSuite), - spkiHash(connState.PeerCertificates[0]), - connState.NegotiatedProtocol, - ) - } - return conn, nil -} - -func roundTrip(w http.ResponseWriter, req *http.Request) { - if req.Method == http.MethodHead { - http.Error(w, "go away", http.StatusMethodNotAllowed) - return - } - sinkReq <- fmt.Sprintf("%s %s", req.Method, req.URL.String()) - host := strings.TrimSuffix(req.URL.Host, ":443") - - for _, spy := range SpyDomains { - if strings.HasSuffix(host, spy) { - http.NotFound(w, req) - sinkOther <- fmt.Sprintf( - "%s %s\t%d\tspy one", - req.Method, - req.URL.String(), - http.StatusNotFound, - ) - return - } - } - - if host == "www.reddit.com" { - req.URL.Host = "old.reddit.com" - http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently) - return - } - - if host == "habrastorage.org" && strings.Contains(req.URL.Path, "r/w780q1") { - req.URL.Path = strings.Replace(req.URL.Path, "r/w780q1/", "", 1) - http.Redirect(w, req, req.URL.String(), http.StatusFound) - return - } - - resp, err := transport.RoundTrip(req) - if err != nil { - sinkErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error()) - w.WriteHeader(http.StatusBadGateway) - w.Write([]byte(err.Error())) - return - } - - for k, vs := range resp.Header { - if k == "Location" || k == "Content-Type" || k == "Content-Length" { - continue - } - for _, v := range vs { - w.Header().Add(k, v) - } - } - - switch resp.Header.Get("Content-Type") { - case "application/font-woff", "application/font-sfnt": - // Those are deprecated types - fallthrough - case "font/otf", "font/ttf", "font/woff", "font/woff2": - http.NotFound(w, req) - sinkOther <- fmt.Sprintf( - "%s %s\t%d\tfonts are not allowed", - req.Method, - req.URL.String(), - http.StatusNotFound, - ) - resp.Body.Close() - return - case "image/webp": - if strings.Contains(req.Header.Get("User-Agent"), "AppleWebKit/538.15") { - // My Xombrero - break - } - tmpFd, err := ioutil.TempFile("", "tofuproxy.*.webp") - if err != nil { - log.Fatalln(err) - } - defer tmpFd.Close() - defer os.Remove(tmpFd.Name()) - defer resp.Body.Close() - if _, err = io.Copy(tmpFd, resp.Body); err != nil { - log.Printf("Error during %s: %+v\n", req.URL, err) - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - tmpFd.Close() - cmd := exec.Command(CmdDWebP, tmpFd.Name(), "-o", "-") - data, err := cmd.Output() - if err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - w.Header().Add("Content-Type", "image/png") - w.WriteHeader(http.StatusOK) - w.Write(data) - sinkOther <- fmt.Sprintf( - "%s %s\t%d\tWebP transcoded to PNG", - req.Method, - req.URL.String(), - http.StatusOK, - ) - return - case "image/jxl": - tmpFd, err := ioutil.TempFile("", "tofuproxy.*.jxl") - if err != nil { - log.Fatalln(err) - } - defer tmpFd.Close() - defer os.Remove(tmpFd.Name()) - defer resp.Body.Close() - if _, err = io.Copy(tmpFd, resp.Body); err != nil { - log.Printf("Error during %s: %+v\n", req.URL, err) - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - tmpFd.Close() - dstFn := tmpFd.Name() + ".png" - cmd := exec.Command(CmdDJXL, tmpFd.Name(), dstFn) - err = cmd.Run() - defer os.Remove(dstFn) - if err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - data, err := ioutil.ReadFile(dstFn) - if err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - w.Header().Add("Content-Type", "image/png") - w.WriteHeader(http.StatusOK) - w.Write(data) - sinkOther <- fmt.Sprintf( - "%s %s\t%d\tJPEG XL transcoded to PNG", - req.Method, - req.URL.String(), - http.StatusOK, - ) - return - } - - if req.Method == http.MethodGet { - var redirType string - switch resp.StatusCode { - case http.StatusMovedPermanently, http.StatusPermanentRedirect: - redirType = "permanent" - goto Redir - case http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect: - if strings.Contains(req.Header.Get("User-Agent"), "newsboat/") { - goto NoRedir - } - if _, ok := imageExts[filepath.Ext(req.URL.Path)]; ok { - goto NoRedir - } - redirType = "temporary" - default: - goto NoRedir - } - Redir: - resp.Body.Close() - w.Header().Add("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - location := resp.Header.Get("Location") - w.Write([]byte( - fmt.Sprintf( - `%d %s: %s redirection -Redirection to %s`, - resp.StatusCode, http.StatusText(resp.StatusCode), - redirType, location, location, - ))) - sinkRedir <- fmt.Sprintf( - "%s %s\t%s\t%s", req.Method, resp.Status, req.URL.String(), location, - ) - return - } - -NoRedir: - for _, h := range []string{"Location", "Content-Type", "Content-Length"} { - 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", - req.Method, - req.URL.String(), - resp.Status, - resp.Header.Get("Content-Type"), - humanize.IBytes(uint64(n)), - ) - if resp.StatusCode == http.StatusOK { - sinkOK <- msg - } else { - sinkOther <- msg - } -} - -type Handler struct{} - -func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodConnect { - roundTrip(w, req) - return - } - hj, ok := w.(http.Hijacker) - if !ok { - log.Fatalln("no hijacking") - } - conn, _, err := hj.Hijack() - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - conn.Write([]byte(fmt.Sprintf( - "%s %d %s\r\n\r\n", - req.Proto, - http.StatusOK, http.StatusText(http.StatusOK), - ))) - host := strings.Split(req.Host, ":")[0] - hostCertsM.Lock() - keypair, ok := hostCerts[host] - if !ok || !keypair.cert.NotAfter.After(time.Now().Add(time.Hour)) { - keypair = newKeypair(host, caCert, caPrv) - hostCerts[host] = keypair - } - hostCertsM.Unlock() - tlsConn := tls.Server(conn, &tls.Config{ - Certificates: []tls.Certificate{{ - Certificate: [][]byte{keypair.cert.Raw}, - PrivateKey: keypair.prv, - }}, - }) - if err = tlsConn.Handshake(); err != nil { - log.Printf("TLS error %s: %+v\n", host, err) - return - } - srv := http.Server{ - Handler: &HTTPSHandler{host: req.Host}, - TLSNextProto: tlsNextProtoS, - } - err = srv.Serve(&SingleListener{conn: tlsConn}) - if err != nil { - if _, ok := err.(AlreadyAccepted); !ok { - log.Printf("TLS serve error %s: %+v\n", host, err) - return - } - } -} - -type HTTPSHandler struct { - host string -} - -func (h *HTTPSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - req.URL.Scheme = "https" - req.URL.Host = h.host - roundTrip(w, req) -} - -func main() { - crtPath := flag.String("cert", "cert.pem", "Path to server X.509 certificate") - prvPath := flag.String("key", "prv.pem", "Path to server PKCS#8 private key") - bind := flag.String("bind", "[::1]:8080", "Bind address") - certs = flag.String("certs", "certs", "Directory with pinned certificates") - dnsSrv = flag.String("dns", "[::1]:53", "DNS server") - fifos = flag.String("fifos", "fifos", "Directory with FIFOs") - notai = flag.Bool("notai", false, "Do not prepend TAI64N to logs") - flag.Parse() - log.SetFlags(log.Lshortfile) - fifoInit() - - var err error - _, caCert, err = ucspi.CertificateFromFile(*crtPath) - if err != nil { - log.Fatalln(err) - } - caPrv, err = ucspi.PrivateKeyFromFile(*prvPath) - if err != nil { - log.Fatalln(err) - } - - ln, err := net.Listen("tcp", *bind) - if err != nil { - log.Fatalln(err) - } - srv := http.Server{ - Handler: &Handler{}, - TLSNextProto: tlsNextProtoS, - } - log.Println("listening:", *bind) - if err := srv.Serve(ln); err != nil { - log.Fatalln(err) - } -} diff --git a/prv.pem.do b/prv.pem.do index c2ee376..c14af89 100644 --- a/prv.pem.do +++ b/prv.pem.do @@ -1 +1,2 @@ -certtool --generate-privkey --bits 256 --ecc +umask 077 +certtool --generate-privkey --bits 256 --ecc > $3 diff --git a/rounds/05noHead.go b/rounds/05noHead.go new file mode 100644 index 0000000..2396e02 --- /dev/null +++ b/rounds/05noHead.go @@ -0,0 +1,40 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "net/http" + "strings" +) + +func isXombrero(req *http.Request) bool { + return strings.Contains(req.Header.Get("User-Agent"), "AppleWebKit/538.15") +} + +func RoundNoHead( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + if req.Method == http.MethodHead && isXombrero(req) { + http.Error(w, "deny HEAD", http.StatusMethodNotAllowed) + return false, nil + } + return true, nil +} diff --git a/rounds/10log.go b/rounds/10log.go new file mode 100644 index 0000000..135394d --- /dev/null +++ b/rounds/10log.go @@ -0,0 +1,35 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "fmt" + "net/http" + + "go.stargrave.org/tofuproxy/fifos" +) + +func RoundLog( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + fifos.SinkReq <- fmt.Sprintf("%s %s", req.Method, req.URL.String()) + return true, nil +} diff --git a/rounds/15spy.go b/rounds/15spy.go new file mode 100644 index 0000000..3efb433 --- /dev/null +++ b/rounds/15spy.go @@ -0,0 +1,69 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "fmt" + "net/http" + "strings" + + "go.stargrave.org/tofuproxy/fifos" +) + +var spyDomains = []string{ + "google-analytics.com", + "goo.gl", + "ads.google.com", + "facebook.com", + "facebook.net", + "fbcdn.com", + "fbcdn.net", + "advertising.yandex.ru", + "an.yandex.ru", + "awaps.yandex.ru", + "bs.yandex.ru", + "informer.yandex.ru", + "mc.yandex.ru", + "metrika.yandex.ru", + "reklama-direct-yandex.ru", + "yandexadexchange.net", + "yandexreklama.com", + "doubleclick.net", + "tns-counter.ru", +} + +func RoundDenySpy( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + for _, spy := range spyDomains { + if strings.HasSuffix(host, spy) { + http.NotFound(w, req) + fifos.SinkOther <- fmt.Sprintf( + "%s %s\t%d\tdeny spy", + req.Method, + req.URL.String(), + http.StatusNotFound, + ) + return false, nil + } + } + return true, nil +} diff --git a/rounds/20reddit.go b/rounds/20reddit.go new file mode 100644 index 0000000..7b5668e --- /dev/null +++ b/rounds/20reddit.go @@ -0,0 +1,36 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "net/http" +) + +func RoundRedditOld( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + if host == "www.reddit.com" { + req.URL.Host = "old.reddit.com" + http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently) + return false, nil + } + return true, nil +} diff --git a/rounds/25habrImage.go b/rounds/25habrImage.go new file mode 100644 index 0000000..86f21aa --- /dev/null +++ b/rounds/25habrImage.go @@ -0,0 +1,37 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "net/http" + "strings" +) + +func RoundHabrImage( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + if host == "habrastorage.org" && strings.Contains(req.URL.Path, "r/w780q1/") { + req.URL.Path = strings.Replace(req.URL.Path, "r/w780q1/", "", 1) + http.Redirect(w, req, req.URL.String(), http.StatusFound) + return false, nil + } + return true, nil +} diff --git a/rounds/35denyFonts.go b/rounds/35denyFonts.go new file mode 100644 index 0000000..4fe1195 --- /dev/null +++ b/rounds/35denyFonts.go @@ -0,0 +1,49 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "fmt" + "net/http" + + "go.stargrave.org/tofuproxy/fifos" +) + +func RoundDenyFonts( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + switch resp.Header.Get("Content-Type") { + case "application/font-woff", "application/font-sfnt": + // Those are deprecated types + fallthrough + case "font/otf", "font/ttf", "font/woff", "font/woff2": + http.NotFound(w, req) + fifos.SinkOther <- fmt.Sprintf( + "%s %s\t%d\tdeny fonts", + req.Method, + req.URL.String(), + http.StatusNotFound, + ) + resp.Body.Close() + return false, nil + } + return true, nil +} diff --git a/rounds/40transcodeWebP.go b/rounds/40transcodeWebP.go new file mode 100644 index 0000000..43938ec --- /dev/null +++ b/rounds/40transcodeWebP.go @@ -0,0 +1,70 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + + "go.stargrave.org/tofuproxy/fifos" +) + +const CmdDWebP = "dwebp" + +func RoundTranscodeWebP( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + if resp.Header.Get("Content-Type") != "image/webp" || isXombrero(req) { + return true, nil + } + tmpFd, err := ioutil.TempFile("", "tofuproxy.*.webp") + if err != nil { + log.Fatalln(err) + } + defer os.Remove(tmpFd.Name()) + defer tmpFd.Close() + defer resp.Body.Close() + if _, err = io.Copy(tmpFd, resp.Body); err != nil { + log.Printf("Error during %s: %+v\n", req.URL, err) + return false, err + } + tmpFd.Close() + cmd := exec.Command(CmdDWebP, tmpFd.Name(), "-o", "-") + data, err := cmd.Output() + if err != nil { + return false, err + } + w.Header().Add("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + w.Write(data) + fifos.SinkOther <- fmt.Sprintf( + "%s %s\t%d\tWebP transcoded to PNG", + req.Method, + req.URL.String(), + http.StatusOK, + ) + return false, nil +} diff --git a/rounds/45transcodeJXL.go b/rounds/45transcodeJXL.go new file mode 100644 index 0000000..2d748fa --- /dev/null +++ b/rounds/45transcodeJXL.go @@ -0,0 +1,76 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + + "go.stargrave.org/tofuproxy/fifos" +) + +const CmdDJXL = "djxl" + +func RoundTranscodeJXL( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + if resp.Header.Get("Content-Type") != "image/jxl" { + return true, nil + } + tmpFd, err := ioutil.TempFile("", "tofuproxy.*.jxl") + if err != nil { + log.Fatalln(err) + } + defer os.Remove(tmpFd.Name()) + defer tmpFd.Close() + defer resp.Body.Close() + if _, err = io.Copy(tmpFd, resp.Body); err != nil { + log.Printf("Error during %s: %+v\n", req.URL, err) + return false, err + } + tmpFd.Close() + dstFn := tmpFd.Name() + ".png" + cmd := exec.Command(CmdDJXL, tmpFd.Name(), dstFn) + err = cmd.Run() + defer os.Remove(dstFn) + if err != nil { + return false, err + } + data, err := ioutil.ReadFile(dstFn) + if err != nil { + return false, err + } + w.Header().Add("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + w.Write(data) + fifos.SinkOther <- fmt.Sprintf( + "%s %s\t%d\tJPEG XL transcoded to PNG", + req.Method, + req.URL.String(), + http.StatusOK, + ) + return false, nil +} diff --git a/rounds/50redirectHTML.go b/rounds/50redirectHTML.go new file mode 100644 index 0000000..e92a4f9 --- /dev/null +++ b/rounds/50redirectHTML.go @@ -0,0 +1,89 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 rounds + +import ( + "fmt" + "net/http" + "path/filepath" + "strings" + + "go.stargrave.org/tofuproxy/fifos" +) + +var imageExts = map[string]struct{}{ + ".apng": {}, + ".avif": {}, + ".gif": {}, + ".heic": {}, + ".jp2": {}, + ".jpeg": {}, + ".jpg": {}, + ".jxl": {}, + ".mng": {}, + ".png": {}, + ".svg": {}, + ".tif": {}, + ".tiff": {}, + ".webp": {}, +} + +func isNewsboat(req *http.Request) bool { + return strings.Contains(req.Header.Get("User-Agent"), "newsboat/") +} + +func RoundRedirectHTML( + host string, + resp *http.Response, + w http.ResponseWriter, + req *http.Request, +) (bool, error) { + if req.Method != http.MethodGet { + return true, nil + } + var redirType string + switch resp.StatusCode { + case http.StatusMovedPermanently, http.StatusPermanentRedirect: + redirType = "permanent" + case http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect: + if isNewsboat(req) { + return true, nil + } + if _, ok := imageExts[filepath.Ext(req.URL.Path)]; ok { + return true, nil + } + redirType = "temporary" + default: + return true, nil + } + resp.Body.Close() + w.Header().Add("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + location := resp.Header.Get("Location") + w.Write([]byte( + fmt.Sprintf( + `%d %s: %s redirection +Redirection to %s`, + resp.StatusCode, http.StatusText(resp.StatusCode), + redirType, location, location, + ))) + fifos.SinkRedir <- fmt.Sprintf( + "%s %s\t%s\t%s", req.Method, resp.Status, req.URL.String(), location, + ) + return false, nil +} diff --git a/spy.go b/spy.go deleted file mode 100644 index b4e1349..0000000 --- a/spy.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -var SpyDomains = []string{ - "google-analytics.com", - "goo.gl", - "ads.google.com", - "facebook.com", - "facebook.net", - "fbcdn.com", - "fbcdn.net", - "advertising.yandex.ru", - "an.yandex.ru", - "awaps.yandex.ru", - "bs.yandex.ru", - "informer.yandex.ru", - "mc.yandex.ru", - "metrika.yandex.ru", - "reklama-direct-yandex.ru", - "yandexadexchange.net", - "yandexreklama.com", - "doubleclick.net", - "tns-counter.ru", -} diff --git a/tls.go b/tls.go new file mode 100644 index 0000000..4c29a6f --- /dev/null +++ b/tls.go @@ -0,0 +1,148 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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 ( + "context" + "crypto" + "crypto/tls" + "crypto/x509" + "fmt" + "log" + "net" + "net/http" + "strings" + "time" + + "go.cypherpunks.ru/ucspi" + "go.stargrave.org/tofuproxy/fifos" +) + +var ( + TLSNextProtoS = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + CACert *x509.Certificate + CAPrv crypto.PrivateKey + sessionCache = tls.NewLRUClientSessionCache(1024) +) + +type Handler struct{} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodConnect { + roundTrip(w, req) + return + } + hj, ok := w.(http.Hijacker) + if !ok { + log.Fatalln("no hijacking") + } + conn, _, err := hj.Hijack() + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + conn.Write([]byte(fmt.Sprintf( + "%s %d %s\r\n\r\n", + req.Proto, + http.StatusOK, http.StatusText(http.StatusOK), + ))) + host := strings.Split(req.Host, ":")[0] + hostCertsM.Lock() + keypair, ok := hostCerts[host] + if !ok || !keypair.cert.NotAfter.After(time.Now().Add(time.Hour)) { + keypair = newKeypair(host, CACert, CAPrv) + hostCerts[host] = keypair + } + hostCertsM.Unlock() + tlsConn := tls.Server(conn, &tls.Config{ + Certificates: []tls.Certificate{{ + Certificate: [][]byte{keypair.cert.Raw}, + PrivateKey: keypair.prv, + }}, + }) + if err = tlsConn.Handshake(); err != nil { + log.Printf("TLS error %s: %+v\n", host, err) + return + } + srv := http.Server{ + Handler: &HTTPSHandler{host: req.Host}, + TLSNextProto: TLSNextProtoS, + } + err = srv.Serve(&SingleListener{conn: tlsConn}) + if err != nil { + if _, ok := err.(AlreadyAccepted); !ok { + log.Printf("TLS serve error %s: %+v\n", host, err) + return + } + } +} + +type HTTPSHandler struct { + host string +} + +func (h *HTTPSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + req.URL.Scheme = "https" + req.URL.Host = h.host + roundTrip(w, req) +} + +func dialTLS(ctx context.Context, network, addr string) (net.Conn, error) { + host := strings.TrimSuffix(addr, ":443") + cfg := tls.Config{ + VerifyPeerCertificate: func( + rawCerts [][]byte, + verifiedChains [][]*x509.Certificate, + ) error { + return verifyCert(host, nil, rawCerts, verifiedChains) + }, + ClientSessionCache: sessionCache, + NextProtos: []string{"h2", "http/1.1"}, + } + conn, dialErr := tls.Dial(network, addr, &cfg) + if dialErr != nil { + if _, ok := dialErr.(ErrRejected); ok { + return nil, dialErr + } + cfg.InsecureSkipVerify = true + cfg.VerifyPeerCertificate = func( + rawCerts [][]byte, + verifiedChains [][]*x509.Certificate, + ) error { + return verifyCert(host, dialErr, rawCerts, verifiedChains) + } + var err error + conn, err = tls.Dial(network, addr, &cfg) + if err != nil { + fifos.SinkErr <- fmt.Sprintf("%s\t%s", addr, dialErr.Error()) + return nil, err + } + } + connState := conn.ConnectionState() + if connState.DidResume { + fifos.SinkTLS <- fmt.Sprintf( + "%s\t%s %s\t%s\t%s", + strings.TrimSuffix(addr, ":443"), + ucspi.TLSVersion(connState.Version), + tls.CipherSuiteName(connState.CipherSuite), + spkiHash(connState.PeerCertificates[0]), + connState.NegotiatedProtocol, + ) + } + return conn, nil +} diff --git a/tofuproxy.do b/tofuproxy.do new file mode 100644 index 0000000..f8223e7 --- /dev/null +++ b/tofuproxy.do @@ -0,0 +1,3 @@ +redo-ifchange *.go cmd/tofuproxy/*.go rounds/*.go +GO_LDFLAGS="${GO_LDFLAGS:--ldflags=-s}" +${GO:-go} build -o $3 $GO_LDFLAGS ./cmd/tofuproxy diff --git a/trip.go b/trip.go new file mode 100644 index 0000000..019c408 --- /dev/null +++ b/trip.go @@ -0,0 +1,130 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 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/fifos" + "go.stargrave.org/tofuproxy/rounds" +) + +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: 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) { + host := strings.TrimSuffix(req.URL.Host, ":443") + for _, round := range []Round{ + rounds.RoundNoHead, + rounds.RoundLog, + rounds.RoundDenySpy, + rounds.RoundRedditOld, + rounds.RoundHabrImage, + } { + if cont, _ := round(host, nil, w, req); !cont { + return + } + } + + resp, err := transport.RoundTrip(req) + if err != nil { + fifos.SinkErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error()) + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + + 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.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", + req.Method, + req.URL.String(), + resp.Status, + resp.Header.Get("Content-Type"), + humanize.IBytes(uint64(n)), + ) + if resp.StatusCode == http.StatusOK { + fifos.SinkOK <- msg + } else { + fifos.SinkOther <- msg + } +} diff --git a/verify.go b/verify.go index 7844d14..4c20c0e 100644 --- a/verify.go +++ b/verify.go @@ -1,4 +1,5 @@ /* +tofuproxy -- HTTP proxy with TLS certificates management Copyright (C) 2021 Sergey Matveev This program is free software: you can redistribute it and/or modify @@ -14,7 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package main +package tofuproxy import ( "bytes" @@ -31,13 +32,14 @@ import ( "sync" "go.cypherpunks.ru/ucspi" + "go.stargrave.org/tofuproxy/fifos" ) var ( CmdCerttool = "certtool" - CmdWish = "wish8.7" + CmdWish = "wish8.6" - certs *string + Certs string accepted = make(map[string]string) acceptedM sync.RWMutex rejected = make(map[string]string) @@ -121,18 +123,18 @@ func verifyCert( daneExists, daneMatched := dane(host, certTheir) if daneExists { if daneMatched { - sinkCert <- fmt.Sprintf("DANE\t%s\tmatched", host) + fifos.SinkCert <- fmt.Sprintf("DANE\t%s\tmatched", host) } else { - sinkErr <- fmt.Sprintf("DANE\t%s\tnot matched", host) + fifos.SinkErr <- fmt.Sprintf("DANE\t%s\tnot matched", host) } } - fn := filepath.Join(*certs, host) + fn := filepath.Join(Certs, host) certsOur, _, err := ucspi.CertPoolFromFile(fn) if err == nil || dialErr != nil || (daneExists && !daneMatched) { if certsOur != nil && certTheirHash == spkiHash(certsOur[0]) { acceptedAdd(host, certTheirHash) if bytes.Compare(certsOur[0].Raw, rawCerts[0]) != 0 { - sinkCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash) + fifos.SinkCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash) goto CertUpdate } return nil @@ -201,32 +203,32 @@ grid columnconfigure . 0 -weight 1 err = cmd.Run() exitError, ok := err.(*exec.ExitError) if !ok { - sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash) + fifos.SinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash) return ErrRejected{host} } switch exitError.ExitCode() { case 10: - sinkCert <- fmt.Sprintf("ADD\t%s\t%s", host, certTheirHash) + fifos.SinkCert <- fmt.Sprintf("ADD\t%s\t%s", host, certTheirHash) goto CertUpdate case 11: - sinkCert <- fmt.Sprintf("ONCE\t%s\t%s", host, certTheirHash) + fifos.SinkCert <- fmt.Sprintf("ONCE\t%s\t%s", host, certTheirHash) acceptedAdd(host, certTheirHash) return nil case 12: rejectedAdd(host, certTheirHash) fallthrough default: - sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash) + fifos.SinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash) return ErrRejected{host} } } else { if !os.IsNotExist(err) { return err } - sinkCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash) + fifos.SinkCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash) } CertUpdate: - tmp, err := os.CreateTemp(*certs, "") + tmp, err := os.CreateTemp(Certs, "") if err != nil { log.Fatalln(err) } diff --git a/x509.go b/x509.go index d0e54a1..f818af2 100644 --- a/x509.go +++ b/x509.go @@ -1,4 +1,5 @@ /* +tofuproxy -- HTTP proxy with TLS certificates management Copyright (C) 2021 Sergey Matveev This program is free software: you can redistribute it and/or modify @@ -14,7 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package main +package tofuproxy import ( "crypto" -- 2.44.0