diff --git a/cli/flags.go b/cli/flags.go index 58004b3..0aed4a9 100644 --- a/cli/flags.go +++ b/cli/flags.go @@ -81,11 +81,11 @@ var ( Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting", EnvVars: []string{"RAW_DOMAIN"}, }, - &cli.StringFlag{ + &cli.StringSliceFlag{ Name: "canonical-domain-file", - Usage: "specifies the file from which the canonical domain shall be specified in", - EnvVars: []string{"CANONICAL_DOMAIN_FILE"}, - Value: ".domains", + Usage: "specifies the file from which the canonical domain may be specified in. Use this flag multiple times to support multiple different file names, if multiple files exist in a single repository they will be merged.", + EnvVars: []string{"CANONICAL_DOMAIN_FILES"}, + Value: cli.NewStringSlice(".domains"), }, // ######################### diff --git a/config/config.go b/config/config.go index 954cdc9..d8c3da1 100644 --- a/config/config.go +++ b/config/config.go @@ -9,16 +9,16 @@ type Config struct { } type ServerConfig struct { - Host string `default:"[::]"` - Port uint16 `default:"443"` - HttpPort uint16 `default:"80"` - HttpServerEnabled bool `default:"true"` - MainDomain string - RawDomain string - CanonicalDomainFile string `default:".domains"` - PagesBranches []string - AllowedCorsDomains []string - BlacklistedPaths []string + Host string `default:"[::]"` + Port uint16 `default:"443"` + HttpPort uint16 `default:"80"` + HttpServerEnabled bool `default:"true"` + MainDomain string + RawDomain string + CanonicalDomainFiles []string `default:"[\".domains\"]"` + PagesBranches []string `default:"[\"main\", \"master\", \"pages\"]"` + AllowedCorsDomains []string + BlacklistedPaths []string } type ForgeConfig struct { diff --git a/config/setup.go b/config/setup.go index 5b56500..2f131c5 100644 --- a/config/setup.go +++ b/config/setup.go @@ -20,9 +20,6 @@ func NewDefaultConfig() Config { panic(err) } - // defaults does not support setting arrays from strings - config.Server.PagesBranches = []string{"main", "master", "pages"} - return config } @@ -76,7 +73,7 @@ func mergeServerConfig(ctx *cli.Context, config *ServerConfig) { config.RawDomain = ctx.String("raw-domain") } if ctx.IsSet("canonical-domain-file") { - config.CanonicalDomainFile = ctx.String("canonical-domain-file") + config.CanonicalDomainFiles = ctx.StringSlice("canonical-domain-file") } if ctx.IsSet("pages-branch") { config.PagesBranches = ctx.StringSlice("pages-branch") diff --git a/config/setup_test.go b/config/setup_test.go index f9a5e1f..dea9fd2 100644 --- a/config/setup_test.go +++ b/config/setup_test.go @@ -136,16 +136,16 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T cfg := &Config{ LogLevel: "original", Server: ServerConfig{ - Host: "original", - Port: 8080, - HttpPort: 80, - HttpServerEnabled: false, - MainDomain: "original", - RawDomain: "original", - CanonicalDomainFile: "original", - PagesBranches: []string{"original"}, - AllowedCorsDomains: []string{"original"}, - BlacklistedPaths: []string{"original"}, + Host: "original", + Port: 8080, + HttpPort: 80, + HttpServerEnabled: false, + MainDomain: "original", + RawDomain: "original", + CanonicalDomainFiles: []string{"original"}, + PagesBranches: []string{"original"}, + AllowedCorsDomains: []string{"original"}, + BlacklistedPaths: []string{"original"}, }, Forge: ForgeConfig{ Root: "original", @@ -177,16 +177,16 @@ func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T expectedConfig := &Config{ LogLevel: "changed", Server: ServerConfig{ - Host: "changed", - Port: 8443, - HttpPort: 443, - HttpServerEnabled: true, - MainDomain: "changed", - RawDomain: "changed", - CanonicalDomainFile: "changed", - PagesBranches: []string{"changed"}, - AllowedCorsDomains: []string{"changed"}, - BlacklistedPaths: append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...), + Host: "changed", + Port: 8443, + HttpPort: 443, + HttpServerEnabled: true, + MainDomain: "changed", + RawDomain: "changed", + CanonicalDomainFiles: []string{"changed"}, + PagesBranches: []string{"changed"}, + AllowedCorsDomains: []string{"changed"}, + BlacklistedPaths: append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...), }, Forge: ForgeConfig{ Root: "changed", @@ -276,29 +276,29 @@ func TestMergeServerConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *tes t, func(ctx *cli.Context) error { cfg := &ServerConfig{ - Host: "original", - Port: 8080, - HttpPort: 80, - HttpServerEnabled: false, - MainDomain: "original", - RawDomain: "original", - CanonicalDomainFile: "original", - AllowedCorsDomains: []string{"original"}, - BlacklistedPaths: []string{"original"}, + Host: "original", + Port: 8080, + HttpPort: 80, + HttpServerEnabled: false, + MainDomain: "original", + RawDomain: "original", + CanonicalDomainFiles: []string{"original"}, + AllowedCorsDomains: []string{"original"}, + BlacklistedPaths: []string{"original"}, } mergeServerConfig(ctx, cfg) expectedConfig := &ServerConfig{ - Host: "changed", - Port: 8443, - HttpPort: 443, - HttpServerEnabled: true, - MainDomain: "changed", - RawDomain: "changed", - CanonicalDomainFile: "changed", - AllowedCorsDomains: fixArrayFromCtx(ctx, "allowed-cors-domains", []string{"changed"}), - BlacklistedPaths: fixArrayFromCtx(ctx, "blacklisted-paths", append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...)), + Host: "changed", + Port: 8443, + HttpPort: 443, + HttpServerEnabled: true, + MainDomain: "changed", + RawDomain: "changed", + CanonicalDomainFiles: fixArrayFromCtx(ctx, "canonical-domain-file", []string{"changed"}), + AllowedCorsDomains: fixArrayFromCtx(ctx, "allowed-cors-domains", []string{"changed"}), + BlacklistedPaths: fixArrayFromCtx(ctx, "blacklisted-paths", append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...)), } assert.Equal(t, expectedConfig, cfg) @@ -332,7 +332,7 @@ func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgE {args: []string{"--enable-http-server"}, callback: func(sc *ServerConfig) { sc.HttpServerEnabled = true }}, {args: []string{"--pages-domain", "changed"}, callback: func(sc *ServerConfig) { sc.MainDomain = "changed" }}, {args: []string{"--raw-domain", "changed"}, callback: func(sc *ServerConfig) { sc.RawDomain = "changed" }}, - {args: []string{"--canonical-domain-file", "changed"}, callback: func(sc *ServerConfig) { sc.CanonicalDomainFile = "changed" }}, + {args: []string{"--canonical-domain-file", "changed"}, callback: func(sc *ServerConfig) { sc.CanonicalDomainFiles = []string{"changed"} }}, {args: []string{"--pages-branch", "changed"}, callback: func(sc *ServerConfig) { sc.PagesBranches = []string{"changed"} }}, {args: []string{"--allowed-cors-domains", "changed"}, callback: func(sc *ServerConfig) { sc.AllowedCorsDomains = []string{"changed"} }}, {args: []string{"--blacklisted-paths", "changed"}, callback: func(sc *ServerConfig) { sc.BlacklistedPaths = []string{"changed"} }}, @@ -343,22 +343,23 @@ func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgE t, func(ctx *cli.Context) error { cfg := ServerConfig{ - Host: "original", - Port: 8080, - HttpPort: 80, - HttpServerEnabled: false, - MainDomain: "original", - RawDomain: "original", - CanonicalDomainFile: "original", - PagesBranches: []string{"original"}, - AllowedCorsDomains: []string{"original"}, - BlacklistedPaths: []string{"original"}, + Host: "original", + Port: 8080, + HttpPort: 80, + HttpServerEnabled: false, + MainDomain: "original", + RawDomain: "original", + CanonicalDomainFiles: []string{"original"}, + PagesBranches: []string{"original"}, + AllowedCorsDomains: []string{"original"}, + BlacklistedPaths: []string{"original"}, } expectedConfig := cfg pair.callback(&expectedConfig) expectedConfig.BlacklistedPaths = append(expectedConfig.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...) + expectedConfig.CanonicalDomainFiles = fixArrayFromCtx(ctx, "canonical-domain-file", expectedConfig.CanonicalDomainFiles) expectedConfig.PagesBranches = fixArrayFromCtx(ctx, "pages-branch", expectedConfig.PagesBranches) expectedConfig.AllowedCorsDomains = fixArrayFromCtx(ctx, "allowed-cors-domains", expectedConfig.AllowedCorsDomains) expectedConfig.BlacklistedPaths = fixArrayFromCtx(ctx, "blacklisted-paths", expectedConfig.BlacklistedPaths) diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go index aa368de..2386609 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -32,7 +32,7 @@ func TLSConfig(mainDomainSuffix string, giteaClient *gitea.Client, acmeClient *AcmeClient, firstDefaultBranch string, - canonicalDomainConfig string, + canonicalDomainConfigs []string, challengeCache, canonicalDomainCache cache.ICache, certDB database.CertDB, noDNS01 bool, @@ -101,10 +101,10 @@ func TLSConfig(mainDomainSuffix string, TargetRepo: targetRepo, TargetBranch: targetBranch, } - _, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainConfig, canonicalDomainCache) + _, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainConfigs, canonicalDomainCache) if !valid { // We shouldn't obtain a certificate when we cannot check if the - // repository has specified this domain in the `.domains` file. + // repository has specified this domain in the specified canonical domain file such as the `.domains` file. mayObtainCert = false } } diff --git a/server/handler/handler.go b/server/handler/handler.go index 22325ec..fdc47a5 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -92,7 +92,7 @@ func Handler( cfg.MainDomain, trimmedHost, pathElements, - cfg.CanonicalDomainFile, + cfg.CanonicalDomainFiles, canonicalDomainCache, redirectsCache) } else if strings.HasSuffix(trimmedHost, cfg.MainDomain) { log.Debug().Msg("subdomain request detected") @@ -101,7 +101,7 @@ func Handler( cfg.PagesBranches, trimmedHost, pathElements, - cfg.CanonicalDomainFile, + cfg.CanonicalDomainFiles, canonicalDomainCache, redirectsCache) } else { log.Debug().Msg("custom domain request detected") @@ -110,7 +110,7 @@ func Handler( trimmedHost, pathElements, cfg.PagesBranches[0], - cfg.CanonicalDomainFile, + cfg.CanonicalDomainFiles, canonicalDomainCache, redirectsCache) } } diff --git a/server/handler/handler_custom_domain.go b/server/handler/handler_custom_domain.go index a655b9b..3776fb8 100644 --- a/server/handler/handler_custom_domain.go +++ b/server/handler/handler_custom_domain.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" "path" "strings" @@ -19,7 +20,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g trimmedHost string, pathElements []string, firstDefaultBranch string, - canonicalDomainConfig string, + canonicalDomainConfigs []string, canonicalDomainCache, redirectsCache cache.ICache, ) { // Serve pages from custom domains @@ -48,9 +49,10 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g TargetBranch: targetBranch, TargetPath: path.Join(pathParts...), }, canonicalLink); works { - canonicalDomain, valid := targetOpt.CheckCanonicalDomain(giteaClient, trimmedHost, mainDomainSuffix, canonicalDomainConfig, canonicalDomainCache) + canonicalDomain, valid := targetOpt.CheckCanonicalDomain(giteaClient, trimmedHost, mainDomainSuffix, canonicalDomainConfigs, canonicalDomainCache) if !valid { - html.ReturnErrorPage(ctx, "domain not specified in .domains file", http.StatusMisdirectedRequest) + msg := fmt.Sprintf("canonical domain not specified, files checked: %s", strings.Join(canonicalDomainConfigs, ", ")) + html.ReturnErrorPage(ctx, msg, http.StatusMisdirectedRequest) return } else if canonicalDomain != trimmedHost { // only redirect if the target is also a codeberg page! @@ -65,7 +67,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g } log.Debug().Msg("tryBranch, now trying upstream 7") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) return } diff --git a/server/handler/handler_raw_domain.go b/server/handler/handler_raw_domain.go index 1aba584..279e7a7 100644 --- a/server/handler/handler_raw_domain.go +++ b/server/handler/handler_raw_domain.go @@ -19,7 +19,7 @@ func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Clie mainDomainSuffix string, trimmedHost string, pathElements []string, - canonicalDomainConfig string, + canonicalDomainConfigs []string, canonicalDomainCache, redirectsCache cache.ICache, ) { // Serve raw content from RawDomain @@ -46,7 +46,7 @@ func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Clie TargetPath: path.Join(pathElements[3:]...), }, true); works { log.Trace().Msg("tryUpstream: serve raw domain with specified branch") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) return } log.Debug().Msg("missing branch info") @@ -63,7 +63,7 @@ func handleRaw(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Clie TargetPath: path.Join(pathElements[2:]...), }, true); works { log.Trace().Msg("tryUpstream: serve raw domain with default branch") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) } else { html.ReturnErrorPage(ctx, fmt.Sprintf("raw domain could not find repo %s/%s or repo is empty", targetOpt.TargetOwner, targetOpt.TargetRepo), diff --git a/server/handler/handler_sub_domain.go b/server/handler/handler_sub_domain.go index 9a19e81..571a6e0 100644 --- a/server/handler/handler_sub_domain.go +++ b/server/handler/handler_sub_domain.go @@ -21,7 +21,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite defaultPagesBranches []string, trimmedHost string, pathElements []string, - canonicalDomainConfig string, + canonicalDomainConfigs []string, canonicalDomainCache, redirectsCache cache.ICache, ) { // Serve pages from subdomains of MainDomainSuffix @@ -54,7 +54,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite TargetPath: path.Join(pathElements[2:]...), }, true); works { log.Trace().Msg("tryUpstream: serve with specified repo and branch") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) } else { html.ReturnErrorPage( ctx, @@ -86,7 +86,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite TargetPath: path.Join(pathElements[1:]...), }, true); works { log.Trace().Msg("tryUpstream: serve default pages repo with specified branch") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) } else { html.ReturnErrorPage( ctx, @@ -111,7 +111,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite TargetPath: path.Join(pathElements[1:]...), }, false); works { log.Debug().Msg("tryBranch, now trying upstream 5") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) return } } @@ -127,7 +127,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite TargetPath: path.Join(pathElements...), }, false); works { log.Debug().Msg("tryBranch, now trying upstream 6") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) return } } @@ -142,7 +142,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite TargetPath: path.Join(pathElements...), }, false); works { log.Debug().Msg("tryBranch, now trying upstream 6") - tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfig, canonicalDomainCache, redirectsCache) + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainConfigs, canonicalDomainCache, redirectsCache) return } diff --git a/server/handler/try.go b/server/handler/try.go index 8cdf3e9..87c30b7 100644 --- a/server/handler/try.go +++ b/server/handler/try.go @@ -18,13 +18,13 @@ import ( func tryUpstream(ctx *context.Context, giteaClient *gitea.Client, mainDomainSuffix, trimmedHost string, options *upstream.Options, - canonicalDomainConfig string, + canonicalDomainConfigs []string, canonicalDomainCache cache.ICache, redirectsCache cache.ICache, ) { // check if a canonical domain exists on a request on MainDomain if strings.HasSuffix(trimmedHost, mainDomainSuffix) && !options.ServeRaw { - canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainConfig, canonicalDomainCache) + canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainConfigs, canonicalDomainCache) if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix) { canonicalPath := ctx.Req.RequestURI if options.TargetRepo != defaultPagesRepo { diff --git a/server/startup.go b/server/startup.go index eab847a..b7b5717 100644 --- a/server/startup.go +++ b/server/startup.go @@ -101,7 +101,7 @@ func Serve(ctx *cli.Context) error { giteaClient, acmeClient, cfg.Server.PagesBranches[0], - cfg.Server.CanonicalDomainFile, + cfg.Server.CanonicalDomainFiles, challengeCache, canonicalDomainCache, certDB, cfg.ACME.NoDNS01, diff --git a/server/upstream/domains.go b/server/upstream/domains.go index b5d93fc..1200320 100644 --- a/server/upstream/domains.go +++ b/server/upstream/domains.go @@ -15,56 +15,91 @@ import ( var canonicalDomainCacheTimeout = 15 * time.Minute // CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file). -func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix, canonicalDomainConfig string, canonicalDomainCache cache.ICache) (domain string, valid bool) { - canonicalDomainCacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch + "/" + canonicalDomainConfig +func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainConfigs []string, canonicalDomainCache cache.ICache) (domain string, valid bool) { + canonicalDomainCacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch + "/(" + strings.Join(canonicalDomainConfigs, "|") + ")" + + var domains []string // Check if this request is cached. if cachedValue, ok := canonicalDomainCache.Get(canonicalDomainCacheKey); ok { - domains := cachedValue.([]string) - for _, domain := range domains { - if domain == actualDomain { - valid = true - break - } - } - return domains[0], valid + domains = cachedValue.([]string) + } else { + // Create cache entry for future invocations. + domains = o.canonicalDomainList(giteaClient, mainDomainSuffix, canonicalDomainConfigs) + + // Add result to cache. + _ = canonicalDomainCache.Set(canonicalDomainCacheKey, domains, canonicalDomainCacheTimeout) } - body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig) - 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) - } - - var domains []string - for _, domain := range strings.Split(string(body), "\n") { - domain = strings.ToLower(domain) - domain = strings.TrimSpace(domain) - domain = strings.TrimPrefix(domain, "http://") - domain = strings.TrimPrefix(domain, "https://") - if domain != "" && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') { - domains = append(domains, domain) - } + for _, domain := range domains { if domain == actualDomain { valid = true + break } } - // Add [owner].[pages-domain] as valid domain. - domains = append(domains, o.TargetOwner+mainDomainSuffix) - if domains[len(domains)-1] == actualDomain { - valid = true - } - - // If the target repository isn't called pages, add `/[repository]` to the - // previous valid domain. - if o.TargetRepo != "" && o.TargetRepo != "pages" { - domains[len(domains)-1] += "/" + o.TargetRepo - } - - // Add result to cache. - _ = canonicalDomainCache.Set(canonicalDomainCacheKey, domains, canonicalDomainCacheTimeout) - // Return the first domain from the list and return if any of the domains // matched the requested domain. return domains[0], valid } + +// canonicalDomainList returns a list of normalized canonical domains as reported by the repository being served. +func (o *Options) canonicalDomainList(giteaClient *gitea.Client, mainDomainSuffix string, canonicalDomainConfigs []string) []string { + domainConfigMerge := "" + + for _, canonicalDomainConfig := range canonicalDomainConfigs { + body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig) + 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) + continue + } + + // Ensures files that don't end with a `\n` don't cause domains to be concatenated when combining files. + domainConfigMerge = domainConfigMerge + "\n" + string(body) + } + + domains := normalizeDomainEntries(domainConfigMerge) + + // Add [owner].[pages-domain] as valid domain. + domains = append(domains, o.pageMainDomain(mainDomainSuffix)) + + return domains +} + +// pageMainDomain returns the [owner].[pages-domain] domain. +func (o *Options) pageMainDomain(mainDomainSuffix string) string { + pageMainDomain := o.TargetOwner + mainDomainSuffix + + // If the target repository isn't called pages, add `/[repository]` to the + // previous valid domain. + if o.TargetRepo != "" && o.TargetRepo != "pages" { + pageMainDomain += "/" + o.TargetRepo + } + + return pageMainDomain +} + +// normalizeDomainEntries returns a list of domains, ill formatted domains are skipped. +// domainEntries is a new-line separated list of domains. +func normalizeDomainEntries(domainEntries string) []string { + domains := []string{} + + for _, domain := range strings.Split(domainEntries, "\n") { + domain = strings.ToLower(domain) + domain = strings.TrimSpace(domain) + domain = strings.TrimPrefix(domain, "http://") + domain = strings.TrimPrefix(domain, "https://") + + // Skip blank lines. + // Skip commented lines. + // Skip poorly formatted lines. + // Skip domains without '.'. + if domain == "" || strings.HasPrefix(domain, "#") || strings.ContainsAny(domain, "\t /") || !strings.ContainsRune(domain, '.') { + continue + } + + domains = append(domains, domain) + } + + return domains +} diff --git a/server/upstream/domains_test.go b/server/upstream/domains_test.go new file mode 100644 index 0000000..8956ad1 --- /dev/null +++ b/server/upstream/domains_test.go @@ -0,0 +1,75 @@ +package upstream + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPageMainDomainGeneratesTheExpectedDomain(t *testing.T) { + defaultOptions := Options{ + TargetOwner: "", + TargetRepo: "", + TargetBranch: "", + TargetPath: "", + Host: "", + TryIndexPages: false, + BranchTimestamp: time.Time{}, + appendTrailingSlash: false, + redirectIfExists: "", + ServeRaw: false, + } + + for _, tc := range []struct { + targetOwner string + targetRepo string + domainSuffix string + expectedDomain string + }{ + {"foo", "", ".localhost.mock.directory", "foo.localhost.mock.directory"}, + {"foo", "pages", ".localhost.mock.directory", "foo.localhost.mock.directory"}, + {"foo", "bar", ".localhost.mock.directory", "foo.localhost.mock.directory/bar"}, + } { + options := defaultOptions + options.TargetOwner = tc.targetOwner + options.TargetRepo = tc.targetRepo + + actualDomain := options.pageMainDomain(tc.domainSuffix) + + assert.Equal(t, tc.expectedDomain, actualDomain) + } +} + +func TestNormalizeDomainEntries(t *testing.T) { + for _, tc := range []struct { + domain string + }{ + {"abc.com"}, + {"ABC.com"}, + {" ABC.com"}, + {"ABC.com "}, + {" ABC.com "}, + {"http://ABC.com"}, + {"https://ABC.com"}, + } { + actualDomains := normalizeDomainEntries(tc.domain) + expectedDomains := []string{"abc.com"} + + assert.Equal(t, expectedDomains, actualDomains) + } + + for _, tc := range []struct { + domains string + expectedDomains []string + }{ + {"", []string{}}, + {"ABC.com", []string{"abc.com"}}, + {"ABC.com\nhttps://example.com", []string{"abc.com", "example.com"}}, + {"\n\nABC.com\n\nhttps://example.com\n", []string{"abc.com", "example.com"}}, + } { + actualDomains := normalizeDomainEntries(tc.domains) + + assert.Equal(t, tc.expectedDomains, actualDomains) + } +}