mirror of
https://codeberg.org/Codeberg/pages-server.git
synced 2024-11-18 10:29:43 +00:00
Commit all current changes before vacation...
This commit is contained in:
parent
4494023086
commit
33f7a5d0df
4 changed files with 64 additions and 24 deletions
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||||
|
|
11
handler.go
11
handler.go
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue