]> Sergey Matveev's repositories - tofuproxy.git/commitdiff
WebP/JPEG-XL transcoding
authorSergey Matveev <stargrave@stargrave.org>
Sun, 5 Sep 2021 15:33:38 +0000 (18:33 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sun, 5 Sep 2021 15:33:38 +0000 (18:33 +0300)
doc/index.texi
main.go

index 2b1e75576bb78db18de1a438cb1f715f4352252f..40806995a656fc712e768b81f2a905af6a3c0c2c 100644 (file)
@@ -17,7 +17,7 @@ Copyright @copyright{} 2021 @email{stargrave@@stargrave.org, Sergey Matveev}
 aggregators) use various TLS libraries with different features. NSS,
 GnuTLS, OpenSSL... All of them sucks, comparing to Go's @code{crypto/tls}.
 
-@item I am tired that everyone provides very limited certificates trust
+@item I tired that everyone provides very limited certificates trust
 management capabilities, like either certificate or SPKI
 @url{https://en.wikipedia.org/wiki/Certificate_pinning, pinning} with
 @url{https://en.wikipedia.org/wiki/Trust_on_first_use, TOFU}. Even my
@@ -28,20 +28,24 @@ more sufficient and convenient to work with.
 @item I am tired that many clients provides very few information about
 certificates and connections at all.
 
-@item I am tired that hardly anyone can control (no automatic silent
+@item I hate that hardly anyone can control (no automatic silent
 transparent following) HTTP redirections. Although Firefox had proper
 extensions for that.
 
-@item I am tired that you have got small control on URLs. The best you
-can is to use some kind of @url{https://en.wikipedia.org/wiki/Privoxy,
-Privoxy}, but it is not friendly with TLS connections, obviously.
+@item I am sick of tiny control on URLs. The best you can is to use some
+kind of @url{https://en.wikipedia.org/wiki/Privoxy, Privoxy}, but it is
+not friendly with TLS connections, obviously.
 
 @item Hardly anyone does
 @url{https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities, DANE}
 checks.
 
 @item And there is insanity of downloading fonts.
-    Why the hell people just do not send PostScript documents instead!?
+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
+everywhere.
 
 @end itemize
 
@@ -65,10 +69,15 @@ creating some kind of complex configuration framework.
 @item Various spying domains (advertisement, tracking counters) are
     responded with 404 error.
 
+@item Web fonts downloads are replaced with 404 errors.
+
 @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 (except if User-Agent is Xombrero browser) and JPEG XL images
+    are transparently transcoded to PNG.
+
 @item Default Go's checks are applied to all certificates. If they pass,
     then certificate chain is saved on the disk. Future connections are
     compared against it, warning you about SPKI change and waiting for
@@ -150,8 +159,6 @@ What I am planning possibly to do? Just brainstorming:
 
 @itemize
 
-@item JPEG-XL/WebP transparent converter to JPEG/PNG.
-
 @item HTTP authorization dialog.
 
 @item TLS client certificates usage capability.
diff --git a/main.go b/main.go
index 16fae3d6c0742f5ab38723ce1dfa4ae6f704cf35..79e355ebdc131ae2bc32f9d89607c1a0865ec9cf 100644 (file)
--- a/main.go
+++ b/main.go
@@ -26,9 +26,12 @@ import (
        "flag"
        "fmt"
        "io"
+       "io/ioutil"
        "log"
        "net"
        "net/http"
+       "os"
+       "os/exec"
        "strings"
        "sync"
        "time"
@@ -56,6 +59,9 @@ var (
        acceptedM sync.RWMutex
        rejected  = make(map[string]string)
        rejectedM sync.RWMutex
+
+       CmdDWebP = "dwebp"
+       CmdDJXL  = "djxl"
 )
 
 func spkiHash(cert *x509.Certificate) string {
@@ -128,6 +134,7 @@ func roundTrip(w http.ResponseWriter, req *http.Request) {
        }
        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)
@@ -140,11 +147,13 @@ func roundTrip(w http.ResponseWriter, req *http.Request) {
                        return
                }
        }
+
        if strings.HasPrefix(req.URL.Host, "www.reddit.com") {
                req.URL.Host = "old.reddit.com"
                http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently)
                return
        }
+
        resp, err := transport.RoundTrip(req)
        if err != nil {
                sinkErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
@@ -152,8 +161,17 @@ func roundTrip(w http.ResponseWriter, req *http.Request) {
                w.Write([]byte(err.Error()))
                return
        }
-       contentType := resp.Header.Get("Content-Type")
-       switch contentType {
+
+       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
@@ -167,15 +185,79 @@ func roundTrip(w http.ResponseWriter, req *http.Request) {
                )
                resp.Body.Close()
                return
-       }
-       for k, vs := range resp.Header {
-               if k == "Location" || k == "Content-Type" || k == "Content-Length" {
-                       continue
+       case "image/webp":
+               if strings.Contains(req.Header.Get("User-Agent"), "AppleWebKit/538.15") {
+                       // My Xombrero
+                       break
                }
-               for _, v := range vs {
-                       w.Header().Add(k, v)
+               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 {
@@ -209,6 +291,7 @@ func roundTrip(w http.ResponseWriter, req *http.Request) {
                )
                return
        }
+
 NoRedir:
        for _, h := range []string{"Location", "Content-Type", "Content-Length"} {
                if v := resp.Header.Get(h); v != "" {