diff --git a/src/Empostor.Api.Innersloth.Generator/packages.lock.json b/src/Empostor.Api.Innersloth.Generator/packages.lock.json index fb68fad..5850ca3 100644 --- a/src/Empostor.Api.Innersloth.Generator/packages.lock.json +++ b/src/Empostor.Api.Innersloth.Generator/packages.lock.json @@ -150,6 +150,7 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } } - } + }, + ".NETStandard,Version=v2.0/win-x64": {} } } \ No newline at end of file diff --git a/src/Empostor.Api/Config/AuthApiConfig.cs b/src/Empostor.Api/Config/AuthApiConfig.cs index a02f7c1..bb0c0ee 100644 --- a/src/Empostor.Api/Config/AuthApiConfig.cs +++ b/src/Empostor.Api/Config/AuthApiConfig.cs @@ -5,6 +5,8 @@ public enum AuthApiMode Innersloth, Niko, Both, + Relay, + Ume, } public class AuthApiConfig @@ -16,4 +18,12 @@ public class AuthApiConfig public string NikoApiKey { get; set; } = ""; public string NikoApiBaseUrl { get; set; } = "https://au-verify.niko233.top"; + + public string RelayApiBaseUrl { get; set; } = "http://localhost:5100"; + + public string RelayApiKey { get; set; } = ""; + + public string UmeApiBaseUrl { get; set; } = "https://auverify.hayashiume.top"; + + public string UmeApiKey { get; set; } = "sk-empostor-globalapikey"; } diff --git a/src/Empostor.Api/packages.lock.json b/src/Empostor.Api/packages.lock.json index 9d991b4..a26a5dc 100644 --- a/src/Empostor.Api/packages.lock.json +++ b/src/Empostor.Api/packages.lock.json @@ -34,6 +34,12 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[8.0.27, )", + "resolved": "8.0.27", + "contentHash": "rQi9TxifHRnXP7lVRZH05DxD2/XGbJp12q0ozcbrlBlBnyyzssFTH/2vLhtKWUp2CT1qVscTrcYTFiwTyKPKRg==" + }, "StyleCop.Analyzers": { "type": "Direct", "requested": "[1.2.0-beta.556, )", @@ -139,6 +145,7 @@ "NextFast.Hazel.Abstractions": { "type": "Project" } - } + }, + "net8.0/win-x64": {} } } \ No newline at end of file diff --git a/src/Empostor.Server/Http/TokenController.cs b/src/Empostor.Server/Http/TokenController.cs index 63825d2..957eec9 100644 --- a/src/Empostor.Server/Http/TokenController.cs +++ b/src/Empostor.Server/Http/TokenController.cs @@ -155,12 +155,52 @@ public sealed class TokenController : ControllerBase string? friendCode = null; - if (mode == AuthApiMode.Niko || mode == AuthApiMode.Both) + if (mode == AuthApiMode.Relay) + { + friendCode = await FetchFromRelayAsync(eosToken, productUserId); + } + + if (string.IsNullOrEmpty(friendCode) && mode == AuthApiMode.Ume) + { + friendCode = await FetchFromUmeAsync(eosToken, productUserId); + } + + if (string.IsNullOrEmpty(friendCode) && mode == AuthApiMode.Niko) { friendCode = await FetchFromNikoAsync(eosToken, productUserId); } - if (string.IsNullOrEmpty(friendCode) && (mode == AuthApiMode.Innersloth || mode == AuthApiMode.Both)) + if (string.IsNullOrEmpty(friendCode) && mode == AuthApiMode.Both) + { + var nikoKeyIsCustom = !string.IsNullOrEmpty(_authApiConfig.NikoApiKey) + && _authApiConfig.NikoApiKey != "niko-request-api-key"; + + if (nikoKeyIsCustom) + { + _logger.LogDebug("[TokenController] Both mode: trying Niko first (custom key)"); + friendCode = await FetchFromNikoAsync(eosToken, productUserId); + if (string.IsNullOrEmpty(friendCode)) + { + friendCode = await FetchFromUmeAsync(eosToken, productUserId); + } + } + else + { + _logger.LogDebug("[TokenController] Both mode: trying Ume first (default Niko key)"); + friendCode = await FetchFromUmeAsync(eosToken, productUserId); + if (string.IsNullOrEmpty(friendCode)) + { + friendCode = await FetchFromNikoAsync(eosToken, productUserId); + } + } + + if (string.IsNullOrEmpty(friendCode)) + { + friendCode = await FetchFromInnerslothAsync(eosToken, productUserId); + } + } + + if (string.IsNullOrEmpty(friendCode) && mode == AuthApiMode.Innersloth) { friendCode = await FetchFromInnerslothAsync(eosToken, productUserId); } @@ -172,8 +212,10 @@ public sealed class TokenController : ControllerBase "[TokenController] FriendCode fetch failed for PUID={Puid}, using fallback: {FC}", productUserId, friendCode); } - - SaveFriendCodeToCache(productUserId, friendCode); + else + { + SaveFriendCodeToCache(productUserId, friendCode); + } return friendCode; } @@ -357,6 +399,15 @@ public sealed class TokenController : ControllerBase if (!string.IsNullOrEmpty(result.FriendCode) && (result.VerifyStatus == "HttpPending" || result.VerifyStatus == "Verified")) { + if (!string.IsNullOrEmpty(result.Puid) + && !string.Equals(result.Puid, productUserId, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning( + "[TokenController] Niko PUID mismatch: expected={Expected} got={Got}", + productUserId, result.Puid); + return null; + } + _nikoFriendCodeConfirmed = true; _logger.LogInformation( "[TokenController] FriendCode fetched from Niko: PUID={Puid} FC={FC} Status={Status}", @@ -430,6 +481,154 @@ public sealed class TokenController : ControllerBase } } + private async Task FetchFromRelayAsync(string eosToken, string productUserId) + { + if (string.IsNullOrEmpty(_authApiConfig.RelayApiBaseUrl) + || string.IsNullOrEmpty(_authApiConfig.RelayApiKey)) + { + _logger.LogWarning("[TokenController] RelayApi config incomplete, skipping relay"); + return null; + } + + try + { + using var client = _httpClientFactory.CreateClient("relay"); + var body = JsonSerializer.SerializeToUtf8Bytes(new RelayVerifyRequest + { + EosToken = eosToken, + ProductUserId = productUserId, + }); + + var req = new HttpRequestMessage(HttpMethod.Post, + $"{_authApiConfig.RelayApiBaseUrl.TrimEnd('/')}/api/verify") + { + Content = new ByteArrayContent(body), + }; + req.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _authApiConfig.RelayApiKey); + req.Content.Headers.TryAddWithoutValidation("Content-Type", "application/json"); + + var resp = await client.SendAsync(req); + if (!resp.IsSuccessStatusCode) + { + _logger.LogWarning( + "[TokenController] Relay returned {Status} for PUID={Puid}", + resp.StatusCode, productUserId); + return null; + } + + var json = await resp.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + if (result == null || string.IsNullOrEmpty(result.FriendCode)) + { + _logger.LogWarning( + "[TokenController] Relay response missing FriendCode for PUID={Puid}", + productUserId); + return null; + } + + if (string.IsNullOrEmpty(result.ProductUserId) + || !string.Equals(result.ProductUserId, productUserId, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning( + "[TokenController] Relay PUID mismatch: expected={Expected} got={Got}", + productUserId, result.ProductUserId); + return null; + } + + _logger.LogInformation( + "[TokenController] FriendCode from Relay: PUID={Puid} FC={FC}", + productUserId, result.FriendCode); + return result.FriendCode; + } + catch (Exception ex) + { + _logger.LogError(ex, "[TokenController] Exception calling Relay for PUID={Puid}", productUserId); + } + + return null; + } + + private async Task FetchFromUmeAsync(string eosToken, string productUserId) + { + if (string.IsNullOrEmpty(_authApiConfig.UmeApiBaseUrl) + || string.IsNullOrEmpty(_authApiConfig.UmeApiKey)) + { + _logger.LogWarning("[TokenController] UmeApi config incomplete, skipping Ume"); + return null; + } + + try + { + using var client = _httpClientFactory.CreateClient("ume"); + var body = JsonSerializer.SerializeToUtf8Bytes(new UmeVerifyRequest + { + ApiKey = _authApiConfig.UmeApiKey, + EosToken = eosToken, + }); + + var req = new HttpRequestMessage(HttpMethod.Post, + $"{_authApiConfig.UmeApiBaseUrl.TrimEnd('/')}/api/verify") + { + Content = new ByteArrayContent(body), + }; + req.Content.Headers.TryAddWithoutValidation("Content-Type", "application/json"); + + var resp = await client.SendAsync(req); + if (!resp.IsSuccessStatusCode) + { + _logger.LogWarning( + "[TokenController] Ume returned {Status} for PUID={Puid}", + resp.StatusCode, productUserId); + return null; + } + + var json = await resp.Content.ReadAsStringAsync(); + _logger.LogInformation("[TokenController] Ume raw response: {Json}", json); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + var status = root.TryGetProperty("VerifyStatus", out var s) ? s.GetString() : null; + if (status != "Verified") + { + _logger.LogWarning( + "[TokenController] Ume VerifyStatus={Status} for PUID={Puid}", + status, productUserId); + return null; + } + + var friendCode = root.TryGetProperty("FriendCode", out var fc) ? fc.GetString() : null; + if (string.IsNullOrEmpty(friendCode)) + { + _logger.LogWarning( + "[TokenController] Ume response missing FriendCode for PUID={Puid}", + productUserId); + return null; + } + + var returnedPuid = root.TryGetProperty("ProductUserId", out var rp) ? rp.GetString() : null; + if (string.IsNullOrEmpty(returnedPuid) + || !string.Equals(returnedPuid, productUserId, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning( + "[TokenController] Ume PUID mismatch: expected={Expected} got={Got}", + productUserId, returnedPuid); + return null; + } + + _logger.LogInformation( + "[TokenController] FriendCode from Ume: PUID={Puid} FC={FC}", + productUserId, friendCode); + return friendCode; + } + catch (Exception ex) + { + _logger.LogError(ex, "[TokenController] Exception calling Ume for PUID={Puid}", productUserId); + } + + return null; + } + private static string GenerateMatchmakerToken(string productUserId) { var salt = RandomNumberGenerator.GetBytes(16); @@ -523,5 +722,32 @@ public sealed class TokenController : ControllerBase [JsonPropertyName("verifycode")] public required string VerifyCode { get; init; } } + + private sealed class RelayVerifyRequest + { + [JsonPropertyName("EosToken")] + public required string EosToken { get; init; } + + [JsonPropertyName("ProductUserId")] + public required string ProductUserId { get; init; } + } + + private sealed class RelayVerifyResult + { + [JsonPropertyName("FriendCode")] + public string? FriendCode { get; init; } + + [JsonPropertyName("ProductUserId")] + public string? ProductUserId { get; init; } + } + + private sealed class UmeVerifyRequest + { + [JsonPropertyName("ApiKey")] + public required string ApiKey { get; init; } + + [JsonPropertyName("EosToken")] + public required string EosToken { get; init; } + } #endregion } diff --git a/src/Empostor.Server/Net/MatchmakerService.cs b/src/Empostor.Server/Net/MatchmakerService.cs index e4d7f78..b9c4cc8 100644 --- a/src/Empostor.Server/Net/MatchmakerService.cs +++ b/src/Empostor.Server/Net/MatchmakerService.cs @@ -47,12 +47,12 @@ namespace Empostor.Server.Net // NOTE: If this warning annoys you, set your PublicIp to "localhost" _logger.LogError("Your PublicIp is set to the default value of 127.0.0.1."); _logger.LogError("To allow people on other devices to connect to your server, change this value to your Public IP address"); - _logger.LogError("For more info on how to do this see https://github.com/Empostor/Empostor/blob/master/docs/Server-configuration.md"); + _logger.LogError("For more info on how to do this see https://empostor.github.io/Server-configuration"); } else if (_httpServerConfig.ListenIp == "0.0.0.0" && runningOutsideContainer) { _logger.LogWarning("Since Among Us 16.0.5 it is required to support HTTPS for players to connect, we recommend setting up a reverse proxy:"); - _logger.LogWarning("See https://github.com/Empostor/Empostor/blob/master/docs/Http-server.md for instructions"); + _logger.LogWarning("See https://empostor.github.io/Http-server for instructions"); } } diff --git a/src/Empostor.Server/Program.cs b/src/Empostor.Server/Program.cs index 8b43106..0410b6d 100644 --- a/src/Empostor.Server/Program.cs +++ b/src/Empostor.Server/Program.cs @@ -128,6 +128,14 @@ namespace Empostor.Server "UnityPlayer/2022.3.44f1 (UnityWebRequest/1.0, libcurl/7.84.0-DEV)"); client.DefaultRequestHeaders.Add("X-Unity-Version", "2022.3.44f1"); }); + services.AddHttpClient("relay", client => + { + client.Timeout = TimeSpan.FromSeconds(15); + }); + services.AddHttpClient("ume", client => + { + client.Timeout = TimeSpan.FromSeconds(15); + }); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Empostor.Server/config.json b/src/Empostor.Server/config.json index 9b186de..98a3929 100644 --- a/src/Empostor.Server/config.json +++ b/src/Empostor.Server/config.json @@ -39,7 +39,11 @@ "AuthApi": { "Mode": "Both", "NikoApiKey": "niko-request-api-key", - "NikoApiBaseUrl": "https://au-verify.niko233.top" + "NikoApiBaseUrl": "https://au-verify.niko233.top", + "RelayApiBaseUrl": "http://localhost:5100", + "RelayApiKey": "empostor-relay-api-key-change-me", + "UmeApiBaseUrl": "https://auverify.hayashiume.top", + "UmeApiKey": "sk-empostor-globalapikey" }, "Admin": { "Password": "CHANGE-ME", diff --git a/src/Empostor.Server/packages.lock.json b/src/Empostor.Server/packages.lock.json index 9d36848..cc34b07 100644 --- a/src/Empostor.Server/packages.lock.json +++ b/src/Empostor.Server/packages.lock.json @@ -58,6 +58,12 @@ "resolved": "8.0.1", "contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA==" }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[8.0.27, )", + "resolved": "8.0.27", + "contentHash": "rQi9TxifHRnXP7lVRZH05DxD2/XGbJp12q0ozcbrlBlBnyyzssFTH/2vLhtKWUp2CT1qVscTrcYTFiwTyKPKRg==" + }, "Serilog.Extensions.Hosting": { "type": "Direct", "requested": "[8.0.0, )",