From da30ec9fa60bdc47aaa48d1fcdb86e2a0940ca36 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 31 Dec 2023 22:41:51 +0100 Subject: [PATCH] Add more code --- account.go | 126 ++++++++++++++++++ acme.go | 343 +++++++++++++++++++++++++++++++++++++++++++++++++ certificate.go | 41 ++++++ dns.go | 39 ++++++ go.mod | 12 +- go.sum | 24 ++++ main.go | 106 ++++++++++++++- repo.go | 18 ++- 8 files changed, 701 insertions(+), 8 deletions(-) create mode 100644 account.go create mode 100644 acme.go create mode 100644 certificate.go diff --git a/account.go b/account.go new file mode 100644 index 0000000..f5f6241 --- /dev/null +++ b/account.go @@ -0,0 +1,126 @@ +package main + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "encoding/base64" + "io/ioutil" + + "github.com/go-acme/lego/v4/acme" + "github.com/go-acme/lego/v4/certcrypto" + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/registration" +) + +type AcmeAccount struct { + Email string + Registration *registration.Resource + Key crypto.PrivateKey +} + +func (a *AcmeAccount) GetEmail() string { + return a.Email +} + +func (a *AcmeAccount) GetPrivateKey() crypto.PrivateKey { + return a.Key +} + +func (a *AcmeAccount) GetRegistration() *registration.Resource { + return a.Registration +} + +func (a *AcmeAccount) FlushToDisk(storage string) { + data := map[string]interface{}{ + "email": a.Email, + "reg": a.Registration, + "private_key_encoded": base64.StdEncoding.EncodeToString( + certcrypto.PEMEncode(a.Key), + ), + } + raw, _ := json.Marshal(data) + ioutil.WriteFile(storage, raw, 0600) +} + +func ClientFromFile(storage, acmeServer string) (*lego.Client, error) { + file, err := ioutil.ReadFile(storage) + if err != nil { + return nil, err + } + + var data map[string]interface{} + _ = json.Unmarshal(file, &data) + reg, _ := data["reg"].(map[string]interface{}) + accountRaw := reg["body"].(map[string]interface{}) + contact := accountRaw["contact"].([]interface{}) + contacts := make([]string, 0) + for _, v := range contact { + contacts = append(contacts, v.(string)) + } + + + registration := registration.Resource{ + URI: reg["uri"].(string), + Body: acme.Account{ + Status: accountRaw["status"].(string), + Contact: contacts, + TermsOfServiceAgreed: true, + //Orders: accountRaw["orders"].(string), + }, + } + + pkEncoded, err := base64.StdEncoding.DecodeString(data["private_key_encoded"].(string)) + pk, err := certcrypto.ParsePEMPrivateKey(pkEncoded) + account := AcmeAccount{ + Email: data["email"].(string), + Key: pk.(*ecdsa.PrivateKey), + Registration: ®istration, + } + config := lego.NewConfig(&account) + config.CADirURL = acmeServer + config.Certificate.KeyType = certcrypto.RSA2048 + + client, err := lego.NewClient(config) + if err != nil { + return nil, err + } + + return client, nil +} + +func GenerateNewAccount(email, storage, acmeServer string) (*lego.Client, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + account := AcmeAccount{ + Email: email, + Key: privateKey, + } + config := lego.NewConfig(&account) + config.CADirURL = acmeServer + config.Certificate.KeyType = certcrypto.RSA2048 + + client, err := lego.NewClient(config) + if err != nil { + return nil, err + } + + // Register it + req, err := client.Registration.Register( + registration.RegisterOptions{ + TermsOfServiceAgreed: true, + }, + ) + if err != nil { + return nil, err + } + account.Registration = req + account.FlushToDisk(storage) + + return client, nil +} diff --git a/acme.go b/acme.go new file mode 100644 index 0000000..1e53e3e --- /dev/null +++ b/acme.go @@ -0,0 +1,343 @@ +package main + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "io/ioutil" + "math/big" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-acme/lego/v4/certcrypto" + "github.com/go-acme/lego/v4/certificate" + "github.com/go-acme/lego/v4/lego" + + log "github.com/sirupsen/logrus" +) + +const ( + AcmeChallengePathPrefix = "/.well-known/acme-challenge/" +) + +var ( + // Well-known -> Challenge solution + runningChallenges = make(map[string]string) + Certificates = CertificatesCache{ + Certificates: make(map[string]CertificateWrapper), + } + + // To access requestingDomains, first acquire the lock. + requestingLock = sync.Mutex{} + + // Domain -> _. Check if domain is a key here to see if we're already requeting + // a certificate for it. + requestingDomains = make(map[string]bool) +) + +func lockIfUnlockedDomain(domain string) bool { + requestingLock.Lock() + defer requestingLock.Unlock() + + _, found := requestingDomains[domain] + if !found { + requestingDomains[domain] = true + } + + return found +} + +func unlockDomain(domain string) { + requestingLock.Lock() + defer requestingLock.Unlock() + + delete(requestingDomains, domain) +} + +type CertificateWrapper struct { + TlsCertificate *tls.Certificate `json:"-"` + Domain string `json:"domain"` + NotAfter time.Time `json:"not_after"` + PrivateKeyEncoded string `json:"private_key"` + Certificate []byte `json:"certificate"` + IssuerCertificate []byte `json:"issuer_certificate"` + CertificateUrl string `json:"certificate_url"` +} + +func (c *CertificateWrapper) GetPrivateKey() *rsa.PrivateKey { + data, _ := base64.StdEncoding.DecodeString(c.PrivateKeyEncoded) + pk, _ := certcrypto.ParsePEMPrivateKey(data); + + return pk.(*rsa.PrivateKey) +} + +type CertificatesCache struct { + FallbackCertificate *CertificateWrapper + Certificates map[string]CertificateWrapper +} + +type CertificatesStore struct { + FallbackCertificate CertificateWrapper `json:"fallback"` + Certificates []CertificateWrapper `json:"certificates"` +} + +func (c *CertificatesCache) toStoreData() string { + certs := make([]CertificateWrapper, 0) + for _, cert := range c.Certificates { + certs = append(certs, cert) + } + + result, err := json.Marshal(CertificatesStore{ + FallbackCertificate: *c.FallbackCertificate, + Certificates: certs, + }) + if err != nil { + log.Errorf("Failed to Marshal cache: %v", err) + } + return string(result) +} + +func (c *CertificateWrapper) initTlsCertificate() { + pk, _ := base64.StdEncoding.DecodeString(c.PrivateKeyEncoded) + tlsCert, _ := tls.X509KeyPair( + c.Certificate, + pk, + ) + c.TlsCertificate = &tlsCert +} + +func CertificateFromStoreData(rawJson string) CertificatesCache { + var store CertificatesStore + _ = json.Unmarshal([]byte(rawJson), &store) + + store.FallbackCertificate.initTlsCertificate() + cache := CertificatesCache{ + FallbackCertificate: &store.FallbackCertificate, + } + + certs := make(map[string]CertificateWrapper) + for _, cert := range store.Certificates { + cert.initTlsCertificate() + certs[cert.Domain] = cert + } + cache.Certificates = certs + + return cache +} + +func LoadCertificateStoreFromFile(path string) error { + content, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + Certificates = CertificateFromStoreData(string(content)) + return nil +} + +func FlushCertificateStoreToFile(path string) { + data := Certificates.toStoreData() + ioutil.WriteFile(path, []byte(data), 0600) +} + +func InitialiseFallbackCert(pagesDomain string) error { + cert, err := fallbackCert(pagesDomain) + Certificates.FallbackCertificate = cert + return err +} + +func fallbackCert(pagesDomain string) (*CertificateWrapper, error) { + key, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048) + if err != nil { + return nil, err + } + + notAfter := time.Now().Add(time.Hour*24*7); + cert := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: pagesDomain, + Organization: []string{"Pages Server"}, + }, + NotAfter: notAfter, + NotBefore: time.Now(), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + certBytes, err := x509.CreateCertificate( + rand.Reader, + &cert, + &cert, + &key.(*rsa.PrivateKey).PublicKey, + key, + ) + if err != nil { + return nil, err + } + + out := &bytes.Buffer{} + err = pem.Encode(out, &pem.Block{ + Bytes: certBytes, + Type: "CERTIFICATE", + }) + if err != nil { + return nil, err + } + + outBytes := out.Bytes() + res := &certificate.Resource{ + PrivateKey: certcrypto.PEMEncode(key), + Certificate: outBytes, + IssuerCertificate: outBytes, + Domain: pagesDomain, + } + tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) + if err != nil { + return nil, err + } + return &CertificateWrapper{ + TlsCertificate: &tlsCertificate, + Domain: pagesDomain, + NotAfter: notAfter, + PrivateKeyEncoded: base64.StdEncoding.EncodeToString(certcrypto.PEMEncode(key)), + Certificate: outBytes, + IssuerCertificate: outBytes, + CertificateUrl: "localhost", + }, nil +} + +func isCertStillValid(cert CertificateWrapper) bool { + return time.Now().Compare(cert.NotAfter) <= -1 +} + +func makeTlsConfig(pagesDomain, path string, acmeClient *lego.Client) *tls.Config { + return &tls.Config{ + InsecureSkipVerify: true, + GetCertificate: func (info *tls.ClientHelloInfo) (*tls.Certificate, error) { + // Validate that we should even care about this domain + 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. + cname, _ := lookupCNAME(info.ServerName) + if !strings.HasSuffix(cname, pagesDomain) { + log.Warnf("Got ServerName for Domain %s that we're not responsible for", info.ServerName) + return Certificates.FallbackCertificate.TlsCertificate, nil + } + } + + // If we want to access ., then we can just + // use a wildcard certificate. + domain := info.ServerName + /*if strings.HasSuffix(info.ServerName, pagesDomain) { + domain = "*." + pagesDomain + }*/ + + cert, found := Certificates.Certificates[info.ServerName] + if found { + if isCertStillValid(cert) { + return cert.TlsCertificate, nil + } else { + // If we're already working on the domain, + // return the old certificate + if lockIfUnlockedDomain(domain) { + return cert.TlsCertificate, nil + } + defer unlockDomain(domain) + + // TODO: Renew + log.Debugf("Certificate for %s expired, renewing", domain) + } + } else { + // Don't request if we're already requesting. + if lockIfUnlockedDomain(domain) { + return Certificates.FallbackCertificate.TlsCertificate, nil + } + defer unlockDomain(domain) + + // Request new certificate + log.Debugf("Obtaining new certificate for %s...", domain) + err := ObtainNewCertificate( + []string{domain}, + path, + acmeClient, + ) + if err != nil { + log.Errorf( + "Failed to get certificate for %s: %v", + domain, + err, + ) + return Certificates.FallbackCertificate.TlsCertificate, nil + } + + cert, _ = Certificates.Certificates[domain] + return cert.TlsCertificate, nil + } + + log.Debugf("TLS ServerName: %s", info.ServerName) + return Certificates.FallbackCertificate.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, + }, + } +} + +func addChallenge(id, token string) { + runningChallenges[id] = token +} + +func removeChallenge(id string) { + delete(runningChallenges, id) +} + +func getChallenge(id string) string { + if value, found := runningChallenges[id]; found { + return value + } + + return "" +} + +func handleLetsEncryptChallenge(w http.ResponseWriter, req *http.Request) bool { + if !strings.HasPrefix(req.URL.Path, AcmeChallengePathPrefix) { + return false + } + + log.Debug("Handling ACME challenge path") + id := strings.TrimPrefix(req.URL.Path, AcmeChallengePathPrefix) + challenge := getChallenge(id) + if id == "" { + w.WriteHeader(404) + return true + } + + w.WriteHeader(200) + w.Write([]byte(challenge)) + + removeChallenge(id) + return true +} diff --git a/certificate.go b/certificate.go new file mode 100644 index 0000000..8b8bf33 --- /dev/null +++ b/certificate.go @@ -0,0 +1,41 @@ +package main + +import ( + "crypto/tls" + "encoding/base64" + "time" + + // "github.com/go-acme/lego/v4/certcrypto" + "github.com/go-acme/lego/v4/certificate" + "github.com/go-acme/lego/v4/lego" +) + +func ObtainNewCertificate(domains []string, path string, acmeClient *lego.Client) error { + req := certificate.ObtainRequest{ + Domains: domains, + Bundle: true, + } + cert, err := acmeClient.Certificate.Obtain(req) + if err != nil { + return err + } + + tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey) + if err != nil { + return err + } + + wrapper := CertificateWrapper{ + TlsCertificate: &tlsCert, + Domain: cert.Domain, + //NotAfter: tlsCert.Leaf.NotAfter, + NotAfter: time.Now().Add(time.Hour * 24 * 60), + PrivateKeyEncoded: base64.StdEncoding.EncodeToString(cert.PrivateKey), + Certificate: cert.Certificate, + IssuerCertificate: cert.IssuerCertificate, + CertificateUrl: cert.CertURL, + } + Certificates.Certificates[cert.Domain] = wrapper + FlushCertificateStoreToFile(path) + return nil +} diff --git a/dns.go b/dns.go index 32eaba5..6c4b42f 100644 --- a/dns.go +++ b/dns.go @@ -1,25 +1,64 @@ package main import ( + "errors" "net" + "strings" "time" "github.com/patrickmn/go-cache" ) +const ( + TxtRepoKey = "repo=" +) + var ( cnameCache = cache.New(1 * time.Hour, 1 * time.Hour) + txtRepoCache = cache.New(1 * time.Hour, 1 * time.Hour) ) +func lookupRepoTXT(domain string) (string, error) { + repoLookup, found := txtRepoCache.Get(domain) + if found { + return repoLookup.(string), nil + } + + txts, err := net.LookupTXT("_rio-pages." + domain) + if err != nil { + return "", err + } + + repo := "" + for _, txt := range txts { + if !strings.HasPrefix(txt, TxtRepoKey) { + continue + } + + repo = strings.TrimPrefix(txt, TxtRepoKey) + break + } + + txtRepoCache.Set(domain, repo, cache.DefaultExpiration) + return repo, nil +} + func lookupCNAME(domain string) (string, error) { cname, found := cnameCache.Get(domain) if found { + if cname == "" { + return "", errors.New("Previous request failure") + } + return cname.(string), nil } cname, err := net.LookupCNAME(domain) if err == nil { + cnameCache.Set(domain, cname, cache.DefaultExpiration) return cname.(string), nil } + + cnameCache.Set(domain, "", cache.DefaultExpiration) return "", err } diff --git a/go.mod b/go.mod index 5845f58..f4550cc 100644 --- a/go.mod +++ b/go.mod @@ -4,18 +4,26 @@ go 1.20 require ( code.gitea.io/sdk/gitea v0.17.0 + github.com/go-acme/lego/v4 v4.14.2 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli/v2 v2.27.1 ) require ( + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/go-fed/httpsig v1.1.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/hashicorp/go-version v1.5.0 // indirect + github.com/miekg/dns v1.1.55 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/net v0.11.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect + golang.org/x/tools v0.10.0 // indirect ) diff --git a/go.sum b/go.sum index 43c4b76..7aa6da9 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,24 @@ code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34= code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/go-acme/lego/v4 v4.14.2 h1:/D/jqRgLi8Cbk33sLGtu2pX2jEg3bGJWHyV8kFuUHGM= +github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E= github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -18,19 +27,27 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -39,10 +56,17 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 65a2530..93cb119 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "os" "fmt" "net" @@ -10,13 +11,12 @@ import ( "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" + "github.com/go-acme/lego/v4/challenge/http01" log "github.com/sirupsen/logrus" ) const ( - //PagesBranch "pages" - // TODO: Change back for testing - PagesBranch = "main" + PagesBranch = "pages" ) func handleSubdomain(domain string, cname string, path string, giteaClient *gitea.Client, w http.ResponseWriter) { @@ -50,6 +50,10 @@ func Handler(pagesDomain string, giteaClient *gitea.Client) http.HandlerFunc { w.Header().Set("Server", "rio") if strings.HasSuffix(req.Host, pagesDomain){ + if handleLetsEncryptChallenge(w, req) { + return + } + log.Debug("Domain can be directly handled") handleSubdomain(req.Host, "", req.URL.Path, giteaClient, w) return @@ -65,6 +69,11 @@ func Handler(pagesDomain string, giteaClient *gitea.Client) http.HandlerFunc { if strings.HasSuffix(cname, pagesDomain) { log.Debugf("%s is alias of %s", req.Host, cname) + if handleLetsEncryptChallenge(w, req) { + return + } + + log.Debugf("Domain can be handled after a CNAME query") handleSubdomain(cname, cname, req.URL.Path, giteaClient, w) return } @@ -75,11 +84,56 @@ func Handler(pagesDomain string, giteaClient *gitea.Client) http.HandlerFunc { } func runServer(ctx *cli.Context) error { + log.SetLevel(log.DebugLevel) giteaUrl := ctx.String("gitea-url") addr := ctx.String("listen-host") + ":" + ctx.String("listen-port") domain := ctx.String("pages-domain") + certsFile := ctx.String("certs-file") + acmeEmail := ctx.String("acme-email") + acmeServer := ctx.String("acme-server") + acmeFile := ctx.String("acme-file") + acmeHost := ctx.String("acme-host") + acmePort := ctx.String("acme-port") - log.SetLevel(log.DebugLevel) + err := LoadCertificateStoreFromFile(certsFile) + if err != nil { + log.Debugf("Generating cert") + err := InitialiseFallbackCert(domain) + if err != nil { + log.Fatalf("Failed to generate fallback certificate: %v", err) + return err + } + + FlushCertificateStoreToFile(certsFile) + log.Debug("Certificate wrote to disk") + } else { + log.Debug("Certificate store read from disk") + } + + // Create an ACME client, if we failed to load one + acmeClient, err := ClientFromFile(acmeFile, acmeServer) + if err != nil { + log.Warn("Failed to load ACME client data from disk. Generating new account") + acmeClient, err = GenerateNewAccount(acmeEmail, acmeFile, acmeServer) + if err != nil { + log.Fatalf("Failed to generate new ACME client: %v", err) + return err + } + log.Info("ACME account registered") + } else { + log.Info("ACME client data read from disk") + } + + // Set up the HTTP01 listener + err = acmeClient.Challenge.SetHTTP01Provider( + http01.NewProviderServer(acmeHost, acmePort), + ) + if err != nil { + log.Fatalf("Failed to setup HTTP01 challenge listener: %v", err) + return err + } + + // Setup the HTTPS stuff httpClient := http.Client{Timeout: 10 * time.Second} client, err := gitea.NewClient( giteaUrl, @@ -93,6 +147,14 @@ func runServer(ctx *cli.Context) error { fmt.Errorf("Failed to create listener: %v", err) return err } + + tlsConfig := makeTlsConfig( + domain, + certsFile, + acmeClient, + ) + listener = tls.NewListener(listener, tlsConfig) + if err := http.Serve(listener, Handler(domain, client)); err != nil { fmt.Printf("Listening failed") return err @@ -123,12 +185,48 @@ func main() { EnvVars: []string{"PORT"}, Value: "8888", }, + &cli.StringFlag{ + Name: "acme-host", + Usage: "The host to bind to for ACME challenges", + EnvVars: []string{"ACME_HOST"}, + Value: "", + }, + &cli.StringFlag{ + Name: "acme-port", + Usage: "The port to listen on for ACME challenges", + EnvVars: []string{"ACME_PORT"}, + Value: "8889", + }, &cli.StringFlag{ Name: "pages-domain", Usage: "The domain on which the server is reachable", EnvVars: []string{"PAGES_DOMAIN"}, Required: true, }, + &cli.StringFlag{ + Name: "certs-file", + Usage: "File to store certificates in", + EnvVars: []string{"CERTS_FILE"}, + Required: true, + }, + &cli.StringFlag{ + Name: "acme-file", + Usage: "File to store ACME configuration in", + EnvVars: []string{"ACME_FILE"}, + Required: true, + }, + &cli.StringFlag{ + Name: "acme-email", + Usage: "Email to use for an ACME account", + EnvVars: []string{"ACME_EMAIL"}, + Required: true, + }, + &cli.StringFlag{ + Name: "acme-server", + Usage: "CA Directory to use", + EnvVars: []string{"ACME_SERVER"}, + Value: "https://acme-staging-v02.api.letsencrypt.org/directory", + }, }, } diff --git a/repo.go b/repo.go index 26b2365..4e43b2b 100644 --- a/repo.go +++ b/repo.go @@ -59,7 +59,6 @@ func lookupRepositoryAndCache(username, reponame, host, domain, path, cname stri } // Cache data - cnameCache.Set(host, domain, cache.DefaultExpiration) pathCache.Set( makePageCacheKey(domain, path), PageCacheEntry{ @@ -100,9 +99,24 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) } } - // Allow naming the repository "example.org" + // Allow specifying the repository name in the TXT record reponame := domain + lookupDomain := domain if cname != "" { + lookupDomain = cname + } + repoLookup, err := lookupRepoTXT(lookupDomain) + if err != nil || repoLookup != "" { + log.Infof( + "TXT lookup for %s resulted in choosing repository %s", + lookupDomain, + repoLookup, + ) + reponame = repoLookup + } + + // Allow naming the repository "example.org" (But give the TXT record preference) + if cname != "" && repoLookup == "" && err == nil { reponame = cname; }