Fixed nexus logins

This commit is contained in:
Timothy Baldridge 2022-05-15 16:38:22 -06:00
parent cb7baa5382
commit 2ea4eda9b4
13 changed files with 236 additions and 129 deletions

View File

@ -1,7 +1,10 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
@ -9,6 +12,7 @@ using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.UserIntervention;
namespace Wabbajack.LoginManagers;
@ -17,6 +21,7 @@ public class NexusLoginManager : ViewModel, INeedsLogin
private readonly ILogger<NexusLoginManager> _logger;
private readonly ITokenProvider<NexusApiState> _token;
private readonly IUserInterventionHandler _handler;
private readonly IServiceProvider _serviceProvider;
public string SiteName { get; } = "Nexus Mods";
public ICommand TriggerLogin { get; set; }
@ -27,10 +32,11 @@ public class NexusLoginManager : ViewModel, INeedsLogin
[Reactive]
public bool HaveLogin { get; set; }
public NexusLoginManager(ILogger<NexusLoginManager> logger, ITokenProvider<NexusApiState> token)
public NexusLoginManager(ILogger<NexusLoginManager> logger, ITokenProvider<NexusApiState> token, IServiceProvider serviceProvider)
{
_logger = logger;
_token = token;
_serviceProvider = serviceProvider;
RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () =>
@ -45,10 +51,13 @@ public class NexusLoginManager : ViewModel, INeedsLogin
TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Logging into {SiteName}", SiteName);
await NexusLogin.Send();
RefreshTokenState();
_logger.LogInformation("Logging into {SiteName}", SiteName);
MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService<NexusLoginHandler>()));
}, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v));
MessageBus.Current.Listen<CloseBrowserTab>()
.Subscribe(x => RefreshTokenState())
.DisposeWith(CompositeDisposable);
}
private void RefreshTokenState()

View File

@ -0,0 +1,11 @@
namespace Wabbajack.Messages;
public class CloseBrowserTab
{
public BrowserTabViewModel ViewModel { get; init; }
public CloseBrowserTab(BrowserTabViewModel viewModel)
{
ViewModel = viewModel;
}
}

View File

@ -1,16 +1,17 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs.Logins;
using Wabbajack.Models;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention;
public class LoversLabLoginHandler : OAuth2LoginHandler<Messages.LoversLabLogin, DTOs.Logins.LoversLabLoginState>
{
public LoversLabLoginHandler(ILogger<LoversLabLoginHandler> logger, HttpClient client, ITokenProvider<DTOs.Logins.LoversLabLoginState> tokenProvider,
WebBrowserVM browser, CefService service)
: base(logger, client, tokenProvider, browser, service)
public LoversLabLoginHandler(ILogger<LoversLabLoginHandler> logger, HttpClient httpClient, EncryptedJsonTokenProvider<LoversLabLoginState> tokenProvider)
: base(logger, httpClient, tokenProvider)
{
}
}

View File

@ -1,104 +1,98 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Fizzler.Systems.HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Models;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention;
public class NexusLoginHandler : WebUserInterventionBase<NexusLogin>
public class NexusLoginHandler : BrowserTabViewModel
{
private readonly ITokenProvider<NexusApiState> _provider;
private readonly EncryptedJsonTokenProvider<NexusApiState> _tokenProvider;
public NexusLoginHandler(ILogger<NexusLoginHandler> logger, WebBrowserVM browserVM, ITokenProvider<NexusApiState> provider, CefService service)
: base(logger, browserVM, service)
public NexusLoginHandler(EncryptedJsonTokenProvider<NexusApiState> tokenProvider)
{
_provider = provider;
HeaderText = "Nexus Login";
_tokenProvider = tokenProvider;
}
public override async Task Begin()
protected override async Task Run(CancellationToken token)
{
try
{
Messages.NavigateTo.Send(Browser);
UpdateStatus("Please log into the Nexus");
await Driver.WaitForInitialized();
await NavigateTo(new Uri("https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com"));
token.ThrowIfCancellationRequested();
Cookie[] cookies = {};
while (true)
Instructions = "Please log into the Nexus";
await NavigateTo(new Uri(
"https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com"));
Cookie[] cookies = { };
while (true)
{
cookies = await GetCookies("nexusmods.com", token);
if (cookies.Any(c => c.Name == "member_id"))
break;
token.ThrowIfCancellationRequested();
await Task.Delay(500, token);
}
Instructions = "Getting API Key...";
await NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
var key = "";
while (true)
{
try
{
cookies = await Driver.GetCookies("nexusmods.com");
if (cookies.Any(c => c.Name == "member_id"))
break;
Message.Token.ThrowIfCancellationRequested();
await Task.Delay(500, Message.Token);
key = (await GetDom(token))
.DocumentNode
.QuerySelectorAll("input[value=wabbajack]")
.SelectMany(p => p.ParentNode.ParentNode.QuerySelectorAll("textarea.application-key"))
.Select(node => node.InnerHtml)
.FirstOrDefault() ?? "";
}
catch (Exception)
{
// ignored
}
if (!string.IsNullOrEmpty(key))
break;
await NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
UpdateStatus("Looking for API Key");
var key = "";
while (true)
try
{
try
{
key = await Driver.EvaluateJavaScript(
"document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"textarea.application-key\").innerHTML");
}
catch (Exception)
{
// ignored
}
if (!string.IsNullOrEmpty(key))
{
break;
}
try
{
await Driver.EvaluateJavaScript(
"var found = document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"form button[type=submit]\");" +
"found.onclick= function() {return true;};" +
"found.class = \" \"; " +
"found.click();" +
"found.remove(); found = undefined;"
);
UpdateStatus("Generating API Key, Please Wait...");
}
catch (Exception)
{
// ignored
}
Message.Token.ThrowIfCancellationRequested();
await Task.Delay(500, Message.Token);
await EvaluateJavaScript(
"var found = document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"form button[type=submit]\");" +
"found.onclick= function() {return true;};" +
"found.class = \" \"; " +
"found.click();" +
"found.remove(); found = undefined;"
);
Instructions = "Generating API Key, Please Wait...";
}
catch (Exception)
{
// ignored
}
await _provider.SetToken(new NexusApiState()
{
ApiKey = key,
Cookies = cookies
});
((NexusLogin)Message).CompletionSource.SetResult();
Messages.NavigateTo.Send(PrevPane);
token.ThrowIfCancellationRequested();
await Task.Delay(500, token);
}
catch (Exception ex)
Instructions = "Success, saving information...";
await _tokenProvider.SetToken(new NexusApiState
{
Logger.LogError(ex, "While logging into Nexus Mods");
Message.SetException(ex);
Messages.NavigateTo.Send(PrevPane);
}
Cookies = cookies,
ApiKey = key
});
}
}

View File

@ -22,45 +22,57 @@ public abstract class OAuth2LoginHandler<TIntervention, TLoginType> : WebUserInt
where TLoginType : OAuth2LoginState, new()
{
private readonly HttpClient _httpClient;
private readonly ITokenProvider<TLoginType> _tokenProvider;
private readonly EncryptedJsonTokenProvider<TLoginType> _tokenProvider;
private readonly ILogger _logger;
public OAuth2LoginHandler(ILogger logger, HttpClient httpClient,
ITokenProvider<TLoginType> tokenProvider, WebBrowserVM browserVM, CefService service) : base(logger, browserVM, service)
EncryptedJsonTokenProvider<TLoginType> tokenProvider)
{
var tlogin = new TLoginType();
HeaderText = $"{tlogin.SiteName} Login";
_logger = logger;
_httpClient = httpClient;
_tokenProvider = tokenProvider;
}
public override async Task Begin()
protected override async Task Run(CancellationToken token)
{
Messages.NavigateTo.Send(Browser);
var tlogin = new TLoginType();
await Driver.WaitForInitialized();
using var handler = Driver.WithSchemeHandler(uri => uri.Scheme == "wabbajack");
UpdateStatus($"Please log in and allow Wabbajack to access your {tlogin.SiteName} account");
var tcs = new TaskCompletionSource<Uri>();
await WaitForReady();
Browser!.Browser.CoreWebView2.Settings.UserAgent = "Wabbajack";
Browser!.Browser.NavigationStarting += (sender, args) =>
{
var uri = new Uri(args.Uri);
if (uri.Scheme == "wabbajack")
{
tcs.TrySetResult(uri);
}
};
Instructions = $"Please log in and allow Wabbajack to access your {tlogin.SiteName} account";
var scopes = string.Join(" ", tlogin.Scopes);
var state = Guid.NewGuid().ToString();
await NavigateTo(new Uri(tlogin.AuthorizationEndpoint + $"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}"));
await NavigateTo(new Uri(tlogin.AuthorizationEndpoint +
$"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}"));
var uri = await handler.Task.WaitAsync(Message.Token);
var uri = await tcs.Task.WaitAsync(token);
var cookies = await Driver.GetCookies(tlogin.AuthorizationEndpoint.Host);
var cookies = await GetCookies(tlogin.AuthorizationEndpoint.Host, token);
var parsed = HttpUtility.ParseQueryString(uri.Query);
if (parsed.Get("state") != state)
{
Logger.LogCritical("Bad OAuth state, this shouldn't happen");
_logger.LogCritical("Bad OAuth state, this shouldn't happen");
throw new Exception("Bad OAuth State");
}
if (parsed.Get("code") == null)
{
Logger.LogCritical("Bad code result from OAuth");
_logger.LogCritical("Bad code result from OAuth");
throw new Exception("Bad code result from OAuth");
}
@ -81,8 +93,8 @@ public abstract class OAuth2LoginHandler<TIntervention, TLoginType> : WebUserInt
msg.Headers.Add("Cookie", string.Join(";", cookies.Select(c => $"{c.Name}={c.Value}")));
msg.Content = new FormUrlEncodedContent(formData.ToList());
using var response = await _httpClient.SendAsync(msg, Message.Token);
var data = await response.Content.ReadFromJsonAsync<OAuthResultState>(cancellationToken: Message.Token);
using var response = await _httpClient.SendAsync(msg, token);
var data = await response.Content.ReadFromJsonAsync<OAuthResultState>(cancellationToken: token);
await _tokenProvider.SetToken(new TLoginType
{
@ -90,6 +102,5 @@ public abstract class OAuth2LoginHandler<TIntervention, TLoginType> : WebUserInt
ResultState = data!
});
Messages.NavigateTo.Send(PrevPane);
}
}

View File

@ -1,15 +1,16 @@
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs.Logins;
using Wabbajack.Models;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention;
public class VectorPlexusLoginHandler : OAuth2LoginHandler<Messages.VectorPlexusLogin, DTOs.Logins.VectorPlexusLoginState>
{
public VectorPlexusLoginHandler(ILogger<VectorPlexusLoginHandler> logger, HttpClient client, ITokenProvider<DTOs.Logins.VectorPlexusLoginState> tokenProvider,
WebBrowserVM browser, CefService service)
: base(logger, client, tokenProvider, browser, service)
public VectorPlexusLoginHandler(ILogger<VectorPlexusLoginHandler> logger, HttpClient httpClient, EncryptedJsonTokenProvider<VectorPlexusLoginState> tokenProvider)
: base(logger, httpClient, tokenProvider)
{
}
}

View File

@ -1,46 +1,99 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using ReactiveUI.Fody.Helpers;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Interventions;
using Wabbajack.Models;
using Wabbajack.Views;
using Wabbajack.WebAutomation;
namespace Wabbajack.UserIntervention;
public abstract class WebUserInterventionBase<T>
public abstract class WebUserInterventionBase<T> : ViewModel
where T : IUserIntervention
{
protected readonly WebBrowserVM Browser;
protected readonly ILogger Logger;
protected T Message;
protected ViewModel PrevPane;
protected IWebDriver Driver;
[Reactive]
public string HeaderText { get; set; }
[Reactive]
public string Instructions { get; set; }
protected WebUserInterventionBase(ILogger logger, WebBrowserVM browser, CefService service)
public BrowserView? Browser { get; set; }
private WebView2 _browser => Browser!.Browser;
public async Task RunWrapper(CancellationToken token)
{
Logger = logger;
Browser = browser;
//Driver = new CefSharpWrapper(logger, browser.Browser, service);
await Run(token);
}
public void Configure(ViewModel prevPane, T message)
protected abstract Task Run(CancellationToken token);
protected async Task WaitForReady()
{
Message = message;
PrevPane = prevPane;
while (Browser?.Browser.CoreWebView2 == null)
{
await Task.Delay(250);
}
}
protected void UpdateStatus(string status)
public async Task NavigateTo(Uri uri)
{
Browser.Instructions = status;
var tcs = new TaskCompletionSource();
void Completed(object? o, CoreWebView2NavigationCompletedEventArgs a)
{
if (a.IsSuccess)
{
tcs.TrySetResult();
}
else
{
tcs.TrySetException(new Exception($"Navigation error to {uri}"));
}
}
_browser.NavigationCompleted += Completed;
_browser.Source = uri;
await tcs.Task;
_browser.NavigationCompleted -= Completed;
}
public async Task<Cookie[]> GetCookies(string domainEnding, CancellationToken token)
{
var cookies = (await _browser.CoreWebView2.CookieManager.GetCookiesAsync(""))
.Where(c => c.Domain.EndsWith(domainEnding));
return cookies.Select(c => new Cookie
{
Domain = c.Domain,
Name = c.Name,
Path = c.Path,
Value = c.Value
}).ToArray();
}
protected async Task NavigateTo(Uri uri)
public async Task<string> EvaluateJavaScript(string js)
{
await Driver.NavigateTo(uri, Message.Token);
return await _browser.ExecuteScriptAsync(js);
}
public abstract Task Begin();
public async Task<HtmlDocument> GetDom(CancellationToken token)
{
var v = HttpUtility.UrlDecode("\u003D");
var source = await EvaluateJavaScript("document.body.outerHTML");
var decoded = JsonSerializer.Deserialize<string>(source);
var doc = new HtmlDocument();
doc.LoadHtml(decoded);
return doc;
}
}

View File

@ -7,8 +7,10 @@ using System.Web;
using HtmlAgilityPack;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Views;
namespace Wabbajack;
@ -26,6 +28,7 @@ public abstract class BrowserTabViewModel : ViewModel
public async Task RunWrapper(CancellationToken token)
{
await Run(token);
MessageBus.Current.SendMessage(new CloseBrowserTab(this));
}
protected abstract Task Run(CancellationToken token);

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
@ -164,22 +165,20 @@ namespace Wabbajack
private void HandleLogin(NexusLogin nexusLogin)
{
var handler = _serviceProvider.GetRequiredService<NexusLoginHandler>();
handler.Configure(ActivePane, nexusLogin);
handler.Begin().FireAndForget();
handler.RunWrapper(CancellationToken.None).FireAndForget();
}
private void HandleLogin(LoversLabLogin loversLabLogin)
{
var handler = _serviceProvider.GetRequiredService<LoversLabLoginHandler>();
handler.Configure(ActivePane, loversLabLogin);
handler.Begin().FireAndForget();
handler.RunWrapper(CancellationToken.None).FireAndForget();
}
private void HandleLogin(VectorPlexusLogin vectorPlexusLogin)
{
var handler = _serviceProvider.GetRequiredService<VectorPlexusLoginHandler>();
handler.Configure(ActivePane, vectorPlexusLogin);
handler.Begin().FireAndForget();
handler.RunWrapper(CancellationToken.None).FireAndForget();
}
private void HandleNavigateBack(NavigateBack navigateBack)

View File

@ -44,7 +44,8 @@ public partial class BrowserTabView : IDisposable
private void ClickClose(object sender, RoutedEventArgs e)
{
var tc = (TabControl) this.Parent;
tc.Items.Remove(this);
if (tc.Items.Contains(this))
tc.Items.Remove(this);
this.Dispose();
}
}

View File

@ -37,7 +37,10 @@
<TabControl Grid.Row="0" x:Name="Tabs">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="16" Margin="0, 0, 8, 0">WABBAJACK 3.0.0</TextBlock>
<Button Name="SettingsButton"><icon:Material Kind="Cog"></icon:Material></Button>
</StackPanel>
</TabItem.Header>
<Grid>

View File

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows;
@ -51,6 +52,12 @@ namespace Wabbajack
TaskbarItemInfo.ProgressState = u.State;
});
MessageBus.Current.Listen<OpenBrowserTab>()
.Subscribe(OnOpenBrowserTab);
MessageBus.Current.Listen<CloseBrowserTab>()
.Subscribe(OnCloseBrowserTab);
_logger.LogInformation("Wabbajack Build - {Sha}",ThisAssembly.Git.Sha);
_logger.LogInformation("Running in {EntryPoint}", KnownFolders.EntryPoint);
@ -97,6 +104,9 @@ namespace Wabbajack
{
this.Topmost = false;
};
((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand)
.BindTo(this, view => view.SettingsButton.Command);
}
catch (Exception ex)
{
@ -196,5 +206,15 @@ namespace Wabbajack
Tabs.Items.Add(tab);
Tabs.SelectedItem = tab;
}
private void OnCloseBrowserTab(CloseBrowserTab msg)
{
foreach (var tab in Tabs.Items.OfType<BrowserTabView>())
{
if (tab.DataContext != msg.ViewModel) continue;
Tabs.Items.Remove(tab);
break;
}
}
}
}

View File

@ -63,6 +63,7 @@
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.1.0">
<NoWarn>NU1701</NoWarn>
</PackageReference>
<PackageReference Include="Fizzler.Systems.HtmlAgilityPack" Version="1.2.1" />
<PackageReference Include="Fody" Version="6.5.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -76,7 +77,7 @@
<PackageReference Include="MahApps.Metro.IconPacks" Version="4.8.0" />
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1189-prerelease" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1248-prerelease" />
<PackageReference Include="PInvoke.User32" Version="0.7.104" />
<PackageReference Include="ReactiveUI" Version="16.2.6" />
<PackageReference Include="ReactiveUI.Fody" Version="16.2.6" />