Document more flags & make http port customizable (#183)

Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/183
This commit is contained in:
6543 2023-02-13 20:14:45 +00:00
parent 46316f9e2f
commit 9a3d1c36dc
9 changed files with 97 additions and 77 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ build/
vendor/ vendor/
pages pages
certs.sqlite certs.sqlite
.bash_history

View File

@ -50,3 +50,6 @@ integration:
integration-run TEST: integration-run TEST:
go test -race -tags 'integration {{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/... go test -race -tags 'integration {{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/...
docker:
docker run --rm -it --user $(id -u) -v $(pwd):/work --workdir /work -e HOME=/work codeberg.org/6543/docker-images/golang_just

View File

@ -8,11 +8,13 @@ var (
CertStorageFlags = []cli.Flag{ CertStorageFlags = []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "db-type", Name: "db-type",
Usage: "Specify the database driver. Valid options are \"sqlite3\", \"mysql\" and \"postgres\". Read more at https://xorm.io",
Value: "sqlite3", Value: "sqlite3",
EnvVars: []string{"DB_TYPE"}, EnvVars: []string{"DB_TYPE"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "db-conn", Name: "db-conn",
Usage: "Specify the database connection. For \"sqlite3\" it's the filepath. Read more at https://go.dev/doc/tutorial/database-access",
Value: "certs.sqlite", Value: "certs.sqlite",
EnvVars: []string{"DB_CONN"}, EnvVars: []string{"DB_CONN"},
}, },
@ -87,15 +89,21 @@ var (
EnvVars: []string{"HOST"}, EnvVars: []string{"HOST"},
Value: "[::]", Value: "[::]",
}, },
&cli.StringFlag{ &cli.UintFlag{
Name: "port", Name: "port",
Usage: "specifies port of listening address", Usage: "specifies the https port to listen to ssl requests",
EnvVars: []string{"PORT"}, EnvVars: []string{"PORT", "HTTPS_PORT"},
Value: "443", Value: 443,
},
&cli.UintFlag{
Name: "http-port",
Usage: "specifies the http port, you also have to enable http server via ENABLE_HTTP_SERVER=true",
EnvVars: []string{"HTTP_PORT"},
Value: 80,
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "enable-http-server", Name: "enable-http-server",
// TODO: desc Usage: "start a http server to redirect to https and respond to http acme challenges",
EnvVars: []string{"ENABLE_HTTP_SERVER"}, EnvVars: []string{"ENABLE_HTTP_SERVER"},
}, },
&cli.StringFlag{ &cli.StringFlag{
@ -126,22 +134,22 @@ var (
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "acme-accept-terms", Name: "acme-accept-terms",
// TODO: Usage Usage: "To accept the ACME ToS",
EnvVars: []string{"ACME_ACCEPT_TERMS"}, EnvVars: []string{"ACME_ACCEPT_TERMS"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "acme-eab-kid", Name: "acme-eab-kid",
// TODO: Usage Usage: "Register the current account to the ACME server with external binding.",
EnvVars: []string{"ACME_EAB_KID"}, EnvVars: []string{"ACME_EAB_KID"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "acme-eab-hmac", Name: "acme-eab-hmac",
// TODO: Usage Usage: "Register the current account to the ACME server with external binding.",
EnvVars: []string{"ACME_EAB_HMAC"}, EnvVars: []string{"ACME_EAB_HMAC"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "dns-provider", Name: "dns-provider",
Usage: "Use DNS-Challenge for main domain\n\nRead more at: https://go-acme.github.io/lego/dns/", Usage: "Use DNS-Challenge for main domain. Read more at: https://go-acme.github.io/lego/dns/",
EnvVars: []string{"DNS_PROVIDER"}, EnvVars: []string{"DNS_PROVIDER"},
}, },
&cli.StringFlag{ &cli.StringFlag{

View File

@ -14,7 +14,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"codeberg.org/codeberg/pages/server"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/certificates" "codeberg.org/codeberg/pages/server/certificates"
"codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/gitea"
@ -48,7 +47,9 @@ func Serve(ctx *cli.Context) error {
rawDomain := ctx.String("raw-domain") rawDomain := ctx.String("raw-domain")
mainDomainSuffix := ctx.String("pages-domain") mainDomainSuffix := ctx.String("pages-domain")
rawInfoPage := ctx.String("raw-info-page") rawInfoPage := ctx.String("raw-info-page")
listeningAddress := fmt.Sprintf("%s:%s", ctx.String("host"), ctx.String("port")) listeningHost := ctx.String("host")
listeningSSLAddress := fmt.Sprintf("%s:%d", listeningHost, ctx.Uint("port"))
listeningHTTPAddress := fmt.Sprintf("%s:%d", listeningHost, ctx.Uint("http-port"))
enableHTTPServer := ctx.Bool("enable-http-server") enableHTTPServer := ctx.Bool("enable-http-server")
allowedCorsDomains := AllowedCorsDomains allowedCorsDomains := AllowedCorsDomains
@ -91,22 +92,14 @@ func Serve(ctx *cli.Context) error {
return err return err
} }
// Create handler based on settings // Create listener for SSL connections
httpsHandler := handler.Handler(mainDomainSuffix, rawDomain, log.Info().Msgf("Listening on https://%s", listeningSSLAddress)
giteaClient, listener, err := net.Listen("tcp", listeningSSLAddress)
rawInfoPage,
BlacklistedPaths, allowedCorsDomains,
dnsLookupCache, canonicalDomainCache)
httpHandler := server.SetupHTTPACMEChallengeServer(challengeCache)
// Setup listener and TLS
log.Info().Msgf("Listening on https://%s", listeningAddress)
listener, err := net.Listen("tcp", listeningAddress)
if err != nil { if err != nil {
return fmt.Errorf("couldn't create listener: %v", err) return fmt.Errorf("couldn't create listener: %v", err)
} }
// Setup listener for SSL connections
listener = tls.NewListener(listener, certificates.TLSConfig(mainDomainSuffix, listener = tls.NewListener(listener, certificates.TLSConfig(mainDomainSuffix,
giteaClient, giteaClient,
acmeClient, acmeClient,
@ -119,18 +112,29 @@ func Serve(ctx *cli.Context) error {
go certificates.MaintainCertDB(certMaintainCtx, interval, acmeClient, mainDomainSuffix, certDB) go certificates.MaintainCertDB(certMaintainCtx, interval, acmeClient, mainDomainSuffix, certDB)
if enableHTTPServer { if enableHTTPServer {
// Create handler for http->https redirect and http acme challenges
httpHandler := certificates.SetupHTTPACMEChallengeServer(challengeCache)
// Create listener for http and start listening
go func() { go func() {
log.Info().Msg("Start HTTP server listening on :80") log.Info().Msgf("Start HTTP server listening on %s", listeningHTTPAddress)
err := http.ListenAndServe("[::]:80", httpHandler) err := http.ListenAndServe(listeningHTTPAddress, httpHandler)
if err != nil { if err != nil {
log.Panic().Err(err).Msg("Couldn't start HTTP fastServer") log.Panic().Err(err).Msg("Couldn't start HTTP fastServer")
} }
}() }()
} }
// Start the web fastServer // Create ssl handler based on settings
sslHandler := handler.Handler(mainDomainSuffix, rawDomain,
giteaClient,
rawInfoPage,
BlacklistedPaths, allowedCorsDomains,
dnsLookupCache, canonicalDomainCache)
// Start the ssl listener
log.Info().Msgf("Start listening on %s", listener.Addr()) log.Info().Msgf("Start listening on %s", listener.Addr())
if err := http.Serve(listener, httpsHandler); err != nil { if err := http.Serve(listener, sslHandler); err != nil {
log.Panic().Err(err).Msg("Couldn't start fastServer") log.Panic().Err(err).Msg("Couldn't start fastServer")
} }

View File

@ -43,6 +43,11 @@ func createAcmeClient(ctx *cli.Context, enableHTTPServer bool, challengeCache ca
if (!acmeAcceptTerms || dnsProvider == "") && acmeAPI != "https://acme.mock.directory" { if (!acmeAcceptTerms || dnsProvider == "") && acmeAPI != "https://acme.mock.directory" {
return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig) return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig)
} }
if acmeEabHmac != "" && acmeEabKID == "" {
return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig)
} else if acmeEabHmac == "" && acmeEabKID != "" {
return nil, fmt.Errorf("%w: ACME_EAB_KID also needs ACME_EAB_HMAC to be set", ErrAcmeMissConfig)
}
return certificates.NewAcmeClient( return certificates.NewAcmeClient(
acmeAccountConf, acmeAccountConf,

View File

@ -14,6 +14,8 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const challengePath = "/.well-known/acme-challenge/"
func setupAcmeConfig(configFile, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) { func setupAcmeConfig(configFile, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) {
var myAcmeAccount AcmeAccount var myAcmeAccount AcmeAccount
var myAcmeConfig *lego.Config var myAcmeConfig *lego.Config

View File

@ -1,11 +1,15 @@
package certificates package certificates
import ( import (
"net/http"
"strings"
"time" "time"
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/context"
"codeberg.org/codeberg/pages/server/utils"
) )
type AcmeTLSChallengeProvider struct { type AcmeTLSChallengeProvider struct {
@ -39,3 +43,18 @@ func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
a.challengeCache.Remove(domain + "/" + token) a.challengeCache.Remove(domain + "/" + token)
return nil return nil
} }
func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
ctx := context.New(w, req)
if strings.HasPrefix(ctx.Path(), challengePath) {
challenge, ok := challengeCache.Get(utils.TrimHostPort(ctx.Host()) + "/" + strings.TrimPrefix(ctx.Path(), challengePath))
if !ok || challenge == nil {
ctx.String("no challenge for this token", http.StatusNotFound)
}
ctx.String(challenge.(string))
} else {
ctx.Redirect("https://"+ctx.Host()+ctx.Path(), http.StatusMovedPermanently)
}
}
}

View File

@ -36,22 +36,23 @@ func TLSConfig(mainDomainSuffix string,
return &tls.Config{ return &tls.Config{
// check DNS name & get certificate from Let's Encrypt // check DNS name & get certificate from Let's Encrypt
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
sni := strings.ToLower(strings.TrimSpace(info.ServerName)) domain := strings.ToLower(strings.TrimSpace(info.ServerName))
if len(sni) < 1 { if len(domain) < 1 {
return nil, errors.New("missing sni") return nil, errors.New("missing domain info via SNI (RFC 4366, Section 3.1)")
} }
// https request init is actually a acme challenge
if info.SupportedProtos != nil { if info.SupportedProtos != nil {
for _, proto := range info.SupportedProtos { for _, proto := range info.SupportedProtos {
if proto != tlsalpn01.ACMETLS1Protocol { if proto != tlsalpn01.ACMETLS1Protocol {
continue continue
} }
challenge, ok := challengeCache.Get(sni) challenge, ok := challengeCache.Get(domain)
if !ok { if !ok {
return nil, errors.New("no challenge for this domain") return nil, errors.New("no challenge for this domain")
} }
cert, err := tlsalpn01.ChallengeCert(sni, challenge.(string)) cert, err := tlsalpn01.ChallengeCert(domain, challenge.(string))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -61,22 +62,22 @@ func TLSConfig(mainDomainSuffix string,
targetOwner := "" targetOwner := ""
mayObtainCert := true mayObtainCert := true
if strings.HasSuffix(sni, mainDomainSuffix) || strings.EqualFold(sni, mainDomainSuffix[1:]) { if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) {
// deliver default certificate for the main domain (*.codeberg.page) // deliver default certificate for the main domain (*.codeberg.page)
sni = mainDomainSuffix domain = mainDomainSuffix
} else { } else {
var targetRepo, targetBranch string var targetRepo, targetBranch string
targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(sni, mainDomainSuffix, dnsLookupCache) targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, dnsLookupCache)
if targetOwner == "" { if targetOwner == "" {
// DNS not set up, return main certificate to redirect to the docs // DNS not set up, return main certificate to redirect to the docs
sni = mainDomainSuffix domain = mainDomainSuffix
} else { } else {
targetOpt := &upstream.Options{ targetOpt := &upstream.Options{
TargetOwner: targetOwner, TargetOwner: targetOwner,
TargetRepo: targetRepo, TargetRepo: targetRepo,
TargetBranch: targetBranch, TargetBranch: targetBranch,
} }
_, valid := targetOpt.CheckCanonicalDomain(giteaClient, sni, mainDomainSuffix, canonicalDomainCache) _, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainCache)
if !valid { if !valid {
// We shouldn't obtain a certificate when we cannot check if the // We shouldn't obtain a certificate when we cannot check if the
// repository has specified this domain in the `.domains` file. // repository has specified this domain in the `.domains` file.
@ -85,30 +86,34 @@ func TLSConfig(mainDomainSuffix string,
} }
} }
if tlsCertificate, ok := keyCache.Get(sni); ok { if tlsCertificate, ok := keyCache.Get(domain); ok {
// we can use an existing certificate object // we can use an existing certificate object
return tlsCertificate.(*tls.Certificate), nil return tlsCertificate.(*tls.Certificate), nil
} }
var tlsCertificate *tls.Certificate var tlsCertificate *tls.Certificate
var err error var err error
if tlsCertificate, err = acmeClient.retrieveCertFromDB(sni, mainDomainSuffix, false, certDB); err != nil { if tlsCertificate, err = acmeClient.retrieveCertFromDB(domain, mainDomainSuffix, false, certDB); err != nil {
// request a new certificate if !errors.Is(err, database.ErrNotFound) {
if strings.EqualFold(sni, mainDomainSuffix) { return nil, err
}
// we could not find a cert in db, request a new certificate
// first check if we are allowed to obtain a cert for this domain
if strings.EqualFold(domain, mainDomainSuffix) {
return nil, errors.New("won't request certificate for main domain, something really bad has happened") return nil, errors.New("won't request certificate for main domain, something really bad has happened")
} }
if !mayObtainCert { if !mayObtainCert {
return nil, fmt.Errorf("won't request certificate for %q", sni) return nil, fmt.Errorf("won't request certificate for %q", domain)
} }
tlsCertificate, err = acmeClient.obtainCert(acmeClient.legoClient, []string{sni}, nil, targetOwner, false, mainDomainSuffix, certDB) tlsCertificate, err = acmeClient.obtainCert(acmeClient.legoClient, []string{domain}, nil, targetOwner, false, mainDomainSuffix, certDB)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if err := keyCache.Set(sni, tlsCertificate, 15*time.Minute); err != nil { if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil {
return nil, err return nil, err
} }
return tlsCertificate, nil return tlsCertificate, nil
@ -164,7 +169,7 @@ func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProv
if !strings.EqualFold(sni, mainDomainSuffix) { if !strings.EqualFold(sni, mainDomainSuffix) {
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0]) tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsin leaf tlsCert: %w", err) return nil, fmt.Errorf("error parsing leaf tlsCert: %w", err)
} }
// renew certificates 7 days before they expire // renew certificates 7 days before they expire

View File

@ -1,27 +0,0 @@
package server
import (
"net/http"
"strings"
"codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/context"
"codeberg.org/codeberg/pages/server/utils"
)
func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFunc {
challengePath := "/.well-known/acme-challenge/"
return func(w http.ResponseWriter, req *http.Request) {
ctx := context.New(w, req)
if strings.HasPrefix(ctx.Path(), challengePath) {
challenge, ok := challengeCache.Get(utils.TrimHostPort(ctx.Host()) + "/" + strings.TrimPrefix(ctx.Path(), challengePath))
if !ok || challenge == nil {
ctx.String("no challenge for this token", http.StatusNotFound)
}
ctx.String(challenge.(string))
} else {
ctx.Redirect("https://"+ctx.Host()+ctx.Path(), http.StatusMovedPermanently)
}
}
}