]> Sergey Matveev's repositories - tofuproxy.git/commitdiff
Refactoring
authorSergey Matveev <stargrave@stargrave.org>
Tue, 7 Sep 2021 13:49:23 +0000 (16:49 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Tue, 7 Sep 2021 13:49:23 +0000 (16:49 +0300)
29 files changed:
.gitignore [new file with mode: 0644]
README
all.do [new file with mode: 0644]
cmd/tofuproxy/main.go [new file with mode: 0644]
conn.go
dane.go
doc/index.texi
doc/usage.texi [new file with mode: 0644]
fifo.go [deleted file]
fifos/ensure.do [moved from mkfifos.sh with 54% similarity, mode: 0644]
fifos/fifos.go [new file with mode: 0644]
fifos/multitail.sh [moved from multitail.sh with 55% similarity]
main.go [deleted file]
prv.pem.do
rounds/05noHead.go [new file with mode: 0644]
rounds/10log.go [new file with mode: 0644]
rounds/15spy.go [new file with mode: 0644]
rounds/20reddit.go [new file with mode: 0644]
rounds/25habrImage.go [new file with mode: 0644]
rounds/35denyFonts.go [new file with mode: 0644]
rounds/40transcodeWebP.go [new file with mode: 0644]
rounds/45transcodeJXL.go [new file with mode: 0644]
rounds/50redirectHTML.go [new file with mode: 0644]
spy.go [deleted file]
tls.go [new file with mode: 0644]
tofuproxy.do [new file with mode: 0644]
trip.go [new file with mode: 0644]
verify.go
x509.go

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..7902b80
--- /dev/null
@@ -0,0 +1,4 @@
+/cert.pem
+/certs
+/prv.pem
+/tofuproxy
diff --git a/README b/README
index c62d94db72ac637c5e28a2d3a4098945a45d3ddd..7f9ebc5e09b729d049c10beb5f3e7f78092b93fc 100644 (file)
--- 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 (file)
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 (file)
index 0000000..5ad6e62
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 fbb8a5c19aa13a7d858493fa9182d03e791074cf..baa7c8da8479b274adbab27d4ff7982e328f69fe 100644 (file)
--- a/conn.go
+++ b/conn.go
@@ -1,4 +1,5 @@
 /*
+tofuproxy -- HTTP proxy with TLS certificates management
 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
 
 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 <http://www.gnu.org/licenses/>.
 */
 
-package main
+package tofuproxy
 
 import (
        "net"
diff --git a/dane.go b/dane.go
index f660f026cc0797b69e4928561179ac1812a69e03..cd97220f6d7ff4ed9d7fdd477fca846e0de2b8e3 100644 (file)
--- a/dane.go
+++ b/dane.go
@@ -1,4 +1,5 @@
 /*
+tofuproxy -- HTTP proxy with TLS certificates management
 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
 
 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 <http://www.gnu.org/licenses/>.
 */
 
-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
index 353d2d514c42305dac5c7381fd24c00f0cb3fc36..5839a6b3f11e99759e98a25cab17d32b198f97e5 100644 (file)
@@ -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 (file)
index 0000000..2b606e2
--- /dev/null
@@ -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 (file)
index 948db85..0000000
--- a/fifo.go
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
-
-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 <http://www.gnu.org/licenses/>.
-*/
-
-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"))
-}
old mode 100755 (executable)
new mode 100644 (file)
similarity index 54%
rename from mkfifos.sh
rename to fifos/ensure.do
index c822253..682d687
@@ -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 (file)
index 0000000..6e78096
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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"))
+}
similarity index 55%
rename from multitail.sh
rename to fifos/multitail.sh
index f52773caee0771713b5576db6ae7a737a42dcc58..e6f29fb096f4871fdc2e5f3142012bc65a8cf50e 100755 (executable)
@@ -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 (file)
index a48ec24..0000000
--- a/main.go
+++ /dev/null
@@ -1,412 +0,0 @@
-/*
-Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
-
-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 <http://www.gnu.org/licenses/>.
-*/
-
-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(
-                               `<html><head><title>%d %s: %s redirection</title></head>
-<body>Redirection to <a href="%s">%s</a></body></html>`,
-                               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)
-       }
-}
index c2ee3769846baf1ec78c2b3c1d0ffb6e819cb2c9..c14af8936b408582390e2f9dc4528c52b63060e3 100644 (file)
@@ -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 (file)
index 0000000..2396e02
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..135394d
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..3efb433
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..7b5668e
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..86f21aa
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..4fe1195
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..43938ec
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..2d748fa
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..e92a4f9
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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(
+                       `<html><head><title>%d %s: %s redirection</title></head>
+<body>Redirection to <a href="%s">%s</a></body></html>`,
+                       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 (file)
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 (file)
index 0000000..4c29a6f
--- /dev/null
+++ b/tls.go
@@ -0,0 +1,148 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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 (file)
index 0000000..f8223e7
--- /dev/null
@@ -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 (file)
index 0000000..019c408
--- /dev/null
+++ b/trip.go
@@ -0,0 +1,130 @@
+/*
+tofuproxy -- HTTP proxy with TLS certificates management
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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
+       }
+}
index 7844d14f97ee8bc1542614b930971e24348064dc..4c20c0e6f55f8c1e65e1a5899e8bad1c957e5fa0 100644 (file)
--- a/verify.go
+++ b/verify.go
@@ -1,4 +1,5 @@
 /*
+tofuproxy -- HTTP proxy with TLS certificates management
 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
 
 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 <http://www.gnu.org/licenses/>.
 */
 
-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 d0e54a1607a6c86beab2af8220e1fbb50df231e5..f818af2ee162951d8ec2937a3316548d6991b508 100644 (file)
--- a/x509.go
+++ b/x509.go
@@ -1,4 +1,5 @@
 /*
+tofuproxy -- HTTP proxy with TLS certificates management
 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
 
 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 <http://www.gnu.org/licenses/>.
 */
 
-package main
+package tofuproxy
 
 import (
        "crypto"