Commit all current changes before vacation...

This commit is contained in:
Moritz Marquardt 2021-08-22 17:59:30 +02:00
parent 4494023086
commit 33f7a5d0df
No known key found for this signature in database
GPG key ID: D5788327BEE388B6
4 changed files with 64 additions and 24 deletions

View file

@ -25,6 +25,7 @@ import (
"time" "time"
"github.com/akrylysov/pogreb" "github.com/akrylysov/pogreb"
"github.com/reugn/equalizer"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
@ -107,12 +108,6 @@ var tlsConfig = &tls.Config{
} else { } else {
// request a new certificate // request a new certificate
// TODO: rate-limit certificates per owner
// LE Rate Limits:
// - 300 new orders per account per 3 hours
// - 20 requests per second
// - 10 Accounts per IP per 3 hours
if bytes.Equal(sniBytes, MainDomainSuffix) { if bytes.Equal(sniBytes, MainDomainSuffix) {
return nil, errors.New("won't request certificate for main domain, something really bad has happened") return nil, errors.New("won't request certificate for main domain, something really bad has happened")
} }
@ -123,6 +118,10 @@ var tlsConfig = &tls.Config{
return nil, err return nil, err
} }
key = x509.MarshalPKCS1PrivateKey(privateKey) key = x509.MarshalPKCS1PrivateKey(privateKey)
acmeClient, err := acmeClientFromPool(targetOwner)
if err != nil {
// TODO
}
res, err := acmeClient.Certificate.Obtain(certificate.ObtainRequest{ res, err := acmeClient.Certificate.Obtain(certificate.ObtainRequest{
Domains: []string{sni}, Domains: []string{sni},
PrivateKey: key, PrivateKey: key,
@ -259,7 +258,17 @@ func (u AcmeAccount) GetRegistration() *registration.Resource {
func (u *AcmeAccount) GetPrivateKey() crypto.PrivateKey { func (u *AcmeAccount) GetPrivateKey() crypto.PrivateKey {
return u.key return u.key
} }
var acmeClient *lego.Client
// rate-limit certificates per owner, based on LE Rate Limits:
// - 300 new orders per account per 3 hours
// - 20 requests per second
// - 10 Accounts per IP per 3 hours
var acmeClientPool []*lego.Client
var lastAcmeClient = 0
var acmeClientRequestLimit = equalizer.NewTokenBucket(10, time.Second) // LE allows 20 requests per second, but we want to give other applications a chancem so we want 10 here at most.
var acmeClientRegistrationLimit = equalizer.NewTokenBucket(5, time.Hour * 3) // LE allows 10 registrations in 3 hours per IP, we want at most 5 of them.
var acmeClientCertificateLimitPerRegistration = []*equalizer.TokenBucket{}
var acmeClientCertificateLimitPerUser = map[string]*equalizer.TokenBucket{}
type AcmeTLSChallengeProvider struct{} type AcmeTLSChallengeProvider struct{}
var _ challenge.Provider = AcmeTLSChallengeProvider{} var _ challenge.Provider = AcmeTLSChallengeProvider{}
@ -271,19 +280,31 @@ func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
return nil return nil
} }
func init() { func acmeClientFromPool(user string) (*lego.Client, error) {
FallbackCertificate() userLimit, ok := acmeClientCertificateLimitPerUser[user]
if !ok {
// Each Codeberg user can only add 10 new domains per day.
userLimit = equalizer.NewTokenBucket(10, time.Hour * 24)
acmeClientCertificateLimitPerUser[user] = userLimit
var err error }
keyDatabase, err = pogreb.Open("key-database.pogreb", &pogreb.Options{ if !userLimit.Ask() {
BackgroundSyncInterval: 30 * time.Second, return nil, errors.New("rate limit exceeded: 10 certificates per user per 24 hours")
BackgroundCompactionInterval: 6 * time.Hour,
FileSystem: fs.OSMMap,
})
if err != nil {
panic(err)
} }
if len(acmeClientPool) < 1 {
acmeClientPool = append(acmeClientPool, newAcmeClient())
acmeClientCertificateLimitPerRegistration = append(acmeClientCertificateLimitPerRegistration, equalizer.NewTokenBucket(290, time.Hour * 3))
}
if !acmeClientCertificateLimitPerRegistration[(lastAcmeClient + 1) % len(acmeClientPool)].Ask() {
}
equalizer.NewTokenBucket(290, time.Hour * 3) // LE allows 300 certificates per account, to be sure to catch it earlier, we limit that to 290.
// TODO: limit domains by file in repo
}
func newAcmeClient() *lego.Client {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
panic(err) panic(err)
@ -295,7 +316,7 @@ func init() {
config := lego.NewConfig(&myUser) config := lego.NewConfig(&myUser)
config.CADirURL = envOr("ACME_API", "https://acme-v02.api.letsencrypt.org/directory") config.CADirURL = envOr("ACME_API", "https://acme-v02.api.letsencrypt.org/directory")
config.Certificate.KeyType = certcrypto.RSA2048 config.Certificate.KeyType = certcrypto.RSA2048
acmeClient, err = lego.NewClient(config) acmeClient, err := lego.NewClient(config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -314,6 +335,21 @@ func init() {
} else { } else {
log.Printf("Warning: not using ACME certificates as ACME_ACCEPT_TERMS is false!") log.Printf("Warning: not using ACME certificates as ACME_ACCEPT_TERMS is false!")
} }
return acmeClient
}
func init() {
FallbackCertificate()
var err error
keyDatabase, err = pogreb.Open("key-database.pogreb", &pogreb.Options{
BackgroundSyncInterval: 30 * time.Second,
BackgroundCompactionInterval: 6 * time.Hour,
FileSystem: fs.OSMMap,
})
if err != nil {
panic(err)
}
// generate certificate for main domain // generate certificate for main domain
if os.Getenv("ACME_ACCEPT_TERMS") != "true" || os.Getenv("DNS_PROVIDER") == "" { if os.Getenv("ACME_ACCEPT_TERMS") != "true" || os.Getenv("DNS_PROVIDER") == "" {
@ -340,7 +376,7 @@ func init() {
panic(err) panic(err)
} }
mainKey := x509.MarshalPKCS1PrivateKey(mainPrivateKey) mainKey := x509.MarshalPKCS1PrivateKey(mainPrivateKey)
res, err := acmeClient.Certificate.Obtain(certificate.ObtainRequest{ res, err := dnsAcmeClient.Certificate.Obtain(certificate.ObtainRequest{
Domains: []string{"*" + string(MainDomainSuffix), string(MainDomainSuffix[1:])}, Domains: []string{"*" + string(MainDomainSuffix), string(MainDomainSuffix[1:])},
PrivateKey: mainKey, PrivateKey: mainKey,
Bundle: true, Bundle: true,

1
go.mod
View file

@ -8,6 +8,7 @@ require (
github.com/andybalholm/brotli v1.0.3 // indirect github.com/andybalholm/brotli v1.0.3 // indirect
github.com/go-acme/lego/v4 v4.4.0 github.com/go-acme/lego/v4 v4.4.0
github.com/klauspost/compress v1.13.1 // indirect github.com/klauspost/compress v1.13.1 // indirect
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad
github.com/valyala/fasthttp v1.28.0 github.com/valyala/fasthttp v1.28.0
github.com/valyala/fastjson v1.6.3 github.com/valyala/fastjson v1.6.3
) )

2
go.sum
View file

@ -409,6 +409,8 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad h1:WtSUHi5zthjudjIi3L6QmL/V9vpJPbc/j/F2u55d3fs=
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad/go.mod h1:h0+DiDRe2Y+6iHTjIq/9HzUq7NII/Nffp0HkFrsAKq4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

View file

@ -91,6 +91,7 @@ func handler(ctx *fasthttp.RequestCtx) {
targetRepo = repo targetRepo = repo
targetPath = strings.Trim(strings.Join(path, "/"), "/") targetPath = strings.Trim(strings.Join(path, "/"), "/")
targetBranch = branchTimestampResult.branch targetBranch = branchTimestampResult.branch
targetOptions.BranchTimestamp = branchTimestampResult.timestamp targetOptions.BranchTimestamp = branchTimestampResult.timestamp
if canonicalLink != "" { if canonicalLink != "" {
@ -314,7 +315,7 @@ type fileResponse struct {
} }
// getBranchTimestamp finds the default branch (if branch is "") and returns the last modification time of the branch // getBranchTimestamp finds the default branch (if branch is "") and returns the last modification time of the branch
// (or an empty time.Time if the branch doesn't exist) // (or nil if the branch doesn't exist)
func getBranchTimestamp(owner, repo, branch string) *branchTimestamp { func getBranchTimestamp(owner, repo, branch string) *branchTimestamp {
if result, ok := branchTimestampCache.Get(owner + "/" + repo + "/" + branch); ok { if result, ok := branchTimestampCache.Get(owner + "/" + repo + "/" + branch); ok {
if result == nil { if result == nil {
@ -394,7 +395,7 @@ func upstream(ctx *fasthttp.RequestCtx, targetOwner string, targetRepo string, t
var res *fasthttp.Response var res *fasthttp.Response
var cachedResponse fileResponse var cachedResponse fileResponse
var err error var err error
if cachedValue, ok := fileResponseCache.Get(uri); ok { if cachedValue, ok := fileResponseCache.Get(uri + "?timestamp=" + strconv.FormatInt(options.BranchTimestamp.Unix(), 10)); ok {
cachedResponse = cachedValue.(fileResponse) cachedResponse = cachedValue.(fileResponse)
} else { } else {
req = fasthttp.AcquireRequest() req = fasthttp.AcquireRequest()
@ -414,7 +415,7 @@ func upstream(ctx *fasthttp.RequestCtx, targetOwner string, targetRepo string, t
optionsForIndexPages.AppendTrailingSlash = true optionsForIndexPages.AppendTrailingSlash = true
for _, indexPage := range IndexPages { for _, indexPage := range IndexPages {
if upstream(ctx, targetOwner, targetRepo, targetBranch, strings.TrimSuffix(targetPath, "/")+"/"+indexPage, &optionsForIndexPages) { if upstream(ctx, targetOwner, targetRepo, targetBranch, strings.TrimSuffix(targetPath, "/")+"/"+indexPage, &optionsForIndexPages) {
_ = fileResponseCache.Set(uri, fileResponse{ _ = fileResponseCache.Set(uri + "?timestamp=" + strconv.FormatInt(options.BranchTimestamp.Unix(), 10), fileResponse{
exists: false, exists: false,
}, FileCacheTimeout) }, FileCacheTimeout)
return true return true
@ -424,7 +425,7 @@ func upstream(ctx *fasthttp.RequestCtx, targetOwner string, targetRepo string, t
ctx.Response.SetStatusCode(fasthttp.StatusNotFound) ctx.Response.SetStatusCode(fasthttp.StatusNotFound)
if res != nil { if res != nil {
// Update cache if the request is fresh // Update cache if the request is fresh
_ = fileResponseCache.Set(uri, fileResponse{ _ = fileResponseCache.Set(uri + "?timestamp=" + strconv.FormatInt(options.BranchTimestamp.Unix(), 10), fileResponse{
exists: false, exists: false,
}, FileCacheTimeout) }, FileCacheTimeout)
} }
@ -484,7 +485,7 @@ func upstream(ctx *fasthttp.RequestCtx, targetOwner string, targetRepo string, t
cachedResponse.exists = true cachedResponse.exists = true
cachedResponse.mimeType = mimeType cachedResponse.mimeType = mimeType
cachedResponse.body = cacheBodyWriter.Bytes() cachedResponse.body = cacheBodyWriter.Bytes()
_ = fileResponseCache.Set(uri, cachedResponse, FileCacheTimeout) _ = fileResponseCache.Set(uri + "?timestamp=" + strconv.FormatInt(options.BranchTimestamp.Unix(), 10), cachedResponse, FileCacheTimeout)
} }
return true return true