From ded4294c451b2aac77ecf51133b5d48de1370f9d Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Fri, 19 Mar 2021 10:10:47 +0100 Subject: [PATCH] Add support for Tor --- Dockerfile | 2 +- config.go | 1 + example-config.yml | 2 ++ go.mod | 5 +-- go.sum | 11 ++++--- http.go | 29 +++++++++++++---- tor.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 tor.go diff --git a/Dockerfile b/Dockerfile index 587d956..f886ec6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app RUN go build --tags "libsqlite3 linux sqlite_fts5" FROM alpine:3.13 -RUN apk add --no-cache sqlite-dev tzdata +RUN apk add --no-cache sqlite-dev tzdata tor COPY templates/ /app/templates/ COPY --from=build /app/GoBlog /bin/ WORKDIR /app diff --git a/config.go b/config.go index eb68b68..68867af 100644 --- a/config.go +++ b/config.go @@ -30,6 +30,7 @@ type configServer struct { PublicAddress string `mapstructure:"publicAddress"` ShortPublicAddress string `mapstructure:"shortPublicAddress"` PublicHTTPS bool `mapstructure:"publicHttps"` + Tor bool `mapstructure:"tor"` SecurityHeaders bool `mapstructure:"securityHeaders"` CSPDomains []string `mapstructure:"cspDomains"` LetsEncryptMail string `mapstructure:"letsEncryptMail"` diff --git a/example-config.yml b/example-config.yml index ef53fa3..e292b11 100644 --- a/example-config.yml +++ b/example-config.yml @@ -23,6 +23,8 @@ server: - media.example.com # Cookies jwtSecret: changeThisWeakSecret # JWT secret to use for Json Web Token in cookies (login and captcha) + # Tor + tor: true # Publish onion service, requires Tor to be installed and available in path # Cache cache: diff --git a/go.mod b/go.mod index f428ee3..254440d 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e github.com/boombuler/barcode v1.0.1 // indirect github.com/caddyserver/certmagic v0.12.0 + github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dgraph-io/ristretto v0.0.4-0.20210311064603-e4f298c8aa88 github.com/dgrijalva/jwt-go v3.2.0+incompatible @@ -36,7 +37,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.6 github.com/mholt/acmez v0.1.3 // indirect github.com/microcosm-cc/bluemonday v1.0.4 - github.com/miekg/dns v1.1.40 // indirect + github.com/miekg/dns v1.1.41 // indirect github.com/mitchellh/go-server-timing v1.0.1 github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/pelletier/go-toml v1.8.1 // indirect @@ -60,7 +61,7 @@ require ( golang.org/x/mod v0.4.1 // indirect golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect + golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/text v0.3.5 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 6458c7d..f6aa32e 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca h1:Q2r7AxHdJwWfLtBZwvW621M3sPqxPc6ITv2j1FGsYpw= +github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -234,8 +236,8 @@ github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDE github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA= -github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-server-timing v1.0.1 h1:f00/aIe8T3MrnLhQHu3tSWvnwc5GV/p5eutuu3hF/tE= @@ -449,9 +451,10 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M= -golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d h1:jbzgAvDZn8aEnytae+4ou0J0GwFZoHR0hOrTg4qH8GA= +golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/http.go b/http.go index ec2ed0e..c8f853e 100644 --- a/http.go +++ b/http.go @@ -63,6 +63,14 @@ func startServer() (err error) { if err != nil { return } + // Start Onion service + if appConfig.Server.Tor { + go func() { + torErr := startOnionService(finalHandler) + log.Println("Tor failed:", torErr.Error()) + }() + } + // Start HTTP(s) server localAddress := ":" + strconv.Itoa(appConfig.Server.Port) if appConfig.Server.PublicHTTPS { certmagic.Default.Storage = &certmagic.FileStorage{Path: "data/https"} @@ -74,8 +82,6 @@ func startServer() (err error) { hosts = append(hosts, appConfig.Server.shortPublicHostname) } err = certmagic.HTTPS(hosts, finalHandler) - } else if appConfig.Server.SecurityHeaders { - err = http.ListenAndServe(localAddress, finalHandler) } else { err = http.ListenAndServe(localAddress, finalHandler) } @@ -457,23 +463,32 @@ func buildHandler() (http.Handler, error) { return r, nil } -func securityHeaders(next http.Handler) http.Handler { - extraCSPDomains := "" +var cspDomains = "" + +func refreshCSPDomains() { + cspDomains = "" if mp := appConfig.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" { if u, err := url.Parse(mp.MediaURL); err == nil { - extraCSPDomains += " " + u.Hostname() + cspDomains += " " + u.Hostname() } } if len(appConfig.Server.CSPDomains) > 0 { - extraCSPDomains += " " + strings.Join(appConfig.Server.CSPDomains, " ") + cspDomains += " " + strings.Join(appConfig.Server.CSPDomains, " ") } +} + +func securityHeaders(next http.Handler) http.Handler { + refreshCSPDomains() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Strict-Transport-Security", "max-age=31536000;") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "SAMEORIGIN") w.Header().Set("X-Xss-Protection", "1; mode=block") - w.Header().Set("Content-Security-Policy", "default-src 'self'"+extraCSPDomains) + w.Header().Set("Content-Security-Policy", "default-src 'self'"+cspDomains) + if appConfig.Server.Tor && torAddress != "" { + w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", torAddress, r.URL.Path)) + } next.ServeHTTP(w, r) }) } diff --git a/tor.go b/tor.go new file mode 100644 index 0000000..820f39c --- /dev/null +++ b/tor.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "crypto" + "crypto/ed25519" + "crypto/x509" + "encoding/pem" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/cretz/bine/tor" +) + +var ( + torAddress string +) + +func startOnionService(h http.Handler) error { + torDataPath, err := filepath.Abs("data/tor") + if err != nil { + return err + } + err = os.MkdirAll(torDataPath, 0644) + if err != nil { + return err + } + // Initialize private key + torKeyPath := filepath.Join(torDataPath, "onion.pk") + var torKey crypto.PrivateKey + if _, err := os.Stat(torKeyPath); os.IsNotExist(err) { + _, torKey, err = ed25519.GenerateKey(nil) + if err != nil { + return err + } + x509Encoded, err := x509.MarshalPKCS8PrivateKey(torKey) + if err != nil { + return err + } + pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509Encoded}) + os.WriteFile(torKeyPath, pemEncoded, os.ModePerm) + } else { + d, _ := os.ReadFile(torKeyPath) + block, _ := pem.Decode(d) + x509Encoded := block.Bytes + torKey, err = x509.ParsePKCS8PrivateKey(x509Encoded) + if err != nil { + return err + } + } + // Start tor with default config (can set start conf's DebugWriter to os.Stdout for debug logs) + log.Println("Starting and registering onion service, please wait a couple of minutes...") + t, err := tor.Start(nil, &tor.StartConf{ + TempDataDirBase: os.TempDir(), + }) + if err != nil { + return err + } + defer t.Close() + // Wait at most a few minutes to publish the service + listenCtx, listenCancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer listenCancel() + // Create a v3 onion service to listen on any port but show as 80 + onion, err := t.Listen(listenCtx, &tor.ListenConf{ + Version3: true, + Key: torKey, + LocalPort: 8888, + RemotePorts: []int{80}, + }) + if err != nil { + return err + } + defer onion.Close() + torAddress = onion.String() + log.Println("Onion service published on http://" + torAddress) + // Serve handler + return http.Serve(onion, h) +}