diff --git a/acme.go b/acme.go new file mode 100644 index 0000000..a506bdd --- /dev/null +++ b/acme.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/base64" + + "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert" +) + +func (a *goBlog) getAutocertManager() *autocert.Manager { + if a.tailscaleEnabled() || !a.cfg.Server.PublicHTTPS { + return nil + } + if a.autocertManager != nil { + return a.autocertManager + } + // Not initialized yet + a.autocertInit.Do(func() { + // Create hosts whitelist + hosts := []string{a.cfg.Server.publicHostname} + if shn := a.cfg.Server.shortPublicHostname; shn != "" { + hosts = append(hosts, shn) + } + if mhn := a.cfg.Server.mediaHostname; mhn != "" { + hosts = append(hosts, mhn) + } + // Create autocert manager + acmeDir := acme.LetsEncryptURL + if a.cfg.Server.AcmeDir != "" { + acmeDir = a.cfg.Server.AcmeDir + } + m := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(hosts...), + Cache: &httpsCache{db: a.db}, + Client: &acme.Client{DirectoryURL: acmeDir, HTTPClient: a.httpClient}, + } + // Set external account binding + if a.cfg.Server.AcmeEabKid != "" && a.cfg.Server.AcmeEabKey != "" { + key, err := base64.RawURLEncoding.DecodeString(a.cfg.Server.AcmeEabKey) + if err != nil { + return + } + m.ExternalAccountBinding = &acme.ExternalAccountBinding{ + KID: a.cfg.Server.AcmeEabKid, + Key: key, + } + } + // Save + a.autocertManager = m + }) + // Return + return a.autocertManager +} diff --git a/activityStreams.go b/activityStreams.go index 60dcfb6..068243b 100644 --- a/activityStreams.go +++ b/activityStreams.go @@ -209,5 +209,5 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt return } w.Header().Set(contentType, contenttype.ASUTF8) - a.min.Get().Minify(contenttype.AS, w, buf) + _ = a.min.Get().Minify(contenttype.AS, w, buf) } diff --git a/app.go b/app.go index 6d71c5d..0ea9651 100644 --- a/app.go +++ b/app.go @@ -13,6 +13,7 @@ import ( rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/yuin/goldmark" "go.goblog.app/app/pkgs/minify" + "golang.org/x/crypto/acme/autocert" "golang.org/x/sync/singleflight" "tailscale.com/tsnet" ) @@ -29,6 +30,9 @@ type goBlog struct { // Assets assetFileNames map[string]string assetFiles map[string]*assetFile + // Autocert + autocertManager *autocert.Manager + autocertInit sync.Once // Blogroll blogrollCacheGroup singleflight.Group // Blogstats diff --git a/config.go b/config.go index 65234ce..9a88214 100644 --- a/config.go +++ b/config.go @@ -42,6 +42,9 @@ type configServer struct { ShortPublicAddress string `mapstructure:"shortPublicAddress"` MediaAddress string `mapstructure:"mediaAddress"` PublicHTTPS bool `mapstructure:"publicHttps"` + AcmeDir string `mapstructure:"acmeDir"` + AcmeEabKid string `mapstructure:"acmeEabKid"` + AcmeEabKey string `mapstructure:"acmeEabKey"` TailscaleHTTPS bool `mapstructure:"tailscaleHttps"` Tailscale *configTailscale `mapstructure:"tailscale"` Tor bool `mapstructure:"tor"` diff --git a/example-config.yml b/example-config.yml index 84ab060..7974ac2 100644 --- a/example-config.yml +++ b/example-config.yml @@ -28,6 +28,10 @@ server: mediaAddress: https://media.example.com # Optional domain to use for serving media files # Security publicHttps: true # Use Let's Encrypt and serve site with HTTPS + # To use another ACME server like ZeroSSL, set the following + # acmeDir: https://acme.zerossl.com/v2/DV90 + # acmeEabKid: "kid" # Key ID for the EAB key + # acmeEabKey: "key" # Key for the EAB key securityHeaders: true # Set security HTTP headers (to always use HTTPS etc.) cspDomains: # Specify additional domains to allow embedded content with enabled securityHeaders - media.example.com diff --git a/go.mod b/go.mod index c0b9098..5f25a0f 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/paulmach/go.geojson v1.4.0 github.com/posener/wstest v1.2.0 github.com/pquerna/otp v1.3.0 - github.com/samber/lo v1.11.0 + github.com/samber/lo v1.12.0 github.com/schollz/sqlite3dump v1.3.1 github.com/snabb/sitemap v1.0.0 github.com/spf13/cast v1.4.1 @@ -57,8 +57,8 @@ require ( github.com/yuin/goldmark v1.4.11 // master github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38 - golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 - golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 + golang.org/x/net v0.0.0-20220412020605-290c469a71a5 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b diff --git a/go.sum b/go.sum index 0f75ccb..a4db0e5 100644 --- a/go.sum +++ b/go.sum @@ -391,8 +391,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.11.0 h1:JfeYozXL1xfkhRUFOfH13ociyeiLSC/GRJjGKI668xM= -github.com/samber/lo v1.11.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= +github.com/samber/lo v1.12.0 h1:UZXQYR2N6FST+QWxnjV7gPmT9KkXL9eWbZxkrefVs88= +github.com/samber/lo v1.12.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA= github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= @@ -488,8 +488,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= -golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -575,8 +575,8 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= -golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/http.go b/http.go index 747f2c0..7343606 100644 --- a/http.go +++ b/http.go @@ -62,9 +62,13 @@ func (a *goBlog) startServer() (err error) { if a.cfg.Server.PublicHTTPS || a.cfg.Server.TailscaleHTTPS { go func() { // Start HTTP server for redirects + h := http.Handler(http.HandlerFunc(a.redirectToHttps)) + if m := a.getAutocertManager(); m != nil { + h = m.HTTPHandler(h) + } httpServer := &http.Server{ Addr: ":80", - Handler: http.HandlerFunc(a.redirectToHttps), + Handler: h, ReadTimeout: 5 * time.Minute, WriteTimeout: 5 * time.Minute, } diff --git a/httpListener.go b/httpListener.go index 3df251e..bcebd78 100644 --- a/httpListener.go +++ b/httpListener.go @@ -2,11 +2,10 @@ package main import ( "crypto/tls" + "errors" "net" "net/http" - "golang.org/x/crypto/acme" - "golang.org/x/crypto/acme/autocert" "tailscale.com/client/tailscale" ) @@ -15,24 +14,11 @@ func (a *goBlog) getTCPListener(s *http.Server) (net.Listener, error) { // Tailscale listener return a.getTailscaleListener(s.Addr) } else if s.Addr == ":443" && a.cfg.Server.PublicHTTPS { - // Listener with public HTTPS - hosts := []string{a.cfg.Server.publicHostname} - if shn := a.cfg.Server.shortPublicHostname; shn != "" { - hosts = append(hosts, shn) + m := a.getAutocertManager() + if m == nil { + return nil, errors.New("autocert not initialized") } - if mhn := a.cfg.Server.mediaHostname; mhn != "" { - hosts = append(hosts, mhn) - } - acmeDir := acme.LetsEncryptURL - // Uncomment for Staging Let's Encrypt - // acmeDir = "https://acme-staging-v02.api.letsencrypt.org/directory" - m := &autocert.Manager{ - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(hosts...), - Cache: &httpsCache{db: a.db}, - Client: &acme.Client{DirectoryURL: acmeDir}, - } - return m.Listener(), nil + return a.getAutocertManager().Listener(), nil } else if s.Addr == ":443" && a.cfg.Server.TailscaleHTTPS { // Listener with Tailscale TLS config ln, err := net.Listen("tcp", s.Addr)