From 2f2722847c29747d64c5dd1cfbb92eb62179231c Mon Sep 17 00:00:00 2001 From: Moritz <101179677+mxritzdev@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:00:58 +0200 Subject: [PATCH 01/10] added basic placeholder system --- LinkRouter/App/Configuration/Config.cs | 84 ++++++++++++++++++- .../Http/Controllers/RedirectController.cs | 50 +++++++---- LinkRouter/App/Models/CompiledRoute.cs | 10 +++ LinkRouter/App/Services/ConfigWatcher.cs | 12 +++ 4 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 LinkRouter/App/Models/CompiledRoute.cs diff --git a/LinkRouter/App/Configuration/Config.cs b/LinkRouter/App/Configuration/Config.cs index 4164f50..f96f90f 100644 --- a/LinkRouter/App/Configuration/Config.cs +++ b/LinkRouter/App/Configuration/Config.cs @@ -1,4 +1,7 @@ -using LinkRouter.App.Models; +using System.Text; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using LinkRouter.App.Models; namespace LinkRouter.App.Configuration; @@ -26,4 +29,83 @@ public class NotFoundBehaviorConfig public bool RedirectOn404 { get; set; } = false; public string RedirectUrl { get; set; } = "https://example.com/404"; } + + [JsonIgnore] + public static Regex PlaceholderPattern => new(@"\\\{(\d|\w+)\}"); + + [JsonIgnore] + public CompiledRoute[]? CompiledRoutes { get; set; } + + public void CompileRoutes() + { + var compiledRoutes = new List(); + + foreach (var route in Routes) + { + if (!route.Route.StartsWith("/")) + route.Route = "/" + route.Route; + + if (!route.Route.EndsWith("/")) + route.Route += "/"; + + var compiled = new CompiledRoute + { + Route = route.Route, + RedirectUrl = route.RedirectUrl + }; + + var replacements = new List<(int Index, int Length, string NewText)>(); + + var escaped = Regex.Escape(route.Route); + + var matches = PlaceholderPattern.Matches(escaped); + + + foreach (var match in matches.Select(x => x)) + { + // Check if the placeholder is immediately followed by another placeholder + + Console.WriteLine("matchlenght: "+ (match.Index + match.Length + 2) + " route lenght" + escaped.Length); + + if (escaped.Length >= match.Index + match.Length + 2 && escaped.Substring(match.Index + match.Length, 2) == "\\{") + throw new InvalidOperationException( + $"Placeholder {match.Groups[1].Value} cannot be immediately followed by another placeholder. " + + $"Please add a string literal as separator."); + + replacements.Add((match.Index, match.Length, "(.+)")); + } + + var compiledRouteBuilder = new StringBuilder(escaped); + + foreach (var replacement in replacements.OrderByDescending(r => r.Index)) + { + compiledRouteBuilder.Remove(replacement.Index, replacement.Length); + compiledRouteBuilder.Insert(replacement.Index, replacement.NewText); + } + + compiled.CompiledPattern = new Regex(compiledRouteBuilder.ToString(), RegexOptions.Compiled); + + Console.WriteLine(compiled.CompiledPattern.ToString()); + + var duplicate = matches + .Select((m, i) => m.Groups[1].Value) + .GroupBy(x => x) + .FirstOrDefault(x => x.Count() > 1); + + if (duplicate != null) + throw new InvalidOperationException("Cannot use a placeholder twice in the route: " + duplicate.Key); + + compiled.Placeholders = matches + .Select((m, i) => m.Groups[1].Value) + .Distinct() + .Select((name, i) => (name, i)) + .ToDictionary(x => x.name, x => x.i + 1); + + compiledRoutes.Add(compiled); + } + + CompiledRoutes = compiledRoutes + .ToArray(); + } + } \ No newline at end of file diff --git a/LinkRouter/App/Http/Controllers/RedirectController.cs b/LinkRouter/App/Http/Controllers/RedirectController.cs index 63dbe6b..b0eaa37 100644 --- a/LinkRouter/App/Http/Controllers/RedirectController.cs +++ b/LinkRouter/App/Http/Controllers/RedirectController.cs @@ -37,38 +37,54 @@ public RedirectController(Config config) } [HttpGet("/{*path}")] - public IActionResult RedirectToExternalUrl(string path) + public async Task RedirectToExternalUrl(string path) { - var redirectRoute = Config.Routes.FirstOrDefault(x => x.Route == path || x.Route == path + "/" || x.Route == "/" + path); - - if (redirectRoute != null) + + if (!path.EndsWith("/")) + path += "/"; + + path = "/" + path; + + Console.WriteLine(path); + + var redirectRoute = Config.CompiledRoutes?.FirstOrDefault(x => x.CompiledPattern.IsMatch(path)); + + if (redirectRoute == null) { - RouteCounter - .WithLabels(redirectRoute.Route) + NotFoundCounter + .WithLabels(path) .Inc(); - return Redirect(redirectRoute.RedirectUrl); - } + if (Config.NotFoundBehavior.RedirectOn404) + return Redirect(Config.NotFoundBehavior.RedirectUrl); - NotFoundCounter - .WithLabels("/" + path) - .Inc(); + return NotFound(); + } + + var match = redirectRoute.CompiledPattern.Match(path); - if (Config.NotFoundBehavior.RedirectOn404) - return Redirect(Config.NotFoundBehavior.RedirectUrl); + Console.WriteLine(redirectRoute.CompiledPattern); - return NotFound(); + string redirectUrl = redirectRoute.RedirectUrl; + + foreach (var placeholder in redirectRoute.Placeholders) + { + var value = match.Groups[placeholder.Value].Value; + redirectUrl = redirectUrl.Replace("{" + placeholder.Key + "}", value); + } + + return Redirect(redirectUrl); } - + [HttpGet("/")] public IActionResult GetRootRoute() { RouteCounter .WithLabels("/") .Inc(); - + string url = Config.RootRoute; - + return Redirect(url); } } \ No newline at end of file diff --git a/LinkRouter/App/Models/CompiledRoute.cs b/LinkRouter/App/Models/CompiledRoute.cs new file mode 100644 index 0000000..f3b9051 --- /dev/null +++ b/LinkRouter/App/Models/CompiledRoute.cs @@ -0,0 +1,10 @@ +using System.Text.RegularExpressions; + +namespace LinkRouter.App.Models; + +public class CompiledRoute : RedirectRoute +{ + public Regex CompiledPattern { get; set; } + + public Dictionary Placeholders { get; set; } = new(); +} \ No newline at end of file diff --git a/LinkRouter/App/Services/ConfigWatcher.cs b/LinkRouter/App/Services/ConfigWatcher.cs index 18484c8..e47c0d5 100644 --- a/LinkRouter/App/Services/ConfigWatcher.cs +++ b/LinkRouter/App/Services/ConfigWatcher.cs @@ -18,6 +18,7 @@ public ConfigWatcher(ILogger logger, Config config) protected override Task ExecuteAsync(CancellationToken stoppingToken) { + if (!File.Exists(ConfigPath)) { Logger.LogWarning("Watched file does not exist: {FilePath}", ConfigPath); @@ -29,6 +30,8 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime }; + OnChanged(Watcher, new FileSystemEventArgs(WatcherChangeTypes.Created, string.Empty, string.Empty)); + Watcher.Changed += OnChanged; Watcher.EnableRaisingEvents = true; @@ -48,6 +51,15 @@ private void OnChanged(object sender, FileSystemEventArgs e) Config.RootRoute = config?.RootRoute ?? "https://example.com"; Logger.LogInformation("Config file changed."); + + try + { + Config.CompileRoutes(); + } catch (InvalidOperationException ex) + { + Logger.LogError("Failed to compile routes: " + ex.Message); + Environment.Exit(1); + } } catch (IOException ex) { From 9f7dbd85700ae22d0651bf4dcae21500d6b93eb2 Mon Sep 17 00:00:00 2001 From: Moritz <101179677+mxritzdev@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:44:54 +0200 Subject: [PATCH 02/10] refactored config compiling, cleaned up project --- LinkRouter/App/Configuration/Config.cs | 56 +++++++++---------- .../Http/Controllers/RedirectController.cs | 2 - LinkRouter/App/Models/CompiledRoute.cs | 2 +- LinkRouter/App/Models/RedirectRoute.cs | 2 +- LinkRouter/App/Services/ConfigWatcher.cs | 22 ++++---- LinkRouter/Program.cs | 34 ++++++----- 6 files changed, 54 insertions(+), 64 deletions(-) diff --git a/LinkRouter/App/Configuration/Config.cs b/LinkRouter/App/Configuration/Config.cs index f96f90f..e215b8c 100644 --- a/LinkRouter/App/Configuration/Config.cs +++ b/LinkRouter/App/Configuration/Config.cs @@ -11,7 +11,8 @@ public class Config public NotFoundBehaviorConfig NotFoundBehavior { get; set; } = new(); - public RedirectRoute[] Routes { get; set; } = [ + public RedirectRoute[] Routes { get; set; } = + [ new RedirectRoute() { Route = "/instagram", @@ -23,70 +24,64 @@ public class Config RedirectUrl = "https://example.com" }, ]; - + public class NotFoundBehaviorConfig { public bool RedirectOn404 { get; set; } = false; public string RedirectUrl { get; set; } = "https://example.com/404"; } - [JsonIgnore] - public static Regex PlaceholderPattern => new(@"\\\{(\d|\w+)\}"); - - [JsonIgnore] - public CompiledRoute[]? CompiledRoutes { get; set; } - + [JsonIgnore] public CompiledRoute[]? CompiledRoutes { get; set; } + public void CompileRoutes() { var compiledRoutes = new List(); - + foreach (var route in Routes) { if (!route.Route.StartsWith("/")) route.Route = "/" + route.Route; - + if (!route.Route.EndsWith("/")) route.Route += "/"; - + var compiled = new CompiledRoute { Route = route.Route, RedirectUrl = route.RedirectUrl }; - + var replacements = new List<(int Index, int Length, string NewText)>(); var escaped = Regex.Escape(route.Route); - - var matches = PlaceholderPattern.Matches(escaped); - - + + var pattern = new Regex(@"\\{(\d|\w+)\}"); + + var matches = pattern.Matches(escaped); + foreach (var match in matches.Select(x => x)) { // Check if the placeholder is immediately followed by another placeholder - - Console.WriteLine("matchlenght: "+ (match.Index + match.Length + 2) + " route lenght" + escaped.Length); - - if (escaped.Length >= match.Index + match.Length + 2 && escaped.Substring(match.Index + match.Length, 2) == "\\{") - throw new InvalidOperationException( - $"Placeholder {match.Groups[1].Value} cannot be immediately followed by another placeholder. " + - $"Please add a string literal as separator."); + if (escaped.Length >= match.Index + match.Length + 2 + && escaped.Substring(match.Index + match.Length, 2) == "\\{") + throw new InvalidOperationException( + $"Placeholder {match.Groups[1].Value} cannot be immediately followed by another placeholder. " + + $"Please add any separator."); replacements.Add((match.Index, match.Length, "(.+)")); } - + var compiledRouteBuilder = new StringBuilder(escaped); - + foreach (var replacement in replacements.OrderByDescending(r => r.Index)) { compiledRouteBuilder.Remove(replacement.Index, replacement.Length); compiledRouteBuilder.Insert(replacement.Index, replacement.NewText); } - compiled.CompiledPattern = new Regex(compiledRouteBuilder.ToString(), RegexOptions.Compiled); + compiled.CompiledPattern = new Regex(compiledRouteBuilder.ToString(), + RegexOptions.Compiled | RegexOptions.CultureInvariant); - Console.WriteLine(compiled.CompiledPattern.ToString()); - var duplicate = matches .Select((m, i) => m.Groups[1].Value) .GroupBy(x => x) @@ -94,18 +89,17 @@ public void CompileRoutes() if (duplicate != null) throw new InvalidOperationException("Cannot use a placeholder twice in the route: " + duplicate.Key); - + compiled.Placeholders = matches .Select((m, i) => m.Groups[1].Value) .Distinct() .Select((name, i) => (name, i)) .ToDictionary(x => x.name, x => x.i + 1); - + compiledRoutes.Add(compiled); } CompiledRoutes = compiledRoutes .ToArray(); } - } \ No newline at end of file diff --git a/LinkRouter/App/Http/Controllers/RedirectController.cs b/LinkRouter/App/Http/Controllers/RedirectController.cs index b0eaa37..f3a04ad 100644 --- a/LinkRouter/App/Http/Controllers/RedirectController.cs +++ b/LinkRouter/App/Http/Controllers/RedirectController.cs @@ -63,8 +63,6 @@ public async Task RedirectToExternalUrl(string path) var match = redirectRoute.CompiledPattern.Match(path); - Console.WriteLine(redirectRoute.CompiledPattern); - string redirectUrl = redirectRoute.RedirectUrl; foreach (var placeholder in redirectRoute.Placeholders) diff --git a/LinkRouter/App/Models/CompiledRoute.cs b/LinkRouter/App/Models/CompiledRoute.cs index f3b9051..1d5d81a 100644 --- a/LinkRouter/App/Models/CompiledRoute.cs +++ b/LinkRouter/App/Models/CompiledRoute.cs @@ -5,6 +5,6 @@ namespace LinkRouter.App.Models; public class CompiledRoute : RedirectRoute { public Regex CompiledPattern { get; set; } - + public Dictionary Placeholders { get; set; } = new(); } \ No newline at end of file diff --git a/LinkRouter/App/Models/RedirectRoute.cs b/LinkRouter/App/Models/RedirectRoute.cs index daefe87..5cf2d7a 100644 --- a/LinkRouter/App/Models/RedirectRoute.cs +++ b/LinkRouter/App/Models/RedirectRoute.cs @@ -3,6 +3,6 @@ public class RedirectRoute { public string Route { get; set; } - + public string RedirectUrl { get; set; } } \ No newline at end of file diff --git a/LinkRouter/App/Services/ConfigWatcher.cs b/LinkRouter/App/Services/ConfigWatcher.cs index e47c0d5..a78570d 100644 --- a/LinkRouter/App/Services/ConfigWatcher.cs +++ b/LinkRouter/App/Services/ConfigWatcher.cs @@ -18,44 +18,44 @@ public ConfigWatcher(ILogger logger, Config config) protected override Task ExecuteAsync(CancellationToken stoppingToken) { - if (!File.Exists(ConfigPath)) { Logger.LogWarning("Watched file does not exist: {FilePath}", ConfigPath); } - + Watcher = new FileSystemWatcher(Path.GetDirectoryName(ConfigPath) ?? throw new InvalidOperationException()) { Filter = Path.GetFileName(ConfigPath), NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime }; - + OnChanged(Watcher, new FileSystemEventArgs(WatcherChangeTypes.Created, string.Empty, string.Empty)); - + Watcher.Changed += OnChanged; - + Watcher.EnableRaisingEvents = true; - + return Task.CompletedTask; } - + private void OnChanged(object sender, FileSystemEventArgs e) { try { var content = File.ReadAllText(ConfigPath); - + var config = JsonSerializer.Deserialize(content); - + Config.Routes = config?.Routes ?? []; Config.RootRoute = config?.RootRoute ?? "https://example.com"; - + Logger.LogInformation("Config file changed."); try { Config.CompileRoutes(); - } catch (InvalidOperationException ex) + } + catch (InvalidOperationException ex) { Logger.LogError("Failed to compile routes: " + ex.Message); Environment.Exit(1); diff --git a/LinkRouter/Program.cs b/LinkRouter/Program.cs index 3ba680d..4c510be 100644 --- a/LinkRouter/Program.cs +++ b/LinkRouter/Program.cs @@ -12,41 +12,39 @@ public abstract class Program public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); - + Directory.CreateDirectory(PathBuilder.Dir("data")); - + builder.Services.AddControllers(); - + var loggerProviders = LoggerBuildHelper.BuildFromConfiguration(configuration => { configuration.Console.Enable = true; configuration.Console.EnableAnsiMode = true; }); - + builder.Logging.ClearProviders(); builder.Logging.AddProviders(loggerProviders); builder.Services.AddHostedService(); - + var configPath = Path.Combine("data", "config.json"); - + if (!File.Exists(configPath)) File.WriteAllText( - configPath, - JsonSerializer.Serialize(new Config(), new JsonSerializerOptions {WriteIndented = true} + configPath, + JsonSerializer.Serialize(new Config(), new JsonSerializerOptions { WriteIndented = true } )); - + Config config = JsonSerializer.Deserialize(File.ReadAllText(configPath)) ?? new Config(); - File.WriteAllText(configPath, JsonSerializer.Serialize(config, new JsonSerializerOptions {WriteIndented = true})); - + File.WriteAllText(configPath, + JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true })); + builder.Services.AddSingleton(config); - - builder.Services.AddMetricServer(options => - { - options.Port = 5000; - }); - + + builder.Services.AddMetricServer(options => { options.Port = 5000; }); + var app = builder.Build(); app.UseMetricServer(); @@ -54,4 +52,4 @@ public static void Main(string[] args) app.Run(); } -} +} \ No newline at end of file From b5e7aa9d11054338302bae8dbedca754191f1089 Mon Sep 17 00:00:00 2001 From: Moritz <101179677+mxritzdev@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:58:59 +0200 Subject: [PATCH 03/10] added error code return --- LinkRouter/App/Configuration/Config.cs | 2 ++ LinkRouter/App/Http/Controllers/RedirectController.cs | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/LinkRouter/App/Configuration/Config.cs b/LinkRouter/App/Configuration/Config.cs index e215b8c..280176b 100644 --- a/LinkRouter/App/Configuration/Config.cs +++ b/LinkRouter/App/Configuration/Config.cs @@ -102,4 +102,6 @@ public void CompileRoutes() CompiledRoutes = compiledRoutes .ToArray(); } + + [JsonIgnore] public static Regex ErrorCodePattern = new(@"\s*\-\>\s*(\d+)\s*$", RegexOptions.Compiled | RegexOptions.CultureInvariant); } \ No newline at end of file diff --git a/LinkRouter/App/Http/Controllers/RedirectController.cs b/LinkRouter/App/Http/Controllers/RedirectController.cs index f3a04ad..7842d80 100644 --- a/LinkRouter/App/Http/Controllers/RedirectController.cs +++ b/LinkRouter/App/Http/Controllers/RedirectController.cs @@ -10,7 +10,6 @@ public class RedirectController : Controller { private readonly Config Config; - private readonly Counter RouteCounter = Metrics.CreateCounter( "linkrouter_requests", @@ -39,7 +38,6 @@ public RedirectController(Config config) [HttpGet("/{*path}")] public async Task RedirectToExternalUrl(string path) { - if (!path.EndsWith("/")) path += "/"; @@ -65,6 +63,13 @@ public async Task RedirectToExternalUrl(string path) string redirectUrl = redirectRoute.RedirectUrl; + if (Config.ErrorCodePattern.IsMatch(redirectUrl)) + { + var errorCodeMatch = Config.ErrorCodePattern.Match(redirectUrl); + var errorCode = int.Parse(errorCodeMatch.Groups[1].Value); + return StatusCode(errorCode); + } + foreach (var placeholder in redirectRoute.Placeholders) { var value = match.Groups[placeholder.Value].Value; From eaab92a49e291d187fb80ff91e0d2a4d26641165 Mon Sep 17 00:00:00 2001 From: Moritz <101179677+mxritzdev@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:03:13 +0200 Subject: [PATCH 04/10] updated the readme to describe the new features --- LinkRouter/App/Configuration/Config.cs | 1 + README.md | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/LinkRouter/App/Configuration/Config.cs b/LinkRouter/App/Configuration/Config.cs index 280176b..3590735 100644 --- a/LinkRouter/App/Configuration/Config.cs +++ b/LinkRouter/App/Configuration/Config.cs @@ -104,4 +104,5 @@ public void CompileRoutes() } [JsonIgnore] public static Regex ErrorCodePattern = new(@"\s*\-\>\s*(\d+)\s*$", RegexOptions.Compiled | RegexOptions.CultureInvariant); + } \ No newline at end of file diff --git a/README.md b/README.md index f362f18..887408e 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,13 @@ [⬇️How to install⬇️](#installation) ## Features - - **Path-based Redirection:** Reads a config file that maps paths to redirect URLs. When a request hits a registered path, the router issues an HTTP redirect to the corresponding target. +- **Path-based Redirection:** Reads a config file that maps paths to redirect URLs. When a request hits a registered path, the router issues an HTTP redirect to the corresponding target. - **Hot Reloading:** The config is cached at startup and automatically reloaded when the file changes — no restart required. [Example config](#example-config) - **Low Resource Usage:** Uses less than 50MB of RAM, making it ideal for constrained environments. - **Metrics Endpoint:** Exposes Prometheus-compatible metrics at `:5000/metrics` for easy observability and monitoring. [How to use](#metrics) - - **Docker-Deployable:** Comes with a minimal Dockerfile for easy containerized deployment. +- **Docker-Deployable:** Comes with a minimal Dockerfile for easy containerized deployment. +- **Placeholders:** Supports placeholders in redirect URLs, allowing dynamic URL generation based on the requested path. For example, a route defined as `/user/{username}` can redirect to `https://example.com/profile/{username}`, where `{username}` is replaced with the actual value from the request. +- **Status Code:** You are able to configure if the redirect should redirect to an url or just return a status code of your choice ## Configuration Routes are managed via a configuration file, `/data/config.json`. You can define paths and their corresponding URLs in this file. The application automatically normalizes routes to handle both trailing and non-trailing slashes. @@ -24,11 +26,15 @@ Routes are managed via a configuration file, `/data/config.json`. You can define "Routes": [ { "Route": "/instagram", // has to start with a slash - "RedirectUrl": "https://instagram.com/{yourname}" + "RedirectUrl": "https://instagram.com/maxmustermann" }, { - "Route": "/example", // has to start with a slash - "RedirectUrl": "https://example.com" + "Route": "/article/{id}", // {id} is a placeholder + "RedirectUrl": "https://example.com/article/{id}", // {id} will be replaced with the actual value from the request + }, + { + "Route": "/teapot", + "RedirectUrl": "-> 418" // will return a 418 status code (I'm a teapot :) ) } ] } From 8893bea5118a3eefd13522944056ec28f66d0e35 Mon Sep 17 00:00:00 2001 From: Moritz <101179677+mxritzdev@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:24:46 +0200 Subject: [PATCH 05/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 887408e..565b33b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - **Metrics Endpoint:** Exposes Prometheus-compatible metrics at `:5000/metrics` for easy observability and monitoring. [How to use](#metrics) - **Docker-Deployable:** Comes with a minimal Dockerfile for easy containerized deployment. - **Placeholders:** Supports placeholders in redirect URLs, allowing dynamic URL generation based on the requested path. For example, a route defined as `/user/{username}` can redirect to `https://example.com/profile/{username}`, where `{username}` is replaced with the actual value from the request. -- **Status Code:** You are able to configure if the redirect should redirect to an url or just return a status code of your choice +- **Status Code:** You are able to configure if the redirect should redirect to an url or just return a custom status code of your choice. Example `"RedirectUrl": "-> 418"` will return the status code 418 (I'm a teapot :) ) ## Configuration Routes are managed via a configuration file, `/data/config.json`. You can define paths and their corresponding URLs in this file. The application automatically normalizes routes to handle both trailing and non-trailing slashes. From d12aade263e1ea1322cc5b869b5135d5c1c1785f Mon Sep 17 00:00:00 2001 From: Moritz <101179677+mxritzdev@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:03:08 +0200 Subject: [PATCH 06/10] added console.writeline for debugging --- LinkRouter/App/Configuration/Config.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LinkRouter/App/Configuration/Config.cs b/LinkRouter/App/Configuration/Config.cs index 3590735..339191d 100644 --- a/LinkRouter/App/Configuration/Config.cs +++ b/LinkRouter/App/Configuration/Config.cs @@ -82,6 +82,8 @@ public void CompileRoutes() compiled.CompiledPattern = new Regex(compiledRouteBuilder.ToString(), RegexOptions.Compiled | RegexOptions.CultureInvariant); + Console.WriteLine(compiled.CompiledPattern.ToString()); + var duplicate = matches .Select((m, i) => m.Groups[1].Value) .GroupBy(x => x) From d72abbd0a16cf4ff025529209b754c987e4c51f9 Mon Sep 17 00:00:00 2001 From: Moritz <101179677+mxritzdev@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:13:21 +0200 Subject: [PATCH 07/10] added more debug logs --- LinkRouter/App/Configuration/Config.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/LinkRouter/App/Configuration/Config.cs b/LinkRouter/App/Configuration/Config.cs index 339191d..2575ad0 100644 --- a/LinkRouter/App/Configuration/Config.cs +++ b/LinkRouter/App/Configuration/Config.cs @@ -55,10 +55,14 @@ public void CompileRoutes() var escaped = Regex.Escape(route.Route); - var pattern = new Regex(@"\\{(\d|\w+)\}"); + Console.WriteLine(escaped); + + var pattern = new Regex(@"\\\{(\d|\w+)\}"); var matches = pattern.Matches(escaped); + Console.WriteLine(matches.Count + " matches found."); + foreach (var match in matches.Select(x => x)) { // Check if the placeholder is immediately followed by another placeholder From 4f57a78305afeeae98bbca5360b09a0a83344b96 Mon Sep 17 00:00:00 2001 From: mxritzdev Date: Mon, 29 Sep 2025 21:40:35 +0200 Subject: [PATCH 08/10] fixed #6, status codes can now be accessed everywhere --- LinkRouter/App/Configuration/Config.cs | 8 +------- .../App/Http/Controllers/RedirectController.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/LinkRouter/App/Configuration/Config.cs b/LinkRouter/App/Configuration/Config.cs index 2575ad0..5166264 100644 --- a/LinkRouter/App/Configuration/Config.cs +++ b/LinkRouter/App/Configuration/Config.cs @@ -54,14 +54,10 @@ public void CompileRoutes() var replacements = new List<(int Index, int Length, string NewText)>(); var escaped = Regex.Escape(route.Route); - - Console.WriteLine(escaped); - var pattern = new Regex(@"\\\{(\d|\w+)\}"); + var pattern = new Regex(@"\\\{(\d|\w+)\}", RegexOptions.CultureInvariant); var matches = pattern.Matches(escaped); - - Console.WriteLine(matches.Count + " matches found."); foreach (var match in matches.Select(x => x)) { @@ -86,8 +82,6 @@ public void CompileRoutes() compiled.CompiledPattern = new Regex(compiledRouteBuilder.ToString(), RegexOptions.Compiled | RegexOptions.CultureInvariant); - Console.WriteLine(compiled.CompiledPattern.ToString()); - var duplicate = matches .Select((m, i) => m.Groups[1].Value) .GroupBy(x => x) diff --git a/LinkRouter/App/Http/Controllers/RedirectController.cs b/LinkRouter/App/Http/Controllers/RedirectController.cs index 7842d80..d4a1750 100644 --- a/LinkRouter/App/Http/Controllers/RedirectController.cs +++ b/LinkRouter/App/Http/Controllers/RedirectController.cs @@ -54,7 +54,13 @@ public async Task RedirectToExternalUrl(string path) .Inc(); if (Config.NotFoundBehavior.RedirectOn404) - return Redirect(Config.NotFoundBehavior.RedirectUrl); + if (Config.ErrorCodePattern.IsMatch(Config.NotFoundBehavior.RedirectUrl)) + { + var errorCodeMatch = Config.ErrorCodePattern.Match(Config.NotFoundBehavior.RedirectUrl); + var errorCode = int.Parse(errorCodeMatch.Groups[1].Value); + return StatusCode(errorCode); + } else + return Redirect(Config.NotFoundBehavior.RedirectUrl); return NotFound(); } @@ -87,6 +93,13 @@ public IActionResult GetRootRoute() .Inc(); string url = Config.RootRoute; + + if (Config.ErrorCodePattern.IsMatch(url)) + { + var errorCodeMatch = Config.ErrorCodePattern.Match(url); + var errorCode = int.Parse(errorCodeMatch.Groups[1].Value); + return StatusCode(errorCode); + } return Redirect(url); } From 01aa0bd7d9d064e6c30b2ed25f09e7a56fcc4132 Mon Sep 17 00:00:00 2001 From: mxritzdev Date: Mon, 29 Sep 2025 21:52:27 +0200 Subject: [PATCH 09/10] update RedirectController.cs --- LinkRouter/App/Http/Controllers/RedirectController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LinkRouter/App/Http/Controllers/RedirectController.cs b/LinkRouter/App/Http/Controllers/RedirectController.cs index d4a1750..9237562 100644 --- a/LinkRouter/App/Http/Controllers/RedirectController.cs +++ b/LinkRouter/App/Http/Controllers/RedirectController.cs @@ -45,7 +45,7 @@ public async Task RedirectToExternalUrl(string path) Console.WriteLine(path); - var redirectRoute = Config.CompiledRoutes?.FirstOrDefault(x => x.CompiledPattern.IsMatch(path)); + var redirectRoute = Config.CompiledRoutes!.FirstOrDefault(x => x.CompiledPattern.IsMatch(path)); if (redirectRoute == null) { From 1f082322abbf6719f8af4fc2b9a19ce371094ac1 Mon Sep 17 00:00:00 2001 From: mxritzdev Date: Mon, 29 Sep 2025 21:54:26 +0200 Subject: [PATCH 10/10] update --- LinkRouter/App/Http/Controllers/RedirectController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LinkRouter/App/Http/Controllers/RedirectController.cs b/LinkRouter/App/Http/Controllers/RedirectController.cs index 9237562..d4a1750 100644 --- a/LinkRouter/App/Http/Controllers/RedirectController.cs +++ b/LinkRouter/App/Http/Controllers/RedirectController.cs @@ -45,7 +45,7 @@ public async Task RedirectToExternalUrl(string path) Console.WriteLine(path); - var redirectRoute = Config.CompiledRoutes!.FirstOrDefault(x => x.CompiledPattern.IsMatch(path)); + var redirectRoute = Config.CompiledRoutes?.FirstOrDefault(x => x.CompiledPattern.IsMatch(path)); if (redirectRoute == null) {