Merge Master

This commit is contained in:
Timothy Baldridge 2020-04-04 16:15:55 -06:00
commit 895a555802
10 changed files with 326 additions and 43 deletions

View File

@ -11,6 +11,7 @@ using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@ -1136,5 +1137,19 @@ namespace Wabbajack.Common
await using var d = File.Create(dest);
await s.CopyToAsync(d);
}
public static string ToNormalString(this SecureString value)
{
var valuePtr = IntPtr.Zero;
try
{
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
return Marshal.PtrToStringUni(valuePtr) ?? "";
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
}
}
}
}

View File

@ -1,11 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Security;
using ReactiveUI;
namespace Wabbajack.Lib.Downloaders
@ -20,4 +15,21 @@ namespace Wabbajack.Lib.Downloaders
Uri SiteURL { get; }
Uri IconUri { get; }
}
public struct LoginReturnMessage
{
public string Message;
public bool Failure;
public LoginReturnMessage(string message, bool failure)
{
Message = message;
Failure = failure;
}
}
public interface INeedsLoginCredentials : INeedsLogin
{
LoginReturnMessage LoginWithCredentials(string username, SecureString password);
}
}

View File

@ -1,12 +1,77 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Security;
using System.Threading.Tasks;
using CG.Web.MegaApiClient;
using ReactiveUI;
using Wabbajack.Common;
namespace Wabbajack.Lib.Downloaders
{
public class MegaDownloader : IDownloader, IUrlDownloader
public class MegaDownloader : IUrlDownloader, INeedsLoginCredentials
{
public MegaApiClient MegaApiClient;
private const string DataName = "mega-cred";
public LoginReturnMessage LoginWithCredentials(string username, SecureString password)
{
MegaApiClient.AuthInfos authInfos;
try
{
authInfos = MegaApiClient.GenerateAuthInfos(username, password.ToNormalString());
username = null;
password = null;
}
catch (ApiException e)
{
return e.ApiResultCode switch
{
ApiResultCode.BadArguments => new LoginReturnMessage($"Email or password was wrong! {e.Message}",
true),
ApiResultCode.InternalError => new LoginReturnMessage(
$"Internal error occured! Please report this to the Wabbajack Team! {e.Message}", true),
_ => new LoginReturnMessage($"Error generating authentication information! {e.Message}", true)
};
}
try
{
MegaApiClient.Login(authInfos);
}
catch (ApiException e)
{
if ((int)e.ApiResultCode == -26)
{
return new LoginReturnMessage("Two-Factor Authentication needs to be disabled before login!", true);
}
return e.ApiResultCode switch
{
ApiResultCode.InternalError => new LoginReturnMessage(
$"Internal error occured! Please report this to the Wabbajack Team! {e.Message}", true),
_ => new LoginReturnMessage($"Error during login: {e.Message}", true)
};
}
if(MegaApiClient.IsLoggedIn)
authInfos.ToEcryptedJson(DataName);
return new LoginReturnMessage("Logged in successfully, you can now close this window.",
!MegaApiClient.IsLoggedIn || !Utils.HaveEncryptedJson(DataName));
}
public MegaDownloader()
{
MegaApiClient = new MegaApiClient();
TriggerLogin = ReactiveCommand.Create(() => { },
IsLoggedIn.Select(b => !b).ObserveOnGuiThread());
ClearLogin = ReactiveCommand.Create(() => Utils.CatchAndLog(() => Utils.DeleteEncryptedJson(DataName)),
IsLoggedIn.ObserveOnGuiThread());
}
public async Task<AbstractDownloadState> GetDownloaderState(dynamic archiveINI, bool quickMode)
{
var url = archiveINI?.General?.directURL;
@ -16,7 +81,7 @@ namespace Wabbajack.Lib.Downloaders
public AbstractDownloadState GetDownloaderState(string url)
{
if (url != null && url.StartsWith(Consts.MegaPrefix))
return new State { Url = url };
return new State { Url = url, MegaApiClient = MegaApiClient};
return null;
}
@ -26,27 +91,44 @@ namespace Wabbajack.Lib.Downloaders
public class State : HTTPDownloader.State
{
public MegaApiClient MegaApiClient;
private void MegaLogin()
{
if (MegaApiClient.IsLoggedIn)
return;
if (!Utils.HaveEncryptedJson(DataName))
{
Utils.Status("Logging into MEGA (as anonymous)");
MegaApiClient.LoginAnonymous();
}
else
{
Utils.Status("Logging into MEGA with saved credentials.");
var authInfo = Utils.FromEncryptedJson<MegaApiClient.AuthInfos>(DataName);
MegaApiClient.Login(authInfo);
}
}
public override async Task<bool> Download(Archive a, AbsolutePath destination)
{
var client = new MegaApiClient();
Utils.Status("Logging into MEGA (as anonymous)");
client.LoginAnonymous();
MegaLogin();
var fileLink = new Uri(Url);
var node = client.GetNodeFromLink(fileLink);
Utils.Status($"Downloading MEGA file: {a.Name}");
client.DownloadFile(fileLink, (string)destination);
await MegaApiClient.DownloadFileAsync(fileLink, (string)destination, new Progress<double>(p => Utils.Status($"Downloading MEGA File: {a.Name}", Percent.FactoryPutInRange(p))));
return true;
}
public override async Task<bool> Verify(Archive a)
{
var client = new MegaApiClient();
Utils.Status("Logging into MEGA (as anonymous)");
client.LoginAnonymous();
MegaLogin();
var fileLink = new Uri(Url);
try
{
var node = client.GetNodeFromLink(fileLink);
var node = MegaApiClient.GetNodeFromLink(fileLink);
return true;
}
catch (Exception)
@ -55,5 +137,13 @@ namespace Wabbajack.Lib.Downloaders
}
}
}
public ReactiveCommand<Unit, Unit> TriggerLogin { get; }
public ReactiveCommand<Unit, Unit> ClearLogin { get; }
public IObservable<bool> IsLoggedIn => Utils.HaveEncryptedJsonObservable(DataName);
public string SiteName => "MEGA";
public IObservable<string> MetaInfo => Observable.Return("");
public Uri SiteURL => new Uri("https://mega.nz/");
public Uri IconUri => new Uri("https://mega.nz/favicon.ico");
}
}

View File

@ -0,0 +1,40 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Security;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack
{
public class CredentialsLoginVM : ViewModel
{
[Reactive]
public string Username { get; set; }
[Reactive]
public LoginReturnMessage ReturnMessage { get; set; }
private readonly ObservableAsPropertyHelper<bool> _loginEnabled;
public bool LoginEnabled => _loginEnabled.Value;
private readonly INeedsLoginCredentials _downloader;
public CredentialsLoginVM(INeedsLoginCredentials downloader)
{
_downloader = downloader;
_loginEnabled = this.WhenAny(x => x.Username)
.Select(username => !string.IsNullOrWhiteSpace(username))
.ToGuiProperty(this,
nameof(LoginEnabled));
}
public void Login(SecureString password)
{
ReturnMessage = _downloader.LoginWithCredentials(Username, password);
password.Clear();
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
@ -27,16 +28,39 @@ namespace Wabbajack
public class LoginTargetVM : ViewModel
{
private readonly ObservableAsPropertyHelper<string> _MetaInfo;
public string MetaInfo => _MetaInfo.Value;
private readonly ObservableAsPropertyHelper<string> _metaInfo;
public string MetaInfo => _metaInfo.Value;
public INeedsLogin Login { get; }
public INeedsLoginCredentials LoginWithCredentials { get; }
public bool UsesCredentials { get; }
public ReactiveCommand<Unit, Unit> TriggerCredentialsLogin;
public LoginTargetVM(INeedsLogin login)
{
Login = login;
_MetaInfo = (login.MetaInfo ?? Observable.Return(""))
if (login is INeedsLoginCredentials loginWithCredentials)
{
UsesCredentials = true;
LoginWithCredentials = loginWithCredentials;
}
_metaInfo = (login.MetaInfo ?? Observable.Return(""))
.ToGuiProperty(this, nameof(MetaInfo));
if (!UsesCredentials)
return;
TriggerCredentialsLogin = ReactiveCommand.Create(() =>
{
if (!(login is INeedsLoginCredentials))
return;
var loginWindow = new LoginWindowView(LoginWithCredentials);
loginWindow.Show();
}, LoginWithCredentials.IsLoggedIn.Select(b => !b).ObserveOnGuiThread());
}
}
}

View File

@ -0,0 +1,34 @@
<rxui:ReactiveUserControl
x:Class="Wabbajack.CredentialsLoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:rxui="http://reactiveui.net"
x:TypeArguments="local:CredentialsLoginVM"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="60"/>
<RowDefinition Height="60"/>
<RowDefinition Height="40"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<TextBox FontSize="20" x:Name="Username" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"/>
<PasswordBox Margin="0 16 0 -8" FontSize="20" x:Name="Password" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
<Button Margin="8 8 0 0" x:Name="LoginButton" Grid.Row="2" Grid.Column="2" Click="LoginButton_OnClick">
<TextBlock FontSize="14">Login</TextBlock>
</Button>
<TextBox x:Name="Message" FontSize="20" Margin="0 16 0 0" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"/>
</Grid>
</rxui:ReactiveUserControl>

View File

@ -0,0 +1,37 @@
using System.Reactive.Disposables;
using System.Windows;
using ReactiveUI;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack
{
public partial class CredentialsLoginView
{
public INeedsLoginCredentials Downloader { get; set; }
public CredentialsLoginView(INeedsLoginCredentials downloader)
{
Downloader = downloader;
if (ViewModel == null)
ViewModel = new CredentialsLoginVM(downloader);
InitializeComponent();
this.WhenActivated(disposable =>
{
this.Bind(ViewModel, x => x.Username, x => x.Username.Text)
.DisposeWith(disposable);
this.Bind(ViewModel, x => x.LoginEnabled, x => x.LoginButton.IsEnabled)
.DisposeWith(disposable);
this.OneWayBind(ViewModel, x => x.ReturnMessage.Message, x => x.Message.Text)
.DisposeWith(disposable);
});
}
private void LoginButton_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.Login(Password.SecurePassword);
}
}
}

View File

@ -1,40 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ReactiveUI;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for LoginItemView.xaml
/// </summary>
public partial class LoginItemView : ReactiveUserControl<LoginTargetVM>
public partial class LoginItemView
{
public LoginItemView()
{
InitializeComponent();
this.WhenActivated(disposable =>
{
this.OneWayBindStrict(this.ViewModel, x => x.Login.SiteName, x => x.SiteNameText.Text)
this.OneWayBindStrict(ViewModel, x => x.Login.SiteName, x => x.SiteNameText.Text)
.DisposeWith(disposable);
this.OneWayBindStrict(this.ViewModel, x => x.Login.TriggerLogin, x => x.LoginButton.Command)
if (!ViewModel.UsesCredentials)
{
this.OneWayBindStrict(ViewModel, x => x.Login.TriggerLogin, x => x.LoginButton.Command)
.DisposeWith(disposable);
}
else
{
this.OneWayBindStrict(ViewModel, x => x.TriggerCredentialsLogin, x => x.LoginButton.Command)
.DisposeWith(disposable);
this.OneWayBindStrict(this.ViewModel, x => x.Login.ClearLogin, x => x.LogoutButton.Command)
}
this.OneWayBindStrict(ViewModel, x => x.Login.ClearLogin, x => x.LogoutButton.Command)
.DisposeWith(disposable);
this.OneWayBindStrict(this.ViewModel, x => x.MetaInfo, x => x.MetaText.Text)
this.OneWayBindStrict(ViewModel, x => x.MetaInfo, x => x.MetaText.Text)
.DisposeWith(disposable);
// Modify label state
@ -42,13 +36,14 @@ namespace Wabbajack
.Switch()
.Subscribe(x =>
{
this.LoginButton.Content = x ? "Login" : "Logged In";
LoginButton.Content = x ? "Login" : "Logged In";
});
this.WhenAny(x => x.ViewModel.Login.ClearLogin.CanExecute)
.Switch()
.Subscribe(x =>
{
this.LogoutButton.Content = x ? "Logout" : "Logged Out";
LogoutButton.Content = x ? "Logout" : "Logged Out";
});
});
}

View File

@ -0,0 +1,16 @@
<mah:MetroWindow x:Class="Wabbajack.LoginWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wabbajack"
mc:Ignorable="d"
WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}"
Style="{StaticResource {x:Type Window}}"
d:DataContext="{d:DesignInstance local:LoginWindowView }"
Title="Login" Height="450" Width="800">
<Grid x:Name="Grid">
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,20 @@
using Wabbajack.Lib.Downloaders;
namespace Wabbajack
{
public partial class LoginWindowView
{
public INeedsLoginCredentials Downloader { get; set; }
public LoginWindowView(INeedsLoginCredentials downloader)
{
Downloader = downloader;
InitializeComponent();
var loginView = new CredentialsLoginView(downloader);
Grid.Children.Add(loginView);
}
}
}