Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require User-Agent for updates #41

Merged
merged 1 commit into from
Nov 28, 2023
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
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
Loading