package upstream import ( "encoding/json" "strconv" "strings" "time" "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/context" "codeberg.org/codeberg/pages/server/gitea" "github.com/rs/zerolog/log" ) type Redirect struct { From string To string StatusCode int } // redirectsCacheTimeout specifies the timeout for the redirects cache. var redirectsCacheTimeout = 10 * time.Minute const redirectsConfig = "_redirects" // getRedirects returns redirects specified in the _redirects file. func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.ICache) []Redirect { var redirects []Redirect cacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch // Check for cached redirects if cachedValue, ok := redirectsCache.Get(cacheKey); ok { redirects := []Redirect{} err := json.Unmarshal([]byte(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 { // Get _redirects file and parse body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, redirectsConfig) if err == nil { for _, line := range strings.Split(string(body), "\n") { redirectArr := strings.Fields(line) // Ignore comments and invalid lines if strings.HasPrefix(line, "#") || len(redirectArr) < 2 { continue } // Get redirect status code statusCode := 301 if len(redirectArr) == 3 { statusCode, err = strconv.Atoi(redirectArr[2]) if err != nil { log.Info().Err(err).Msgf("could not read %s of %s/%s", redirectsConfig, o.TargetOwner, o.TargetRepo) } } redirects = append(redirects, Redirect{ From: redirectArr[0], To: redirectArr[1], StatusCode: statusCode, }) } } 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, string(redirectsJson), redirectsCacheTimeout) } } return redirects } func (o *Options) matchRedirects(ctx *context.Context, giteaClient *gitea.Client, redirects []Redirect, redirectsCache cache.ICache) (final bool) { if len(redirects) > 0 { for _, redirect := range redirects { reqUrl := ctx.Req.RequestURI // remove repo and branch from request url reqUrl = strings.TrimPrefix(reqUrl, "/"+o.TargetRepo) reqUrl = strings.TrimPrefix(reqUrl, "/@"+o.TargetBranch) // check if from url matches request url if strings.TrimSuffix(redirect.From, "/") == strings.TrimSuffix(reqUrl, "/") { // do rewrite if status code is 200 if redirect.StatusCode == 200 { o.TargetPath = redirect.To o.Upstream(ctx, giteaClient, redirectsCache) return true } else { ctx.Redirect(redirect.To, redirect.StatusCode) return true } } // handle wildcard redirects trimmedFromUrl := strings.TrimSuffix(redirect.From, "/*") if strings.HasSuffix(redirect.From, "/*") && strings.HasPrefix(reqUrl, trimmedFromUrl) { if strings.Contains(redirect.To, ":splat") { splatUrl := strings.ReplaceAll(redirect.To, ":splat", strings.TrimPrefix(reqUrl, trimmedFromUrl)) // do rewrite if status code is 200 if redirect.StatusCode == 200 { o.TargetPath = splatUrl o.Upstream(ctx, giteaClient, redirectsCache) return true } else { ctx.Redirect(splatUrl, redirect.StatusCode) return true } } else { // do rewrite if status code is 200 if redirect.StatusCode == 200 { o.TargetPath = redirect.To o.Upstream(ctx, giteaClient, redirectsCache) return true } else { ctx.Redirect(redirect.To, redirect.StatusCode) return true } } } } } return false }