feat: redirect engine with loop refusal and chain notices
- rule set: exact sources by key, patterns by literal prefix, regex by id (stdlib RE2, compiled once per source); ignore case, trailing slash, query ignore/pass, a source with ? matches its query exactly - targets: captures escaped, a relative target never leaves the site, an absolute one keeps its host - SaveRedirect validates, simulates the rule in place (exact, pattern and regex probes, 10 hops) and refuses a loop; chains answer the notice - resolve follows the chain to its final target, 410 answers gone, a runtime cycle answers not_found and is logged once
This commit is contained in:
@@ -360,12 +360,16 @@ func (r *Resolver) Resolve(ctx context.Context, rawURL string) (Result, error) {
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
redirect, err := exactRedirect(db, path)
|
||||
set, err := LoadRedirectSet(db)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if redirect != nil {
|
||||
return r.redirectAnswer(svc.SiteURL()+path, *redirect), nil
|
||||
chain, err := set.Follow(svc.SiteURL(), path, rawQueryOf(rawURL), MaxRedirectHops)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if len(chain.Hops) > 0 {
|
||||
return r.redirectAnswer(svc.SiteURL()+joinQuery(path, rawQueryOf(rawURL)), chain), nil
|
||||
}
|
||||
key, match, rec, err := r.lookup(ctx, path)
|
||||
if err != nil {
|
||||
@@ -567,16 +571,21 @@ func (r *Resolver) resolveAt(ctx context.Context, db *gorm.DB, settings models.S
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// redirectAnswer is the answer of a matched redirect: gone for a 410,
|
||||
// else redirect with the rule's status and its target absolute on the
|
||||
// site URL.
|
||||
func (r *Resolver) redirectAnswer(self string, rule models.Redirect) Result {
|
||||
// redirectAnswer is the answer of a matched redirect chain (P-21): gone
|
||||
// for a chain that ends on a 410, not_found (logged once per URL) for a
|
||||
// cycle, else redirect with the chain's status and its final target
|
||||
// absolute on the site URL, so the visitor makes one hop.
|
||||
func (r *Resolver) redirectAnswer(self string, ch Chain) Result {
|
||||
res := Result{URL: self, Staging: r.Service.Staging(), Sources: map[string]string{}}
|
||||
if rule.Status == models.Status410 {
|
||||
switch {
|
||||
case ch.Loop:
|
||||
logRedirectCycle(self, ch)
|
||||
res.Action, res.Status = ActionNotFound, 404
|
||||
case ch.Gone:
|
||||
res.Action, res.Status = ActionGone, models.Status410
|
||||
return res
|
||||
default:
|
||||
res.Action, res.Status, res.Location = ActionRedirect, ch.Status, AbsoluteURL(r.Service.SiteURL(), ch.Final)
|
||||
}
|
||||
res.Action, res.Status, res.Location = ActionRedirect, rule.Status, AbsoluteURL(r.Service.SiteURL(), rule.Target)
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user