From b579e0bd1ef4487d093d2192e50d42cceca125a9 Mon Sep 17 00:00:00 2001 From: ImoutoChan Date: Tue, 3 Oct 2023 04:40:45 +0400 Subject: [PATCH] Rewrite new beta auth --- .../Loaders/Fixtures/SankakuLoaderFixture.cs | 6 - .../Loaders/SankakuLoaderTests.cs | 52 +++++- .../SankakuAuthManagerTests.cs | 138 ---------------- .../Sankaku/SankakuAuthManager.cs | 149 ++++++++++++++++-- .../Sankaku/SankakuSettings.cs | 6 - 5 files changed, 179 insertions(+), 172 deletions(-) delete mode 100644 Imouto.BooruParser.Tests/SankakuAuthManagerTests.cs diff --git a/Imouto.BooruParser.Tests/Loaders/Fixtures/SankakuLoaderFixture.cs b/Imouto.BooruParser.Tests/Loaders/Fixtures/SankakuLoaderFixture.cs index bf5cae0..5d59f0a 100644 --- a/Imouto.BooruParser.Tests/Loaders/Fixtures/SankakuLoaderFixture.cs +++ b/Imouto.BooruParser.Tests/Loaders/Fixtures/SankakuLoaderFixture.cs @@ -19,14 +19,8 @@ public class SankakuLoaderFixture private readonly IOptions _authorizedOptions = Options.Create( new SankakuSettings { - SaveTokensCallbackAsync = tokens => - { - Console.WriteLine($"new token: {tokens.AccessToken}, {tokens.RefreshToken}"); - return Task.CompletedTask; - }, PauseBetweenRequestsInMs = 1, Login = "testuser159", - //PassHash = "69f56a924a71774358c31e9233fc8e3c9a1b7d55", Password = "testuser159" }); diff --git a/Imouto.BooruParser.Tests/Loaders/SankakuLoaderTests.cs b/Imouto.BooruParser.Tests/Loaders/SankakuLoaderTests.cs index 74dfc08..2f3d8e9 100644 --- a/Imouto.BooruParser.Tests/Loaders/SankakuLoaderTests.cs +++ b/Imouto.BooruParser.Tests/Loaders/SankakuLoaderTests.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Imouto.BooruParser.Extensions; using Imouto.BooruParser.Implementations; +using Imouto.BooruParser.Implementations.Sankaku; using Imouto.BooruParser.Tests.Loaders.Fixtures; using Xunit; @@ -113,6 +114,43 @@ public async Task ShouldGetPostByMd5Async() post.FileSizeInBytes.Should().Be(617163); post.UgoiraFrameDelays.Should().BeEmpty(); } + + [Fact] + public async Task ShouldGetPostByMd5Async_2a00599e108817e0d9a4eb2e3f353abb() + { + var loader = _loaderFixture.GetLoaderWithAuth(); + + var post = await loader.GetPostByMd5Async("dc9da74597ecd47b2848fb4d68fce77a"); + + post.Should().NotBeNull(); + post!.OriginalUrl.Should().StartWith("https://v.sankakucomplex.com/data/dc/9d/dc9da74597ecd47b2848fb4d68fce77a.mp4"); + post.Id.Id.Should().Be(34486935); + post.Id.Md5Hash.Should().Be("dc9da74597ecd47b2848fb4d68fce77a"); + post.Notes.Should().BeEmpty(); + post.Tags.Should().HaveCount(75); + + foreach (var postTag in post.Tags) + { + postTag.Name.Should().NotBeNullOrWhiteSpace(); + postTag.Type.Should().NotBeNullOrWhiteSpace(); + postTag.Type.Should().BeOneOf(new[] { "meta", "general", "copyright", "character", "circle", "artist", "medium", "genre" }); + } + + post.Parent.Should().BeNull(); + post.Pools.Should().HaveCount(0); + post.Rating.Should().Be(Rating.Explicit); + post.RatingSafeLevel.Should().Be(RatingSafeLevel.None); + post.Source.Should().Be(""); + post.ChildrenIds.Should().BeEmpty(); + post.ExistState.Should().Be(ExistState.Exist); + post.FileResolution.Should().Be(new Size(1644, 1862)); + post.PostedAt.Should().Be(new DateTimeOffset(2023, 09, 29, 5, 54, 09, 0, TimeSpan.Zero)); + post.SampleUrl.Should().StartWith("https://v.sankakucomplex.com/data/dc/9d/dc9da74597ecd47b2848fb4d68fce77a.mp4"); + post.UploaderId.Id.Should().Be(568254); + post.UploaderId.Name.Should().Be("Just_some_guy"); + post.FileSizeInBytes.Should().Be(22152413); + post.UgoiraFrameDelays.Should().BeEmpty(); + } [Fact] public async Task ShouldContainLinkWithoutAmp() @@ -176,7 +214,7 @@ public GetTagHistoryFirstPageAsyncMethod(SankakuLoaderFixture loaderFixture) { } - [Fact(Skip = "only local invokation")] + [Fact] public void ShouldThrowWithoutCredentials() { var loader = _loaderFixture.GetLoaderWithoutAuth(); @@ -186,7 +224,7 @@ public void ShouldThrowWithoutCredentials() action.Should().ThrowAsync(); } - [Fact(Skip = "only local invokation")] + [Fact] public async Task ShouldReturnWithCredentials() { var loader = _loaderFixture.GetLoaderWithAuth(); @@ -204,7 +242,7 @@ public GetTagHistoryToDateTimeAsyncMethod(SankakuLoaderFixture loaderFixture) { } - [Fact(Skip = "only local invokation")] + [Fact] public void ShouldNotLoadTagsHistoryToDateWithoutCredentials() { var loader = _loaderFixture.GetLoaderWithoutAuth(); @@ -215,7 +253,7 @@ public void ShouldNotLoadTagsHistoryToDateWithoutCredentials() action.Should().ThrowAsync(); } - [Fact(Skip = "only local invokation")] + [Fact] public async Task ShouldLoadTagsHistoryToDateWithCredentials() { var loader = _loaderFixture.GetLoaderWithAuth(); @@ -232,7 +270,7 @@ public GetTagHistoryFromIdToPresentAsyncMethod(SankakuLoaderFixture loaderFixtur { } - [Fact(Skip = "only local invokation")] + [Fact] public async Task ShouldLoadTagsHistoryFromIdWithCredentials() { var loader = _loaderFixture.GetLoaderWithAuth(); @@ -244,7 +282,7 @@ public async Task ShouldLoadTagsHistoryFromIdWithCredentials() notesHistory.Should().NotBeEmpty(); } - [Fact(Skip = "only local invokation")] + [Fact] public async Task ShouldLoadTagsHistoryFromIdAndHaveAllDataWithCredentials() { var loader = _loaderFixture.GetLoaderWithAuth(); @@ -398,7 +436,7 @@ public FavoritePostAsyncMethod(SankakuLoaderFixture loaderFixture) { } - [Fact(Skip = "Auth is required")] + [Fact] public async Task ShouldFavoritePost() { var api = _loaderFixture.GetAccessorWithAuth(); diff --git a/Imouto.BooruParser.Tests/SankakuAuthManagerTests.cs b/Imouto.BooruParser.Tests/SankakuAuthManagerTests.cs deleted file mode 100644 index baa94a8..0000000 --- a/Imouto.BooruParser.Tests/SankakuAuthManagerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using FluentAssertions; -using Flurl.Http.Configuration; -using Flurl.Http.Testing; -using Imouto.BooruParser.Implementations.Sankaku; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Imouto.BooruParser.Tests; - -public class SankakuAuthManagerTests -{ - private const string ExpiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.E9bQ6QAil4HpH825QC5PtjNGEDQTtMpcj0SO2W8vmag"; - private const string NonExpiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjQ3NzE1OCwic3ViTHZsIjowLCJpc3MiOiJodHRwczovL2NhcGktdjIuc2Fua2FrdWNvbXBsZXguY29tIiwidHlwZSI6IkJlYXJlciIsImF1ZCI6ImNvbXBsZXgiLCJzY29wZSI6ImNvbXBsZXgiLCJpYXQiOjE2NjY3NTE0NDcsImV4cCI6MTk3NjkzNDI0N30.WvPanPsDYpteA2yy_tzluKKCTEe1CBd8CYXvbKOUWeA"; - - [Fact] - public async Task ShouldReturnRefreshedToken() - { - // arrange - var newAccessToken = Guid.NewGuid().ToString(); - var newRefreshToken = Guid.NewGuid().ToString(); - - using var httpTest = new HttpTest(); - httpTest.RespondWithJson(new { access_token = newAccessToken, refresh_token = newRefreshToken }); - - var cacheMock = new MemoryCache(new MemoryCacheOptions()); - - var simpleOptions = Options.Create( - new SankakuSettings() - { - AccessToken = ExpiredToken, - RefreshToken = "refresh-token" - }); - - var manager = new SankakuAuthManager( - cacheMock, - simpleOptions, - new PerBaseUrlFlurlClientFactory()); - - // act - var token = await manager.GetTokenAsync(); - - // assert - token.Should().Be(newAccessToken); - } - - [Fact] - public async Task ShouldReturnNonExpiredToken() - { - // arrange - var cacheMock = new MemoryCache(new MemoryCacheOptions()); - var simpleOptions = Options.Create( - new SankakuSettings - { - AccessToken = NonExpiredToken, - RefreshToken = "refresh-token" - }); - - var manager = new SankakuAuthManager( - cacheMock, - simpleOptions, - new PerBaseUrlFlurlClientFactory()); - - // act - var token = await manager.GetTokenAsync(); - - // assert - token.Should().Be(NonExpiredToken); - } - - [Fact] - public async Task ShouldReturnNonExpiredTokenFromMemoryCache() - { - const string refreshToken = "refresh-token"; - - // arrange - var cacheMock = new MemoryCache(new MemoryCacheOptions()); - cacheMock.Set("sankaku_complex_tokens", new Tokens(NonExpiredToken, refreshToken)); - - var simpleOptions = Options.Create( - new SankakuSettings() - { - AccessToken = "1", - RefreshToken = "2" - }); - - var manager = new SankakuAuthManager( - cacheMock, - simpleOptions, - new PerBaseUrlFlurlClientFactory()); - - // act - var token = await manager.GetTokenAsync(); - - // assert - token.Should().Be(NonExpiredToken); - } - - [Fact] - public async Task ShouldCallSaveTokensCallback() - { - // arrange - var newAccessToken = Guid.NewGuid().ToString(); - var newRefreshToken = Guid.NewGuid().ToString(); - var savedAccessToken = ""; - var savedRefreshToken = ""; - - using var httpTest = new HttpTest(); - httpTest.RespondWithJson(new { access_token = newAccessToken, refresh_token = newRefreshToken }); - - var cacheMock = new MemoryCache(new MemoryCacheOptions()); - - var simpleOptions = Options.Create( - new SankakuSettings() - { - AccessToken = ExpiredToken, - RefreshToken = "refresh-token", - SaveTokensCallbackAsync = tokens => - { - savedAccessToken = tokens.AccessToken; - savedRefreshToken = tokens.RefreshToken; - return Task.CompletedTask; - } - }); - - var manager = new SankakuAuthManager( - cacheMock, - simpleOptions, - new PerBaseUrlFlurlClientFactory()); - - // act - await manager.GetTokenAsync(); - - // assert - savedAccessToken.Should().Be(newAccessToken); - savedRefreshToken.Should().Be(newRefreshToken); - } -} diff --git a/Imouto.BooruParser/Implementations/Sankaku/SankakuAuthManager.cs b/Imouto.BooruParser/Implementations/Sankaku/SankakuAuthManager.cs index c748cc4..509fde1 100644 --- a/Imouto.BooruParser/Implementations/Sankaku/SankakuAuthManager.cs +++ b/Imouto.BooruParser/Implementations/Sankaku/SankakuAuthManager.cs @@ -1,4 +1,5 @@ using System.IdentityModel.Tokens.Jwt; +using System.Text.Json.Serialization; using Flurl; using Flurl.Http; using Flurl.Http.Configuration; @@ -31,9 +32,9 @@ public SankakuAuthManager( public async ValueTask GetTokenAsync() { - var tokens = GetTokens(); + var tokens = await GetTokensAsync(); - if (tokens.AccessToken is null) + if (tokens?.AccessToken is null) return null; if (!IsExpired(tokens.AccessToken)) @@ -43,7 +44,7 @@ public SankakuAuthManager( return null; var (accessToken, refreshToken) = await RefreshTokenAsync(tokens.RefreshToken); - await SaveTokensAsync(new Tokens(accessToken, refreshToken)); + _memoryCache.Set(Key, new Tokens(accessToken, refreshToken)); return accessToken; } @@ -104,22 +105,20 @@ private static bool IsExpired(string accessToken) return isExpired; } - private Tokens GetTokens() + private async ValueTask GetTokensAsync() { var cached = _memoryCache.Get(Key); - var access = cached != null ? cached.AccessToken : _options.Value.AccessToken; - var refresh = cached != null ? cached.RefreshToken : _options.Value.RefreshToken; + if (cached != null) + return new(cached.AccessToken!, cached.RefreshToken!); - return new(access, refresh); - } - - private async Task SaveTokensAsync(Tokens tokens) - { - _memoryCache.Set(Key, tokens); - var task = _options.Value.SaveTokensCallbackAsync?.Invoke(tokens); - if (task != null) - await task; + if (_options.Value.Login is null || _options.Value.Password is null) + return null; + + var newTokens = await SankakuFullLoginAsync(); + _memoryCache.Set(Key, newTokens); + + return newTokens; } private async Task<(string newAccessToken, string newRefreshToken)> RefreshTokenAsync(string refreshToken) @@ -132,4 +131,124 @@ private async Task SaveTokensAsync(Tokens tokens) return (response.AccessToken, response.RefreshToken); } + + private async Task SankakuFullLoginAsync() + { + var factory = _factory; + var login = _options.Value.Login; + var password = _options.Value.Password; + + var cookieJar = new CookieJar(); + + var loginClient = factory.Get("https://login.sankakucomplex.com") + .WithHeader("sec-ch-ua", "\"Chromium\";v=\"106\", \"Google Chrome\";v=\"106\", \"Not;A=Brand\";v=\"99\"") + .WithHeader("sec-ch-ua-mobile", "?0") + .WithHeader("sec-ch-ua-platform", "\"Windows\"") + .WithHeader("DNT", "1") + .WithHeader("Upgrade-Insecure-Requests", "1") + .WithHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36") + .WithHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + .WithHeader("Sec-Fetch-Site", "none") + .WithHeader("Sec-Fetch-Mode", "navigate") + .WithHeader("Sec-Fetch-User", "?1") + .WithHeader("Sec-Fetch-Dest", "document") + .WithHeader("Accept-Language", "en"); + + var capiClient = factory.Get("https://capi-v2.sankakucomplex.com") + .WithHeader("sec-ch-ua", "\"Chromium\";v=\"106\", \"Google Chrome\";v=\"106\", \"Not;A=Brand\";v=\"99\"") + .WithHeader("sec-ch-ua-mobile", "?0") + .WithHeader("sec-ch-ua-platform", "\"Windows\"") + .WithHeader("DNT", "1") + .WithHeader("Upgrade-Insecure-Requests", "1") + .WithHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36") + .WithHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + .WithHeader("Sec-Fetch-Site", "none") + .WithHeader("Sec-Fetch-Mode", "navigate") + .WithHeader("Sec-Fetch-User", "?1") + .WithHeader("Sec-Fetch-Dest", "document") + .WithHeader("Accept-Language", "en"); + + var responses = new List(); + + // https://login.sankakucomplex.com/oidc/auth?response_type=code&scope=openid&client_id=sankaku-web-app&redirect_uri=https://sankaku.app/sso/callback&state=return_uri=https://sankaku.app/&theme=black&route=login&lang=en + var result = await loginClient + .Request("oidc", "auth") + .WithCookies(cookieJar) + .SetQueryParam("response_type", "code") + .SetQueryParam("scope", "openid") + .SetQueryParam("client_id", "sankaku-web-app") + .SetQueryParam("redirect_uri", "https://sankaku.app/sso/callback") + .SetQueryParam("state", "return_uri=https://sankaku.app/") + .SetQueryParam("theme", "black") + .SetQueryParam("route", "login") + .SetQueryParam("lang", "en") + .OnRedirect(x => responses.Add(x.Response)) + .GetAsync(); + + // https://login.sankakucomplex.com/user/mfa-state + var result1 = await loginClient + .Request("user", "mfa-state") + .WithCookies(cookieJar) + .PostJsonAsync(new + { + login = login, + password = password, + browserInfo = "Chrome" + }); + + // https://login.sankakucomplex.com/auth/token + var result2 = await loginClient + .Request("auth", "token") + .WithCookies(cookieJar) + .PostJsonAsync(new + { + login = login, + password = password, + }) + .ReceiveJson(); + + + var interactionId = responses.First().Headers + .First(x => x.Name == "Location").Value.ToString()! + .Split('?')[0] + .Split('/').Last(); + + + var interactionResponses = new List(); + // https://login.sankakucomplex.com/oidc/interaction/<>/login + var result3 = await loginClient + .Request("oidc", "interaction", interactionId, "login") + .WithCookies(cookieJar) + .OnRedirect(x => interactionResponses.Add(x.Response)) + .PostUrlEncodedAsync(new + { + access_token = result2.AccessToken, + state = "lang=en&theme=black&return_uri=https://sankaku.app/", + }); + + var code = interactionResponses.Last().Headers + .First(x => x.Name == "Location").Value.ToString()! + .Split('?')[1] + .Split('&')[0] + .Split('=')[1]; + + // https://capi-v2.sankakucomplex.com/sso/finalize?lang=en + var result4 = await capiClient + .Request("sso", "finalize") + .WithCookies(cookieJar) + .SetQueryParam("lang", "en") + .PostJsonAsync(new + { + code = code, + client_id = "sankaku-web-app", + redirect_uri = "https://sankaku.app/sso/callback", + }) + .ReceiveJson(); + + return new(result4.AccessToken, result4.RefreshToken); + } + + private record SankakuTokenResponse( + [property: JsonPropertyName("access_token")] string AccessToken, + [property: JsonPropertyName("refresh_token")] string RefreshToken); } diff --git a/Imouto.BooruParser/Implementations/Sankaku/SankakuSettings.cs b/Imouto.BooruParser/Implementations/Sankaku/SankakuSettings.cs index 7b30714..ea57c5a 100644 --- a/Imouto.BooruParser/Implementations/Sankaku/SankakuSettings.cs +++ b/Imouto.BooruParser/Implementations/Sankaku/SankakuSettings.cs @@ -2,10 +2,6 @@ namespace Imouto.BooruParser.Implementations.Sankaku; public record SankakuSettings { - public string? AccessToken { get; set; } - - public string? RefreshToken { get; set; } - public string? Login { get; set; } public string? Password { get; set; } @@ -13,8 +9,6 @@ public record SankakuSettings public int PauseBetweenRequestsInMs { get; set; } = 1; public TimeSpan PauseBetweenRequests => TimeSpan.FromMilliseconds(PauseBetweenRequestsInMs); - - public Func SaveTokensCallbackAsync { get; set; } = _ => Task.CompletedTask; } public record Tokens(string? AccessToken, string? RefreshToken);