Support custom ACME server, HTTP-01 challenge and external accout binding for automatic HTTPS

This commit is contained in:
Jan-Lukas Else 2022-04-12 08:48:09 +02:00
parent b04bf3be46
commit 634f139bae
9 changed files with 85 additions and 30 deletions

54
acme.go Normal file
View File

@ -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
}

View File

@ -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)
}

4
app.go
View File

@ -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

View File

@ -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"`

View File

@ -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

6
go.mod
View File

@ -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

12
go.sum
View File

@ -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=

View File

@ -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,
}

View File

@ -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)