diff --git a/dotnet/Services/Identity/App/Application.cs b/dotnet/Services/Identity/App/Application.cs index 1b285669..5dab3500 100644 --- a/dotnet/Services/Identity/App/Application.cs +++ b/dotnet/Services/Identity/App/Application.cs @@ -1,3 +1,4 @@ +using Branch.Services.Identity.Libraries; using Branch.Services.Identity.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -10,16 +11,19 @@ public partial class Application private ILogger _logger { get; } private IRedisClientsManager _redisClientsManager { get; } private Config _config { get; } + private XblIdentityCache _xblIdentityCache { get; } public Application( ILoggerFactory loggerFactory, IRedisClientsManager redisClientsManager, - IOptionsMonitor options + IOptionsMonitor options, + XblIdentityCache xblIdentityCache ) { _logger = loggerFactory.CreateLogger(typeof(Application)); _redisClientsManager = redisClientsManager; _config = options.CurrentValue; + _xblIdentityCache = xblIdentityCache; } } } diff --git a/dotnet/Services/Identity/App/GetXblIdentity.cs b/dotnet/Services/Identity/App/GetXblIdentity.cs index bc44b293..a7da9c9a 100644 --- a/dotnet/Services/Identity/App/GetXblIdentity.cs +++ b/dotnet/Services/Identity/App/GetXblIdentity.cs @@ -9,15 +9,10 @@ public partial class Application public async Task GetXblIdentity(HttpContext ctx, GetXblIdentityReq req) { - var useCache = !req.IgnoreCache; + if (req.Type == "gamertag") + return await _xblIdentityCache.GetByGamertag(req.Value, req.IgnoreCache); - using (var client = _redisClientsManager.GetClient()) - { - return new GetXblIdentityRes - { - CacheInfo = null, - }; - } + return await _xblIdentityCache.GetByGamertag(req.Value, req.IgnoreCache); } } } diff --git a/dotnet/Services/Identity/Libraries/XblIdentityCache.cs b/dotnet/Services/Identity/Libraries/XblIdentityCache.cs new file mode 100644 index 00000000..bd61a55e --- /dev/null +++ b/dotnet/Services/Identity/Libraries/XblIdentityCache.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Branch.Global.Contracts; +using Branch.Global.Extensions; +using Branch.Global.Libraries; +using Branch.Global.Models.XboxLive; +using Branch.Services.Token; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using ServiceStack.Redis; + +namespace Branch.Services.Identity.Libraries +{ + public class XblIdentityCache + { + private ILogger _logger { get; } + private IRedisClientsManager _redisClientsManager { get; } + private TokenClient _tokenClient { get; } + private JsonClient _client { get; } + + private string _authHeader = "XBL3.0 x={0};{1}"; + private string _pathTemplate = "/users/{0}({1})/profile/settings"; + + public XblIdentityCache( + ILoggerFactory loggerFactory, + IRedisClientsManager redisClientsManager, + TokenClient tokenClient + ) + { + _logger = loggerFactory.CreateLogger(typeof(XblIdentityCache)); + _redisClientsManager = redisClientsManager; + _tokenClient = tokenClient; + _client = new JsonClient("https://profile.xboxlive.com"); + } + + public async Task GetByGamertag(string gamertag, bool useCache) + { + return await GetIdentity(LookupType.Gamertag, gamertag, useCache); + } + + public async Task GetByXuid(string xuid, bool useCache) + { + return await GetIdentity(LookupType.Xuid, xuid, useCache); + } + + private async Task GetIdentity(LookupType type, string value, bool useCache) + { + using (var client = _redisClientsManager.GetClient()) + { + if (useCache) + { + var ident = client.GetJson(GenerateRedisKey(type, value)); + if (ident != null) + return ident; + } + + var (gamertag, xuid) = await GetProfileSettings(type, value); + var identity = new GetXblIdentityRes + { + CacheInfo = new CacheInfo(DateTime.UtcNow, TimeSpan.FromMinutes(30)), + Gamertag = gamertag, + Xuid = xuid, + }; + + var jsonStr = JsonConvert.SerializeObject(identity); + var expiry = (DateTime) identity.CacheInfo.ExpiresAt; + + client.Set(GenerateRedisKey(LookupType.Gamertag, gamertag.ToSlug()), jsonStr, expiry); + client.Set(GenerateRedisKey(LookupType.Xuid, xuid), jsonStr, expiry); + + return identity; + } + } + + private async Task<(string gamertag, string xuid)> GetProfileSettings(LookupType type, string value) + { + var token = await _tokenClient.GetXblToken(null, new GetTokenRequest()); + var options = new HttpClientOptions(); + var query = new Dictionary(); + var path = string.Format(_pathTemplate, type.ToString().ToLowerInvariant(), value); + + options.Headers.Add("authorization", string.Format(_authHeader, token.Uhs, token.Token)); + options.Headers.Add("X-XBL-Contract-Version", "2"); + query.Add("settings", "gamertag"); + + var response = await _client.Do("GET", path, query, options); + var user = response.ProfileUsers[0]; + var xuid = user.ID.ToString(); + var gamertag = user.Settings.First(s => s.ID == "Gamertag").Value; + + return (gamertag, xuid); + } + + private string GenerateRedisKey(LookupType type, string value) + { + return $"{type.ToString().ToLowerInvariant()}-{value.Trim().ToSlug()}"; + } + + private enum LookupType + { + Gamertag, + Xuid + } + } +} diff --git a/dotnet/Services/Identity/Models/Config.cs b/dotnet/Services/Identity/Models/Config.cs index 220a57fc..5f1d4ee9 100644 --- a/dotnet/Services/Identity/Models/Config.cs +++ b/dotnet/Services/Identity/Models/Config.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using Branch.Global.Contracts; +using Branch.Global.Libraries; + namespace Branch.Services.Identity.Models { public class Config @@ -6,12 +10,19 @@ public class Config public string RedisConnectionString { get; set; } + public ServiceConfig TokenConfig { get; set; } + public static Config CreateDefault() { return new Config { - InternalKeys = new string[] {"test"}, + InternalKeys = new string[] { "test" }, RedisConnectionString = "redis://127.0.0.1:6379?db=1", + TokenConfig = new ServiceConfig + { + Url = "https://service-token.branch.golf", + Key = LocalSecrets.GetConfigValue("prod", "token", "InternalKeys[0]"), + } }; } } diff --git a/dotnet/Services/Identity/Startup.cs b/dotnet/Services/Identity/Startup.cs index aad85289..777fc78c 100644 --- a/dotnet/Services/Identity/Startup.cs +++ b/dotnet/Services/Identity/Startup.cs @@ -1,8 +1,11 @@ using System.Threading.Tasks; using Branch.Global.Attributes; +using Branch.Global.Contracts; using Branch.Services.Identity.App; +using Branch.Services.Identity.Libraries; using Branch.Services.Identity.Models; using Branch.Services.Identity.Server; +using Branch.Services.Token; using Crpc; using Crpc.Registration; using Microsoft.AspNetCore.Builder; @@ -27,8 +30,11 @@ public void ConfigureServices(IServiceCollection services) services.Configure(Configuration); var redisConnectionString = Configuration.GetSection("RedisConnectionString").Get(); + var tokenCfg = Configuration.GetSection("TokenConfig").Get(); services.AddSingleton(new BasicRedisClientManager(redisConnectionString)); + services.AddSingleton(new TokenClient(tokenCfg.Url, tokenCfg.Key)); + services.AddSingleton(); services.AddCrpc(opts => {