Compare commits

...

15 Commits

Author SHA1 Message Date
Moritz Marquardt 42a4502206 Run gofumpt 2024-04-18 19:08:47 +02:00
Moritz Marquardt f6ed4285bc Do not cache incomplete requests 2024-04-18 19:07:34 +02:00
Moritz Marquardt 18d09a163c Use hashicorp's LRU cache for DNS & certificates
DNS caching is also limited to 30 seconds now instead of 5 minutes
2024-04-18 19:07:34 +02:00
Moritz Marquardt 7694deec83 Fix missing return on redis parsing issues
Suggested-By: Gusted (https://codeberg.org/Codeberg/pages-server/pulls/301/files#issuecomment-1732054)
2024-04-18 19:07:34 +02:00
Moritz Marquardt 5bf538c5ec Format go code 2024-04-18 19:07:34 +02:00
Moritz Marquardt 584ba5c74d Fix cached error when .domains is not readable (fixes https://codeberg.org/Codeberg/Community/issues/1512)
Co-authored-by: @algernon
2024-04-18 19:07:34 +02:00
Moritz Marquardt f6d3147ba1 Fix tests not running 2024-04-18 19:07:34 +02:00
Moritz Marquardt 48e919a7bf Cache empty files & fix #303 (missing content cache) 2024-04-18 19:07:34 +02:00
Moritz Marquardt 46c8daacba Move redis config to CacheConfig struct, add cache prefixes & trace logging 2024-04-18 19:07:34 +02:00
Moritz Marquardt e1a22d5f4c Make it possible to actually use redis for caching through the config flags 2024-04-18 19:07:34 +02:00
Moritz Marquardt c4181d1206 Move to []byte for caching and make it compile 2024-04-18 19:07:34 +02:00
Moritz Marquardt 5b6eecc75f Add redis for caching, first try during a train ride so expect it to not be working yet 2024-04-18 19:07:34 +02:00
Jean-Marie 'Histausse' Mineau b8b9886ee1 fix lint 2024-04-18 19:07:34 +02:00
Jean-Marie 'Histausse' Mineau c89ce83b6b simplify wildecard logic 2024-04-18 19:07:34 +02:00
Jean-Marie 'Histausse' Mineau 03881382a4 Add option to disable DNS ACME provider (#290)
This PR add the `$NO_DNS_01` option (disabled by default) that removes the DNS ACME provider, and replaces the wildcard certificate by individual certificates obtained using the TLS ACME provider.

This option allows an instance to work without having to manage access tokens for the DNS provider. On the flip side, this means that a certificate can be requested for each subdomains. To limit the risk of DOS, the existence of the user/org corresponding to a subdomain is checked before requesting a cert, however, this limitation is not enough for an forge with a high number of users/orgs.

Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/290
Reviewed-by: Moritz Marquardt <momar@noreply.codeberg.org>
Co-authored-by: Jean-Marie 'Histausse' Mineau <histausse@protonmail.com>
Co-committed-by: Jean-Marie 'Histausse' Mineau <histausse@protonmail.com>
2024-04-18 17:05:20 +00:00
24 changed files with 360 additions and 120 deletions

View File

@ -80,7 +80,9 @@ and especially have a look at [this section of the haproxy.cfg](https://codeberg
- `ENABLE_HTTP_SERVER` (default: false): Set this to true to enable the HTTP-01 challenge and redirect all other HTTP requests to HTTPS. Currently only works with port 80. - `ENABLE_HTTP_SERVER` (default: false): Set this to true to enable the HTTP-01 challenge and redirect all other HTTP requests to HTTPS. Currently only works with port 80.
- `DNS_PROVIDER` (default: use self-signed certificate): Code of the ACME DNS provider for the main domain wildcard. - `DNS_PROVIDER` (default: use self-signed certificate): Code of the ACME DNS provider for the main domain wildcard.
See <https://go-acme.github.io/lego/dns/> for available values & additional environment variables. See <https://go-acme.github.io/lego/dns/> for available values & additional environment variables.
- `NO_DNS_01` (default: `false`): Disable the use of ACME DNS. This means that the wildcard certificate is self-signed and all domains and subdomains will have a distinct certificate. Because this may lead to a rate limit from the ACME provider, this option is not recommended for Gitea/Forgejo instances with open registrations or a great number of users/orgs.
- `LOG_LEVEL` (default: warn): Set this to specify the level of logging. - `LOG_LEVEL` (default: warn): Set this to specify the level of logging.
- `REDIS_URL` (default: use in-memory cache): Set this to use Redis as a cache server.
## Contributing to the development ## Contributing to the development

View File

@ -126,6 +126,13 @@ var (
EnvVars: []string{"BLACKLISTED_PATHS"}, EnvVars: []string{"BLACKLISTED_PATHS"},
}, },
&cli.StringFlag{
Name: "redis-url",
Value: "",
Usage: "use redis instead of the built-in memory cache (recommended)",
EnvVars: []string{"REDIS_URL"},
},
&cli.StringFlag{ &cli.StringFlag{
Name: "log-level", Name: "log-level",
Value: "warn", Value: "warn",
@ -178,6 +185,11 @@ var (
Usage: "Use DNS-Challenge for main domain. Read more at: https://go-acme.github.io/lego/dns/", Usage: "Use DNS-Challenge for main domain. Read more at: https://go-acme.github.io/lego/dns/",
EnvVars: []string{"DNS_PROVIDER"}, EnvVars: []string{"DNS_PROVIDER"},
}, },
&cli.BoolFlag{
Name: "no-dns-01",
Usage: "Always use individual certificates instead of a DNS-01 wild card certificate",
EnvVars: []string{"NO_DNS_01"},
},
&cli.StringFlag{ &cli.StringFlag{
Name: "acme-account-config", Name: "acme-account-config",
Usage: "json file of acme account", Usage: "json file of acme account",

View File

@ -6,6 +6,7 @@ type Config struct {
Gitea GiteaConfig Gitea GiteaConfig
Database DatabaseConfig Database DatabaseConfig
ACME ACMEConfig ACME ACMEConfig
Cache CacheConfig
} }
type ServerConfig struct { type ServerConfig struct {
@ -42,5 +43,10 @@ type ACMEConfig struct {
EAB_HMAC string EAB_HMAC string
EAB_KID string EAB_KID string
DNSProvider string DNSProvider string
NoDNS01 bool `default:"false"`
AccountConfigFile string `default:"acme-account.json"` AccountConfigFile string `default:"acme-account.json"`
} }
type CacheConfig struct {
RedisURL string `default:""`
}

View File

@ -54,6 +54,7 @@ func MergeConfig(ctx *cli.Context, config *Config) {
mergeGiteaConfig(ctx, &config.Gitea) mergeGiteaConfig(ctx, &config.Gitea)
mergeDatabaseConfig(ctx, &config.Database) mergeDatabaseConfig(ctx, &config.Database)
mergeACMEConfig(ctx, &config.ACME) mergeACMEConfig(ctx, &config.ACME)
mergeCacheConfig(ctx, &config.Cache)
} }
func mergeServerConfig(ctx *cli.Context, config *ServerConfig) { func mergeServerConfig(ctx *cli.Context, config *ServerConfig) {
@ -141,7 +142,16 @@ func mergeACMEConfig(ctx *cli.Context, config *ACMEConfig) {
if ctx.IsSet("dns-provider") { if ctx.IsSet("dns-provider") {
config.DNSProvider = ctx.String("dns-provider") config.DNSProvider = ctx.String("dns-provider")
} }
if ctx.IsSet("no-dns-01") {
config.NoDNS01 = ctx.Bool("no-dns-01")
}
if ctx.IsSet("acme-account-config") { if ctx.IsSet("acme-account-config") {
config.AccountConfigFile = ctx.String("acme-account-config") config.AccountConfigFile = ctx.String("acme-account-config")
} }
} }
func mergeCacheConfig(ctx *cli.Context, config *CacheConfig) {
if ctx.IsSet("redis-url") {
config.RedisURL = ctx.String("redis-url")
}
}

View File

@ -166,6 +166,7 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
EAB_HMAC: "original", EAB_HMAC: "original",
EAB_KID: "original", EAB_KID: "original",
DNSProvider: "original", DNSProvider: "original",
NoDNS01: false,
AccountConfigFile: "original", AccountConfigFile: "original",
}, },
} }
@ -205,6 +206,7 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
EAB_HMAC: "changed", EAB_HMAC: "changed",
EAB_KID: "changed", EAB_KID: "changed",
DNSProvider: "changed", DNSProvider: "changed",
NoDNS01: true,
AccountConfigFile: "changed", AccountConfigFile: "changed",
}, },
} }
@ -243,6 +245,7 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T
"--acme-eab-hmac", "changed", "--acme-eab-hmac", "changed",
"--acme-eab-kid", "changed", "--acme-eab-kid", "changed",
"--dns-provider", "changed", "--dns-provider", "changed",
"--no-dns-01",
"--acme-account-config", "changed", "--acme-account-config", "changed",
}, },
) )
@ -517,6 +520,7 @@ func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testi
EAB_HMAC: "original", EAB_HMAC: "original",
EAB_KID: "original", EAB_KID: "original",
DNSProvider: "original", DNSProvider: "original",
NoDNS01: false,
AccountConfigFile: "original", AccountConfigFile: "original",
} }
@ -530,6 +534,7 @@ func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testi
EAB_HMAC: "changed", EAB_HMAC: "changed",
EAB_KID: "changed", EAB_KID: "changed",
DNSProvider: "changed", DNSProvider: "changed",
NoDNS01: true,
AccountConfigFile: "changed", AccountConfigFile: "changed",
} }
@ -545,6 +550,7 @@ func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testi
"--acme-eab-hmac", "changed", "--acme-eab-hmac", "changed",
"--acme-eab-kid", "changed", "--acme-eab-kid", "changed",
"--dns-provider", "changed", "--dns-provider", "changed",
"--no-dns-01",
"--acme-account-config", "changed", "--acme-account-config", "changed",
}, },
) )
@ -563,6 +569,7 @@ func TestMergeACMEConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExi
{args: []string{"--acme-eab-hmac", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_HMAC = "changed" }}, {args: []string{"--acme-eab-hmac", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_HMAC = "changed" }},
{args: []string{"--acme-eab-kid", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_KID = "changed" }}, {args: []string{"--acme-eab-kid", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_KID = "changed" }},
{args: []string{"--dns-provider", "changed"}, callback: func(gc *ACMEConfig) { gc.DNSProvider = "changed" }}, {args: []string{"--dns-provider", "changed"}, callback: func(gc *ACMEConfig) { gc.DNSProvider = "changed" }},
{args: []string{"--no-dns-01"}, callback: func(gc *ACMEConfig) { gc.NoDNS01 = true }},
{args: []string{"--acme-account-config", "changed"}, callback: func(gc *ACMEConfig) { gc.AccountConfigFile = "changed" }}, {args: []string{"--acme-account-config", "changed"}, callback: func(gc *ACMEConfig) { gc.AccountConfigFile = "changed" }},
} }

6
go.mod
View File

@ -7,13 +7,16 @@ toolchain go1.21.4
require ( require (
code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f code.gitea.io/sdk/gitea v0.16.1-0.20231115014337-e23e8aa3004f
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a
github.com/creasty/defaults v1.7.0
github.com/go-acme/lego/v4 v4.5.3 github.com/go-acme/lego/v4 v4.5.3
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/joho/godotenv v1.4.0 github.com/joho/godotenv v1.4.0
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.7
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.26
github.com/pelletier/go-toml/v2 v2.1.0 github.com/pelletier/go-toml/v2 v2.1.0
github.com/redis/go-redis/v9 v9.5.1
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad
github.com/rs/zerolog v1.27.0 github.com/rs/zerolog v1.27.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
@ -42,13 +45,14 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/cloudflare-go v0.20.0 // indirect github.com/cloudflare/cloudflare-go v0.20.0 // indirect
github.com/cpu/goacmedns v0.1.1 // indirect github.com/cpu/goacmedns v0.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/creasty/defaults v1.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/deepmap/oapi-codegen v1.6.1 // indirect github.com/deepmap/oapi-codegen v1.6.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go v0.70.1 // indirect github.com/dnsimple/dnsimple-go v0.70.1 // indirect
github.com/exoscale/egoscale v0.67.0 // indirect github.com/exoscale/egoscale v0.67.0 // indirect

12
go.sum
View File

@ -97,6 +97,10 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@ -105,6 +109,8 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -143,6 +149,8 @@ github.com/deepmap/oapi-codegen v1.6.1 h1:2BvsmRb6pogGNtr8Ann+esAbSKFXx2CZN18VpA
github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
@ -324,6 +332,8 @@ github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@ -615,6 +625,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
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/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad h1:WtSUHi5zthjudjIi3L6QmL/V9vpJPbc/j/F2u55d3fs= github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad h1:WtSUHi5zthjudjIi3L6QmL/V9vpJPbc/j/F2u55d3fs=

View File

@ -13,8 +13,8 @@ var ErrAcmeMissConfig = errors.New("ACME client has wrong config")
func CreateAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*certificates.AcmeClient, error) { func CreateAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*certificates.AcmeClient, error) {
// check config // check config
if (!cfg.AcceptTerms || cfg.DNSProvider == "") && cfg.APIEndpoint != "https://acme.mock.directory" { if (!cfg.AcceptTerms || (cfg.DNSProvider == "" && !cfg.NoDNS01)) && cfg.APIEndpoint != "https://acme.mock.directory" {
return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig) return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER or $NO_DNS_01, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig)
} }
if cfg.EAB_HMAC != "" && cfg.EAB_KID == "" { if cfg.EAB_HMAC != "" && cfg.EAB_KID == "" {
return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig) return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig)

View File

@ -4,7 +4,7 @@ import "time"
// ICache is an interface that defines how the pages server interacts with the cache. // ICache is an interface that defines how the pages server interacts with the cache.
type ICache interface { type ICache interface {
Set(key string, value interface{}, ttl time.Duration) error Set(key string, value []byte, ttl time.Duration) error
Get(key string) (interface{}, bool) Get(key string) ([]byte, bool)
Remove(key string) Remove(key string)
} }

View File

@ -1,7 +1,32 @@
package cache package cache
import "github.com/OrlovEvgeny/go-mcache" import (
"time"
"github.com/OrlovEvgeny/go-mcache"
)
type MCache struct {
mcache *mcache.CacheDriver
}
func (m *MCache) Set(key string, value []byte, ttl time.Duration) error {
return m.mcache.Set(key, value, ttl)
}
func (m *MCache) Get(key string) ([]byte, bool) {
val, ok := m.mcache.Get(key)
if ok {
return val.([]byte), true
} else {
return nil, false
}
}
func (m *MCache) Remove(key string) {
m.mcache.Remove(key)
}
func NewInMemoryCache() ICache { func NewInMemoryCache() ICache {
return mcache.New() return &MCache{mcache.New()}
} }

51
server/cache/redis.go vendored Normal file
View File

@ -0,0 +1,51 @@
package cache
import (
"context"
"errors"
"time"
"github.com/redis/go-redis/v9"
"github.com/rs/zerolog/log"
)
type RedisCache struct {
name string
ctx context.Context
rdb *redis.Client
}
func (r *RedisCache) Set(key string, value []byte, ttl time.Duration) error {
log.Trace().Str("key", r.name+"::"+key).Int("len(value)", len(value)).Bytes("value", value).Msg("Set in Redis.")
return r.rdb.Set(r.ctx, r.name+"::"+key, value, ttl).Err()
}
func (r *RedisCache) Get(key string) ([]byte, bool) {
val, err := r.rdb.Get(r.ctx, r.name+"::"+key).Bytes()
if err != nil {
if !errors.Is(err, redis.Nil) {
log.Error().Err(err).Str("key", r.name+"::"+key).Msg("Couldn't request key from cache.")
} else {
log.Trace().Str("key", r.name+"::"+key).Msg("Get from Redis, doesn't exist.")
}
return nil, false
} else {
log.Trace().Str("key", r.name+"::"+key).Int("len(value)", len(val)).Msg("Get from Redis.")
return val, true
}
}
func (r *RedisCache) Remove(key string) {
err := r.rdb.Del(r.ctx, r.name+"::"+key).Err()
if err != nil {
log.Error().Err(err).Str("key", r.name+"::"+key).Msg("Couldn't delete key from cache.")
}
}
func NewRedisCache(name string, opts *redis.Options) ICache {
return &RedisCache{
name,
context.Background(),
redis.NewClient(opts),
}
}

View File

@ -56,11 +56,8 @@ func NewAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
} else { } else {
if cfg.DNSProvider == "" { if cfg.DNSProvider == "" {
// using mock server, don't use wildcard certs // using mock wildcard certs
err := mainDomainAcmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache}) mainDomainAcmeClient = nil
if err != nil {
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
}
} else { } else {
// use DNS-Challenge https://go-acme.github.io/lego/dns/ // use DNS-Challenge https://go-acme.github.io/lego/dns/
provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider) provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider)

View File

@ -22,7 +22,7 @@ type AcmeTLSChallengeProvider struct {
var _ challenge.Provider = AcmeTLSChallengeProvider{} var _ challenge.Provider = AcmeTLSChallengeProvider{}
func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error { func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error {
return a.challengeCache.Set(domain, keyAuth, 1*time.Hour) return a.challengeCache.Set(domain, []byte(keyAuth), 1*time.Hour)
} }
func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error { func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
@ -38,7 +38,7 @@ type AcmeHTTPChallengeProvider struct {
var _ challenge.Provider = AcmeHTTPChallengeProvider{} var _ challenge.Provider = AcmeHTTPChallengeProvider{}
func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error { func (a AcmeHTTPChallengeProvider) Present(domain, token, keyAuth string) error {
return a.challengeCache.Set(domain+"/"+token, keyAuth, 1*time.Hour) return a.challengeCache.Set(domain+"/"+token, []byte(keyAuth), 1*time.Hour)
} }
func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error { func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
@ -60,12 +60,12 @@ func SetupHTTPACMEChallengeServer(challengeCache cache.ICache, sslPort uint) htt
// it's an acme request // it's an acme request
if strings.HasPrefix(ctx.Path(), challengePath) { if strings.HasPrefix(ctx.Path(), challengePath) {
challenge, ok := challengeCache.Get(domain + "/" + strings.TrimPrefix(ctx.Path(), challengePath)) challenge, ok := challengeCache.Get(domain + "/" + strings.TrimPrefix(ctx.Path(), challengePath))
if !ok || challenge == nil { if !ok {
log.Info().Msgf("HTTP-ACME challenge for '%s' failed: token not found", domain) log.Info().Msgf("HTTP-ACME challenge for '%s' failed: token not found", domain)
ctx.String("no challenge for this token", http.StatusNotFound) ctx.String("no challenge for this token", http.StatusNotFound)
} }
log.Info().Msgf("HTTP-ACME challenge for '%s' succeeded", domain) log.Info().Msgf("HTTP-ACME challenge for '%s' succeeded", domain)
ctx.String(challenge.(string)) ctx.String(string(challenge))
return return
} }

View File

@ -10,6 +10,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/hashicorp/golang-lru/v2"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/challenge/tlsalpn01"
@ -26,13 +28,17 @@ import (
var ErrUserRateLimitExceeded = errors.New("rate limit exceeded: 10 certificates per user per 24 hours") var ErrUserRateLimitExceeded = errors.New("rate limit exceeded: 10 certificates per user per 24 hours")
var keyCache *lru.Cache[string, tls.Certificate]
// TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates. // TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates.
func TLSConfig(mainDomainSuffix string, func TLSConfig(mainDomainSuffix string,
giteaClient *gitea.Client, giteaClient *gitea.Client,
acmeClient *AcmeClient, acmeClient *AcmeClient,
firstDefaultBranch string, firstDefaultBranch string,
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.ICache, challengeCache, canonicalDomainCache cache.ICache,
certDB database.CertDB, certDB database.CertDB,
noDNS01 bool,
rawDomain string,
) *tls.Config { ) *tls.Config {
return &tls.Config{ return &tls.Config{
// check DNS name & get certificate from Let's Encrypt // check DNS name & get certificate from Let's Encrypt
@ -54,7 +60,7 @@ func TLSConfig(mainDomainSuffix string,
if !ok { if !ok {
return nil, errors.New("no challenge for this domain") return nil, errors.New("no challenge for this domain")
} }
cert, err := tlsalpn01.ChallengeCert(domain, challenge.(string)) cert, err := tlsalpn01.ChallengeCert(domain, string(challenge))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -64,12 +70,27 @@ func TLSConfig(mainDomainSuffix string,
targetOwner := "" targetOwner := ""
mayObtainCert := true mayObtainCert := true
if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) { if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) {
// deliver default certificate for the main domain (*.codeberg.page) if noDNS01 {
domain = mainDomainSuffix // Limit the domains allowed to request a certificate to pages-server domains
// and domains for an existing user of org
if !strings.EqualFold(domain, mainDomainSuffix[1:]) && !strings.EqualFold(domain, rawDomain) {
targetOwner := strings.TrimSuffix(domain, mainDomainSuffix)
owner_exist, err := giteaClient.GiteaCheckIfOwnerExists(targetOwner)
mayObtainCert = owner_exist
if err != nil {
log.Error().Err(err).Msgf("Failed to check '%s' existence on the forge: %s", targetOwner, err)
mayObtainCert = false
}
}
} else {
// deliver default certificate for the main domain (*.codeberg.page)
domain = mainDomainSuffix
}
} else { } else {
var targetRepo, targetBranch string var targetRepo, targetBranch string
targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache) targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch)
if targetOwner == "" { if targetOwner == "" {
// DNS not set up, return main certificate to redirect to the docs // DNS not set up, return main certificate to redirect to the docs
domain = mainDomainSuffix domain = mainDomainSuffix
@ -88,9 +109,17 @@ func TLSConfig(mainDomainSuffix string,
} }
} }
if keyCache == nil {
var err error
keyCache, err = lru.New[string, tls.Certificate](4096)
if err != nil {
panic(err) // This should only happen if 4096 < 0 at the time of writing, which should be reason enough to panic.
}
}
if tlsCertificate, ok := keyCache.Get(domain); ok { if tlsCertificate, ok := keyCache.Get(domain); ok {
// we can use an existing certificate object // we can use an existing certificate object
return tlsCertificate.(*tls.Certificate), nil return &tlsCertificate, nil
} }
var tlsCertificate *tls.Certificate var tlsCertificate *tls.Certificate
@ -115,9 +144,7 @@ func TLSConfig(mainDomainSuffix string,
} }
} }
if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil { keyCache.Add(domain, *tlsCertificate)
return nil, err
}
return tlsCertificate, nil return tlsCertificate, nil
}, },
NextProtos: []string{ NextProtos: []string{
@ -199,9 +226,6 @@ func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProv
func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user string, useDnsProvider bool, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) { func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user string, useDnsProvider bool, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) {
name := strings.TrimPrefix(domains[0], "*") name := strings.TrimPrefix(domains[0], "*")
if useDnsProvider && domains[0] != "" && domains[0][0] == '*' {
domains = domains[1:]
}
// lock to avoid simultaneous requests // lock to avoid simultaneous requests
_, working := c.obtainLocks.LoadOrStore(name, struct{}{}) _, working := c.obtainLocks.LoadOrStore(name, struct{}{})
@ -219,7 +243,11 @@ func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew
defer c.obtainLocks.Delete(name) defer c.obtainLocks.Delete(name)
if acmeClient == nil { if acmeClient == nil {
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase) if useDnsProvider {
return mockCert(domains[0], "DNS ACME client is not defined", mainDomainSuffix, keyDatabase)
} else {
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase)
}
} }
// request actual cert // request actual cert

View File

@ -52,7 +52,6 @@ func (x xDB) Close() error {
func (x xDB) Put(domain string, cert *certificate.Resource) error { func (x xDB) Put(domain string, cert *certificate.Resource) error {
log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db") log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db")
domain = integrationTestReplacements(domain)
c, err := toCert(domain, cert) c, err := toCert(domain, cert)
if err != nil { if err != nil {
return err return err
@ -82,7 +81,6 @@ func (x xDB) Get(domain string) (*certificate.Resource, error) {
if domain[:1] == "." { if domain[:1] == "." {
domain = "*" + domain domain = "*" + domain
} }
domain = integrationTestReplacements(domain)
cert := new(Cert) cert := new(Cert)
log.Trace().Str("domain", domain).Msg("get cert from db") log.Trace().Str("domain", domain).Msg("get cert from db")
@ -99,7 +97,6 @@ func (x xDB) Delete(domain string) error {
if domain[:1] == "." { if domain[:1] == "." {
domain = "*" + domain domain = "*" + domain
} }
domain = integrationTestReplacements(domain)
log.Trace().Str("domain", domain).Msg("delete cert from db") log.Trace().Str("domain", domain).Msg("delete cert from db")
_, err := x.engine.ID(domain).Delete(new(Cert)) _, err := x.engine.ID(domain).Delete(new(Cert))
@ -139,13 +136,3 @@ func supportedDriver(driver string) bool {
return false return false
} }
} }
// integrationTestReplacements is needed because integration tests use a single domain cert,
// while production use a wildcard cert
// TODO: find a better way to handle this
func integrationTestReplacements(domainKey string) string {
if domainKey == "*.localhost.mock.directory" {
return "localhost.mock.directory"
}
return domainKey
}

View File

@ -5,22 +5,35 @@ import (
"strings" "strings"
"time" "time"
"codeberg.org/codeberg/pages/server/cache" "github.com/hashicorp/golang-lru/v2"
) )
// lookupCacheTimeout specifies the timeout for the DNS lookup cache. type lookupCacheEntry struct {
var lookupCacheTimeout = 15 * time.Minute cachedName string
timestamp time.Time
}
var lookupCacheValidity = 30 * time.Second
var lookupCache *lru.Cache[string, lookupCacheEntry]
var defaultPagesRepo = "pages" var defaultPagesRepo = "pages"
// GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix. // GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix.
// If everything is fine, it returns the target data. // If everything is fine, it returns the target data.
func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLookupCache cache.ICache) (targetOwner, targetRepo, targetBranch string) { func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string) (targetOwner, targetRepo, targetBranch string) {
// Get CNAME or TXT // Get CNAME or TXT
var cname string var cname string
var err error var err error
if cachedName, ok := dnsLookupCache.Get(domain); ok {
cname = cachedName.(string) if lookupCache == nil {
lookupCache, err = lru.New[string, lookupCacheEntry](4096)
if err != nil {
panic(err) // This should only happen if 4096 < 0 at the time of writing, which should be reason enough to panic.
}
}
if entry, ok := lookupCache.Get(domain); ok && time.Now().Before(entry.timestamp.Add(lookupCacheValidity)) {
cname = entry.cachedName
} else { } else {
cname, err = net.LookupCNAME(domain) cname, err = net.LookupCNAME(domain)
cname = strings.TrimSuffix(cname, ".") cname = strings.TrimSuffix(cname, ".")
@ -38,7 +51,10 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo
} }
} }
} }
_ = dnsLookupCache.Set(domain, cname, lookupCacheTimeout) _ = lookupCache.Add(domain, lookupCacheEntry{
cname,
time.Now(),
})
} }
if cname == "" { if cname == "" {
return return

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -26,6 +28,9 @@ const (
// TODO: move as option into cache interface // TODO: move as option into cache interface
fileCacheTimeout = 5 * time.Minute fileCacheTimeout = 5 * time.Minute
// ownerExistenceCacheTimeout specifies the timeout for the existence of a repo/org
ownerExistenceCacheTimeout = 5 * time.Minute
// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default. // fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
fileCacheSizeLimit = int64(1000 * 1000) fileCacheSizeLimit = int64(1000 * 1000)
) )
@ -38,6 +43,24 @@ type FileResponse struct {
Body []byte Body []byte
} }
func FileResponseFromMetadataString(metadataString string) FileResponse {
parts := strings.Split(metadataString, "\n")
res := FileResponse{
Exists: parts[0] == "true",
IsSymlink: parts[1] == "true",
ETag: parts[2],
MimeType: parts[3],
}
return res
}
func (f FileResponse) MetadataAsString() string {
return strconv.FormatBool(f.Exists) + "\n" +
strconv.FormatBool(f.IsSymlink) + "\n" +
f.ETag + "\n" +
f.MimeType + "\n"
}
func (f FileResponse) IsEmpty() bool { func (f FileResponse) IsEmpty() bool {
return len(f.Body) == 0 return len(f.Body) == 0
} }
@ -76,32 +99,42 @@ type writeCacheReader struct {
cacheKey string cacheKey string
cache cache.ICache cache cache.ICache
hasError bool hasError bool
doNotCache bool
complete bool
} }
func (t *writeCacheReader) Read(p []byte) (n int, err error) { func (t *writeCacheReader) Read(p []byte) (n int, err error) {
log.Trace().Msgf("[cache] read %q", t.cacheKey) log.Trace().Msgf("[cache] read %q", t.cacheKey)
n, err = t.originalReader.Read(p) n, err = t.originalReader.Read(p)
if err == io.EOF {
t.complete = true
}
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey) log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey)
t.hasError = true t.hasError = true
} else if n > 0 { } else if n > 0 {
_, _ = t.buffer.Write(p[:n]) if t.buffer.Len()+n > int(fileCacheSizeLimit) {
t.doNotCache = true
t.buffer.Reset()
} else {
_, _ = t.buffer.Write(p[:n])
}
} }
return return
} }
func (t *writeCacheReader) Close() error { func (t *writeCacheReader) Close() error {
doWrite := !t.hasError doWrite := !t.hasError && !t.doNotCache && t.complete
fc := *t.fileResponse fc := *t.fileResponse
fc.Body = t.buffer.Bytes() fc.Body = t.buffer.Bytes()
if fc.IsEmpty() {
log.Trace().Msg("[cache] file response is empty")
doWrite = false
}
if doWrite { if doWrite {
err := t.cache.Set(t.cacheKey, fc, fileCacheTimeout) err := t.cache.Set(t.cacheKey+"|Metadata", []byte(fc.MetadataAsString()), fileCacheTimeout)
if err != nil { if err != nil {
log.Trace().Err(err).Msgf("[cache] writer for %q has returned an error", t.cacheKey) log.Trace().Err(err).Msgf("[cache] writer for %q has returned an error", t.cacheKey+"|Metadata")
}
err = t.cache.Set(t.cacheKey+"|Body", fc.Body, fileCacheTimeout)
if err != nil {
log.Trace().Err(err).Msgf("[cache] writer for %q has returned an error", t.cacheKey+"|Body")
} }
} }
log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, doWrite) log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, doWrite)

View File

@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"strconv"
"strings" "strings"
"time" "time"
@ -28,6 +27,7 @@ const (
branchTimestampCacheKeyPrefix = "branchTime" branchTimestampCacheKeyPrefix = "branchTime"
defaultBranchCacheKeyPrefix = "defaultBranch" defaultBranchCacheKeyPrefix = "defaultBranch"
rawContentCacheKeyPrefix = "rawContent" rawContentCacheKeyPrefix = "rawContent"
ownerExistenceKeyPrefix = "ownerExist"
// pages server // pages server
PagesCacheIndicatorHeader = "X-Pages-Cache" PagesCacheIndicatorHeader = "X-Pages-Cache"
@ -114,24 +114,27 @@ func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource str
log := log.With().Str("cache_key", cacheKey).Logger() log := log.With().Str("cache_key", cacheKey).Logger()
log.Trace().Msg("try file in cache") log.Trace().Msg("try file in cache")
// handle if cache entry exist // handle if cache entry exist
if cache, ok := client.responseCache.Get(cacheKey); ok { if cacheMetadata, ok := client.responseCache.Get(cacheKey + "|Metadata"); ok {
cache := cache.(FileResponse) cache := FileResponseFromMetadataString(string(cacheMetadata))
cache.Body, _ = client.responseCache.Get(cacheKey + "|Body")
// TODO: don't grab the content from the cache if the ETag matches?!
cachedHeader, cachedStatusCode := cache.createHttpResponse(cacheKey) cachedHeader, cachedStatusCode := cache.createHttpResponse(cacheKey)
// TODO: check against some timestamp mismatch?!? // TODO: check against some timestamp mismatch?!?
if cache.Exists { if cache.Exists {
log.Debug().Msg("[cache] exists")
if cache.IsSymlink { if cache.IsSymlink {
linkDest := string(cache.Body) linkDest := string(cache.Body)
log.Debug().Msgf("[cache] follow symlink from %q to %q", resource, linkDest) log.Debug().Msgf("[cache] follow symlink from %q to %q", resource, linkDest)
return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest) return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
} else if !cache.IsEmpty() { } else {
log.Debug().Msgf("[cache] return %d bytes", len(cache.Body)) log.Debug().Msgf("[cache] return %d bytes", len(cache.Body))
return io.NopCloser(bytes.NewReader(cache.Body)), cachedHeader, cachedStatusCode, nil return io.NopCloser(bytes.NewReader(cache.Body)), cachedHeader, cachedStatusCode, nil
} else if cache.IsEmpty() {
log.Debug().Msg("[cache] is empty")
} }
} else {
return nil, nil, http.StatusNotFound, ErrorNotFound
} }
} }
// TODO: metadata not written, is close ever called?
log.Trace().Msg("file not in cache") log.Trace().Msg("file not in cache")
// not in cache, open reader via gitea api // not in cache, open reader via gitea api
reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS) reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS)
@ -163,7 +166,11 @@ func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource str
ETag: resp.Header.Get(ETagHeader), ETag: resp.Header.Get(ETagHeader),
} }
log.Trace().Msgf("file response has %d bytes", len(fileResponse.Body)) log.Trace().Msgf("file response has %d bytes", len(fileResponse.Body))
if err := client.responseCache.Set(cacheKey, fileResponse, fileCacheTimeout); err != nil { if err := client.responseCache.Set(cacheKey+"|Metadata", []byte(fileResponse.MetadataAsString()), fileCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
// TODO: Test with binary files, as we convert []byte to string! Using []byte values might makes more sense anyways.
if err := client.responseCache.Set(cacheKey+"|Body", fileResponse.Body, fileCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write") log.Error().Err(err).Msg("[cache] error on cache write")
} }
@ -176,10 +183,6 @@ func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource str
mimeType := client.getMimeTypeByExtension(resource) mimeType := client.getMimeTypeByExtension(resource)
resp.Response.Header.Set(ContentTypeHeader, mimeType) resp.Response.Header.Set(ContentTypeHeader, mimeType)
if !shouldRespBeSavedToCache(resp.Response) {
return reader, resp.Response.Header, resp.StatusCode, err
}
// now we write to cache and respond at the same time // now we write to cache and respond at the same time
fileResp := FileResponse{ fileResp := FileResponse{
Exists: true, Exists: true,
@ -189,10 +192,7 @@ func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource str
return fileResp.CreateCacheReader(reader, client.responseCache, cacheKey), resp.Response.Header, resp.StatusCode, nil return fileResp.CreateCacheReader(reader, client.responseCache, cacheKey), resp.Response.Header, resp.StatusCode, nil
case http.StatusNotFound: case http.StatusNotFound:
if err := client.responseCache.Set(cacheKey, FileResponse{ if err := client.responseCache.Set(cacheKey+"|Metadata", []byte(FileResponse{ETag: resp.Header.Get(ETagHeader)}.MetadataAsString()), fileCacheTimeout); err != nil {
Exists: false,
ETag: resp.Header.Get(ETagHeader),
}, fileCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write") log.Error().Err(err).Msg("[cache] error on cache write")
} }
@ -207,21 +207,28 @@ func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource str
func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (*BranchTimestamp, error) { func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (*BranchTimestamp, error) {
cacheKey := fmt.Sprintf("%s/%s/%s/%s", branchTimestampCacheKeyPrefix, repoOwner, repoName, branchName) cacheKey := fmt.Sprintf("%s/%s/%s/%s", branchTimestampCacheKeyPrefix, repoOwner, repoName, branchName)
if stamp, ok := client.responseCache.Get(cacheKey); ok && stamp != nil { if stamp, ok := client.responseCache.Get(cacheKey); ok {
branchTimeStamp := stamp.(*BranchTimestamp) if len(stamp) == 0 {
if branchTimeStamp.notFound {
log.Trace().Msgf("[cache] use branch %q not found", branchName) log.Trace().Msgf("[cache] use branch %q not found", branchName)
return &BranchTimestamp{}, ErrorNotFound return &BranchTimestamp{}, ErrorNotFound
} }
log.Trace().Msgf("[cache] use branch %q exist", branchName) log.Trace().Msgf("[cache] use branch %q exist", branchName)
return branchTimeStamp, nil // This comes from the refactoring of the caching library.
// The branch as reported by the API was stored in the cache, and I'm not sure if there are
// situations where it differs from the name in the request, hence this is left here.
stampParts := strings.SplitN(string(stamp), "|", 2)
stampTime, _ := time.Parse(time.RFC3339, stampParts[0])
return &BranchTimestamp{
Branch: stampParts[1],
Timestamp: stampTime,
}, nil
} }
branch, resp, err := client.sdkClient.GetRepoBranch(repoOwner, repoName, branchName) branch, resp, err := client.sdkClient.GetRepoBranch(repoOwner, repoName, branchName)
if err != nil { if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound { if resp != nil && resp.StatusCode == http.StatusNotFound {
log.Trace().Msgf("[cache] set cache branch %q not found", branchName) log.Trace().Msgf("[cache] set cache branch %q not found", branchName)
if err := client.responseCache.Set(cacheKey, &BranchTimestamp{Branch: branchName, notFound: true}, branchExistenceCacheTimeout); err != nil { if err := client.responseCache.Set(cacheKey, []byte{}, branchExistenceCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write") log.Error().Err(err).Msg("[cache] error on cache write")
} }
return &BranchTimestamp{}, ErrorNotFound return &BranchTimestamp{}, ErrorNotFound
@ -238,7 +245,7 @@ func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchNam
} }
log.Trace().Msgf("set cache branch [%s] exist", branchName) log.Trace().Msgf("set cache branch [%s] exist", branchName)
if err := client.responseCache.Set(cacheKey, stamp, branchExistenceCacheTimeout); err != nil { if err := client.responseCache.Set(cacheKey, []byte(stamp.Timestamp.Format(time.RFC3339)+"|"+stamp.Branch), branchExistenceCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write") log.Error().Err(err).Msg("[cache] error on cache write")
} }
return stamp, nil return stamp, nil
@ -247,8 +254,8 @@ func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchNam
func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) { func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) {
cacheKey := fmt.Sprintf("%s/%s/%s", defaultBranchCacheKeyPrefix, repoOwner, repoName) cacheKey := fmt.Sprintf("%s/%s/%s", defaultBranchCacheKeyPrefix, repoOwner, repoName)
if branch, ok := client.responseCache.Get(cacheKey); ok && branch != nil { if branch, ok := client.responseCache.Get(cacheKey); ok {
return branch.(string), nil return string(branch), nil
} }
repo, resp, err := client.sdkClient.GetRepo(repoOwner, repoName) repo, resp, err := client.sdkClient.GetRepo(repoOwner, repoName)
@ -260,12 +267,44 @@ func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (str
} }
branch := repo.DefaultBranch branch := repo.DefaultBranch
if err := client.responseCache.Set(cacheKey, branch, defaultBranchCacheTimeout); err != nil { if err := client.responseCache.Set(cacheKey, []byte(branch), defaultBranchCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write") log.Error().Err(err).Msg("[cache] error on cache write")
} }
return branch, nil return branch, nil
} }
func (client *Client) GiteaCheckIfOwnerExists(owner string) (bool, error) {
cacheKey := fmt.Sprintf("%s/%s", ownerExistenceKeyPrefix, owner)
if exist, ok := client.responseCache.Get(cacheKey); ok && exist != nil {
return string(exist) == "true", nil
}
_, resp, err := client.sdkClient.GetUserInfo(owner)
if resp.StatusCode == http.StatusOK && err == nil {
if err := client.responseCache.Set(cacheKey, []byte("true"), ownerExistenceCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
return true, nil
} else if resp.StatusCode != http.StatusNotFound {
return false, err
}
_, resp, err = client.sdkClient.GetOrg(owner)
if resp.StatusCode == http.StatusOK && err == nil {
if err := client.responseCache.Set(cacheKey, []byte("true"), ownerExistenceCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
return true, nil
} else if resp.StatusCode != http.StatusNotFound {
return false, err
}
if err := client.responseCache.Set(cacheKey, []byte("false"), ownerExistenceCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
return false, nil
}
func (client *Client) getMimeTypeByExtension(resource string) string { func (client *Client) getMimeTypeByExtension(resource string) string {
mimeType := mime.TypeByExtension(path.Ext(resource)) mimeType := mime.TypeByExtension(path.Ext(resource))
mimeTypeSplit := strings.SplitN(mimeType, ";", 2) mimeTypeSplit := strings.SplitN(mimeType, ";", 2)
@ -275,22 +314,3 @@ func (client *Client) getMimeTypeByExtension(resource string) string {
log.Trace().Msgf("probe mime of %q is %q", resource, mimeType) log.Trace().Msgf("probe mime of %q is %q", resource, mimeType)
return mimeType return mimeType
} }
func shouldRespBeSavedToCache(resp *http.Response) bool {
if resp == nil {
return false
}
contentLengthRaw := resp.Header.Get(ContentLengthHeader)
if contentLengthRaw == "" {
return false
}
contentLength, err := strconv.ParseInt(contentLengthRaw, 10, 64)
if err != nil {
log.Error().Err(err).Msg("could not parse content length")
}
// if content to big or could not be determined we not cache it
return contentLength > 0 && contentLength < fileCacheSizeLimit
}

View File

@ -23,7 +23,7 @@ const (
func Handler( func Handler(
cfg config.ServerConfig, cfg config.ServerConfig,
giteaClient *gitea.Client, giteaClient *gitea.Client,
dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache, canonicalDomainCache, redirectsCache cache.ICache,
) http.HandlerFunc { ) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) {
log.Debug().Msg("\n----------------------------------------------------------") log.Debug().Msg("\n----------------------------------------------------------")
@ -108,7 +108,7 @@ func Handler(
trimmedHost, trimmedHost,
pathElements, pathElements,
cfg.PagesBranches[0], cfg.PagesBranches[0],
dnsLookupCache, canonicalDomainCache, redirectsCache) canonicalDomainCache, redirectsCache)
} }
} }
} }

View File

@ -19,10 +19,10 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
trimmedHost string, trimmedHost string,
pathElements []string, pathElements []string,
firstDefaultBranch string, firstDefaultBranch string,
dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache, canonicalDomainCache, redirectsCache cache.ICache,
) { ) {
// Serve pages from custom domains // Serve pages from custom domains
targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch, dnsLookupCache) targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch)
if targetOwner == "" { if targetOwner == "" {
html.ReturnErrorPage(ctx, html.ReturnErrorPage(ctx,
"could not obtain repo owner from custom domain", "could not obtain repo owner from custom domain",
@ -53,7 +53,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
return return
} else if canonicalDomain != trimmedHost { } else if canonicalDomain != trimmedHost {
// only redirect if the target is also a codeberg page! // only redirect if the target is also a codeberg page!
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch, dnsLookupCache) targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch)
if targetOwner != "" { if targetOwner != "" {
ctx.Redirect("https://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect) ctx.Redirect("https://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect)
return return

View File

@ -29,7 +29,7 @@ func TestHandlerPerformance(t *testing.T) {
AllowedCorsDomains: []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"}, AllowedCorsDomains: []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"},
PagesBranches: []string{"pages"}, PagesBranches: []string{"pages"},
} }
testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache(), cache.NewInMemoryCache()) testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache())
testCase := func(uri string, status int) { testCase := func(uri string, status int) {
t.Run(uri, func(t *testing.T) { t.Run(uri, func(t *testing.T) {

View File

@ -11,6 +11,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/redis/go-redis/v9"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -70,16 +72,29 @@ func Serve(ctx *cli.Context) error {
} }
defer closeFn() defer closeFn()
keyCache := cache.NewInMemoryCache() var redisErr error = nil
challengeCache := cache.NewInMemoryCache() createCache := func(name string) cache.ICache {
if cfg.Cache.RedisURL != "" {
opts, err := redis.ParseURL(cfg.Cache.RedisURL)
if err != nil {
redisErr = err
return nil
}
return cache.NewRedisCache(name, opts)
}
return cache.NewInMemoryCache()
}
// challengeCache stores the certificate challenges
challengeCache := createCache("challenge")
// canonicalDomainCache stores canonical domains // canonicalDomainCache stores canonical domains
canonicalDomainCache := cache.NewInMemoryCache() canonicalDomainCache := createCache("canonicalDomain")
// dnsLookupCache stores DNS lookups for custom domains
dnsLookupCache := cache.NewInMemoryCache()
// redirectsCache stores redirects in _redirects files // redirectsCache stores redirects in _redirects files
redirectsCache := cache.NewInMemoryCache() redirectsCache := createCache("redirects")
// clientResponseCache stores responses from the Gitea server // clientResponseCache stores responses from the Gitea server
clientResponseCache := cache.NewInMemoryCache() clientResponseCache := createCache("clientResponse")
if redisErr != nil {
return redisErr
}
giteaClient, err := gitea.NewClient(cfg.Gitea, clientResponseCache) giteaClient, err := gitea.NewClient(cfg.Gitea, clientResponseCache)
if err != nil { if err != nil {
@ -108,8 +123,10 @@ func Serve(ctx *cli.Context) error {
giteaClient, giteaClient,
acmeClient, acmeClient,
cfg.Server.PagesBranches[0], cfg.Server.PagesBranches[0],
keyCache, challengeCache, dnsLookupCache, canonicalDomainCache, challengeCache, canonicalDomainCache,
certDB, certDB,
cfg.ACME.NoDNS01,
cfg.Server.RawDomain,
)) ))
interval := 12 * time.Hour interval := 12 * time.Hour
@ -132,7 +149,7 @@ func Serve(ctx *cli.Context) error {
} }
// Create ssl handler based on settings // Create ssl handler based on settings
sslHandler := handler.Handler(cfg.Server, giteaClient, dnsLookupCache, canonicalDomainCache, redirectsCache) sslHandler := handler.Handler(cfg.Server, giteaClient, canonicalDomainCache, redirectsCache)
// Start the ssl listener // Start the ssl listener
log.Info().Msgf("Start SSL server using TCP listener on %s", listener.Addr()) log.Info().Msgf("Start SSL server using TCP listener on %s", listener.Addr())

View File

@ -20,7 +20,7 @@ const canonicalDomainConfig = ".domains"
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.ICache) (domain string, valid bool) { func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.ICache) (domain string, valid bool) {
// Check if this request is cached. // Check if this request is cached.
if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok { if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok {
domains := cachedValue.([]string) domains := strings.Split(string(cachedValue), "\n")
for _, domain := range domains { for _, domain := range domains {
if domain == actualDomain { if domain == actualDomain {
valid = true valid = true
@ -33,6 +33,8 @@ func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain,
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig) body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig)
if err != nil && !errors.Is(err, gitea.ErrorNotFound) { if err != nil && !errors.Is(err, gitea.ErrorNotFound) {
log.Error().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo) log.Error().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo)
// Assume that the domain is valid, as Gitea seems to be broken. Don't cache it though.
return actualDomain, true
} }
var domains []string var domains []string
@ -62,7 +64,7 @@ func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain,
} }
// Add result to cache. // Add result to cache.
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout) _ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, []byte(strings.Join(domains, "\n")), canonicalDomainCacheTimeout)
// Return the first domain from the list and return if any of the domains // Return the first domain from the list and return if any of the domains
// matched the requested domain. // matched the requested domain.

View File

@ -1,6 +1,7 @@
package upstream package upstream
import ( import (
"encoding/json"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -29,7 +30,12 @@ func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.I
// Check for cached redirects // Check for cached redirects
if cachedValue, ok := redirectsCache.Get(cacheKey); ok { if cachedValue, ok := redirectsCache.Get(cacheKey); ok {
redirects = cachedValue.([]Redirect) redirects := []Redirect{}
err := json.Unmarshal(cachedValue, &redirects)
if err != nil {
log.Error().Err(err).Msgf("could not parse redirects for key %s", cacheKey)
// It's okay to continue, the array stays empty.
}
} else { } else {
// Get _redirects file and parse // Get _redirects file and parse
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, redirectsConfig) body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, redirectsConfig)
@ -58,7 +64,12 @@ func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.I
}) })
} }
} }
_ = redirectsCache.Set(cacheKey, redirects, redirectsCacheTimeout) redirectsJson, err := json.Marshal(redirects)
if err != nil {
log.Error().Err(err).Msgf("could not store redirects for key %s", cacheKey)
} else {
_ = redirectsCache.Set(cacheKey, redirectsJson, redirectsCacheTimeout)
}
} }
return redirects return redirects
} }