Skip to content

Commit

Permalink
Require User-Agent for updates (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexMacocian authored Nov 28, 2023
1 parent 5dffe46 commit f48a682
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 19 deletions.
13 changes: 7 additions & 6 deletions GuildWarsPartySearch/Endpoints/PostPartySearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace GuildWarsPartySearch.Server.Endpoints;

[ServiceFilter<ApiKeyProtected>]
[ServiceFilter<UserAgentRequired>]
public sealed class PostPartySearch : WebSocketRouteBase<PostPartySearchRequest, PostPartySearchResponse>
{
private readonly ILiveFeedService liveFeedService;
Expand Down Expand Up @@ -37,12 +38,12 @@ public override async Task ExecuteAsync(PostPartySearchRequest? message, Cancell
{
this.liveFeedService.PushUpdate(new PartySearch
{
Campaign = message.Campaign,
Continent = message.Continent,
District = message.District,
Map = message.Map,
PartySearchEntries = message.PartySearchEntries,
Region = message.Region
Campaign = message?.Campaign,
Continent = message?.Continent,
District = message?.District,
Map = message?.Map,
PartySearchEntries = message?.PartySearchEntries,
Region = message?.Region
}, cancellationToken);
return Success;
},
Expand Down
20 changes: 15 additions & 5 deletions GuildWarsPartySearch/Filters/ApiKeyProtected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,25 @@ public void OnActionExecuting(ActionExecutingContext context)
var serverOptions = context.HttpContext.RequestServices.GetRequiredService<IOptions<ServerOptions>>();
if (serverOptions.Value.ApiKey!.IsNullOrWhiteSpace())
{
context.Result = new ForbiddenResponseActionResult();
context.Result = new ForbiddenResponseActionResult("API Key is not configured");
return;
}

if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeader, out var value) ||
value.FirstOrDefault() is not string headerValue ||
headerValue != serverOptions.Value.ApiKey)
if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeader, out var value))
{
context.Result = new ForbiddenResponseActionResult();
context.Result = new ForbiddenResponseActionResult($"{ApiKeyHeader} header not found");
return;
}

if (value.FirstOrDefault() is not string headerValue)
{
context.Result = new ForbiddenResponseActionResult($"{ApiKeyHeader} header value is invalid");
return;
}

if (headerValue != serverOptions.Value.ApiKey)
{
context.Result = new ForbiddenResponseActionResult($"{ApiKeyHeader} header value is incorrect");
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ namespace GuildWarsPartySearch.Server.Filters;

public class ForbiddenResponseActionResult : IActionResult
{
private readonly string reason;

public ForbiddenResponseActionResult(string reason)
{
this.reason = reason;
}

public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
return context.HttpContext.Response.WriteAsync(reason, context.HttpContext.RequestAborted);
}
}
19 changes: 19 additions & 0 deletions GuildWarsPartySearch/Filters/UserAgentRequired.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc.Filters;

namespace GuildWarsPartySearch.Server.Filters;

public class UserAgentRequired : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}

public void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.Headers.UserAgent.FirstOrDefault() is not string userAgent)
{
context.Result = new ForbiddenResponseActionResult("Missing user agent");
return;
}
}
}
1 change: 1 addition & 0 deletions GuildWarsPartySearch/Launch/ServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public static IServiceCollection SetupServices(this IServiceCollection services)
services.AddMemoryCache();
services.AddInMemoryRateLimiting();
services.AddScoped<ApiKeyProtected>();
services.AddScoped<UserAgentRequired>();
services.AddScoped<IServerLifetimeService, ServerLifetimeService>();
services.AddScoped<IPartySearchDatabase, TableStorageDatabase>();
services.AddScoped<IPartySearchService, PartySearchService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
new OpenApiPathItem
{
Summary = "Connect to WebSocket for updates",
Description = $"WebSocket endpoint for posting party search updates. Protected by {ApiKeyProtected.ApiKeyHeader} header.",
Description = $"WebSocket endpoint for posting party search updates. Protected by {ApiKeyProtected.ApiKeyHeader} header. Requires User-Agent header to be set",
Operations = new Dictionary<OperationType, OpenApiOperation>
{
{
Expand All @@ -131,6 +131,8 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
Protected by *{ApiKeyProtected.ApiKeyHeader}* header.
Requires *User-Agent* header to be set.
Accepts json payloads. Example:
```json
{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,8 @@ public async Task<bool> SetPartySearches(Campaign campaign, Continent continent,
.Where(e =>
{
// Only update entries that have changed
var existingEntry = entries.FirstOrDefault(e2 => e2.RowKey == e.RowKey);
return e.Campaign != existingEntry?.Campaign ||
e.Continent != existingEntry?.Continent ||
e.Region != existingEntry?.Region ||
e.Map != existingEntry?.Map ||
e.District != existingEntry?.District ||
var existingEntry = existingEntries?.FirstOrDefault(e2 => e2.CharName == e.CharName);
return existingEntry is null ||
e.CharName != existingEntry?.CharName ||
e.PartySize != existingEntry?.PartySize ||
e.PartyMaxSize != existingEntry?.PartyMaxSize ||
Expand Down

0 comments on commit f48a682

Please sign in to comment.