shlinkio / shlink

The definitive self-hosted URL shortener
https://shlink.io
MIT License
3.13k stars 253 forks source link

Dynamic rule-based redirects #1914

Closed acelaya closed 6 months ago

acelaya commented 10 months ago

Summary

This RFC describes how dynamic redirects could be improved to support multiple combinations of rules, and then, be able to add more rules in future.

The first iteration should consist on preparing the base implementation for this to work, and potentially migrate device-specific long URLs to be the first supported redirect rule.

Use case

Supporting multiple dynamic redirect rules has been requested in the past, specially since device-specific redirects were implemented.


acelaya commented 6 months ago

Preliminary persistence model

erDiagram
    short_urls 1--many(0) short_url_redirect_rules : has
    short_url_redirect_rules many(1)--many(1) redirect_conditions : has
    short_urls {
        string long_url
    }
    short_url_redirect_rules {
        int short_url_id
        int priority
        string long_url
    }
    redirect_conditions {
        string name UK "Example: device-android, query-foo-bar, language-en-US"
        string type "enum(device, language, query...)"
        string match_key "optional/nullable"
        string match_value
    }

EDIT: While implementing the API model, it was proved that a properly normalized persistence would get in the way, so conditions will finally not have a unique name nor be shared between rules. Every rule will have its set of conditions.

This means more duplicated data in the database, but it should have a very low impact anyway.

acelaya commented 6 months ago

Dynamic redirects

Short URL redirects currently work like this:

  1. RedirectAction is dispatched if an existing short URL could be matched.
  2. ShortUrlRedirectionBuilder is invoked, and tries to resolve the long URL to redirect to.
    • Using the User-Agent header it tries to match a device long URL
    • If it cannot match, it falls back to the regular long URL
  3. Depending on how Shlink is configured, it appends extra path and/or query params, if needed.

With the new system, we can keep steps 1 and 3 unmodified, and the ShortUrlRedirectionBuilder contract, as its only public method already expects the ShortUrl and the Request object, which will be needed to match rules and conditions.

We'll have to inject a new service though, which will be the one iterating over rules and trying to match one. Every condition will try to be matched by using a type-specific plugin which knows how to extract and match the appropriate information from the Request object.

acelaya commented 6 months ago

API model

I'm thinking the simplest solution is to allow editing or listing rules and conditions always linked to a short URL, so the preliminary endpoints could look like this:

Then in the UI, we'll call the first one just before editing a short URL, and the second one after creating/editing a short URL succeeded.

The only drawback about this approach is if the short URL creation works and saving the rules fails, which would leave an inconsistent state, but it's a negligible risk.

The benefit is that it simplifies the short URL creation/edition endpoint, which was something that got more complex when device long URLs were implemented and I always regretted.

acelaya commented 6 months ago

CLI handling

I have realized device long URLs were never supported in the CLI, which I guess it's good now, as it's one less thing to change.

For redirect rules, I think a set of new commands is the best approach. Something like short-url:list-rules and short-url:set-rules.

EDIT: The complexity on the structure of rules and conditions, makes it hard to allow them to be programatically editable via command flags and arguments.

So I will instead add a single command for now, short-url:manage-rules, which will allow to interactively edit the rules and conditions via questions wit multiple steps.

EDIT 2: This is how it looks like. It's not perfect, but it should be usable.

manage-rules-command.webm