Skip to content

Commit

Permalink
Maintenance (#114)
Browse files Browse the repository at this point in the history
Changes:
- Refactor source generator for incremental processing.
- Add multi-reward support to assignments ( fixes #109 ).
- Add header validation for X-Super-Client and X-Super-Contact in RateLimitMiddleware ( fixes #94 ).

General:
- Update package dependencies.
- Update helldivers-2/json.
- Format code with dotnet format.
- Remove unused package reference.
  • Loading branch information
dealloc authored Nov 12, 2024
1 parent 435ce31 commit 24f0620
Show file tree
Hide file tree
Showing 19 changed files with 324 additions and 86 deletions.
21 changes: 21 additions & 0 deletions docs/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,24 @@ make to the application.

Alternatively, if you use the hosted versions you can request an API key that allows for higher rate limits
by sponsoring this project! (if you self-host you can generate your own keys too!).

# Disabling the client validation
If you run the container and navigate to it in your browser
you might come across the following message:
```json
{"message":"The X-Super-Client and X-Super-Contact headers are required"}
```

This is because by default the API will validate all requests send `X-Super-Client` and `X-Super-Contact`
headers along. You can disable this behaviour by setting the following flag:
```json
{
"Helldivers": {
"API": {
"ValidateClients": false
}
}
}
```
Or through environment flags with `-e Helldivers__API__ValidateClients`

5 changes: 5 additions & 0 deletions src/Helldivers-2-API/Configuration/ApiConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public sealed class ApiConfiguration
/// </summary>
public string Blacklist { get; set; } = string.Empty;

/// <summary>
/// Whether X-Super-Client and X-Super-Contact headers are validated.
/// </summary>
public bool ValidateClients { get; set; } = true;

/// <summary>
/// Contains the <see cref="AuthenticationConfiguration" /> for the API.
/// </summary>
Expand Down
10 changes: 5 additions & 5 deletions src/Helldivers-2-API/Helldivers-2-API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@

<!-- Dependencies for all build configurations -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<ProjectReference Include="..\Helldivers-2-Models\Helldivers-2-Models.csproj" />
<ProjectReference Include="..\Helldivers-2-Core\Helldivers-2-Core.csproj" />
Expand All @@ -36,12 +36,12 @@

<!-- Only include swagger dependencies in DEBUG builds -->
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="8.0.3">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="8.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NSwag.AspNetCore" Version="14.0.4" />
<PackageReference Include="NSwag.AspNetCore" Version="14.1.0" />
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions src/Helldivers-2-API/Middlewares/RateLimitMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
using Helldivers.API.Extensions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading.RateLimiting;

namespace Helldivers.API.Middlewares;
Expand All @@ -26,6 +29,12 @@ IMemoryCache cache
/// <inheritdoc />
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (IsValidRequest(context) is false)
{
await RejectRequest(context);
return;
}

var limiter = GetRateLimiter(context);
using var lease = await limiter.AcquireAsync(permitCount: 1, context.RequestAborted);
if (limiter.GetStatistics() is { } statistics)
Expand All @@ -45,6 +54,18 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
await next(context);
}

/// <summary>
/// Checks if the request is "valid" (contains the correct X-Super-* headers).
/// </summary>
private bool IsValidRequest(HttpContext context)
{
if (options.Value.ValidateClients is false)
return true;

return context.Request.Headers.ContainsKey(Constants.CLIENT_HEADER_NAME)
&& context.Request.Headers.ContainsKey(Constants.CONTACT_HEADER_NAME);
}

private RateLimiter GetRateLimiter(HttpContext http)
{
if (http.User.Identity?.IsAuthenticated ?? false)
Expand Down Expand Up @@ -86,4 +107,19 @@ private RateLimiter GetRateLimiterForUser(ClaimsPrincipal user)
});
}) ?? throw new InvalidOperationException($"Creating rate limiter failed for {name}");
}

private async Task RejectRequest(HttpContext context)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.Headers.WWWAuthenticate = "X-Super-Client";
context.Response.ContentType = "application/json";

var writer = new Utf8JsonWriter(context.Response.Body);
writer.WriteStartObject();
writer.WritePropertyName("message");
writer.WriteStringValue("The X-Super-Client and X-Super-Contact headers are required");
writer.WriteEndObject();

await writer.FlushAsync(context.RequestAborted);
}
}
1 change: 1 addition & 0 deletions src/Helldivers-2-API/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"Helldivers": {
"API": {
"ValidateClients": false,
"Authentication": {
"SigningKey": "I4eGmsXbDXfxlRo5N+w0ToRGN8aWSIaYWbZ2zMFqqnI="
}
Expand Down
2 changes: 1 addition & 1 deletion src/Helldivers-2-Core/Helldivers-2-Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2"/>
<ProjectReference Include="..\Helldivers-2-Models\Helldivers-2-Models.csproj"/>
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions src/Helldivers-2-Core/Mapping/V1/AssignmentMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ private Assignment MapToV1(Dictionary<string, Models.ArrowHead.Assignment> trans
Description: LocalizedMessage.FromStrings(descriptions),
Tasks: invariant.Setting.Tasks.Select(MapToV1).ToList(),
Reward: MapToV1(invariant.Setting.Reward),
Rewards: invariant.Setting.Rewards.Select(MapToV1).ToList(),
Expiration: expiration
);
}
Expand Down
4 changes: 3 additions & 1 deletion src/Helldivers-2-Models/ArrowHead/Assignments/Setting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
/// <param name="OverrideBrief">The briefing (description) of this assignment.</param>
/// <param name="TaskDescription">A description of what is expected of Helldivers to complete the assignment.</param>
/// <param name="Tasks">A list of <see cref="Task" />s describing the assignment requirements.</param>
/// <param name="Reward">Contains information on the rewards players willr eceive upon completion.</param>
/// <param name="Reward">Contains information on the reward players will receive upon completion.</param>
/// <param name="Rewards">Contains information on the rewards players will receive upon completion.</param>
/// <param name="Flags">Flags, suspected to be a binary OR'd value, purpose unknown.</param>
public sealed record Setting(
int Type,
Expand All @@ -17,5 +18,6 @@ public sealed record Setting(
string TaskDescription,
List<Task> Tasks,
Reward Reward,
List<Reward> Rewards,
int Flags
);
2 changes: 2 additions & 0 deletions src/Helldivers-2-Models/V1/Assignment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Helldivers.Models.V1;
/// <param name="Description">A very short summary of the description.</param>
/// <param name="Tasks">A list of tasks that need to be completed for this assignment.</param>
/// <param name="Reward">The reward for completing the assignment.</param>
/// <param name="Rewards">A list of rewards for completing the assignment.</param>
/// <param name="Expiration">The date when the assignment will expire.</param>
public sealed record Assignment(
long Id,
Expand All @@ -24,5 +25,6 @@ public sealed record Assignment(
LocalizedMessage Description,
List<Task> Tasks,
Reward Reward,
List<Reward> Rewards,
DateTime Expiration
);
18 changes: 18 additions & 0 deletions src/Helldivers-2-SourceGen/Contracts/IJsonParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace Helldivers.SourceGen.Contracts;

/// <summary>
/// Interface for parsing JSON strings and generating corresponding C# source code.
/// </summary>
public interface IJsonParser
{
/// <summary>
/// Parses the given source text and generates corresponding C# source code.
/// </summary>
/// <param name="file">The source file to be parsed.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> for cancelling the parse process.</param>
/// <returns>A <see cref="SourceText"/> object representing the generated C# source code.</returns>
SourceText Parse(AdditionalText file, CancellationToken cancellationToken = default);
}
7 changes: 3 additions & 4 deletions src/Helldivers-2-SourceGen/Helldivers-2-SourceGen.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.4" PrivateAssets="all" LocalSourceGenerators="true" />
<PackageReference Include="System.Text.Json" Version="8.0.5" PrivateAssets="all" LocalSourceGenerators="true" />
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" PrivateAssets="all" LocalSourceGenerators="true" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" PrivateAssets="all" LocalSourceGenerators="true" />
</ItemGroup>
Expand Down
50 changes: 50 additions & 0 deletions src/Helldivers-2-SourceGen/Parsers/BaseJsonParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Helldivers.SourceGen.Contracts;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace Helldivers.SourceGen.Parsers;

/// <summary>
/// An abstract base class for parsing JSON data and generating corresponding C# source code.
/// </summary>
public abstract class BaseJsonParser : IJsonParser
{
private const string TEMPLATE = @"// <auto-generated />
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using global::System.Collections.Generic;
using global::Helldivers.Models.Domain.Localization;
namespace Helldivers.Models;
public static partial class Static
{{
/// <summary>Public list of {0} entries from {1}</summary>
public static {2} {0} = {3};
}}";

/// <inheritdoc />
public SourceText Parse(AdditionalText file, CancellationToken cancellationToken = default)
{
var filename = Path.GetFileName(file.Path);
var json = file.GetText(cancellationToken)?.ToString();

var name = Path.GetFileNameWithoutExtension(file.Path);
name = $"{char.ToUpper(name[0])}{name.Substring(1)}";

if (string.IsNullOrWhiteSpace(json) is false)
{
var (type, csharp) = Parse(json!);

var output = string.Format(TEMPLATE, name, filename, type, csharp);
return SourceText.From(output, Encoding.UTF8);
}

return SourceText.From("// Could not read JSON file");
}

/// <summary>
/// Convert the JSON string into C# code that can be injected.
/// </summary>
protected abstract (string Type, string Source) Parse(string json);
}
22 changes: 22 additions & 0 deletions src/Helldivers-2-SourceGen/Parsers/BiomesParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text;
using System.Text.Json;

namespace Helldivers.SourceGen.Parsers;

/// <summary>
/// BiomesParser is responsible for parsing a JSON string representation of biomes.
/// </summary>
public class BiomesParser : BaseJsonParser
{
/// <inheritdoc />
protected override (string Type, string Source) Parse(string json)
{
var builder = new StringBuilder("new Dictionary<string, Helldivers.Models.V1.Planets.Biome>()\n\t{\n");
var entries = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(json)!;
foreach (var pair in entries)
builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Biome(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},");

builder.Append("\t}");
return ("IReadOnlyDictionary<string, Helldivers.Models.V1.Planets.Biome>", builder.ToString());
}
}
23 changes: 23 additions & 0 deletions src/Helldivers-2-SourceGen/Parsers/EnvironmentalsParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text;
using System.Text.Json;

namespace Helldivers.SourceGen.Parsers;

/// <summary>
/// The EnvironmentalsParser class is responsible for parsing JSON strings
/// representing environmental hazards and converting them into C# source code.
/// </summary>
public class EnvironmentalsParser : BaseJsonParser
{
/// <inheritdoc />
protected override (string Type, string Source) Parse(string json)
{
var builder = new StringBuilder("new Dictionary<string, Helldivers.Models.V1.Planets.Hazard>()\n\t{\n");
var entries = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(json)!;
foreach (var pair in entries)
builder.AppendLine($@"{'\t'}{'\t'}{{ ""{pair.Key}"", new Helldivers.Models.V1.Planets.Hazard(""{pair.Value["name"]}"", ""{pair.Value["description"]}"") }},");

builder.Append("\t}");
return ("IReadOnlyDictionary<string, Helldivers.Models.V1.Planets.Hazard>", builder.ToString());
}
}
22 changes: 22 additions & 0 deletions src/Helldivers-2-SourceGen/Parsers/FactionsParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text;
using System.Text.Json;

namespace Helldivers.SourceGen.Parsers;

/// <summary>
/// Parses JSON data of factions and generates the corresponding C# source code representation.
/// </summary>
public class FactionsParser : BaseJsonParser
{
/// <inheritdoc />
protected override (string Type, string Source) Parse(string json)
{
var builder = new StringBuilder("new Dictionary<int, string>()\n\t{\n");
var entries = JsonSerializer.Deserialize<Dictionary<string, string>>(json)!;
foreach (var pair in entries)
builder.AppendLine($@"{'\t'}{'\t'}{{ {pair.Key}, ""{pair.Value}"" }},");

builder.Append("\t}");
return ("IReadOnlyDictionary<int, string>", builder.ToString());
}
}
40 changes: 40 additions & 0 deletions src/Helldivers-2-SourceGen/Parsers/PlanetsParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Text;
using System.Text.Json;

namespace Helldivers.SourceGen.Parsers;

/// <summary>
/// Handles parsing the planets.json and generating the resulting planet data.
/// </summary>
public class PlanetsParser : BaseJsonParser
{
/// <inheritdoc />
protected override (string Type, string Source) Parse(string json)
{
var builder = new StringBuilder("new Dictionary<int, (LocalizedMessage Name, string Sector, string Biome, List<string> Environmentals)>()\n\t{\n");
var document = JsonDocument.Parse(json);
foreach (var property in document.RootElement.EnumerateObject())
{
var index = property.Name;
var name = property.Value.GetProperty("name").GetString();
var names = property
.Value
.GetProperty("names")
.EnumerateObject()
.ToDictionary(prop => prop.Name, prop => prop.Value.GetString()!);
var sector = property.Value.GetProperty("sector").GetString();
var biome = property.Value.GetProperty("biome").GetString();
var environmentals = property
.Value
.GetProperty("environmentals")
.EnumerateArray()
.Select(prop => $@"""{prop.GetString()!}""")
.ToList();
builder.AppendLine($@"{'\t'}{'\t'}{{ {index}, (LocalizedMessage.FromStrings([{string.Join(", ", names.Select(pair => $@"new KeyValuePair<string, string>(""{pair.Key}"", ""{pair.Value}"")"))}]), ""{sector}"", ""{biome}"", [{string.Join(", ", environmentals)}]) }},");
}

builder.Append("\t}");
return ("IReadOnlyDictionary<int, (LocalizedMessage Name, string Sector, string Biome, List<string> Environmentals)>", builder.ToString());
}
}
Loading

0 comments on commit 24f0620

Please sign in to comment.