chore: Restructure
This commit is contained in:
125
internal/acme/account.go
Normal file
125
internal/acme/account.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"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
|
||||
}
|
||||
109
internal/certificates/certificate.go
Normal file
109
internal/certificates/certificate.go
Normal file
@@ -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
|
||||
}
|
||||
114
internal/certificates/store.go
Normal file
114
internal/certificates/store.go
Normal file
@@ -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
|
||||
}
|
||||
77
internal/dns/dns.go
Normal file
77
internal/dns/dns.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
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 (
|
||||
// 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)
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
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
|
||||
}
|
||||
124
internal/pages/pages.go
Normal file
124
internal/pages/pages.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
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)
|
||||
)
|
||||
|
||||
type PageContentCache struct {
|
||||
Content []byte
|
||||
mimeType string
|
||||
RequestedAt time.Time
|
||||
}
|
||||
|
||||
func makePageContentCacheEntry(username, path string) string {
|
||||
return username + ":" + path
|
||||
}
|
||||
|
||||
func ServeFile(username, reponame, path, giteaUrl string, w http.ResponseWriter) {
|
||||
// Provide a default
|
||||
if path == "" {
|
||||
path = "/index.html"
|
||||
}
|
||||
|
||||
// Strip away a starting / as it messes with Gitea
|
||||
if path[:1] == "/" {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
key := makePageContentCacheEntry(username, path)
|
||||
entry, found := pageCache.Get(key)
|
||||
var content []byte
|
||||
var mimeType string
|
||||
var err error
|
||||
if found {
|
||||
log.Debugf("Returning %s from cache", path)
|
||||
content = entry.(PageContentCache).Content
|
||||
mimeType = entry.(PageContentCache).mimeType
|
||||
}
|
||||
|
||||
// We have to do the raw request manually because the Gitea SDK does not allow
|
||||
// passing the If-Modfied-Since header.
|
||||
apiUrl := fmt.Sprintf(
|
||||
"%s/api/v1/repos/%s/%s/raw/%s?ref=%s",
|
||||
giteaUrl,
|
||||
username,
|
||||
reponame,
|
||||
path,
|
||||
PagesBranch,
|
||||
)
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", apiUrl, nil)
|
||||
if found {
|
||||
since := entry.(PageContentCache).RequestedAt.Format(time.RFC1123)
|
||||
log.Debugf("Found %s in cache. Adding '%s' as If-Modified-Since", key, since)
|
||||
req.Header.Add("If-Modified-Since", since)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if !found {
|
||||
log.Errorf("Failed to get file %s/%s/%s (%s)", username, reponame, path, err)
|
||||
w.WriteHeader(404)
|
||||
} else {
|
||||
log.Debugf("Request failed but page %s is cached in memory", path)
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Write(content)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
log.Debugf("Gitea API request returned %d", resp.StatusCode)
|
||||
if found && resp.StatusCode == 302 {
|
||||
log.Debugf("Page %s is unchanged and cached in memory", path)
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Write(content)
|
||||
return
|
||||
}
|
||||
|
||||
content, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get file %s/%s/%s (%s)", username, reponame, path, err)
|
||||
w.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
|
||||
pathParts := strings.Split(path, ".")
|
||||
ext := pathParts[len(pathParts)-1]
|
||||
mimeType = mime.TypeByExtension("." + ext)
|
||||
|
||||
now := time.Now()
|
||||
pageCache.Set(
|
||||
key,
|
||||
PageContentCache{
|
||||
content,
|
||||
mimeType,
|
||||
now,
|
||||
},
|
||||
cache.DefaultExpiration,
|
||||
)
|
||||
|
||||
log.Debugf("Page %s requested from Gitea and cached in memory at %v", path, now)
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Write(content)
|
||||
}
|
||||
134
internal/repo/repo.go
Normal file
134
internal/repo/repo.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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"
|
||||
)
|
||||
|
||||
var (
|
||||
pathCache = cache.New(1*time.Hour, 1*time.Hour)
|
||||
)
|
||||
|
||||
type PageCacheEntry struct {
|
||||
Repository *gitea.Repository
|
||||
Path string
|
||||
}
|
||||
|
||||
func makePageCacheKey(domain, path string) string {
|
||||
return domain + "/" + path
|
||||
}
|
||||
|
||||
// / Try to find the repository with name @reponame of the user @username. If @cname
|
||||
// / is not "", then it also verifies that the repository contains a "CNAME" with
|
||||
// / the value of @cname as its content. @host, @domain, and @path are passed for
|
||||
// / caching on success.
|
||||
func lookupRepositoryAndCache(username, reponame, host, domain, path, cname string, giteaClient *gitea.Client) (*gitea.Repository, error) {
|
||||
log.Debugf("Looking up repository %s/%s", username, reponame)
|
||||
repo, _, err := giteaClient.GetRepo(username, reponame)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the CNAME file matches
|
||||
if cname != "" {
|
||||
file, _, err := giteaClient.GetFile(
|
||||
username,
|
||||
repo.Name,
|
||||
pages.PagesBranch,
|
||||
"CNAME",
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Could not verify CNAME of %s/%s: %v\n", username, repo.Name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cnameContent := strings.Trim(
|
||||
string(file[:]),
|
||||
"\n",
|
||||
)
|
||||
if cnameContent != cname {
|
||||
return nil, errors.New("CNAME mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
// Cache data
|
||||
pathCache.Set(
|
||||
makePageCacheKey(domain, path),
|
||||
PageCacheEntry{
|
||||
repo,
|
||||
path,
|
||||
},
|
||||
cache.DefaultExpiration,
|
||||
)
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) (*gitea.Repository, string, error) {
|
||||
domain := host
|
||||
|
||||
// Guess the repository
|
||||
key := makePageCacheKey(domain, path)
|
||||
entry, found := pathCache.Get(key)
|
||||
if found {
|
||||
pageEntry := entry.(PageCacheEntry)
|
||||
return pageEntry.Repository, pageEntry.Path, nil
|
||||
}
|
||||
|
||||
pathParts := strings.Split(path, "/")
|
||||
if len(pathParts) > 1 {
|
||||
log.Debugf("Trying repository %s", pathParts[0])
|
||||
modifiedPath := strings.Join(pathParts[1:], "/")
|
||||
repo, err := lookupRepositoryAndCache(
|
||||
username,
|
||||
pathParts[0],
|
||||
host,
|
||||
domain,
|
||||
modifiedPath,
|
||||
cname,
|
||||
giteaClient,
|
||||
)
|
||||
if err == nil {
|
||||
return repo, modifiedPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Allow specifying the repository name in the TXT record
|
||||
reponame := domain
|
||||
lookupDomain := domain
|
||||
if cname != "" {
|
||||
lookupDomain = cname
|
||||
}
|
||||
repoLookup, err := dns.LookupRepoTXT(lookupDomain)
|
||||
if err != nil && repoLookup != "" {
|
||||
log.Infof(
|
||||
"TXT lookup for %s resulted in choosing repository %s",
|
||||
lookupDomain,
|
||||
repoLookup,
|
||||
)
|
||||
reponame = repoLookup
|
||||
} else if cname != "" {
|
||||
// Allow naming the repository "example.org" (But give the TXT record preference)
|
||||
reponame = cname
|
||||
}
|
||||
|
||||
log.Debugf("Trying repository %s/%s", username, reponame)
|
||||
repo, err := lookupRepositoryAndCache(
|
||||
username,
|
||||
reponame,
|
||||
host,
|
||||
domain,
|
||||
path,
|
||||
cname,
|
||||
giteaClient,
|
||||
)
|
||||
return repo, path, err
|
||||
}
|
||||
129
internal/server/tls.go
Normal file
129
internal/server/tls.go
Normal file
@@ -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 <user>.<pages domain>, 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user