Skip to content
This repository has been archived by the owner on Apr 8, 2024. It is now read-only.

Commit

Permalink
Merge pull request #356 from aspriddell/add-service-specific-token-reqs
Browse files Browse the repository at this point in the history
Add service specific token injectors
  • Loading branch information
aspriddell authored Dec 23, 2022
2 parents 1553082 + fbec41b commit 78e562e
Show file tree
Hide file tree
Showing 16 changed files with 175 additions and 107 deletions.
2 changes: 2 additions & 0 deletions DragonFruit.Six.Api.Tests/Data/AccountLevelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class AccountLevelTests : Dragon6ApiTest
[TestCaseSource(nameof(Accounts))]
public async Task GetAccountLevel(UbisoftAccount account)
{
Assert.Inconclusive("Tests currently suspended until dragon6-tokens updated");

var level = await Client.GetAccountLevelAsync(account).ConfigureAwait(false);

if (level.Level == 0)
Expand Down
2 changes: 2 additions & 0 deletions DragonFruit.Six.Api/Accounts/Requests/AccountLevelRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
using DragonFruit.Data.Parameters;
using DragonFruit.Six.Api.Accounts.Entities;
using DragonFruit.Six.Api.Accounts.Enums;
using DragonFruit.Six.Api.Enums;
using JetBrains.Annotations;

namespace DragonFruit.Six.Api.Accounts.Requests
{
public class AccountLevelRequest : UbiApiRequest
{
public override string Path => Platform.CrossPlatform.SpaceUrl(1) + "/title/r6s/rewards/public_profile";
protected override UbisoftService? RequiredTokenSource => UbisoftService.RainbowSixClient;

public AccountLevelRequest(UbisoftAccount account)
{
Expand Down
70 changes: 70 additions & 0 deletions DragonFruit.Six.Api/Authentication/Entities/ClientTokenAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Dragon6 API Copyright DragonFruit Network <[email protected]>
// Licensed under Apache-2. Refer to the LICENSE file for more info

using System;
using System.Threading.Tasks;
using DragonFruit.Six.Api.Enums;
using DragonFruit.Six.Api.Exceptions;
using Nito.AsyncEx;

namespace DragonFruit.Six.Api.Authentication.Entities
{
public class ClientTokenAccessor
{
private readonly AsyncLock _accessSync;
private readonly UbisoftService _service;
private readonly Func<UbisoftService, string, Task<IUbisoftToken>> _fetchTokenDelegate;

private ClientTokenInjector _currentToken;

internal ClientTokenAccessor(UbisoftService service, Func<UbisoftService, string, Task<IUbisoftToken>> fetchTokenDelegate)
{
_accessSync = new AsyncLock();

_service = service;
_fetchTokenDelegate = fetchTokenDelegate;
}

/// <summary>
/// Gets a <see cref="ClientTokenInjector"/> containing a valid token that can be injected into http requests
/// </summary>
public async ValueTask<ClientTokenInjector> GetInjector()
{
if (_currentToken?.Expired == false)
{
return _currentToken;
}

using (await _accessSync.LockAsync().ConfigureAwait(false))
{
// check again in case of a backlog
if (_currentToken?.Expired == false)
{
return _currentToken;
}

for (int i = 0; i < 2; i++)
{
var token = await _fetchTokenDelegate.Invoke(_service, _currentToken?.Token.SessionId).ConfigureAwait(false);
_currentToken = new ClientTokenInjector(token);

if (!_currentToken.Expired)
{
return _currentToken;
}
}

throw new InvalidTokenException(_currentToken?.Token);
}
}

/// <summary>
/// Gets a valid <see cref="IUbisoftToken"/>
/// </summary>
public async ValueTask<IUbisoftToken> GetToken()
{
var injector = await GetInjector().ConfigureAwait(false);
return injector.Token;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ internal ClientTokenInjector(IUbisoftToken token)
_tokenExpiryOffset = new DateTime(Math.Max(Token.Expiry.Ticks - 3000000000, 0), DateTimeKind.Utc);
}

internal bool Expired => _tokenExpiryOffset < DateTime.UtcNow;
internal IUbisoftToken Token { get; }

public IUbisoftToken Token { get; }
internal bool Expired => _tokenExpiryOffset < DateTime.UtcNow;

/// <summary>
/// Injects Ubisoft authentication headers into the <see cref="ApiRequest"/> provided
Expand All @@ -32,6 +32,7 @@ public void Inject(ApiRequest request)

// modern api requests need both the session id and the expiration headers added
request.WithHeader("Expiration", Token.Expiry.ToString("O"));
request.WithHeader(UbisoftIdentifiers.UbiAppIdHeader, Token.AppId);
request.WithHeader(UbisoftIdentifiers.UbiSessionIdHeader, Token.SessionId);
}
}
Expand Down
7 changes: 7 additions & 0 deletions DragonFruit.Six.Api/Authentication/Entities/Dragon6Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ namespace DragonFruit.Six.Api.Authentication.Entities
/// </summary>
public class Dragon6Token : IUbisoftToken
{
/// <summary>
/// App-Id must be set client side.
/// </summary>
[JsonProperty("appId")]
public string AppId { get; set; }

[JsonProperty("token")]
public string Token { get; set; }

Expand All @@ -25,6 +31,7 @@ public class Dragon6Token : IUbisoftToken
/// </summary>
public static Dragon6Token From(UbisoftToken token) => new()
{
AppId = token.AppId,
Token = token.Token,
Expiry = token.Expiry,
SessionId = token.SessionId
Expand Down
2 changes: 2 additions & 0 deletions DragonFruit.Six.Api/Authentication/Entities/IUbisoftToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace DragonFruit.Six.Api.Authentication.Entities
/// </summary>
public interface IUbisoftToken
{
string AppId { get; }

string Token { get; }
string SessionId { get; }

Expand Down
6 changes: 6 additions & 0 deletions DragonFruit.Six.Api/Authentication/Entities/UbisoftToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ namespace DragonFruit.Six.Api.Authentication.Entities
/// </summary>
public class UbisoftToken : IUbisoftToken
{
/// <summary>
/// App-Id must be set client side.
/// </summary>
[JsonProperty("appId")]
public string AppId { get; set; }

[JsonProperty("expiration")]
public DateTime Expiry { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Dragon6 API Copyright DragonFruit Network <[email protected]>
// Licensed under Apache-2. Refer to the LICENSE file for more info

using System;
using System.Net.Http;
using System.Text;
using DragonFruit.Data;
using DragonFruit.Data.Extensions;
using DragonFruit.Six.Api.Enums;

namespace DragonFruit.Six.Api.Authentication.Requests
{
Expand All @@ -20,11 +20,10 @@ public sealed class UbisoftTokenRequest : ApiRequest
// tokens need an empty request body in UTF8, with app/json type...
protected override HttpContent BodyContent => new StringContent(string.Empty, Encoding.UTF8, "application/json");

internal UbisoftTokenRequest()
public UbisoftTokenRequest(UbisoftService service, string authentication)
{
this.WithHeader(UbisoftIdentifiers.UbiAppIdHeader, service.AppId());
this.WithAuthHeader($"Basic {authentication}");
}

public static UbisoftTokenRequest FromEncodedCredentials(string basicAuth) => new UbisoftTokenRequest().WithAuthHeader($"Basic {basicAuth}");
public static UbisoftTokenRequest FromUsername(string username, string password) => FromEncodedCredentials(Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")));
}
}
60 changes: 23 additions & 37 deletions DragonFruit.Six.Api/Authentication/UbisoftTokenExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Dragon6 API Copyright DragonFruit Network <[email protected]>
// Licensed under Apache-2. Refer to the LICENSE file for more info

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using DragonFruit.Data;
using DragonFruit.Six.Api.Authentication.Entities;
using DragonFruit.Six.Api.Authentication.Requests;
using DragonFruit.Six.Api.Enums;

namespace DragonFruit.Six.Api.Authentication
{
Expand All @@ -15,66 +18,49 @@ public static class UbisoftTokenExtensions
/// Gets a session token for the user credentials provided.
/// </summary>
/// <remarks>
/// You should store this in some form of persistant storage, as requesting these
/// You should store this in some form of persistent storage, as requesting these
/// too many times will result in a cooldown which is reset every time you try to access the resource
/// during said cooldown
/// </remarks>
/// <param name="client">The <see cref="Dragon6Client"/> to use</param>
/// <param name="loginString">The base64 encoded string in the format username:password</param>
/// <param name="service"><see cref="UbisoftService"/> to get the token for. If the <see cref="ApiClient{T}"/> is a <see cref="Dragon6Client"/>, this is optional</param>
/// <param name="token">Optional cancellation token</param>
public static UbisoftToken GetUbiToken(this ApiClient client, string loginString, CancellationToken token = default)
public static async Task<UbisoftToken> GetUbiTokenAsync(this ApiClient client, string loginString, UbisoftService? service = null, CancellationToken token = default)
{
return client.Perform<UbisoftToken>(UbisoftTokenRequest.FromEncodedCredentials(loginString), token);
}
if (client is Dragon6Client d6Client)
{
service ??= d6Client.DefaultService;
}

/// <summary>
/// Gets a session token for the user credentials provided.
/// </summary>
/// <remarks>
/// You should store this in some form of persistant storage, as requesting these
/// too many times will result in a cooldown which is reset every time you try to access the resource
/// during said cooldown
/// </remarks>
/// <param name="client">The <see cref="Dragon6Client"/> to use</param>
/// <param name="loginString">The base64 encoded string in the format username:password</param>
/// <param name="token">Optional cancellation token</param>
public static Task<UbisoftToken> GetUbiTokenAsync(this ApiClient client, string loginString, CancellationToken token = default)
{
return client.PerformAsync<UbisoftToken>(UbisoftTokenRequest.FromEncodedCredentials(loginString), token);
}
if (!service.HasValue)
{
throw new ArgumentException($"{nameof(service)} must be non-null when used with a client that does not inherit from {nameof(Dragon6Client)}");
}

/// <summary>
/// Gets a session token for the user credentials provided.
/// </summary>
/// <remarks>
/// You should store this in some form of persistant storage, as requesting these
/// too many times will result in a cooldown which is reset every time you try to access the resource
/// during said cooldown
/// </remarks>
/// <param name="client">The <see cref="Dragon6Client"/> to use</param>
/// <param name="username">The username to use</param>
/// <param name="password">The password to use</param>
/// <param name="token">Optional cancellation token</param>
public static UbisoftToken GetUbiToken(this ApiClient client, string username, string password, CancellationToken token = default)
{
return client.Perform<UbisoftToken>(UbisoftTokenRequest.FromUsername(username, password), token);
var ubisoftToken = await client.PerformAsync<UbisoftToken>(new UbisoftTokenRequest(service.Value, loginString), token).ConfigureAwait(false);
ubisoftToken.AppId = service.Value.AppId();

return ubisoftToken;
}

/// <summary>
/// Gets a session token for the user credentials provided.
/// </summary>
/// <remarks>
/// You should store this in some form of persistant storage, as requesting these
/// You should store this in some form of persistent storage, as requesting these
/// too many times will result in a cooldown which is reset every time you try to access the resource
/// during said cooldown
/// </remarks>
/// <param name="client">The <see cref="Dragon6Client"/> to use</param>
/// <param name="username">The username to use</param>
/// <param name="password">The password to use</param>
/// <param name="service"><see cref="UbisoftService"/> to get the token for. If the <see cref="ApiClient{T}"/> is a <see cref="Dragon6Client"/>, this is optional</param>
/// <param name="token">Optional cancellation token</param>
public static Task<UbisoftToken> GetUbiTokenAsync(this ApiClient client, string username, string password, CancellationToken token = default)
public static Task<UbisoftToken> GetUbiTokenAsync(this ApiClient client, string username, string password, UbisoftService? service = null, CancellationToken token = default)
{
return client.PerformAsync<UbisoftToken>(UbisoftTokenRequest.FromUsername(username, password), token);
var basicLogin = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
return GetUbiTokenAsync(client, basicLogin, service, token);
}
}
}
57 changes: 15 additions & 42 deletions DragonFruit.Six.Api/Dragon6Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under Apache-2. Refer to the LICENSE file for more info

using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Net;
using System.Net.Http;
Expand All @@ -12,18 +13,15 @@
using DragonFruit.Six.Api.Enums;
using DragonFruit.Six.Api.Exceptions;
using DragonFruit.Six.Api.Legacy.Utils;
using Nito.AsyncEx;

namespace DragonFruit.Six.Api
{
public abstract class Dragon6Client : ApiClient<ApiJsonSerializer>
{
private ClientTokenInjector _access;
private readonly AsyncLock _accessSync = new();
private readonly ConcurrentDictionary<UbisoftService, ClientTokenAccessor> _access = new();

protected Dragon6Client(string userAgent = null, UbisoftService app = UbisoftService.RainbowSixClient)
protected Dragon6Client(string userAgent = null)
{
SetUbiAppId(app);
UserAgent = userAgent ?? "Dragon6-API";
Serializer.Configure<ApiJsonSerializer>(o => o.Serializer.Culture = CultureInfo.InvariantCulture);
}
Expand All @@ -33,20 +31,20 @@ static Dragon6Client()
LegacyStatsMapping.InitialiseStatsBuckets();
}

/// <summary>
/// The default <see cref="UbisoftService"/> to use in requests. Some APIs may override this and require a specific service to be used.
/// </summary>
public UbisoftService DefaultService { get; set; } = UbisoftService.RainbowSix;

/// <summary>
/// Defines the procedure for retrieving a <see cref="UbisoftToken"/> for the client to use.
/// </summary>
/// <param name="service">The service to fetch a token for</param>
/// <param name="sessionId">The last recorded session id. This should be used to check if a new session should be created from the server</param>
/// <remarks>
/// It is recommended to store the token to a file and try to retrieve from there before resorting to the online systems, as accounts can be blocked due to rate-limits
/// </remarks>
protected abstract Task<IUbisoftToken> GetToken(string sessionId);

/// <summary>
/// Updates the Ubi-AppId header to be supplied to each request.
/// Defaults to <see cref="UbisoftService.RainbowSix"/>
/// </summary>
public void SetUbiAppId(UbisoftService service) => Headers[UbisoftIdentifiers.UbiAppIdHeader] = service.AppId();
protected abstract Task<IUbisoftToken> GetToken(UbisoftService service, string sessionId);

/// <summary>
/// Handles the response before trying to deserialize it.
Expand All @@ -63,7 +61,7 @@ protected override async Task<T> ValidateAndProcess<T>(HttpResponseMessage respo
throw new UbisoftErrorException(response.StatusCode, error);

case HttpStatusCode.Unauthorized:
throw new InvalidTokenException(_access.Token);
throw new InvalidTokenException(response.RequestMessage.Headers.Authorization.ToString());

case HttpStatusCode.BadRequest:
throw new ArgumentException("Request was poorly formed. Check the properties passed and try again");
Expand All @@ -76,34 +74,9 @@ protected override async Task<T> ValidateAndProcess<T>(HttpResponseMessage respo
}
}

protected internal async ValueTask<ClientTokenInjector> RequestToken()
{
if (_access?.Expired == false)
{
return _access;
}

using (await _accessSync.LockAsync().ConfigureAwait(false))
{
// check again in case of a backlog
if (_access?.Expired == false)
{
return _access;
}

for (int i = 0; i < 2; i++)
{
var token = await GetToken(_access?.Token.SessionId).ConfigureAwait(false);
_access = new ClientTokenInjector(token);

if (!_access.Expired)
{
return _access;
}
}

throw new InvalidTokenException(_access?.Token);
}
}
/// <summary>
/// Gets a <see cref="ClientTokenAccessor"/> for the requested <see cref="UbisoftService"/>
/// </summary>
protected internal ClientTokenAccessor GetServiceAccessToken(UbisoftService service) => _access.GetOrAdd(service, s => new ClientTokenAccessor(s, GetToken));
}
}
Loading

0 comments on commit 78e562e

Please sign in to comment.