From c97e6fad32fb3f5258ea57276969faf73538dc64 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:39:43 +0200 Subject: [PATCH] Allow upload of direct yml files via api token (#63) --- ReplayBrowser/Controllers/ReplayController.cs | 46 +++++++++++++++++++ .../ReplayDbContextModelSnapshot.cs | 12 +++++ ReplayBrowser/Data/Models/Replay.cs | 4 +- ReplayBrowser/Data/Models/ServerToken.cs | 18 ++++++++ ReplayBrowser/Data/ReplayDbContext.cs | 2 + ReplayBrowser/Models/Ingested/YamlReplay.cs | 5 ++ .../Pages/Shared/ReplayDetails.razor | 8 ++++ .../ReplayParser/ReplayParserService.cs | 2 +- ReplayBrowser/appsettings.json | 8 ++++ 9 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 ReplayBrowser/Data/Models/ServerToken.cs diff --git a/ReplayBrowser/Controllers/ReplayController.cs b/ReplayBrowser/Controllers/ReplayController.cs index 21efb8c..73de348 100644 --- a/ReplayBrowser/Controllers/ReplayController.cs +++ b/ReplayBrowser/Controllers/ReplayController.cs @@ -220,4 +220,50 @@ public async Task FavoriteReplay(int replayId) return Ok(!isFavorited); } + + [HttpPost("replay/upload")] + [AllowAnonymous] + [IgnoreAntiforgeryToken] + public async Task UploadReplay( + IFormCollection form + ) + { + if (!CheckAuthenticationTokenServerApi()) + { + return Unauthorized("No valid token provided."); + } + + var file = form.Files.FirstOrDefault(); + if (file == null) + { + return BadRequest("No file provided."); + } + + var reader = new StreamReader(file.OpenReadStream()); + Replay? replay = null; + try + { + replay = _replayParserService.FinalizeReplayParse(reader, null); + } + catch (Exception e) + { + return BadRequest(e.Message); + } + await _dbContext.Replays.AddAsync(replay); + await _dbContext.SaveChangesAsync(); + return Ok(); + } + + private bool CheckAuthenticationTokenServerApi() + { + var token = Request.Headers.Authorization; + if (token.Count == 0) + { + return false; + } + + var tokenString = token.ToString().Split(" ")[1]; + + return _dbContext.ServerTokens.Any(t => t.Token == tokenString); + } } \ No newline at end of file diff --git a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs index 7f62be0..658f96c 100644 --- a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs +++ b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs @@ -510,6 +510,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ReplayParticipants"); }); + modelBuilder.Entity("ReplayBrowser.Data.Models.ServerToken", b => + { + b.Property("Token") + .HasColumnType("text"); + + b.HasKey("Token"); + + b.HasIndex("Token"); + + b.ToTable("ServerTokens"); + }); + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => { b.HasOne("ReplayBrowser.Data.Models.Account.AccountSettings", "Settings") diff --git a/ReplayBrowser/Data/Models/Replay.cs b/ReplayBrowser/Data/Models/Replay.cs index 95e3b6e..f5d94e2 100644 --- a/ReplayBrowser/Data/Models/Replay.cs +++ b/ReplayBrowser/Data/Models/Replay.cs @@ -86,7 +86,7 @@ public ReplayResult ToResult() }; } - public static Replay FromYaml(YamlReplay replay, string link) + public static Replay FromYaml(YamlReplay replay, string? link) { var participants = replay.RoundEndPlayers? .GroupBy(p => p.PlayerGuid) @@ -97,6 +97,8 @@ public static Replay FromYaml(YamlReplay replay, string link) }) .ToList(); + link ??= replay.Link ?? throw new ArgumentException("Link is required."); + return new Replay { Link = link, ServerId = replay.ServerId, diff --git a/ReplayBrowser/Data/Models/ServerToken.cs b/ReplayBrowser/Data/Models/ServerToken.cs new file mode 100644 index 0000000..2617c58 --- /dev/null +++ b/ReplayBrowser/Data/Models/ServerToken.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ReplayBrowser.Data.Models; + +/// +/// Represents a token that is used to authenticate with the API for ingesting replays. +/// +public class ServerToken : IEntityTypeConfiguration +{ + public required string Token { get; set; } + + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(t => t.Token); + builder.HasIndex(t => t.Token); + } +} \ No newline at end of file diff --git a/ReplayBrowser/Data/ReplayDbContext.cs b/ReplayBrowser/Data/ReplayDbContext.cs index cae9a99..e7f2160 100644 --- a/ReplayBrowser/Data/ReplayDbContext.cs +++ b/ReplayBrowser/Data/ReplayDbContext.cs @@ -47,4 +47,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet JobDepartments { get; set; } public DbSet ReplayParticipants { get; set; } + + public DbSet ServerTokens { get; set; } } \ No newline at end of file diff --git a/ReplayBrowser/Models/Ingested/YamlReplay.cs b/ReplayBrowser/Models/Ingested/YamlReplay.cs index 316ea0f..98e7b89 100644 --- a/ReplayBrowser/Models/Ingested/YamlReplay.cs +++ b/ReplayBrowser/Models/Ingested/YamlReplay.cs @@ -5,6 +5,11 @@ namespace ReplayBrowser.Models.Ingested; public class YamlReplay { public int? RoundId { get; set; } + /// + /// Fallback link to the replay. + /// + public string? Link { get; set; } + [YamlMember(Alias = "server_id", ApplyNamingConventions = false)] public required string ServerId { get; set; } [YamlMember(Alias = "server_name", ApplyNamingConventions = false)] diff --git a/ReplayBrowser/Pages/Shared/ReplayDetails.razor b/ReplayBrowser/Pages/Shared/ReplayDetails.razor index b8e245a..410e967 100644 --- a/ReplayBrowser/Pages/Shared/ReplayDetails.razor +++ b/ReplayBrowser/Pages/Shared/ReplayDetails.razor @@ -9,6 +9,14 @@ @if(Replay != null) {

@NameFormatted

+ @if(Replay.Duration == "00:00:00.0000000") + { + // Replay is a legacy replay, show a warning + + } + @if (Replay.Map == null) {

Maps: @string.Join(", ", Replay.Maps!)

diff --git a/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs b/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs index db2d95f..eb24b07 100644 --- a/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs +++ b/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs @@ -294,7 +294,7 @@ private Replay ParseReplay(Stream stream, string replayLink) return FinalizeReplayParse(reader, replayLink); } - private Replay FinalizeReplayParse(StreamReader stream, string replayLink) + public Replay FinalizeReplayParse(TextReader stream, string? replayLink) { var deserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() diff --git a/ReplayBrowser/appsettings.json b/ReplayBrowser/appsettings.json index 8d0fb89..906cb11 100644 --- a/ReplayBrowser/appsettings.json +++ b/ReplayBrowser/appsettings.json @@ -7,6 +7,14 @@ }, "AllowedHosts": "*", "ReplayUrls": [ + { + "url": "https://cdn.replay.unstablefoundation.de/", + "provider": "dummy", + "fallBackServerName": "wizards", + "fallBackServerId": "wizards", + "replayRegex": "[a-zA-Z0-9-]+-(\\d{4}_\\d{2}_\\d{2}-\\d{2}_\\d{2})-round_\\d+\\.zip$", + "serverNameRegex": "([a-zA-Z0-9-]+)-\\d{4}_\\d{2}_\\d{2}-\\d{2}_\\d{2}-round_\\d+\\.zip$" + }, { "url": "https://cdn.networkgamez.com/replays/hullrot/", "provider": "nginx",