This commit is contained in:
mochaisvibecoded
2026-08-24 17:00:26 -04:00
parent cc1148050c
commit 42f7f57b72
401 changed files with 1108081 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
using Mocha2023.Classes;
using Mocha2023.Classes.DBs;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Mocha2023.Auth;
namespace Mocha2023.Controllers
{
[ApiController]
[Mocha2023.Classes.ApiProtection]
public class PhotonController : ControllerBase
{
private static readonly string[] TokenKeys =
{
"token",
"ticket",
"accessToken",
"authToken",
"photonAccessToken",
"PhotonAccessToken",
"rrToken"
};
private static readonly string[] UserIdKeys =
{
"userId",
"userid",
"user",
"accountId",
"playerId"
};
[HttpGet("/api/photon/config")]
[HttpGet("/photon/config")]
public IActionResult GetPhotonConfig()
{
Response.Headers.CacheControl = "no-store";
return Ok(new
{
Enabled = ServerConfig.PhotonEnabled,
RealtimeAppId = ServerConfig.PhotonRealtimeAppId,
VoiceAppId = ServerConfig.PhotonVoiceAppId,
AppVersion = ServerConfig.PhotonAppVersion,
Region = ServerConfig.PhotonRegion,
UseCustomAuthentication = true,
AuthenticationParameter = "token"
});
}
[HttpGet("/voice/config")]
public IActionResult GetVoiceConfig()
{
Response.Headers.CacheControl = "no-store";
return Ok(new
{
Enabled = ServerConfig.PhotonEnabled &&
!string.IsNullOrWhiteSpace(ServerConfig.PhotonVoiceAppId),
AppId = ServerConfig.PhotonVoiceAppId,
AppIdVoice = ServerConfig.PhotonVoiceAppId,
PhotonVoiceAppId = ServerConfig.PhotonVoiceAppId,
AppVersion = ServerConfig.PhotonAppVersion,
Region = ServerConfig.PhotonRegion,
UseCustomAuthentication = true,
AuthenticationParameter = "token"
});
}
[HttpGet("/photon/authenticate")]
public IActionResult AuthenticateGet()
{
var values = ReadQueryValues();
return Authenticate(values);
}
[HttpPost("/photon/authenticate")]
[RequestSizeLimit(64 * 1024)]
public async Task<IActionResult> AuthenticatePost()
{
var values = ReadQueryValues();
try
{
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync(HttpContext.RequestAborted);
foreach (var pair in form)
values[pair.Key] = pair.Value.FirstOrDefault() ?? string.Empty;
}
else if (Request.ContentLength.GetValueOrDefault() > 0)
{
using var reader = new StreamReader(
Request.Body,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
leaveOpen: true);
string body = await reader.ReadToEndAsync();
MergeBodyValues(values, body, Request.ContentType);
}
}
catch (Exception ex)
{
Console.WriteLine($"[PHOTON AUTH] Could not read request body: {ex.Message}");
return PhotonResult(3, "Invalid authentication request.");
}
return Authenticate(values);
}
private IActionResult Authenticate(Dictionary<string, string> values)
{
Response.Headers.CacheControl = "no-store";
string? configuredSecret = ServerConfig.PhotonCustomAuthSecret;
if (!string.IsNullOrWhiteSpace(configuredSecret))
{
string? suppliedSecret = GetFirst(values, "secret", "apiKey", "key");
if (!FixedTimeEquals(configuredSecret, suppliedSecret))
{
Console.WriteLine("[PHOTON AUTH] Rejected request with invalid dashboard secret.");
return PhotonResult(2, "Authentication failed.");
}
}
string? token = GetFirst(values, TokenKeys);
if (string.IsNullOrWhiteSpace(token))
return PhotonResult(3, "Missing Photon access token.");
long playerId;
long? roomId = null;
long? roomInstanceId = null;
string ticketDisplayName = string.Empty;
string authSource;
if (PhotonTicketService.TryValidate(
token,
out PhotonTicket ticket,
out string failureReason))
{
playerId = ticket.PlayerId;
roomId = ticket.RoomId;
roomInstanceId = ticket.RoomInstanceId;
ticketDisplayName = ticket.DisplayName;
authSource = "photon-ticket";
}
else
{
long? apiPlayerId = TryValidateApiAccessToken(token);
if (apiPlayerId == null)
{
Console.WriteLine(
$"[PHOTON AUTH] Rejected token " +
$"ticketReason={failureReason} " +
$"apiToken=invalid " +
$"fingerprint={PhotonTicketService.GetFingerprint(token)} " +
$"length={token.Length} " +
$"keys={string.Join(',', values.Keys)}");
return PhotonResult(2, "Photon access token is invalid or expired.");
}
playerId = apiPlayerId.Value;
authSource = "api-access-token";
}
string? requestedUserId = GetFirst(values, UserIdKeys);
if (!string.IsNullOrWhiteSpace(requestedUserId) &&
!string.Equals(
requestedUserId.Trim(),
playerId.ToString(),
StringComparison.Ordinal))
{
Console.WriteLine(
$"[PHOTON AUTH] User mismatch: requested={requestedUserId}, " +
$"validated={playerId}, source={authSource}");
return PhotonResult(2, "Photon user does not match the access token.");
}
var player = PlayerDB.Players.FindById(playerId);
if (player?.Player == null)
return PhotonResult(2, "Player account no longer exists.");
string nickname = !string.IsNullOrWhiteSpace(player.Player.DisplayName)
? player.Player.DisplayName!
: player.Player.Username ?? ticketDisplayName;
Console.WriteLine(
$"[PHOTON AUTH] player={playerId} " +
$"source={authSource} " +
$"room={roomId?.ToString() ?? "none"} " +
$"instance={roomInstanceId?.ToString() ?? "none"} accepted");
var response = new Dictionary<string, object?>
{
["ResultCode"] = 1,
["UserId"] = playerId.ToString(),
["Nickname"] = nickname,
["AuthCookie"] = new Dictionary<string, object?>
{
["PlayerId"] = playerId.ToString(),
["RoomId"] = roomId?.ToString() ?? string.Empty,
["RoomInstanceId"] =
roomInstanceId?.ToString() ?? string.Empty
}
};
return new JsonResult(response);
}
private long? TryValidateApiAccessToken(string token)
{
bool hadAuthorization = Request.Headers.ContainsKey("Authorization");
var originalAuthorization = Request.Headers["Authorization"];
try
{
Request.Headers["Authorization"] = $"Bearer {token.Trim()}";
return AuthStuff.GetPlayerId(Request);
}
catch (Exception ex)
{
Console.WriteLine($"[PHOTON AUTH] API token validation failed: {ex.Message}");
return null;
}
finally
{
if (hadAuthorization)
Request.Headers["Authorization"] = originalAuthorization;
else
Request.Headers.Remove("Authorization");
}
}
private IActionResult PhotonResult(int resultCode, string message)
{
return new JsonResult(new Dictionary<string, object?>
{
["ResultCode"] = resultCode,
["Message"] = message
});
}
private Dictionary<string, string> ReadQueryValues()
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in Request.Query)
result[pair.Key] = pair.Value.FirstOrDefault() ?? string.Empty;
return result;
}
private static void MergeBodyValues(
Dictionary<string, string> values,
string body,
string? contentType)
{
if (string.IsNullOrWhiteSpace(body))
return;
if (contentType?.Contains("json", StringComparison.OrdinalIgnoreCase) == true ||
body.TrimStart().StartsWith("{", StringComparison.Ordinal))
{
using JsonDocument document = JsonDocument.Parse(body);
if (document.RootElement.ValueKind != JsonValueKind.Object)
return;
foreach (JsonProperty property in document.RootElement.EnumerateObject())
{
values[property.Name] = property.Value.ValueKind == JsonValueKind.String
? property.Value.GetString() ?? string.Empty
: property.Value.ToString();
}
return;
}
foreach (string part in body.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
string[] pair = part.Split('=', 2);
string key = Uri.UnescapeDataString(pair[0].Replace('+', ' '));
string value = pair.Length > 1
? Uri.UnescapeDataString(pair[1].Replace('+', ' '))
: string.Empty;
if (!string.IsNullOrWhiteSpace(key))
values[key] = value;
}
}
private static string? GetFirst(
IReadOnlyDictionary<string, string> values,
params string[] keys)
{
foreach (string key in keys)
{
if (values.TryGetValue(key, out string? value) &&
!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return null;
}
private static bool FixedTimeEquals(string expected, string? supplied)
{
if (supplied == null)
return false;
byte[] expectedBytes = Encoding.UTF8.GetBytes(expected);
byte[] suppliedBytes = Encoding.UTF8.GetBytes(supplied);
return expectedBytes.Length == suppliedBytes.Length &&
CryptographicOperations.FixedTimeEquals(expectedBytes, suppliedBytes);
}
}
}