2024-01-01 13:19:19 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"git.polynom.me/rio/internal/certificates"
|
|
|
|
"git.polynom.me/rio/internal/dns"
|
2024-01-01 19:05:20 +00:00
|
|
|
"git.polynom.me/rio/internal/repo"
|
2024-01-01 13:19:19 +00:00
|
|
|
|
2024-01-01 19:05:20 +00:00
|
|
|
"code.gitea.io/sdk/gitea"
|
2024-01-01 13:19:19 +00:00
|
|
|
"github.com/go-acme/lego/v4/lego"
|
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// To access requestingDomains, first acquire the lock.
|
2024-01-01 13:47:13 +00:00
|
|
|
domainsLock = sync.Mutex{}
|
2024-01-01 13:19:19 +00:00
|
|
|
|
2024-01-01 13:47:13 +00:00
|
|
|
// Domain -> _. Check if domain is a key here to see if we're already requesting
|
|
|
|
// or renewing a certificate for that domain.
|
|
|
|
workingDomains = make(map[string]bool)
|
2024-01-01 13:19:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func lockIfUnlockedDomain(domain string) bool {
|
2024-01-01 13:47:13 +00:00
|
|
|
domainsLock.Lock()
|
|
|
|
defer domainsLock.Unlock()
|
2024-01-01 13:19:19 +00:00
|
|
|
|
2024-01-01 13:47:13 +00:00
|
|
|
_, found := workingDomains[domain]
|
2024-01-01 13:19:19 +00:00
|
|
|
if !found {
|
2024-01-01 13:47:13 +00:00
|
|
|
workingDomains[domain] = true
|
2024-01-01 13:19:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return found
|
|
|
|
}
|
|
|
|
|
|
|
|
func unlockDomain(domain string) {
|
2024-01-01 13:47:13 +00:00
|
|
|
domainsLock.Lock()
|
|
|
|
defer domainsLock.Unlock()
|
2024-01-01 13:19:19 +00:00
|
|
|
|
2024-01-01 13:47:13 +00:00
|
|
|
delete(workingDomains, domain)
|
2024-01-01 13:19:19 +00:00
|
|
|
}
|
|
|
|
|
2024-01-01 19:05:20 +00:00
|
|
|
func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, giteaClient *gitea.Client) *tls.Config {
|
2024-01-01 13:19:19 +00:00
|
|
|
return &tls.Config{
|
|
|
|
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
|
|
// Validate that we should even care about this domain
|
2024-01-01 19:05:20 +00:00
|
|
|
cname := ""
|
2024-01-01 13:19:19 +00:00
|
|
|
if !strings.HasSuffix(info.ServerName, pagesDomain) {
|
|
|
|
// Note: We do not check err here because err != nil
|
|
|
|
// always implies that cname == "", which does not have
|
|
|
|
// pagesDomain as a suffix.
|
2024-01-01 19:05:20 +00:00
|
|
|
cname, _ = dns.LookupCNAME(info.ServerName)
|
2024-01-01 13:19:19 +00:00
|
|
|
if !strings.HasSuffix(cname, pagesDomain) {
|
|
|
|
log.Warnf("Got ServerName for Domain %s that we're not responsible for", info.ServerName)
|
|
|
|
return cache.FallbackCertificate.TlsCertificate, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we want to access <user>.<pages domain>, then we can just
|
|
|
|
// use a wildcard certificate.
|
|
|
|
domain := info.ServerName
|
|
|
|
/*if strings.HasSuffix(info.ServerName, pagesDomain) {
|
|
|
|
domain = "*." + pagesDomain
|
|
|
|
}*/
|
|
|
|
|
2024-01-01 19:05:20 +00:00
|
|
|
// Figure out a username for later username checks
|
|
|
|
username := ""
|
|
|
|
if cname == "" {
|
|
|
|
// domain ends on pagesDomain
|
|
|
|
username = strings.Split(domain, ".")[0]
|
|
|
|
} else {
|
|
|
|
// cname ends on pagesDomain
|
|
|
|
username = strings.Split(cname, ".")[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the correct certificate
|
2024-01-01 13:19:19 +00:00
|
|
|
cert, found := cache.Certificates[info.ServerName]
|
|
|
|
if found {
|
|
|
|
if cert.IsValid() {
|
|
|
|
return cert.TlsCertificate, nil
|
|
|
|
} else {
|
2024-01-01 19:05:20 +00:00
|
|
|
if !repo.CanRequestCertificate(username, giteaClient) {
|
|
|
|
log.Warnf(
|
|
|
|
"Cannot renew certificate for %s because CanRequestCertificate(%s) returned false",
|
|
|
|
domain,
|
|
|
|
username,
|
|
|
|
)
|
|
|
|
return cert.TlsCertificate, nil
|
|
|
|
}
|
|
|
|
|
2024-01-01 13:19:19 +00:00
|
|
|
// If we're already working on the domain,
|
|
|
|
// return the old certificate
|
|
|
|
if lockIfUnlockedDomain(domain) {
|
|
|
|
return cert.TlsCertificate, nil
|
|
|
|
}
|
|
|
|
defer unlockDomain(domain)
|
|
|
|
|
2024-01-01 19:05:20 +00:00
|
|
|
// Renew the certificate
|
2024-01-01 13:53:57 +00:00
|
|
|
log.Infof("Certificate for %s expired, renewing", domain)
|
2024-01-01 13:47:13 +00:00
|
|
|
newCert, err := certificates.RenewCertificate(&cert, acmeClient)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Failed to renew certificate for %s: %v", domain, err)
|
|
|
|
return cert.TlsCertificate, nil
|
|
|
|
}
|
2024-01-01 13:53:57 +00:00
|
|
|
|
|
|
|
log.Info("Successfully renewed certificate!")
|
2024-01-01 13:47:13 +00:00
|
|
|
cache.AddCert(newCert, cachePath)
|
|
|
|
return newCert.TlsCertificate, nil
|
2024-01-01 13:19:19 +00:00
|
|
|
}
|
|
|
|
} else {
|
2024-01-01 19:05:20 +00:00
|
|
|
if !repo.CanRequestCertificate(username, giteaClient) {
|
|
|
|
log.Warnf(
|
|
|
|
"Cannot request certificate for %s because CanRequestCertificate(%s) returned false",
|
|
|
|
domain,
|
|
|
|
username,
|
|
|
|
)
|
|
|
|
return cache.FallbackCertificate.TlsCertificate, nil
|
|
|
|
}
|
|
|
|
|
2024-01-01 13:19:19 +00:00
|
|
|
// Don't request if we're already requesting.
|
|
|
|
if lockIfUnlockedDomain(domain) {
|
|
|
|
return cache.FallbackCertificate.TlsCertificate, nil
|
|
|
|
}
|
|
|
|
defer unlockDomain(domain)
|
|
|
|
|
|
|
|
// Request new certificate
|
2024-01-01 13:53:57 +00:00
|
|
|
log.Infof("Obtaining new certificate for %s...", domain)
|
2024-01-01 13:19:19 +00:00
|
|
|
cert, err := certificates.ObtainNewCertificate(
|
|
|
|
[]string{domain},
|
|
|
|
acmeClient,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf(
|
|
|
|
"Failed to get certificate for %s: %v",
|
|
|
|
domain,
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
return cache.FallbackCertificate.TlsCertificate, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add to cache and flush
|
2024-01-01 13:53:57 +00:00
|
|
|
log.Info("Successfully obtained new certificate!")
|
2024-01-01 13:19:19 +00:00
|
|
|
cache.AddCert(cert, cachePath)
|
|
|
|
return cert.TlsCertificate, nil
|
|
|
|
}
|
|
|
|
},
|
|
|
|
NextProtos: []string{
|
|
|
|
"http/0.9",
|
|
|
|
"http/1.0",
|
|
|
|
"http/1.1",
|
|
|
|
"h2",
|
|
|
|
"h2c",
|
|
|
|
},
|
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
CipherSuites: []uint16{
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|