completing

This commit is contained in:
Lightczx
2023-12-12 14:22:15 +08:00
parent 217586fece
commit f4547b60de
18 changed files with 177 additions and 161 deletions

View File

@@ -46,7 +46,7 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
}
/// <summary>
@@ -62,7 +62,7 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
client.DefaultRequestHeaders.Add("x-rpc-device_name", string.Empty);
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
@@ -81,7 +81,7 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.OSVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
client.DefaultRequestHeaders.Add("x-rpc-language", "zh-cn");
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
}
/// <summary>

View File

@@ -19,9 +19,4 @@ internal static class Random
{
return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyz".AsSpan(), length));
}
public static string GetLetterAndNumberString(int length)
{
return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".AsSpan(), length));
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core;
internal static class Uuid
{
public static Guid NewV5(string name, Guid namespaceId)
{
Span<byte> namespaceBuffer = stackalloc byte[16];
Verify.Operation(namespaceId.TryWriteBytes(namespaceBuffer), "Failed to copy namespace guid bytes");
Span<byte> nameBytes = Encoding.UTF8.GetBytes(name);
if (BitConverter.IsLittleEndian)
{
ReverseEndianness(namespaceBuffer);
}
Span<byte> data = stackalloc byte[namespaceBuffer.Length + nameBytes.Length];
namespaceBuffer.CopyTo(data);
nameBytes.CopyTo(data[namespaceBuffer.Length..]);
Span<byte> temp = stackalloc byte[20];
Verify.Operation(SHA1.TryHashData(data, temp, out _), "Failed to compute SHA1 hash of UUID");
Span<byte> hash = temp[..16];
if (BitConverter.IsLittleEndian)
{
ReverseEndianness(hash);
}
hash[8] &= 0x3F;
hash[8] |= 0x80;
int versionIndex = BitConverter.IsLittleEndian ? 7 : 6;
hash[versionIndex] &= 0x0F;
hash[versionIndex] |= 0x50;
return new(hash);
}
private static void ReverseEndianness(in Span<byte> guidByte)
{
ExchangeBytes(guidByte, 0, 3);
ExchangeBytes(guidByte, 1, 2);
ExchangeBytes(guidByte, 4, 5);
ExchangeBytes(guidByte, 6, 7);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ExchangeBytes(in Span<byte> guid, int left, int right)
{
(guid[right], guid[left]) = (guid[left], guid[right]);
}
}

View File

@@ -74,7 +74,7 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
SeedTime = $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
ExtFields = JsonSerializer.Serialize(extendProperties),
AppName = "bbs_cn",
BbsDeviceId = HoyolabOptions.DeviceId,
BbsDeviceId = HoyolabOptions.DeviceId36,
DeviceFp = string.IsNullOrEmpty(user.Fingerprint) ? Core.Random.GetLowerHexString(13) : user.Fingerprint,
};

View File

@@ -6,6 +6,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Factory.QrCode;
using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Response;
using System.Collections.Specialized;
@@ -18,7 +19,7 @@ namespace Snap.Hutao.View.Dialog;
internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
{
private readonly ITaskContext taskContext;
private readonly PassportClient2 passportClient2;
private readonly PandaClient pandaClient;
private readonly IQRCodeFactory qrCodeFactory;
private readonly CancellationTokenSource userManualCancellationTokenSource = new();
@@ -29,7 +30,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
passportClient2 = serviceProvider.GetRequiredService<PassportClient2>();
pandaClient = serviceProvider.GetRequiredService<PandaClient>();
qrCodeFactory = serviceProvider.GetRequiredService<IQRCodeFactory>();
}
@@ -100,7 +101,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
private async ValueTask<string> FetchQRCodeAndSetImageAsync(CancellationToken token)
{
Response<UrlWrapper> fetchResponse = await passportClient2.QRCodeFetchAsync(token).ConfigureAwait(false);
Response<UrlWrapper> fetchResponse = await pandaClient.QRCodeFetchAsync(token).ConfigureAwait(false);
if (!fetchResponse.IsOk())
{
return string.Empty;
@@ -136,7 +137,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
Response<GameLoginResult> query = await passportClient2.QRCodeQueryAsync(ticket, token).ConfigureAwait(false);
Response<GameLoginResult> query = await pandaClient.QRCodeQueryAsync(ticket, token).ConfigureAwait(false);
if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } })
{

View File

@@ -158,7 +158,7 @@
Width="{StaticResource LargeAppBarButtonWidth}"
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
Margin="2,-4"
Command="{Binding LoginByQrCodeCommand}"
Command="{Binding LoginByQRCodeCommand}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/>
<AppBarButton

View File

@@ -16,7 +16,6 @@ using Snap.Hutao.View.Dialog;
using Snap.Hutao.View.Page;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Account;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
using System.Text;
@@ -41,7 +40,6 @@ internal sealed partial class UserViewModel : ObservableObject
private readonly ISignInService signInService;
private readonly ITaskContext taskContext;
private readonly IUserService userService;
private readonly SessionAppClient sessionAppClient;
private User? selectedUser;
private ObservableCollection<User>? users;
@@ -177,26 +175,29 @@ internal sealed partial class UserViewModel : ObservableObject
}
}
[Command("LoginByQrCodeCommand")]
private async Task LoginByQrCode()
[Command("LoginByQRCodeCommand")]
private async Task LoginByQRCode()
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync<UserQRCodeDialog>().ConfigureAwait(false);
ValueResult<bool, UidGameToken> result = await dialog.GetUidGameTokenAsync().ConfigureAwait(false);
(bool isOk, UidGameToken? token) = await dialog.GetUidGameTokenAsync().ConfigureAwait(false);
if (result.TryGetValue(out UidGameToken account))
if (!isOk)
{
Response<LoginResult> gameTokenResp = await sessionAppClient.PostSTokenByGameTokenAsync(account).ConfigureAwait(false);
return;
}
if (gameTokenResp.IsOk())
{
Cookie stokenV2 = Cookie.FromLoginResult(gameTokenResp.Data);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false);
Response<LoginResult> sTokenResponse = await serviceProvider
.GetRequiredService<PassportClient2>()
.GetSTokenByGameTokenAsync(token)
.ConfigureAwait(false);
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
}
if (sTokenResponse.IsOk())
{
Cookie stokenV2 = Cookie.FromLoginResult(sTokenResponse.Data);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false);
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
}
}

View File

@@ -233,7 +233,7 @@ internal class MiHoYoJSBridge
// Skip x-rpc-lifecycle_id
{ "x-rpc-app_id", "bll8iq97cem8" },
{ "x-rpc-client_type", "5" },
{ "x-rpc-device_id", HoyolabOptions.DeviceId },
{ "x-rpc-device_id", HoyolabOptions.DeviceId36 },
{ "x-rpc-app_version", userAndUid.IsOversea ? SaltConstants.OSVersion : SaltConstants.CNVersion },
{ "x-rpc-sdk_version", "2.16.0" },
};

View File

@@ -1,13 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Passport;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
/// <summary>
/// 扫码登录结果请求配置
/// </summary>
[HighQuality]
internal sealed class GameLoginResultOptions
internal sealed class GameLoginRequest
{
[JsonPropertyName("app_id")]
public int AppId { get; set; }
@@ -16,11 +13,11 @@ internal sealed class GameLoginResultOptions
public string Device { get; set; } = default!;
[JsonPropertyName("ticket")]
public string Ticket { get; set; } = default!;
public string? Ticket { get; set; }
public static GameLoginResultOptions Create(int appId, string device, string ticket)
public static GameLoginRequest Create(int appId, string device, string? ticket = null)
{
return new GameLoginResultOptions
return new()
{
AppId = appId,
Device = device,

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode;
namespace Snap.Hutao.Web.Hoyolab.Passport;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
/// <summary>
/// 扫码登录结果

View File

@@ -1,11 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
/// <summary>
/// 扫码登录结果Payload
/// </summary>
[HighQuality]
internal sealed partial class GameLoginResultPayload
{

View File

@@ -0,0 +1,49 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc2)]
internal sealed partial class PandaClient
{
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly ILogger<PandaClient> logger;
private readonly HttpClient httpClient;
public async ValueTask<Response<UrlWrapper>> QRCodeFetchAsync(CancellationToken token = default)
{
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeFetch)
.PostJson(options);
Response<UrlWrapper>? resp = await builder
.TryCatchSendAsync<Response<UrlWrapper>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
public async ValueTask<Response<GameLoginResult>> QRCodeQueryAsync(string ticket, CancellationToken token = default)
{
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeQuery)
.PostJson(options);
Response<GameLoginResult>? resp = await builder
.TryCatchSendAsync<Response<GameLoginResult>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Passport;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
internal sealed class UrlWrapper
{

View File

@@ -1,16 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Options;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using System.Collections.Frozen;
using System.Security.Cryptography;
namespace Snap.Hutao.Web.Hoyolab;
/// <summary>
/// 米游社选项
/// </summary>
internal sealed class HoyolabOptions : IOptions<HoyolabOptions>
internal static class HoyolabOptions
{
/// <summary>
/// 米游社请求UA
@@ -35,12 +35,12 @@ internal sealed class HoyolabOptions : IOptions<HoyolabOptions>
/// <summary>
/// 米游社设备Id
/// </summary>
public static string DeviceId { get; } = Guid.NewGuid().ToString();
public static string DeviceId36 { get; } = Guid.NewGuid().ToString();
/// <summary>
/// 扫码登录设备Id
/// </summary>
public static string Device { get; } = Core.Random.GetLetterAndNumberString(64);
public static string DeviceId40 { get; } = GenerateDeviceId40();
/// <summary>
/// 盐
@@ -61,6 +61,16 @@ internal sealed class HoyolabOptions : IOptions<HoyolabOptions>
[SaltType.OSX6] = "okr4obncj8bw5a65hbnn5oo6ixjc3l9w",
}.ToFrozenDictionary();
/// <inheritdoc/>
public HoyolabOptions Value { get => this; }
[SuppressMessage("", "CA1308")]
private static string GenerateDeviceId40()
{
Guid uuid = Core.Uuid.NewV5(DeviceId36, new("9450ea74-be9c-35c0-9568-f97407856768"));
Span<byte> uuidSpan = stackalloc byte[16];
Span<byte> hash = stackalloc byte[20];
Verify.Operation(uuid.TryWriteBytes(uuidSpan), "Failed to write UUID bytes");
Verify.Operation(SHA1.TryHashData(uuidSpan, hash, out _), "Failed to write SHA1 hash");
return Convert.ToHexString(hash).ToLowerInvariant();
}
}

View File

@@ -1,12 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.Account;
namespace Snap.Hutao.Web.Hoyolab.Passport;
internal sealed class GameTokenWrapper
internal sealed class AccountIdGameToken
{
[JsonPropertyName("account_id")]
public int Stuid { get; set; } = default!;
public int AccountId { get; set; } = default!;
[JsonPropertyName("game_token")]
public string GameToken { get; set; } = default!;

View File

@@ -1,26 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 扫码登录请求配置
/// </summary>
[HighQuality]
internal sealed class GameLoginRequestOptions
{
[JsonPropertyName("app_id")]
public int AppId { get; set; }
[JsonPropertyName("device")]
public string Device { get; set; } = default!;
public static GameLoginRequestOptions Create(int appId, string device)
{
return new GameLoginRequestOptions
{
AppId = appId,
Device = device,
};
}
}

View File

@@ -5,9 +5,11 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
using System.Globalization;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Passport;
@@ -68,42 +70,20 @@ internal sealed partial class PassportClient2
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取扫码链接
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>二维码原始链接</returns>
public async ValueTask<Response<UrlWrapper>> QRCodeFetchAsync(CancellationToken token = default)
public async ValueTask<Response<LoginResult>> GetSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default)
{
GameLoginRequestOptions options = GameLoginRequestOptions.Create(4, HoyolabOptions.Device);
AccountIdGameToken data = new()
{
AccountId = int.Parse(account.Uid, CultureInfo.InvariantCulture),
GameToken = account.GameToken,
};
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeFetch)
.PostJson(options);
.SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken)
.PostJson(data);
Response<UrlWrapper>? resp = await builder
.TryCatchSendAsync<Response<UrlWrapper>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取扫码状态
/// </summary>
/// <param name="ticket">扫码链接中的ticket</param>
/// <param name="token">取消令牌</param>
/// <returns>扫码结果</returns>
public async ValueTask<Response<GameLoginResult>> QRCodeQueryAsync(string ticket, CancellationToken token = default)
{
GameLoginResultOptions options = GameLoginResultOptions.Create(4, HoyolabOptions.Device, ticket);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeQuery)
.PostJson(options);
Response<GameLoginResult>? resp = await builder
.TryCatchSendAsync<Response<GameLoginResult>>(httpClient, logger, token)
Response<LoginResult>? resp = await builder
.TryCatchSendAsync<Response<LoginResult>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

@@ -1,47 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
using System.Globalization;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Account;
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc2)]
internal sealed partial class SessionAppClient
{
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly ILogger<SessionAppClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 通过 GameToken 获取 SToken (V2)
/// </summary>
/// <param name="account">扫码获得的账户信息</param>
/// <param name="token">取消令牌</param>
/// <returns>登录结果</returns>
public async ValueTask<Response<LoginResult>> PostSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default)
{
GameTokenWrapper wrapper = new()
{
Stuid = int.Parse(account.Uid, CultureInfo.CurrentCulture),
GameToken = account.GameToken,
};
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken)
.PostJson(wrapper);
Response<LoginResult>? resp = await builder
.TryCatchSendAsync<Response<LoginResult>>(httpClient, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}