diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4afd2f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +./rio \ No newline at end of file diff --git a/acme.go b/acme.go deleted file mode 100644 index 70d309a..0000000 --- a/acme.go +++ /dev/null @@ -1,343 +0,0 @@ -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 deleted file mode 100644 index d80cb99..0000000 --- a/certificate.go +++ /dev/null @@ -1,41 +0,0 @@ -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/main.go b/cmd/rio/main.go similarity index 85% rename from main.go rename to cmd/rio/main.go index f326025..edd5c60 100644 --- a/main.go +++ b/cmd/rio/main.go @@ -10,16 +10,19 @@ import ( "strings" "time" + "git.polynom.me/rio/internal/acme" + "git.polynom.me/rio/internal/certificates" + "git.polynom.me/rio/internal/dns" + "git.polynom.me/rio/internal/pages" + "git.polynom.me/rio/internal/repo" + "git.polynom.me/rio/internal/server" + "code.gitea.io/sdk/gitea" "github.com/go-acme/lego/v4/challenge/http01" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) -const ( - PagesBranch = "pages" -) - func handleSubdomain(domain string, cname string, path, giteaUrl string, giteaClient *gitea.Client, w http.ResponseWriter) { hostParts := strings.Split(domain, ".") username := hostParts[0] @@ -29,7 +32,7 @@ func handleSubdomain(domain string, cname string, path, giteaUrl string, giteaCl path = path[1:] } - repo, path, err := RepoFromPath( + repo, path, err := repo.RepoFromPath( username, domain, cname, @@ -42,7 +45,7 @@ func handleSubdomain(domain string, cname string, path, giteaUrl string, giteaCl return } - serveFile(username, repo.Name, path, giteaUrl, w) + pages.ServeFile(username, repo.Name, path, giteaUrl, w) } func Handler(pagesDomain, giteaUrl string, giteaClient *gitea.Client) http.HandlerFunc { @@ -50,16 +53,12 @@ func Handler(pagesDomain, giteaUrl string, giteaClient *gitea.Client) http.Handl 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, giteaUrl, giteaClient, w) return } - cname, err := lookupCNAME(req.Host) + cname, err := dns.LookupCNAME(req.Host) if err != nil { log.Warningf("CNAME request failed, we don't handle %s", req.Host) w.WriteHeader(400) @@ -68,12 +67,7 @@ func Handler(pagesDomain, giteaUrl string, giteaClient *gitea.Client) http.Handl log.Debugf("Got CNAME %s", cname) 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") + log.Debugf("%s is alias of %s and can be handled after a CNAME query", req.Host, cname) handleSubdomain(cname, cname, req.URL.Path, giteaUrl, giteaClient, w) return } @@ -117,26 +111,28 @@ func runServer(ctx *cli.Context) error { return errors.New("The options acme-file, acme-email, and certs-file are required") } - err := LoadCertificateStoreFromFile(certsFile) + cache, err := certificates.CertificateCacheFromFile(certsFile) if err != nil { log.Debugf("Generating cert") - err := InitialiseFallbackCert(domain) + fallback, err := certificates.MakeFallbackCertificate(domain) if err != nil { log.Fatalf("Failed to generate fallback certificate: %v", err) return err } - FlushCertificateStoreToFile(certsFile) + cache.FallbackCertificate = fallback + cache.Certificates = make(map[string]certificates.CertificateWrapper) + cache.FlushToDisk(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) + acmeClient, err := acme.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) + acmeClient, err = acme.GenerateNewAccount(acmeEmail, acmeFile, acmeServer) if err != nil { log.Fatalf("Failed to generate new ACME client: %v", err) return err @@ -155,9 +151,10 @@ func runServer(ctx *cli.Context) error { return err } - tlsConfig := makeTlsConfig( + tlsConfig := server.MakeTlsConfig( domain, certsFile, + &cache, acmeClient, ) listener = tls.NewListener(listener, tlsConfig) diff --git a/go.mod b/go.mod index f4550cc..7f03595 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ -module paptutuwawa/rio +module git.polynom.me/rio -go 1.20 +go 1.21.5 require ( - code.gitea.io/sdk/gitea v0.17.0 + code.gitea.io/sdk/gitea v0.17.1 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 @@ -16,14 +16,14 @@ require ( 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/hashicorp/go-version v1.6.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.10.0 // indirect + golang.org/x/crypto v0.17.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/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.10.0 // indirect ) diff --git a/go.sum b/go.sum index 7aa6da9..3c414b2 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,11 @@ -code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34= -code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg= +code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8= +code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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= @@ -14,13 +15,15 @@ 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 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 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/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -29,44 +32,70 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs 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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/account.go b/internal/acme/account.go similarity index 99% rename from account.go rename to internal/acme/account.go index 3eac8d3..0d1d8a3 100644 --- a/account.go +++ b/internal/acme/account.go @@ -1,4 +1,4 @@ -package main +package acme import ( "crypto" diff --git a/internal/certificates/certificate.go b/internal/certificates/certificate.go new file mode 100644 index 0000000..67d3f22 --- /dev/null +++ b/internal/certificates/certificate.go @@ -0,0 +1,109 @@ +package certificates + +import ( + "crypto/rsa" + "crypto/tls" + "encoding/base64" + "encoding/pem" + "time" + + "bytes" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + + "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, acmeClient *lego.Client) (CertificateWrapper, error) { + req := certificate.ObtainRequest{ + Domains: domains, + Bundle: true, + } + cert, err := acmeClient.Certificate.Obtain(req) + if err != nil { + return CertificateWrapper{}, err + } + + tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey) + if err != nil { + return CertificateWrapper{}, 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, + } + return wrapper, nil +} + +// Generate a fallback certificate for the domain. +func MakeFallbackCertificate(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 +} diff --git a/internal/certificates/store.go b/internal/certificates/store.go new file mode 100644 index 0000000..640b729 --- /dev/null +++ b/internal/certificates/store.go @@ -0,0 +1,114 @@ +package certificates + +import ( + "crypto/rsa" + "crypto/tls" + "encoding/base64" + "encoding/json" + "io/ioutil" + "time" + + "github.com/go-acme/lego/v4/certcrypto" + log "github.com/sirupsen/logrus" +) + +// A convenience wrapper around a TLS certificate +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"` +} + +// A structure to store all the certificates we know of in. +type CertificatesCache struct { + // The certificate to use as a fallback if all else fails. + FallbackCertificate *CertificateWrapper + + // Mapping of domain name to certificate. + Certificates map[string]CertificateWrapper +} + +// Internal type to let encoding JSON handle the bulk of the work. +type certificatesStore struct { + FallbackCertificate CertificateWrapper `json:"fallback"` + Certificates []CertificateWrapper `json:"certificates"` +} + +// Decodes the private key of the certificate wrapper. +func (c *CertificateWrapper) GetPrivateKey() *rsa.PrivateKey { + data, _ := base64.StdEncoding.DecodeString(c.PrivateKeyEncoded) + pk, _ := certcrypto.ParsePEMPrivateKey(data) + + return pk.(*rsa.PrivateKey) +} + +// Populate the certificate's TlsCertificate field. +func (c *CertificateWrapper) initTlsCertificate() { + pk, _ := base64.StdEncoding.DecodeString(c.PrivateKeyEncoded) + tlsCert, _ := tls.X509KeyPair( + c.Certificate, + pk, + ) + c.TlsCertificate = &tlsCert +} + +// Checks if the certificate is still valid now. +func (c *CertificateWrapper) IsValid() bool { + return time.Now().Compare(c.NotAfter) <= -1 +} + +// Serializes the certificate cache to a JSON string for writing to a file. +func (c *CertificatesCache) toStoreData() []byte { + 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 result +} + +// Saves the cache to disk. +func (c *CertificatesCache) FlushToDisk(path string) { + ioutil.WriteFile(path, c.toStoreData(), 0600) +} + +func (c *CertificatesCache) AddCert(cert CertificateWrapper, path string) { + c.Certificates[cert.Domain] = cert + c.FlushToDisk(path) +} + +// Load the certificate cache from the file. +func CertificateCacheFromFile(path string) (CertificatesCache, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return CertificatesCache{}, err + } + + var store certificatesStore + _ = json.Unmarshal(content, &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, nil +} diff --git a/dns.go b/internal/dns/dns.go similarity index 59% rename from dns.go rename to internal/dns/dns.go index ec1059e..0153950 100644 --- a/dns.go +++ b/internal/dns/dns.go @@ -1,4 +1,4 @@ -package main +package dns import ( "errors" @@ -10,15 +10,26 @@ import ( ) const ( + // TXT record name that lookupRepoTXT will try to lookup. + TxtRepoRecord = "_rio-pages." + + // The key that the TXT record will have to start with, e.g. + // "repo=some-random-repo". TxtRepoKey = "repo=" ) var ( - cnameCache = cache.New(1*time.Hour, 1*time.Hour) + // Cache for CNAME resolution results. + cnameCache = cache.New(1*time.Hour, 1*time.Hour) + + // Cache for TXT resolution results. txtRepoCache = cache.New(1*time.Hour, 1*time.Hour) ) -func lookupRepoTXT(domain string) (string, error) { +// Query the domain for the a repository redirect. +// Returns the new repository name or "", if we could not +// resolve a repository redirect. +func LookupRepoTXT(domain string) (string, error) { repoLookup, found := txtRepoCache.Get(domain) if found { return repoLookup.(string), nil @@ -43,7 +54,9 @@ func lookupRepoTXT(domain string) (string, error) { return repo, nil } -func lookupCNAME(domain string) (string, error) { +// Query the domain for a CNAME record. Returns the resolved +// CNAME or "", if no CNAME could be queried. +func LookupCNAME(domain string) (string, error) { cname, found := cnameCache.Get(domain) if found { if cname == "" { diff --git a/pages.go b/internal/pages/pages.go similarity index 94% rename from pages.go rename to internal/pages/pages.go index 443fb51..ef8aa94 100644 --- a/pages.go +++ b/internal/pages/pages.go @@ -1,4 +1,4 @@ -package main +package pages import ( "fmt" @@ -12,6 +12,11 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + // The branch name on which files must reside. + PagesBranch = "pages" +) + var ( pageCache = cache.New(6*time.Hour, 1*time.Hour) ) @@ -26,7 +31,7 @@ func makePageContentCacheEntry(username, path string) string { return username + ":" + path } -func serveFile(username, reponame, path, giteaUrl string, w http.ResponseWriter) { +func ServeFile(username, reponame, path, giteaUrl string, w http.ResponseWriter) { // Provide a default if path == "" { path = "/index.html" diff --git a/repo.go b/internal/repo/repo.go similarity index 94% rename from repo.go rename to internal/repo/repo.go index 1207417..74cac2b 100644 --- a/repo.go +++ b/internal/repo/repo.go @@ -1,10 +1,13 @@ -package main +package repo import ( "errors" "strings" "time" + "git.polynom.me/rio/internal/dns" + "git.polynom.me/rio/internal/pages" + "code.gitea.io/sdk/gitea" "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" @@ -39,7 +42,7 @@ func lookupRepositoryAndCache(username, reponame, host, domain, path, cname stri file, _, err := giteaClient.GetFile( username, repo.Name, - PagesBranch, + pages.PagesBranch, "CNAME", false, ) @@ -104,7 +107,7 @@ func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) if cname != "" { lookupDomain = cname } - repoLookup, err := lookupRepoTXT(lookupDomain) + repoLookup, err := dns.LookupRepoTXT(lookupDomain) if err != nil && repoLookup != "" { log.Infof( "TXT lookup for %s resulted in choosing repository %s", diff --git a/internal/server/tls.go b/internal/server/tls.go new file mode 100644 index 0000000..0ea9d6d --- /dev/null +++ b/internal/server/tls.go @@ -0,0 +1,129 @@ +package server + +import ( + "crypto/tls" + "strings" + "sync" + + "git.polynom.me/rio/internal/certificates" + "git.polynom.me/rio/internal/dns" + + "github.com/go-acme/lego/v4/lego" + + log "github.com/sirupsen/logrus" +) + +var ( + // 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) +} + +func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, 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, _ := dns.LookupCNAME(info.ServerName) + 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 ., then we can just + // use a wildcard certificate. + domain := info.ServerName + /*if strings.HasSuffix(info.ServerName, pagesDomain) { + domain = "*." + pagesDomain + }*/ + + cert, found := cache.Certificates[info.ServerName] + if found { + if cert.IsValid() { + 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 cache.FallbackCertificate.TlsCertificate, nil + } + defer unlockDomain(domain) + + // Request new certificate + log.Debugf("Obtaining new certificate for %s...", domain) + 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 + cache.AddCert(cert, cachePath) + return cert.TlsCertificate, nil + } + + log.Debugf("TLS ServerName: %s", info.ServerName) + return cache.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, + }, + } +} diff --git a/rio b/rio new file mode 100755 index 0000000..c3d97c4 Binary files /dev/null and b/rio differ