mirror of
https://codeberg.org/Codeberg/pages-server.git
synced 2025-04-25 06:16:58 +00:00
Merge branch 'main' into issue115
This commit is contained in:
commit
e6befc48c8
31 changed files with 1408 additions and 656 deletions
95
server/certificates/acme_client.go
Normal file
95
server/certificates/acme_client.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/providers/dns"
|
||||
"github.com/reugn/equalizer"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
)
|
||||
|
||||
type AcmeClient struct {
|
||||
legoClient *lego.Client
|
||||
dnsChallengerLegoClient *lego.Client
|
||||
|
||||
obtainLocks sync.Map
|
||||
|
||||
acmeUseRateLimits bool
|
||||
|
||||
// limiter
|
||||
acmeClientOrderLimit *equalizer.TokenBucket
|
||||
acmeClientRequestLimit *equalizer.TokenBucket
|
||||
acmeClientFailLimit *equalizer.TokenBucket
|
||||
acmeClientCertificateLimitPerUser map[string]*equalizer.TokenBucket
|
||||
}
|
||||
|
||||
func NewAcmeClient(acmeAccountConf, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID, dnsProvider string, acmeAcceptTerms, enableHTTPServer, acmeUseRateLimits bool, challengeCache cache.SetGetKey) (*AcmeClient, error) {
|
||||
acmeConfig, err := setupAcmeConfig(acmeAccountConf, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID, acmeAcceptTerms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acmeClient, err := lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
if enableHTTPServer {
|
||||
err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create HTTP-01 provider")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainDomainAcmeClient, err := lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
if dnsProvider == "" {
|
||||
// using mock server, don't use wildcard certs
|
||||
err := mainDomainAcmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
} else {
|
||||
// use DNS-Challenge https://go-acme.github.io/lego/dns/
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not create DNS Challenge provider: %w", err)
|
||||
}
|
||||
if err := mainDomainAcmeClient.Challenge.SetDNS01Provider(provider); err != nil {
|
||||
return nil, fmt.Errorf("can not create DNS-01 provider: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &AcmeClient{
|
||||
legoClient: acmeClient,
|
||||
dnsChallengerLegoClient: mainDomainAcmeClient,
|
||||
|
||||
acmeUseRateLimits: acmeUseRateLimits,
|
||||
|
||||
obtainLocks: sync.Map{},
|
||||
|
||||
// limiter
|
||||
|
||||
// rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes
|
||||
// TODO: when this is used a lot, we probably have to think of a somewhat better solution?
|
||||
acmeClientOrderLimit: equalizer.NewTokenBucket(25, 15*time.Minute),
|
||||
// rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests)
|
||||
acmeClientRequestLimit: equalizer.NewTokenBucket(5, 1*time.Second),
|
||||
// rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/
|
||||
acmeClientFailLimit: equalizer.NewTokenBucket(5, 1*time.Hour),
|
||||
// checkUserLimit() use this to rate als per user
|
||||
acmeClientCertificateLimitPerUser: map[string]*equalizer.TokenBucket{},
|
||||
}, nil
|
||||
}
|
100
server/certificates/acme_config.go
Normal file
100
server/certificates/acme_config.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func setupAcmeConfig(configFile, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) {
|
||||
var myAcmeAccount AcmeAccount
|
||||
var myAcmeConfig *lego.Config
|
||||
|
||||
if account, err := os.ReadFile(configFile); err == nil {
|
||||
log.Info().Msgf("found existing acme account config file '%s'", configFile)
|
||||
if err := json.Unmarshal(account, &myAcmeAccount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeAccount.Key, err = certcrypto.ParsePEMPrivateKey([]byte(myAcmeAccount.KeyPEM))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = acmeAPI
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
// Validate Config
|
||||
_, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("config validation failed, you might just delete the config file and let it recreate")
|
||||
return nil, fmt.Errorf("acme config validation failed: %w", err)
|
||||
}
|
||||
return myAcmeConfig, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info().Msgf("no existing acme account config found, try to create a new one")
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeAccount = AcmeAccount{
|
||||
Email: acmeMail,
|
||||
Key: privateKey,
|
||||
KeyPEM: string(certcrypto.PEMEncode(privateKey)),
|
||||
}
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = acmeAPI
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
tempClient, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
// accept terms & log in to EAB
|
||||
if acmeEabKID == "" || acmeEabHmac == "" {
|
||||
reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: acmeAcceptTerms})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
} else {
|
||||
reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: acmeAcceptTerms,
|
||||
Kid: acmeEabKID,
|
||||
HmacEncoded: acmeEabHmac,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
}
|
||||
|
||||
if myAcmeAccount.Registration != nil {
|
||||
acmeAccountJSON, err := json.Marshal(myAcmeAccount)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
log.Info().Msgf("new acme account created. write to config file '%s'", configFile)
|
||||
err = os.WriteFile(configFile, acmeAccountJSON, 0o600)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return myAcmeConfig, nil
|
||||
}
|
41
server/certificates/cached_challengers.go
Normal file
41
server/certificates/cached_challengers.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/cache"
|
||||
)
|
||||
|
||||
type AcmeTLSChallengeProvider struct {
|
||||
challengeCache cache.SetGetKey
|
||||
}
|
||||
|
||||
// make sure AcmeTLSChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeTLSChallengeProvider{}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
|
||||
a.challengeCache.Remove(domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
type AcmeHTTPChallengeProvider struct {
|
||||
challengeCache cache.SetGetKey
|
||||
}
|
||||
|
||||
// make sure AcmeHTTPChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeHTTPChallengeProvider{}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain+"/"+token, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
|
||||
a.challengeCache.Remove(domain + "/" + token)
|
||||
return nil
|
||||
}
|
|
@ -1,30 +1,19 @@
|
|||
package certificates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/providers/dns"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/reugn/equalizer"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
|
@ -35,12 +24,13 @@ import (
|
|||
"codeberg.org/codeberg/pages/server/upstream"
|
||||
)
|
||||
|
||||
var ErrUserRateLimitExceeded = errors.New("rate limit exceeded: 10 certificates per user per 24 hours")
|
||||
|
||||
// TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates.
|
||||
func TLSConfig(mainDomainSuffix string,
|
||||
giteaClient *gitea.Client,
|
||||
firstDefaultBranch,
|
||||
dnsProvider string,
|
||||
acmeUseRateLimits bool,
|
||||
acmeClient *AcmeClient,
|
||||
firstDefaultBranch string,
|
||||
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.SetGetKey,
|
||||
certDB database.CertDB,
|
||||
) *tls.Config {
|
||||
|
@ -71,6 +61,7 @@ func TLSConfig(mainDomainSuffix string,
|
|||
}
|
||||
|
||||
targetOwner := ""
|
||||
mayObtainCert := true
|
||||
if strings.HasSuffix(sni, mainDomainSuffix) || strings.EqualFold(sni, mainDomainSuffix[1:]) {
|
||||
// deliver default certificate for the main domain (*.codeberg.page)
|
||||
sni = mainDomainSuffix
|
||||
|
@ -88,7 +79,9 @@ func TLSConfig(mainDomainSuffix string,
|
|||
}
|
||||
_, valid := targetOpt.CheckCanonicalDomain(giteaClient, sni, mainDomainSuffix, canonicalDomainCache)
|
||||
if !valid {
|
||||
sni = mainDomainSuffix
|
||||
// We shouldn't obtain a certificate when we cannot check if the
|
||||
// repository has specified this domain in the `.domains` file.
|
||||
mayObtainCert = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,27 +91,29 @@ func TLSConfig(mainDomainSuffix string,
|
|||
return tlsCertificate.(*tls.Certificate), nil
|
||||
}
|
||||
|
||||
var tlsCertificate tls.Certificate
|
||||
var tlsCertificate *tls.Certificate
|
||||
var err error
|
||||
var ok bool
|
||||
if tlsCertificate, ok = retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); !ok {
|
||||
if tlsCertificate, err = acmeClient.retrieveCertFromDB(sni, mainDomainSuffix, false, certDB); err != nil {
|
||||
// request a new certificate
|
||||
if strings.EqualFold(sni, mainDomainSuffix) {
|
||||
return nil, errors.New("won't request certificate for main domain, something really bad has happened")
|
||||
}
|
||||
|
||||
tlsCertificate, err = obtainCert(acmeClient, []string{sni}, nil, targetOwner, dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
if !mayObtainCert {
|
||||
return nil, fmt.Errorf("won't request certificate for %q", sni)
|
||||
}
|
||||
|
||||
tlsCertificate, err = acmeClient.obtainCert(acmeClient.legoClient, []string{sni}, nil, targetOwner, false, mainDomainSuffix, certDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := keyCache.Set(sni, &tlsCertificate, 15*time.Minute); err != nil {
|
||||
if err := keyCache.Set(sni, tlsCertificate, 15*time.Minute); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tlsCertificate, nil
|
||||
return tlsCertificate, nil
|
||||
},
|
||||
PreferServerCipherSuites: true,
|
||||
NextProtos: []string{
|
||||
"h2",
|
||||
"http/1.1",
|
||||
|
@ -139,165 +134,115 @@ func TLSConfig(mainDomainSuffix string,
|
|||
}
|
||||
}
|
||||
|
||||
func checkUserLimit(user string) error {
|
||||
userLimit, ok := acmeClientCertificateLimitPerUser[user]
|
||||
func (c *AcmeClient) checkUserLimit(user string) error {
|
||||
userLimit, ok := c.acmeClientCertificateLimitPerUser[user]
|
||||
if !ok {
|
||||
// Each Codeberg user can only add 10 new domains per day.
|
||||
// Each user can only add 10 new domains per day.
|
||||
userLimit = equalizer.NewTokenBucket(10, time.Hour*24)
|
||||
acmeClientCertificateLimitPerUser[user] = userLimit
|
||||
c.acmeClientCertificateLimitPerUser[user] = userLimit
|
||||
}
|
||||
if !userLimit.Ask() {
|
||||
return errors.New("rate limit exceeded: 10 certificates per user per 24 hours")
|
||||
return fmt.Errorf("user '%s' error: %w", user, ErrUserRateLimitExceeded)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
acmeClient, mainDomainAcmeClient *lego.Client
|
||||
acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{}
|
||||
)
|
||||
|
||||
// rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes
|
||||
// TODO: when this is used a lot, we probably have to think of a somewhat better solution?
|
||||
var acmeClientOrderLimit = equalizer.NewTokenBucket(25, 15*time.Minute)
|
||||
|
||||
// rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests)
|
||||
var acmeClientRequestLimit = equalizer.NewTokenBucket(5, 1*time.Second)
|
||||
|
||||
// rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/
|
||||
var acmeClientFailLimit = equalizer.NewTokenBucket(5, 1*time.Hour)
|
||||
|
||||
type AcmeTLSChallengeProvider struct {
|
||||
challengeCache cache.SetGetKey
|
||||
}
|
||||
|
||||
// make sure AcmeTLSChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeTLSChallengeProvider{}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
|
||||
a.challengeCache.Remove(domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
type AcmeHTTPChallengeProvider struct {
|
||||
challengeCache cache.SetGetKey
|
||||
}
|
||||
|
||||
// make sure AcmeHTTPChallengeProvider match Provider interface
|
||||
var _ challenge.Provider = AcmeHTTPChallengeProvider{}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error {
|
||||
return a.challengeCache.Set(domain+"/"+token, keyAuth, 1*time.Hour)
|
||||
}
|
||||
|
||||
func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
|
||||
a.challengeCache.Remove(domain + "/" + token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (tls.Certificate, bool) {
|
||||
func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProvider bool, certDB database.CertDB) (*tls.Certificate, error) {
|
||||
// parse certificate from database
|
||||
res, err := certDB.Get(sni)
|
||||
if err != nil {
|
||||
panic(err) // TODO: no panic
|
||||
}
|
||||
if res == nil {
|
||||
return tls.Certificate{}, false
|
||||
return nil, err
|
||||
} else if res == nil {
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
|
||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: document & put into own function
|
||||
if !strings.EqualFold(sni, mainDomainSuffix) {
|
||||
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("error parsin leaf tlsCert: %w", err)
|
||||
}
|
||||
|
||||
// renew certificates 7 days before they expire
|
||||
if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) {
|
||||
// TODO: add ValidUntil to custom res struct
|
||||
// TODO: use ValidTill of custom cert struct
|
||||
if res.CSR != nil && len(res.CSR) > 0 {
|
||||
// CSR stores the time when the renewal shall be tried again
|
||||
nextTryUnix, err := strconv.ParseInt(string(res.CSR), 10, 64)
|
||||
if err == nil && time.Now().Before(time.Unix(nextTryUnix, 0)) {
|
||||
return tlsCertificate, true
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
}
|
||||
// TODO: make a queue ?
|
||||
go (func() {
|
||||
res.CSR = nil // acme client doesn't like CSR to be set
|
||||
tlsCertificate, err = obtainCert(acmeClient, []string{sni}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
if err != nil {
|
||||
if _, err := c.obtainCert(c.legoClient, []string{sni}, res, "", useDnsProvider, mainDomainSuffix, certDB); err != nil {
|
||||
log.Error().Msgf("Couldn't renew certificate for %s: %v", sni, err)
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
return tlsCertificate, true
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
|
||||
var obtainLocks = sync.Map{}
|
||||
|
||||
func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user, dnsProvider, mainDomainSuffix string, acmeUseRateLimits bool, keyDatabase database.CertDB) (tls.Certificate, error) {
|
||||
func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user string, useDnsProvider bool, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) {
|
||||
name := strings.TrimPrefix(domains[0], "*")
|
||||
if dnsProvider == "" && len(domains[0]) > 0 && domains[0][0] == '*' {
|
||||
if useDnsProvider && len(domains[0]) > 0 && domains[0][0] == '*' {
|
||||
domains = domains[1:]
|
||||
}
|
||||
|
||||
// lock to avoid simultaneous requests
|
||||
_, working := obtainLocks.LoadOrStore(name, struct{}{})
|
||||
_, working := c.obtainLocks.LoadOrStore(name, struct{}{})
|
||||
if working {
|
||||
for working {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, working = obtainLocks.Load(name)
|
||||
_, working = c.obtainLocks.Load(name)
|
||||
}
|
||||
cert, ok := retrieveCertFromDB(name, mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase)
|
||||
if !ok {
|
||||
return tls.Certificate{}, errors.New("certificate failed in synchronous request")
|
||||
cert, err := c.retrieveCertFromDB(name, mainDomainSuffix, useDnsProvider, keyDatabase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate failed in synchronous request: %w", err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
defer obtainLocks.Delete(name)
|
||||
defer c.obtainLocks.Delete(name)
|
||||
|
||||
if acmeClient == nil {
|
||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase), nil
|
||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase)
|
||||
}
|
||||
|
||||
// request actual cert
|
||||
var res *certificate.Resource
|
||||
var err error
|
||||
if renew != nil && renew.CertURL != "" {
|
||||
if acmeUseRateLimits {
|
||||
acmeClientRequestLimit.Take()
|
||||
if c.acmeUseRateLimits {
|
||||
c.acmeClientRequestLimit.Take()
|
||||
}
|
||||
log.Debug().Msgf("Renewing certificate for: %v", domains)
|
||||
res, err = acmeClient.Certificate.Renew(*renew, true, false, "")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Couldn't renew certificate for %v, trying to request a new one", domains)
|
||||
if acmeUseRateLimits {
|
||||
acmeClientFailLimit.Take()
|
||||
if c.acmeUseRateLimits {
|
||||
c.acmeClientFailLimit.Take()
|
||||
}
|
||||
res = nil
|
||||
}
|
||||
}
|
||||
if res == nil {
|
||||
if user != "" {
|
||||
if err := checkUserLimit(user); err != nil {
|
||||
return tls.Certificate{}, err
|
||||
if err := c.checkUserLimit(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if acmeUseRateLimits {
|
||||
acmeClientOrderLimit.Take()
|
||||
acmeClientRequestLimit.Take()
|
||||
if c.acmeUseRateLimits {
|
||||
c.acmeClientOrderLimit.Take()
|
||||
c.acmeClientRequestLimit.Take()
|
||||
}
|
||||
log.Debug().Msgf("Re-requesting new certificate for %v", domains)
|
||||
res, err = acmeClient.Certificate.Obtain(certificate.ObtainRequest{
|
||||
|
@ -305,8 +250,8 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
|
|||
Bundle: true,
|
||||
MustStaple: false,
|
||||
})
|
||||
if acmeUseRateLimits && err != nil {
|
||||
acmeClientFailLimit.Take()
|
||||
if c.acmeUseRateLimits && err != nil {
|
||||
c.acmeClientFailLimit.Take()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -314,161 +259,49 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
|
|||
if renew != nil && renew.CertURL != "" {
|
||||
tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey)
|
||||
if err != nil {
|
||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
|
||||
mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase)
|
||||
if err2 != nil {
|
||||
return nil, errors.Join(err, err2)
|
||||
}
|
||||
return mockC, err
|
||||
}
|
||||
leaf, err := leaf(&tlsCertificate)
|
||||
if err == nil && leaf.NotAfter.After(time.Now()) {
|
||||
// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at
|
||||
renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10))
|
||||
if err := keyDatabase.Put(name, renew); err != nil {
|
||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
|
||||
mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase)
|
||||
if err2 != nil {
|
||||
return nil, errors.Join(err, err2)
|
||||
}
|
||||
return mockC, err
|
||||
}
|
||||
return tlsCertificate, nil
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
}
|
||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
|
||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase)
|
||||
}
|
||||
log.Debug().Msgf("Obtained certificate for %v", domains)
|
||||
|
||||
if err := keyDatabase.Put(name, res); err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return nil, err
|
||||
}
|
||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
return tlsCertificate, nil
|
||||
}
|
||||
|
||||
func SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) {
|
||||
const configFile = "acme-account.json"
|
||||
var myAcmeAccount AcmeAccount
|
||||
var myAcmeConfig *lego.Config
|
||||
|
||||
if account, err := os.ReadFile(configFile); err == nil {
|
||||
if err := json.Unmarshal(account, &myAcmeAccount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeAccount.Key, err = certcrypto.ParsePEMPrivateKey([]byte(myAcmeAccount.KeyPEM))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = acmeAPI
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
// Validate Config
|
||||
_, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
// TODO: should we fail hard instead?
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
}
|
||||
return myAcmeConfig, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myAcmeAccount = AcmeAccount{
|
||||
Email: acmeMail,
|
||||
Key: privateKey,
|
||||
KeyPEM: string(certcrypto.PEMEncode(privateKey)),
|
||||
}
|
||||
myAcmeConfig = lego.NewConfig(&myAcmeAccount)
|
||||
myAcmeConfig.CADirURL = acmeAPI
|
||||
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
|
||||
tempClient, err := lego.NewClient(myAcmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
// accept terms & log in to EAB
|
||||
if acmeEabKID == "" || acmeEabHmac == "" {
|
||||
reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: acmeAcceptTerms})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
} else {
|
||||
reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: acmeAcceptTerms,
|
||||
Kid: acmeEabKID,
|
||||
HmacEncoded: acmeEabHmac,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
|
||||
} else {
|
||||
myAcmeAccount.Registration = reg
|
||||
}
|
||||
}
|
||||
|
||||
if myAcmeAccount.Registration != nil {
|
||||
acmeAccountJSON, err := json.Marshal(myAcmeAccount)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
err = os.WriteFile(configFile, acmeAccountJSON, 0o600)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits")
|
||||
select {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return myAcmeConfig, nil
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
|
||||
func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Config, acmeUseRateLimits, enableHTTPServer bool, challengeCache cache.SetGetKey, certDB database.CertDB) error {
|
||||
func SetupMainDomainCertificates(mainDomainSuffix string, acmeClient *AcmeClient, certDB database.CertDB) error {
|
||||
// getting main cert before ACME account so that we can fail here without hitting rate limits
|
||||
mainCertBytes, err := certDB.Get(mainDomainSuffix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cert database is not working")
|
||||
}
|
||||
|
||||
acmeClient, err = lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
if enableHTTPServer {
|
||||
err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create HTTP-01 provider")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainDomainAcmeClient, err = lego.NewClient(acmeConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
|
||||
} else {
|
||||
if dnsProvider == "" {
|
||||
// using mock server, don't use wildcard certs
|
||||
err := mainDomainAcmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
|
||||
}
|
||||
} else {
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create DNS Challenge provider")
|
||||
}
|
||||
err = mainDomainAcmeClient.Challenge.SetDNS01Provider(provider)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Can't create DNS-01 provider")
|
||||
}
|
||||
}
|
||||
if err != nil && !errors.Is(err, database.ErrNotFound) {
|
||||
return fmt.Errorf("cert database is not working: %w", err)
|
||||
}
|
||||
|
||||
if mainCertBytes == nil {
|
||||
_, err = obtainCert(mainDomainAcmeClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, nil, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
_, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, nil, "", true, mainDomainSuffix, certDB)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Couldn't renew main domain certificate, continuing with mock certs only")
|
||||
}
|
||||
|
@ -477,43 +310,29 @@ func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Co
|
|||
return nil
|
||||
}
|
||||
|
||||
func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) {
|
||||
func MaintainCertDB(ctx context.Context, interval time.Duration, acmeClient *AcmeClient, mainDomainSuffix string, certDB database.CertDB) {
|
||||
for {
|
||||
// clean up expired certs
|
||||
now := time.Now()
|
||||
// delete expired certs that will be invalid until next clean up
|
||||
threshold := time.Now().Add(interval)
|
||||
expiredCertCount := 0
|
||||
keyDatabaseIterator := certDB.Items()
|
||||
key, resBytes, err := keyDatabaseIterator.Next()
|
||||
for err == nil {
|
||||
if !strings.EqualFold(string(key), mainDomainSuffix) {
|
||||
resGob := bytes.NewBuffer(resBytes)
|
||||
resDec := gob.NewDecoder(resGob)
|
||||
res := &certificate.Resource{}
|
||||
err = resDec.Decode(res)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
|
||||
if err != nil || tlsCertificates[0].NotAfter.Before(now) {
|
||||
err := certDB.Delete(string(key))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", string(key))
|
||||
} else {
|
||||
expiredCertCount++
|
||||
certs, err := certDB.Items(0, 0)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not get certs from list")
|
||||
} else {
|
||||
for _, cert := range certs {
|
||||
if !strings.EqualFold(cert.Domain, strings.TrimPrefix(mainDomainSuffix, ".")) {
|
||||
if time.Unix(cert.ValidTill, 0).Before(threshold) {
|
||||
err := certDB.Delete(cert.Domain)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", cert.Domain)
|
||||
} else {
|
||||
expiredCertCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
key, resBytes, err = keyDatabaseIterator.Next()
|
||||
}
|
||||
log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount)
|
||||
|
||||
// compact the database
|
||||
msg, err := certDB.Compact()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Compacting key database failed")
|
||||
} else {
|
||||
log.Debug().Msgf("Compacted key database: %s", msg)
|
||||
log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount)
|
||||
}
|
||||
|
||||
// update main cert
|
||||
|
@ -524,11 +343,12 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
|
|||
log.Error().Msgf("Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted", mainDomainSuffix)
|
||||
} else {
|
||||
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
|
||||
|
||||
// renew main certificate 30 days before it expires
|
||||
if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) {
|
||||
if err != nil {
|
||||
log.Error().Err(fmt.Errorf("could not parse cert for mainDomainSuffix: %w", err))
|
||||
} else if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) {
|
||||
// renew main certificate 30 days before it expires
|
||||
go (func() {
|
||||
_, err = obtainCert(mainDomainAcmeClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
|
||||
_, err = acmeClient.obtainCert(acmeClient.dnsChallengerLegoClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", true, mainDomainSuffix, certDB)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Couldn't renew certificate for main domain")
|
||||
}
|
||||
|
|
|
@ -13,14 +13,15 @@ import (
|
|||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
)
|
||||
|
||||
func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) tls.Certificate {
|
||||
func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) {
|
||||
key, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
|
@ -52,7 +53,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB)
|
|||
key,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
|
@ -61,7 +62,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB)
|
|||
Type: "CERTIFICATE",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
outBytes := out.Bytes()
|
||||
res := &certificate.Resource{
|
||||
|
@ -75,12 +76,12 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB)
|
|||
databaseName = mainDomainSuffix
|
||||
}
|
||||
if err := keyDatabase.Put(databaseName, res); err != nil {
|
||||
panic(err)
|
||||
log.Error().Err(err)
|
||||
}
|
||||
|
||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
return tlsCertificate
|
||||
return &tlsCertificate, nil
|
||||
}
|
||||
|
|
|
@ -3,14 +3,16 @@ package certificates
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"codeberg.org/codeberg/pages/server/database"
|
||||
)
|
||||
|
||||
func TestMockCert(t *testing.T) {
|
||||
db, err := database.NewTmpDB()
|
||||
assert.NoError(t, err)
|
||||
cert := mockCert("example.com", "some error msg", "codeberg.page", db)
|
||||
cert, err := mockCert("example.com", "some error msg", "codeberg.page", db)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotEmpty(t, cert) {
|
||||
assert.NotEmpty(t, cert.Certificate)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"github.com/akrylysov/pogreb"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type CertDB interface {
|
||||
|
@ -10,6 +13,63 @@ type CertDB interface {
|
|||
Put(name string, cert *certificate.Resource) error
|
||||
Get(name string) (*certificate.Resource, error)
|
||||
Delete(key string) error
|
||||
Compact() (string, error)
|
||||
Items() *pogreb.ItemIterator
|
||||
Items(page, pageSize int) ([]*Cert, error)
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
Domain string `xorm:"pk NOT NULL UNIQUE 'domain'"`
|
||||
Created int64 `xorm:"created NOT NULL DEFAULT 0 'created'"`
|
||||
Updated int64 `xorm:"updated NOT NULL DEFAULT 0 'updated'"`
|
||||
ValidTill int64 `xorm:" NOT NULL DEFAULT 0 'valid_till'"`
|
||||
// certificate.Resource
|
||||
CertURL string `xorm:"'cert_url'"`
|
||||
CertStableURL string `xorm:"'cert_stable_url'"`
|
||||
PrivateKey []byte `xorm:"'private_key'"`
|
||||
Certificate []byte `xorm:"'certificate'"`
|
||||
IssuerCertificate []byte `xorm:"'issuer_certificate'"`
|
||||
}
|
||||
|
||||
func (c Cert) Raw() *certificate.Resource {
|
||||
return &certificate.Resource{
|
||||
Domain: c.Domain,
|
||||
CertURL: c.CertURL,
|
||||
CertStableURL: c.CertStableURL,
|
||||
PrivateKey: c.PrivateKey,
|
||||
Certificate: c.Certificate,
|
||||
IssuerCertificate: c.IssuerCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
func toCert(name string, c *certificate.Resource) (*Cert, error) {
|
||||
tlsCertificates, err := certcrypto.ParsePEMBundle(c.Certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tlsCertificates) == 0 || tlsCertificates[0] == nil {
|
||||
err := fmt.Errorf("parsed cert resource has no cert")
|
||||
log.Error().Err(err).Str("domain", c.Domain).Msgf("cert: %v", c)
|
||||
return nil, err
|
||||
}
|
||||
validTill := tlsCertificates[0].NotAfter.Unix()
|
||||
|
||||
// handle wildcard certs
|
||||
if name[:1] == "." {
|
||||
name = "*" + name
|
||||
}
|
||||
if name != c.Domain {
|
||||
err := fmt.Errorf("domain key '%s' and cert domain '%s' not equal", name, c.Domain)
|
||||
log.Error().Err(err).Msg("toCert conversion did discover mismatch")
|
||||
// TODO: fail hard: return nil, err
|
||||
}
|
||||
|
||||
return &Cert{
|
||||
Domain: c.Domain,
|
||||
ValidTill: validTill,
|
||||
|
||||
CertURL: c.CertURL,
|
||||
CertStableURL: c.CertStableURL,
|
||||
PrivateKey: c.PrivateKey,
|
||||
Certificate: c.Certificate,
|
||||
IssuerCertificate: c.IssuerCertificate,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/OrlovEvgeny/go-mcache"
|
||||
"github.com/akrylysov/pogreb"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
)
|
||||
|
||||
|
@ -38,13 +37,8 @@ func (p tmpDB) Delete(key string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p tmpDB) Compact() (string, error) {
|
||||
p.intern.Truncate()
|
||||
return "Truncate done", nil
|
||||
}
|
||||
|
||||
func (p tmpDB) Items() *pogreb.ItemIterator {
|
||||
panic("ItemIterator not implemented for tmpDB")
|
||||
func (p tmpDB) Items(page, pageSize int) ([]*Cert, error) {
|
||||
return nil, fmt.Errorf("items not implemented for tmpDB")
|
||||
}
|
||||
|
||||
func NewTmpDB() (CertDB, error) {
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/akrylysov/pogreb"
|
||||
"github.com/akrylysov/pogreb/fs"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var _ CertDB = aDB{}
|
||||
|
||||
type aDB struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
intern *pogreb.DB
|
||||
syncInterval time.Duration
|
||||
}
|
||||
|
||||
func (p aDB) Close() error {
|
||||
p.cancel()
|
||||
return p.intern.Sync()
|
||||
}
|
||||
|
||||
func (p aDB) Put(name string, cert *certificate.Resource) error {
|
||||
var resGob bytes.Buffer
|
||||
if err := gob.NewEncoder(&resGob).Encode(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.intern.Put([]byte(name), resGob.Bytes())
|
||||
}
|
||||
|
||||
func (p aDB) Get(name string) (*certificate.Resource, error) {
|
||||
cert := &certificate.Resource{}
|
||||
resBytes, err := p.intern.Get([]byte(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resBytes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := gob.NewDecoder(bytes.NewBuffer(resBytes)).Decode(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (p aDB) Delete(key string) error {
|
||||
return p.intern.Delete([]byte(key))
|
||||
}
|
||||
|
||||
func (p aDB) Compact() (string, error) {
|
||||
result, err := p.intern.Compact()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%+v", result), nil
|
||||
}
|
||||
|
||||
func (p aDB) Items() *pogreb.ItemIterator {
|
||||
return p.intern.Items()
|
||||
}
|
||||
|
||||
var _ CertDB = &aDB{}
|
||||
|
||||
func (p aDB) sync() {
|
||||
for {
|
||||
err := p.intern.Sync()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Syncing cert database failed")
|
||||
}
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
case <-time.After(p.syncInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func New(path string) (CertDB, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("path not set")
|
||||
}
|
||||
db, err := pogreb.Open(path, &pogreb.Options{
|
||||
BackgroundSyncInterval: 30 * time.Second,
|
||||
BackgroundCompactionInterval: 6 * time.Hour,
|
||||
FileSystem: fs.OSMMap,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := &aDB{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
intern: db,
|
||||
syncInterval: 5 * time.Minute,
|
||||
}
|
||||
|
||||
go result.sync()
|
||||
|
||||
return result, nil
|
||||
}
|
151
server/database/xorm.go
Normal file
151
server/database/xorm.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"xorm.io/xorm"
|
||||
|
||||
// register sql driver
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var _ CertDB = xDB{}
|
||||
|
||||
var ErrNotFound = errors.New("entry not found")
|
||||
|
||||
type xDB struct {
|
||||
engine *xorm.Engine
|
||||
}
|
||||
|
||||
func NewXormDB(dbType, dbConn string) (CertDB, error) {
|
||||
if !supportedDriver(dbType) {
|
||||
return nil, fmt.Errorf("not supported db type '%s'", dbType)
|
||||
}
|
||||
if dbConn == "" {
|
||||
return nil, fmt.Errorf("no db connection provided")
|
||||
}
|
||||
|
||||
e, err := xorm.NewEngine(dbType, dbConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := e.Sync2(new(Cert)); err != nil {
|
||||
return nil, fmt.Errorf("cound not sync db model :%w", err)
|
||||
}
|
||||
|
||||
return &xDB{
|
||||
engine: e,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (x xDB) Close() error {
|
||||
return x.engine.Close()
|
||||
}
|
||||
|
||||
func (x xDB) Put(domain string, cert *certificate.Resource) error {
|
||||
log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db")
|
||||
|
||||
domain = integrationTestReplacements(domain)
|
||||
c, err := toCert(domain, cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.engine.NewSession()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
if exist, _ := sess.ID(c.Domain).Exist(); exist {
|
||||
if _, err := sess.ID(c.Domain).Update(c); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err = sess.Insert(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func (x xDB) Get(domain string) (*certificate.Resource, error) {
|
||||
// handle wildcard certs
|
||||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
domain = integrationTestReplacements(domain)
|
||||
|
||||
cert := new(Cert)
|
||||
log.Trace().Str("domain", domain).Msg("get cert from db")
|
||||
if found, err := x.engine.ID(domain).Get(cert); err != nil {
|
||||
return nil, err
|
||||
} else if !found {
|
||||
return nil, fmt.Errorf("%w: name='%s'", ErrNotFound, domain)
|
||||
}
|
||||
return cert.Raw(), nil
|
||||
}
|
||||
|
||||
func (x xDB) Delete(domain string) error {
|
||||
// handle wildcard certs
|
||||
if domain[:1] == "." {
|
||||
domain = "*" + domain
|
||||
}
|
||||
domain = integrationTestReplacements(domain)
|
||||
|
||||
log.Trace().Str("domain", domain).Msg("delete cert from db")
|
||||
_, err := x.engine.ID(domain).Delete(new(Cert))
|
||||
return err
|
||||
}
|
||||
|
||||
// Items return al certs from db, if pageSize is 0 it does not use limit
|
||||
func (x xDB) Items(page, pageSize int) ([]*Cert, error) {
|
||||
// paginated return
|
||||
if pageSize > 0 {
|
||||
certs := make([]*Cert, 0, pageSize)
|
||||
if page >= 0 {
|
||||
page = 1
|
||||
}
|
||||
err := x.engine.Limit(pageSize, (page-1)*pageSize).Find(&certs)
|
||||
return certs, err
|
||||
}
|
||||
|
||||
// return all
|
||||
certs := make([]*Cert, 0, 64)
|
||||
err := x.engine.Find(&certs)
|
||||
return certs, err
|
||||
}
|
||||
|
||||
// Supported database drivers
|
||||
const (
|
||||
DriverSqlite = "sqlite3"
|
||||
DriverMysql = "mysql"
|
||||
DriverPostgres = "postgres"
|
||||
)
|
||||
|
||||
func supportedDriver(driver string) bool {
|
||||
switch driver {
|
||||
case DriverMysql, DriverPostgres, DriverSqlite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// integrationTestReplacements is needed because integration tests use a single domain cert,
|
||||
// while production use a wildcard cert
|
||||
// TODO: find a better way to handle this
|
||||
func integrationTestReplacements(domainKey string) string {
|
||||
if domainKey == "*.localhost.mock.directory" {
|
||||
return "localhost.mock.directory"
|
||||
}
|
||||
return domainKey
|
||||
}
|
92
server/database/xorm_test.go
Normal file
92
server/database/xorm_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func newTestDB(t *testing.T) *xDB {
|
||||
e, err := xorm.NewEngine("sqlite3", ":memory:")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, e.Sync2(new(Cert)))
|
||||
return &xDB{engine: e}
|
||||
}
|
||||
|
||||
func TestSanitizeWildcardCerts(t *testing.T) {
|
||||
certDB := newTestDB(t)
|
||||
|
||||
_, err := certDB.Get(".not.found")
|
||||
assert.True(t, errors.Is(err, ErrNotFound))
|
||||
|
||||
// TODO: cert key and domain mismatch are don not fail hard jet
|
||||
// https://codeberg.org/Codeberg/pages-server/src/commit/d8595cee882e53d7f44f1ddc4ef8a1f7b8f31d8d/server/database/interface.go#L64
|
||||
//
|
||||
// assert.Error(t, certDB.Put(".wildcard.de", &certificate.Resource{
|
||||
// Domain: "*.localhost.mock.directory",
|
||||
// Certificate: localhost_mock_directory_certificate,
|
||||
// }))
|
||||
|
||||
// insert new wildcard cert
|
||||
assert.NoError(t, certDB.Put(".wildcard.de", &certificate.Resource{
|
||||
Domain: "*.wildcard.de",
|
||||
Certificate: localhost_mock_directory_certificate,
|
||||
}))
|
||||
|
||||
// update existing cert
|
||||
assert.Error(t, certDB.Put(".wildcard.de", &certificate.Resource{
|
||||
Domain: "*.wildcard.de",
|
||||
Certificate: localhost_mock_directory_certificate,
|
||||
}))
|
||||
|
||||
c1, err := certDB.Get(".wildcard.de")
|
||||
assert.NoError(t, err)
|
||||
c2, err := certDB.Get("*.wildcard.de")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, c1, c2)
|
||||
}
|
||||
|
||||
var localhost_mock_directory_certificate = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDczCCAlugAwIBAgIIJyBaXHmLk6gwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
|
||||
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA0OWE0ZmIwHhcNMjMwMjEwMDEwOTA2
|
||||
WhcNMjgwMjEwMDEwOTA2WjAjMSEwHwYDVQQDExhsb2NhbGhvc3QubW9jay5kaXJl
|
||||
Y3RvcnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIU/CjzS7t62Gj
|
||||
neEMqvP7sn99ULT7AEUzEfWL05fWG2z714qcUg1hXkZLgdVDgmsCpplyddip7+2t
|
||||
ZH/9rLPLMqJphzvOL4CF6jDLbeifETtKyjnt9vUZFnnNWcP3tu8lo8iYSl08qsUI
|
||||
Pp/hiEriAQzCDjTbR5m9xUPNPYqxzcS4ALzmmCX9Qfc4CuuhMkdv2G4TT7rylWrA
|
||||
SCSRPnGjeA7pCByfNrO/uXbxmzl3sMO3k5sqgMkx1QIHEN412V8+vtx88mt2sM6k
|
||||
xjzGZWWKXlRq+oufIKX9KPplhsCjMH6E3VNAzgOPYDqXagtUcGmLWghURltO8Mt2
|
||||
zwM6OgjjAgMBAAGjgaUwgaIwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG
|
||||
AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSMQvlJ1755
|
||||
sarf8i1KNqj7s5o/aDAfBgNVHSMEGDAWgBTcZcxJMhWdP7MecHCCpNkFURC/YzAj
|
||||
BgNVHREEHDAaghhsb2NhbGhvc3QubW9jay5kaXJlY3RvcnkwDQYJKoZIhvcNAQEL
|
||||
BQADggEBACcd7TT28OWwzQN2PcH0aG38JX5Wp2iOS/unDCfWjNAztXHW7nBDMxza
|
||||
VtyebkJfccexpuVuOsjOX+bww0vtEYIvKX3/GbkhogksBrNkE0sJZtMnZWMR33wa
|
||||
YxAy/kJBTmLi02r8fX9ZhwjldStHKBav4USuP7DXZjrgX7LFQhR4LIDrPaYqQRZ8
|
||||
ltC3mM9LDQ9rQyIFP5cSBMO3RUAm4I8JyLoOdb/9G2uxjHr7r6eG1g8DmLYSKBsQ
|
||||
mWGQDOYgR3cGltDe2yMxM++yHY+b1uhxGOWMrDA1+1k7yI19LL8Ifi2FMovDfu/X
|
||||
JxYk1NNNtdctwaYJFenmGQvDaIq1KgE=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDUDCCAjigAwIBAgIIKBJ7IIA6W1swDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
|
||||
AxMVUGViYmxlIFJvb3QgQ0EgNTdmZjE2MCAXDTIzMDIwOTA1MzMxMloYDzIwNTMw
|
||||
MjA5MDUzMzEyWjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDQ5
|
||||
YTRmYjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOvlqRx8SXQFWo2
|
||||
gFCiXxls53eENcyr8+meFyjgnS853eEvplaPxoa2MREKd+ZYxM8EMMfj2XGvR3UI
|
||||
aqR5QyLQ9ihuRqvQo4fG91usBHgH+vDbGPdMX8gDmm9HgnmtOVhSKJU+M2jfE1SW
|
||||
UuWB9xOa3LMreTXbTNfZEMoXf+GcWZMbx5WPgEga3DvfmV+RsfNvB55eD7YAyZgF
|
||||
ZnQ3Dskmnxxlkz0EGgd7rqhFHHNB9jARlL22gITADwoWZidlr3ciM9DISymRKQ0c
|
||||
mRN15fQjNWdtuREgJlpXecbYQMGhdTOmFrqdHkveD1o63rGSC4z+s/APV6xIbcRp
|
||||
aNpO7L8CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNxlzEky
|
||||
FZ0/sx5wcIKk2QVREL9jMB8GA1UdIwQYMBaAFOqfkm9rebIz4z0SDIKW5edLg5JM
|
||||
MA0GCSqGSIb3DQEBCwUAA4IBAQBRG9AHEnyj2fKzVDDbQaKHjAF5jh0gwyHoIeRK
|
||||
FkP9mQNSWxhvPWI0tK/E49LopzmVuzSbDd5kZsaii73rAs6f6Rf9W5veo3AFSEad
|
||||
stM+Zv0f2vWB38nuvkoCRLXMX+QUeuL65rKxdEpyArBju4L3/PqAZRgMLcrH+ak8
|
||||
nvw5RdAq+Km/ZWyJgGikK6cfMmh91YALCDFnoWUWrCjkBaBFKrG59ONV9f0IQX07
|
||||
aNfFXFCF5l466xw9dHjw5iaFib10cpY3iq4kyPYIMs6uaewkCtxWKKjiozM4g4w3
|
||||
HqwyUyZ52WUJOJ/6G9DJLDtN3fgGR+IAp8BhYd5CqOscnt3h
|
||||
-----END CERTIFICATE-----`)
|
|
@ -28,7 +28,7 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo
|
|||
names, err := net.LookupTXT(domain)
|
||||
if err == nil {
|
||||
for _, name := range names {
|
||||
name = strings.TrimSuffix(name, ".")
|
||||
name = strings.TrimSuffix(strings.TrimSpace(name), ".")
|
||||
if strings.HasSuffix(name, mainDomainSuffix) {
|
||||
cname = name
|
||||
break
|
||||
|
|
|
@ -80,7 +80,7 @@ type writeCacheReader struct {
|
|||
|
||||
func (t *writeCacheReader) Read(p []byte) (n int, err error) {
|
||||
n, err = t.originalReader.Read(p)
|
||||
if err != nil {
|
||||
if err != nil && err != io.EOF {
|
||||
log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey)
|
||||
t.hasError = true
|
||||
} else if n > 0 {
|
||||
|
|
|
@ -55,7 +55,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
|
|||
// only redirect if the target is also a codeberg page!
|
||||
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
|
||||
if targetOwner != "" {
|
||||
ctx.Redirect("https://"+canonicalDomain+targetOpt.TargetPath, http.StatusTemporaryRedirect)
|
||||
ctx.Redirect("https://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -16,45 +17,54 @@ var canonicalDomainCacheTimeout = 15 * time.Minute
|
|||
const canonicalDomainConfig = ".domains"
|
||||
|
||||
// CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file).
|
||||
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.SetGetKey) (string, bool) {
|
||||
var (
|
||||
domains []string
|
||||
valid bool
|
||||
)
|
||||
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.SetGetKey) (domain string, valid bool) {
|
||||
// Check if this request is cached.
|
||||
if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok {
|
||||
domains = cachedValue.([]string)
|
||||
domains := cachedValue.([]string)
|
||||
for _, domain := range domains {
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig)
|
||||
if err == nil {
|
||||
for _, domain := range strings.Split(string(body), "\n") {
|
||||
domain = strings.ToLower(domain)
|
||||
domain = strings.TrimSpace(domain)
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
domain = strings.TrimPrefix(domain, "https://")
|
||||
if len(domain) > 0 && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Info().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo)
|
||||
return domains[0], valid
|
||||
}
|
||||
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig)
|
||||
if err != nil && !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo)
|
||||
}
|
||||
|
||||
var domains []string
|
||||
for _, domain := range strings.Split(string(body), "\n") {
|
||||
domain = strings.ToLower(domain)
|
||||
domain = strings.TrimSpace(domain)
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
domain = strings.TrimPrefix(domain, "https://")
|
||||
if len(domain) > 0 && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
domains = append(domains, o.TargetOwner+mainDomainSuffix)
|
||||
if domains[len(domains)-1] == actualDomain {
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
if o.TargetRepo != "" && o.TargetRepo != "pages" {
|
||||
domains[len(domains)-1] += "/" + o.TargetRepo
|
||||
}
|
||||
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout)
|
||||
}
|
||||
|
||||
// Add [owner].[pages-domain] as valid domain.
|
||||
domains = append(domains, o.TargetOwner+mainDomainSuffix)
|
||||
if domains[len(domains)-1] == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
|
||||
// If the target repository isn't called pages, add `/[repository]` to the
|
||||
// previous valid domain.
|
||||
if o.TargetRepo != "" && o.TargetRepo != "pages" {
|
||||
domains[len(domains)-1] += "/" + o.TargetRepo
|
||||
}
|
||||
|
||||
// Add result to cache.
|
||||
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout)
|
||||
|
||||
// Return the first domain from the list and return if any of the domains
|
||||
// matched the requested domain.
|
||||
return domains[0], valid
|
||||
}
|
||||
|
|
|
@ -17,17 +17,17 @@ func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) {
|
|||
// Get default branch
|
||||
defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Could't fetch default branch from repository")
|
||||
log.Err(err).Msg("Couldn't fetch default branch from repository")
|
||||
return false, err
|
||||
}
|
||||
log.Debug().Msgf("Succesfully fetched default branch %q from Gitea", defaultBranch)
|
||||
log.Debug().Msgf("Successfully fetched default branch %q from Gitea", defaultBranch)
|
||||
o.TargetBranch = defaultBranch
|
||||
}
|
||||
|
||||
timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(o.TargetOwner, o.TargetRepo, o.TargetBranch)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msg("Could not get latest commit's timestamp from branch")
|
||||
log.Error().Err(err).Msg("Could not get latest commit timestamp from branch")
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) {
|
|||
return false, fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Succesfully fetched latest commit's timestamp from branch: %#v", timestamp)
|
||||
log.Debug().Msgf("Successfully fetched latest commit timestamp from branch: %#v", timestamp)
|
||||
o.BranchTimestamp = timestamp.Timestamp
|
||||
o.TargetBranch = timestamp.Branch
|
||||
return true, nil
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue