Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions LinkRouter.sln

This file was deleted.

3 changes: 3 additions & 0 deletions LinkRouter.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Solution>
<Project Path="LinkRouter/LinkRouter.csproj" />
</Solution>
40 changes: 30 additions & 10 deletions LinkRouter/App/Http/Controllers/RedirectController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using LinkRouter.App.Configuration;
using LinkRouter.App.Models;
using LinkRouter.App.Services;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -9,22 +9,42 @@ public class RedirectController : Controller
{
private readonly Config Config;
private readonly RedirectionService RedirectionService;
private readonly MetricsService MetricsService;

public RedirectController(Config config, RedirectionService redirectionService)
public RedirectController(Config config, RedirectionService redirectionService, MetricsService metricsService)
{
Config = config;
RedirectionService = redirectionService;
MetricsService = metricsService;
}

[HttpGet("/{*path}")]
public async Task<ActionResult> RedirectToExternalUrl(string path)
[HttpGet("{*path}")]
public async Task<ActionResult> RedirectTo(string? path)
{
return await RedirectionService.GetRedirect(path);
}
Console.WriteLine(path);

[HttpGet("/")]
public async Task<ActionResult> GetRootRoute()
{
return await RedirectionService.GetRedirect(string.Empty);
path = string.IsNullOrWhiteSpace(path)
? "/"
: $"/{path.Trim('/')}/";

if (!RedirectionService.TryGetRedirect(path, out var rawRedirect) || rawRedirect == null)
{
if (string.IsNullOrEmpty(rawRedirect))
return NotFound();

if (RedirectionService.TryGetStatusCode(rawRedirect, out var notFoundStatusCode))
return StatusCode(notFoundStatusCode);

await MetricsService.IncrementNotFound(path);

return Redirect(rawRedirect);
}

if (RedirectionService.TryGetStatusCode(path.Trim('/'), out var code))
return StatusCode(code);

await MetricsService.IncrementFound(path);

return Redirect(rawRedirect);
}
}
58 changes: 58 additions & 0 deletions LinkRouter/App/Implemlementations/LoggingConsoleFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;

namespace LinkRouter.App.Implemlementations;

public class LoggingConsoleFormatter : ConsoleFormatter
{
public LoggingConsoleFormatter() : base(nameof(LoggingConsoleFormatter))
{
}

public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter)
{
var message = logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception)
?? logEntry.State?.ToString();

// Timestamp
textWriter.Write(DateTime.Now.ToString("dd.MM.yy HH:mm:ss"));
textWriter.Write(' ');

// Log level
textWriter.Write(GetLevelText(logEntry.LogLevel));
textWriter.Write(' ');

// Category
textWriter.Write(logEntry.Category);
textWriter.Write(": ");

// Message
textWriter.Write(message);

// Exception (if any)
if (logEntry.Exception != null)
{
textWriter.Write(" | ");
textWriter.Write(logEntry.Exception);
}

textWriter.WriteLine();
}

private static string GetLevelText(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Critical => "CRIT",
LogLevel.Error => "ERRO",
LogLevel.Warning => "WARN",
LogLevel.Information => "INFO",
LogLevel.Debug => "DEBG",
LogLevel.Trace => "TRCE",
_ => "NONE"
};
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
using System.Text;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using LinkRouter.App.Models;

namespace LinkRouter.App.Configuration;
namespace LinkRouter.App.Models;

public class Config
{
public string RootRoute { get; set; } = "https://example.com";
[JsonPropertyName("RootRedirect")] public string? RootRedirect { get; set; } = "https://example.com";

// Legacy property, only used during deserialization
[Obsolete]
[JsonPropertyName("RootRoute")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string? LegacyRootRoute
{
get => null; // never serialize
set
{
if (!string.IsNullOrEmpty(value) && string.IsNullOrEmpty(RootRedirect))
{
RootRedirect = value;
}
}
}

public NotFoundBehaviorConfig NotFoundBehavior { get; set; } = new();

Expand Down Expand Up @@ -39,11 +54,7 @@ public void CompileRoutes()

foreach (var route in Routes)
{
if (!route.Route.StartsWith("/"))
route.Route = "/" + route.Route;

if (!route.Route.EndsWith("/"))
route.Route += "/";
route.Route = "/" + route.Route.Trim('/') + "/";

var compiled = new CompiledRoute
{
Expand All @@ -55,9 +66,9 @@ public void CompileRoutes()

var escaped = Regex.Escape(route.Route);

var pattern = new Regex(@"\\\{(\d|\w+)\}", RegexOptions.CultureInvariant);

var matches = pattern.Matches(escaped);

var matches = Patterns.PlaceholderPattern.Matches(escaped);

foreach (var match in matches.Select(x => x))
{
Expand Down Expand Up @@ -102,7 +113,5 @@ public void CompileRoutes()
CompiledRoutes = compiledRoutes
.ToArray();
}

[JsonIgnore] public static Regex ErrorCodePattern = new(@"\s*\-\>\s*(\d+)\s*$", RegexOptions.Compiled | RegexOptions.CultureInvariant);

}
10 changes: 10 additions & 0 deletions LinkRouter/App/Models/Patterns.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.RegularExpressions;

namespace LinkRouter.App.Models;

public static class Patterns
{
public static Regex ErrorCodePattern = new(@"\s*\-\>\s*(\d+)\s*$", RegexOptions.Compiled | RegexOptions.CultureInvariant);

public static Regex PlaceholderPattern = new (@"\\\{(\d|\w+)\}", RegexOptions.Compiled | RegexOptions.CultureInvariant);
}
4 changes: 2 additions & 2 deletions LinkRouter/App/Services/ConfigWatcher.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Text.Json;
using LinkRouter.App.Configuration;
using LinkRouter.App.Models;

namespace LinkRouter.App.Services;

Expand Down Expand Up @@ -47,7 +47,7 @@ private void OnChanged(object sender, FileSystemEventArgs e)
var config = JsonSerializer.Deserialize<Config>(content);

Config.Routes = config?.Routes ?? [];
Config.RootRoute = config?.RootRoute ?? "https://example.com";
Config.RootRedirect = config?.RootRedirect ?? "https://example.com";

Logger.LogInformation("Config file changed.");

Expand Down
3 changes: 1 addition & 2 deletions LinkRouter/App/Services/MetricsService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using MoonCore.Attributes;
using Prometheus;

namespace LinkRouter.App.Services;

[Singleton]

public class MetricsService
{
private readonly Counter RouteCounter = Metrics.CreateCounter(
Expand Down
61 changes: 23 additions & 38 deletions LinkRouter/App/Services/RedirectionService.cs
Original file line number Diff line number Diff line change
@@ -1,81 +1,66 @@
using LinkRouter.App.Configuration;
using Microsoft.AspNetCore.Mvc;
using MoonCore.Attributes;
using LinkRouter.App.Models;

namespace LinkRouter.App.Services;

[Singleton]
public class RedirectionService
{
private readonly Config Config;
private readonly MetricsService MetricsService;

public RedirectionService(Config config, MetricsService metricsService)
public RedirectionService(Config config)
{
Config = config;
MetricsService = metricsService;
}

public async Task<ActionResult> GetRedirect(string path)
public bool TryGetRedirect(string path, out string? redirectPath)
{
if (path == "")
redirectPath = null;

if (path == "/")
{
var url = Config.RootRoute;
var url = Config.RootRedirect;

if (TryGetErrorCode(url, out var notFoundStatusCode))
return new StatusCodeResult(notFoundStatusCode);
if (string.IsNullOrEmpty(url))
return false;

await MetricsService.IncrementFound("/");
redirectPath = url;

return new RedirectResult(url);
return true;
}

if (!path.EndsWith("/"))
path += "/";

path = "/" + path;


var redirectRoute = Config.CompiledRoutes?.FirstOrDefault(x => x.CompiledPattern.IsMatch(path));


if (redirectRoute == null)
{
await MetricsService.IncrementNotFound(path);

if (!Config.NotFoundBehavior.RedirectOn404)
return new NotFoundResult();

{
return false;
}

if (TryGetErrorCode(Config.NotFoundBehavior.RedirectUrl, out var notFoundStatusCode))
return new StatusCodeResult(notFoundStatusCode);
redirectPath = Config.NotFoundBehavior.RedirectUrl;

return new RedirectResult(Config.NotFoundBehavior.RedirectUrl);
return true;
}

var match = redirectRoute.CompiledPattern.Match(path);

if (TryGetErrorCode(redirectRoute.RedirectUrl, out var statusCode))
return new StatusCodeResult(statusCode);


foreach (var placeholder in redirectRoute.Placeholders)
{
var value = match.Groups[placeholder.Value].Value;
redirectRoute.RedirectUrl = redirectRoute.RedirectUrl.Replace("{" + placeholder.Key + "}", value);
}

await MetricsService.IncrementFound(path);
redirectPath = redirectRoute.RedirectUrl;

return new RedirectResult(redirectRoute.RedirectUrl);
return true;
}

private bool TryGetErrorCode(string url, out int code)
public bool TryGetStatusCode(string path, out int code)
{
if (Config.ErrorCodePattern.IsMatch(url))
var match = Patterns.ErrorCodePattern.Match(path);

if (match.Success)
{
var errorCodeMatch = Config.ErrorCodePattern.Match(url);
code = int.Parse(errorCodeMatch.Groups[1].Value);
code = int.Parse(match.Groups[1].Value);
return true;
}

Expand Down
3 changes: 1 addition & 2 deletions LinkRouter/LinkRouter.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<LangVersion>preview</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MoonCore" Version="2.0.6" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
</ItemGroup>

Expand Down
Loading
Loading