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
2 changes: 1 addition & 1 deletion src/Game.Realtime/Hubs/LobbyHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async ValueTask ConnectAsync(string lobbyId, string playerName)
_lobbyValidator.ValidateLobbyId(lobbyId);
_lobbyValidator.ValidatePlayerName(playerName);

_userId = Context.CallContext.GetHttpContext().User!.GetUserId()!;
_userId = Context.CallContext.GetHttpContext().User.GetRequiredUserId();
_playerName = playerName;
_lobbyId = lobbyId;

Expand Down
22 changes: 18 additions & 4 deletions src/Game.Realtime/Hubs/MatchmakingHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public async ValueTask SubscribeAsync(string gameMode)
{
_matchmakingValidator.ValidateGameMode(gameMode);

_userId = Context.CallContext.GetHttpContext().User!.GetUserId()!;
_userId = Context.CallContext.GetHttpContext().User.GetRequiredUserId();
_gameMode = gameMode;

var queueGroupName = $"matchmaking:{gameMode}";
Expand All @@ -53,7 +53,14 @@ await _subscriber.SubscribeAsync(channel, (_, message) =>
var result = JsonSerializer.Deserialize<MatchResult>(message.ToString());
if (result != null)
{
Client.OnMatchFound(result);
try
{
Client.OnMatchFound(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send match notification to user {UserId}", _userId);
}
}
}
catch (Exception ex)
Expand All @@ -66,9 +73,16 @@ await _subscriber.SubscribeAsync(channel, (_, message) =>
var queueChannel = RedisChannel.Literal($"matchmaking:queue:{gameMode}");
await _subscriber.SubscribeAsync(queueChannel, (_, message) =>
{
if (int.TryParse(message.ToString(), out var count))
try
{
if (int.TryParse(message.ToString(), out var count))
{
Client.OnQueueStatusUpdated(count);
}
}
catch (Exception ex)
{
Client.OnQueueStatusUpdated(count);
_logger.LogError(ex, "Failed to send queue status update to user {UserId}", _userId);
}
});

Expand Down
2 changes: 2 additions & 0 deletions src/Game.Realtime/Services/MatchmakingProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ private async Task ProcessGameModeAsync(string gameMode, int matchSize, Cancella

if (queueCount < matchSize) break;

stoppingToken.ThrowIfCancellationRequested();

var playerIds = await _queueService.DequeueTopPlayersAsync(gameMode, matchSize);
if (playerIds.Length < matchSize)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ public static class ClaimsPrincipalExtensions
return principal?.FindFirst(JwtRegisteredClaimNames.Sub)?.Value
?? principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}

/// <summary>
/// 認証フィルター通過後に使用する。userId が取得できない場合は InvalidOperationException をスローする。
/// </summary>
public static string GetRequiredUserId(this ClaimsPrincipal principal)
{
return principal.GetUserId()
?? throw new InvalidOperationException("UserId claim not found. Ensure authentication filter has executed.");
}
}
27 changes: 18 additions & 9 deletions src/Game.Server/Middleware/RequestSigningMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
private readonly byte[] _serverSecret;
private readonly bool _enabled;

private static readonly string[] ExemptPaths = new[]

Check warning on line 18 in src/Game.Server/Middleware/RequestSigningMiddleware.cs

View workflow job for this annotation

GitHub Actions / dotnet format Check

Naming rule violation: Missing prefix: '_'
{
"/api/auth/guest",
"/api/auth/login",
Expand Down Expand Up @@ -107,8 +107,16 @@
}

// Nonce 重複チェック(Valkey)
var nonceAccepted = await TryAcceptNonce(context, nonce!);
if (!nonceAccepted)
var nonceResult = await TryAcceptNonce(context, nonce!);
if (nonceResult == NonceResult.Unavailable)
{
_logger.LogWarning("Nonce validation unavailable (Valkey down)");
context.Response.StatusCode = 503;
await context.Response.WriteAsJsonAsync(new { error = "SERVICE_UNAVAILABLE", message = "Service temporarily unavailable." });
return;
}

if (nonceResult == NonceResult.Replayed)
{
_logger.LogWarning("Nonce replay detected: {Nonce}", nonce.ToString());
context.Response.StatusCode = 401;
Expand Down Expand Up @@ -158,16 +166,17 @@
return ms.ToArray();
}

private async Task<bool> TryAcceptNonce(HttpContext context, string nonce)
private enum NonceResult { Accepted, Replayed, Unavailable }

private async Task<NonceResult> TryAcceptNonce(HttpContext context, string nonce)
{
try
{
var redis = context.RequestServices.GetService<IConnectionMultiplexer>();
if (redis == null || !redis.IsConnected)
{
// Valkey 未接続時は署名検証のみで通過(Nonce チェックは省略)
_logger.LogWarning("Valkey not available, skipping nonce check");
return true;
_logger.LogWarning("Valkey not available, rejecting request for nonce validation");
return NonceResult.Unavailable;
}

var db = redis.GetDatabase();
Expand All @@ -176,12 +185,12 @@
TimeSpan.FromSeconds(RequestSigningConstants.NonceExpirySeconds),
When.NotExists);

return wasSet;
return wasSet ? NonceResult.Accepted : NonceResult.Replayed;
}
catch (RedisConnectionException ex)
{
_logger.LogWarning(ex, "Valkey connection error during nonce check, allowing request");
return true;
_logger.LogWarning(ex, "Valkey connection error during nonce check, rejecting request");
return NonceResult.Unavailable;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
var mockDb = new Mock<IDatabase>();
mockDb.Setup(d => d.PingAsync(It.IsAny<CommandFlags>()))
.ReturnsAsync(TimeSpan.FromMilliseconds(1));
// Nonce check の StringSetAsync が常に true を返すよう設定
mockDb.SetReturnsDefault(Task.FromResult(true));
var mockRedis = new Mock<IConnectionMultiplexer>();
mockRedis.Setup(r => r.IsConnected).Returns(true);
mockRedis.Setup(r => r.GetDatabase(It.IsAny<int>(), It.IsAny<object>()))
.Returns(mockDb.Object);
services.RemoveAll<IConnectionMultiplexer>();
Expand Down
Loading