From 63005a2c5b0fd04096b5498bffda2a9aae0f1dcc Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sat, 27 Jan 2018 13:03:20 +0000 Subject: [PATCH] User interfaces for auth tokens --- Cmdline/Action/AuthToken.cs | 175 +++++++++++++++++++++++++++++++ Cmdline/CKAN-cmdline.csproj | 1 + Cmdline/Main.cs | 15 ++- Cmdline/Options.cs | 3 + ConsoleUI/AuthTokenAddDialog.cs | 86 +++++++++++++++ ConsoleUI/AuthTokenListScreen.cs | 96 +++++++++++++++++ ConsoleUI/CKAN-ConsoleUI.csproj | 2 + ConsoleUI/ModInfoScreen.cs | 9 +- ConsoleUI/ModListScreen.cs | 9 ++ Core/Net/AutoUpdate.cs | 11 +- Core/Net/NetAsyncDownloader.cs | 2 +- Core/Win32Registry.cs | 15 ++- GUI/SettingsDialog.Designer.cs | 162 ++++++++++++++++++---------- GUI/SettingsDialog.cs | 145 +++++++++++++++++++++++++ 14 files changed, 663 insertions(+), 68 deletions(-) create mode 100644 Cmdline/Action/AuthToken.cs create mode 100644 ConsoleUI/AuthTokenAddDialog.cs create mode 100644 ConsoleUI/AuthTokenListScreen.cs diff --git a/Cmdline/Action/AuthToken.cs b/Cmdline/Action/AuthToken.cs new file mode 100644 index 0000000000..5bb806fc5b --- /dev/null +++ b/Cmdline/Action/AuthToken.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using CommandLine; +using CommandLine.Text; +using log4net; + +namespace CKAN.CmdLine +{ + /// + /// Subcommand for managing authentication tokens + /// + public class AuthToken : ISubCommand + { + /// + /// Initialize the subcommand + /// + public AuthToken() { } + + /// + /// Run the subcommand + /// + /// Command line parameters not yet handled by parser + /// + /// Exit code + /// + public int RunSubCommand(SubCommandOptions unparsed) + { + string[] args = unparsed.options.ToArray(); + int exitCode = Exit.OK; + + Parser.Default.ParseArgumentsStrict(args, new AuthTokenSubOptions(), (string option, object suboptions) => + { + if (!string.IsNullOrEmpty(option) && suboptions != null) + { + CommonOptions options = (CommonOptions)suboptions; + user = new ConsoleUser(options.Headless); + manager = new KSPManager(user); + exitCode = options.Handle(manager, user); + if (exitCode == Exit.OK) + { + switch (option) + { + case "list": + exitCode = listAuthTokens(options); + break; + case "add": + exitCode = addAuthToken((AddAuthTokenOptions)options); + break; + case "remove": + exitCode = removeAuthToken((RemoveAuthTokenOptions)options); + break; + } + } + } + }, () => { exitCode = MainClass.AfterHelp(); }); + return exitCode; + } + + private int listAuthTokens(CommonOptions opts) + { + List hosts = new List(Win32Registry.GetAuthTokenHosts()); + if (hosts.Count > 0) + { + int longestHostLen = hostHeader.Length; + int longestTokenLen = tokenHeader.Length; + foreach (string host in hosts) + { + longestHostLen = Math.Max(longestHostLen, host.Length); + string token; + if (Win32Registry.TryGetAuthToken(host, out token)) + { + longestTokenLen = Math.Max(longestTokenLen, token.Length); + } + } + // Create format string: {0,-longestHostLen} {1,-longestTokenLen} + string fmt = string.Format("{0}0,-{2}{1} {0}1,-{3}{1}", + "{", "}", longestHostLen, longestTokenLen); + user.RaiseMessage(fmt, hostHeader, tokenHeader); + user.RaiseMessage(fmt, + new string('-', longestHostLen), + new string('-', longestTokenLen) + ); + foreach (string host in hosts) + { + string token; + if (Win32Registry.TryGetAuthToken(host, out token)) + { + user.RaiseMessage(fmt, host, token); + } + } + } + return Exit.OK; + } + + private int addAuthToken(AddAuthTokenOptions opts) + { + if (Uri.CheckHostName(opts.host) != UriHostNameType.Unknown) + { + Win32Registry.SetAuthToken(opts.host, opts.token); + } + else + { + user.RaiseError("Invalid host name: {0}", opts.host); + } + return Exit.OK; + } + + private int removeAuthToken(RemoveAuthTokenOptions opts) + { + Win32Registry.SetAuthToken(opts.host, null); + return Exit.OK; + } + + private const string hostHeader = "Host"; + private const string tokenHeader = "Token"; + + private IUser user; + private KSPManager manager; + private static readonly ILog log = LogManager.GetLogger(typeof(AuthToken)); + } + + internal class AuthTokenSubOptions : VerbCommandOptions + { + [VerbOption("list", HelpText = "List auth tokens")] + public CommonOptions ListOptions { get; set; } + + [VerbOption("add", HelpText = "Add an auth token")] + public AddAuthTokenOptions AddOptions { get; set; } + + [VerbOption("remove", HelpText = "Delete an auth token")] + public RemoveAuthTokenOptions RemoveOptions { get; set; } + + [HelpVerbOption] + public string GetUsage(string verb) + { + HelpText ht = HelpText.AutoBuild(this, verb); + // Add a usage prefix line + ht.AddPreOptionsLine(" "); + if (string.IsNullOrEmpty(verb)) + { + ht.AddPreOptionsLine("ckan authtoken - Manage authentication tokens"); + ht.AddPreOptionsLine($"Usage: ckan authtoken [options]"); + } + else + { + ht.AddPreOptionsLine("authtoken " + verb + " - " + GetDescription(verb)); + switch (verb) + { + case "add": + ht.AddPreOptionsLine($"Usage: ckan authtoken {verb} [options] host token"); + break; + case "remove": + ht.AddPreOptionsLine($"Usage: ckan authtoken {verb} [options] host"); + break; + case "list": + ht.AddPreOptionsLine($"Usage: ckan authtoken {verb} [options]"); + break; + } + } + return ht; + } + } + + internal class AddAuthTokenOptions : CommonOptions + { + [ValueOption(0)] public string host { get; set; } + [ValueOption(1)] public string token { get; set; } + } + + internal class RemoveAuthTokenOptions : CommonOptions + { + [ValueOption(0)] public string host { get; set; } + } + +} diff --git a/Cmdline/CKAN-cmdline.csproj b/Cmdline/CKAN-cmdline.csproj index 409b389768..957f2fb960 100644 --- a/Cmdline/CKAN-cmdline.csproj +++ b/Cmdline/CKAN-cmdline.csproj @@ -53,6 +53,7 @@ Properties\GlobalAssemblyInfo.cs + diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs index f2e19adde9..feddcb8f89 100644 --- a/Cmdline/Main.cs +++ b/Cmdline/Main.cs @@ -63,20 +63,19 @@ public static int Main(string[] args) switch (args[0]) { case "repair": - var repair = new Repair(); - return repair.RunSubCommand(new SubCommandOptions(args)); + return (new Repair()).RunSubCommand(new SubCommandOptions(args)); case "ksp": - var ksp = new KSP(); - return ksp.RunSubCommand(new SubCommandOptions(args)); + return (new KSP()).RunSubCommand(new SubCommandOptions(args)); case "compat": - var compat = new CompatSubCommand(); - return compat.RunSubCommand(new SubCommandOptions(args)); + return (new CompatSubCommand()).RunSubCommand(new SubCommandOptions(args)); case "repo": - var repo = new Repo(); - return repo.RunSubCommand(new SubCommandOptions(args)); + return (new Repo()).RunSubCommand(new SubCommandOptions(args)); + + case "authtoken": + return (new AuthToken()).RunSubCommand(new SubCommandOptions(args)); } } catch (NoGameInstanceKraken) diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index e721506cb8..5ceea56985 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -88,6 +88,9 @@ internal class Actions : VerbCommandOptions [VerbOption("ksp", HelpText = "Manage KSP installs")] public SubCommandOptions KSP { get; set; } + [VerbOption("authtoken", HelpText = "Manage authentication tokens")] + public AuthTokenSubOptions AuthToken { get; set; } + [VerbOption("compat", HelpText = "Manage KSP version compatibility")] public SubCommandOptions Compat { get; set; } diff --git a/ConsoleUI/AuthTokenAddDialog.cs b/ConsoleUI/AuthTokenAddDialog.cs new file mode 100644 index 0000000000..2a608b5879 --- /dev/null +++ b/ConsoleUI/AuthTokenAddDialog.cs @@ -0,0 +1,86 @@ +using System; +using CKAN.ConsoleUI.Toolkit; + +namespace CKAN.ConsoleUI { + + /// + /// Popup for adding a new authentication token. + /// + public class AuthTokenAddDialog : ConsoleDialog { + + /// + /// Initialize the popup. + /// + public AuthTokenAddDialog() : base() + { + CenterHeader = () => "Create Authentication Key"; + + int top = (Console.WindowHeight - height) / 2; + SetDimensions(6, top, -6, top + height - 1); + + int l = GetLeft(), + r = GetRight(), + t = GetTop(), + b = GetBottom(); + + AddObject(new ConsoleLabel( + l + 2, t + 2, l + 2 + labelW, + () => "Host:", + () => ConsoleTheme.Current.PopupBg, + () => ConsoleTheme.Current.PopupFg + )); + + hostEntry = new ConsoleField( + l + 2 + labelW + wPad, t + 2, r - 3 + ) { + GhostText = () => "" + }; + AddObject(hostEntry); + + AddObject(new ConsoleLabel( + l + 2, t + 4, l + 2 + labelW, + () => "Token:", + () => ConsoleTheme.Current.PopupBg, + () => ConsoleTheme.Current.PopupFg + )); + + tokenEntry = new ConsoleField( + l + 2 + labelW + wPad, t + 4, r - 3 + ) { + GhostText = () => "" + }; + AddObject(tokenEntry); + + AddTip("Esc", "Cancel"); + AddBinding(Keys.Escape, (object sender) => false); + + AddTip("Enter", "Accept", validKey); + AddBinding(Keys.Enter, (object sender) => { + if (validKey()) { + Win32Registry.SetAuthToken(hostEntry.Value, tokenEntry.Value); + return false; + } else { + // Don't close window on Enter unless adding a key + return true; + } + }); + } + + private bool validKey() + { + string token; + return hostEntry.Value.Length > 0 + && tokenEntry.Value.Length > 0 + && Uri.CheckHostName(hostEntry.Value) != UriHostNameType.Unknown + && !Win32Registry.TryGetAuthToken(hostEntry.Value, out token); + } + + private ConsoleField hostEntry; + private ConsoleField tokenEntry; + + private const int wPad = 2; + private const int labelW = 6; + private const int height = 7; + } + +} diff --git a/ConsoleUI/AuthTokenListScreen.cs b/ConsoleUI/AuthTokenListScreen.cs new file mode 100644 index 0000000000..b043540611 --- /dev/null +++ b/ConsoleUI/AuthTokenListScreen.cs @@ -0,0 +1,96 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using CKAN.ConsoleUI.Toolkit; + +namespace CKAN.ConsoleUI { + + /// + /// Screen for display and editing of authentication tokens. + /// + public class AuthTokenScreen : ConsoleScreen { + + /// + /// Initialize the screen. + /// + public AuthTokenScreen() : base() + { + LeftHeader = () => $"CKAN {Meta.GetVersion()}"; + CenterHeader = () => "Authentication Tokens"; + mainMenu = new ConsolePopupMenu(new List() { + new ConsoleMenuOption("Make a GitHub API token", "", + "Open the web page for creating GitHub API authentication tokens", + true, openGitHubURL) + }); + + AddObject(new ConsoleLabel( + 1, 2, -1, + () => "Authentication tokens for downloads:" + )); + + tokenList = new ConsoleListBox( + 1, 4, -1, -2, + new List(Win32Registry.GetAuthTokenHosts()), + new List>() { + new ConsoleListBoxColumn() { + Header = "Host", + Width = 20, + Renderer = (string s) => s + }, + new ConsoleListBoxColumn() { + Header = "Token", + Width = 50, + Renderer = (string s) => { + string token; + return Win32Registry.TryGetAuthToken(s, out token) + ? token + : missingTokenValue; + } + } + }, + 0, 0, ListSortDirection.Descending + ); + AddObject(tokenList); + + AddObject(new ConsoleLabel( + 3, -1, -1, + () => "NOTE: These values are private! Do not share screenshots of this screen!", + null, + () => ConsoleTheme.Current.AlertFrameFg + )); + + AddTip("Esc", "Back"); + AddBinding(Keys.Escape, (object sender) => false); + + tokenList.AddTip("A", "Add"); + tokenList.AddBinding(Keys.A, (object sender) => { + AuthTokenAddDialog ad = new AuthTokenAddDialog(); + ad.Run(); + DrawBackground(); + tokenList.SetData(new List(Win32Registry.GetAuthTokenHosts())); + return true; + }); + + tokenList.AddTip("R", "Remove", () => tokenList.Selection != null); + tokenList.AddBinding(Keys.R, (object sender) => { + if (tokenList.Selection != null) { + Win32Registry.SetAuthToken(tokenList.Selection, null); + tokenList.SetData(new List(Win32Registry.GetAuthTokenHosts())); + } + return true; + }); + } + + private bool openGitHubURL() + { + ModInfoScreen.LaunchURL(githubTokenURL); + return true; + } + + private ConsoleListBox tokenList; + + private const string missingTokenValue = ""; + private static readonly Uri githubTokenURL = new Uri("https://github.com/settings/tokens"); + } + +} diff --git a/ConsoleUI/CKAN-ConsoleUI.csproj b/ConsoleUI/CKAN-ConsoleUI.csproj index 5483d77a78..5d719bed9f 100644 --- a/ConsoleUI/CKAN-ConsoleUI.csproj +++ b/ConsoleUI/CKAN-ConsoleUI.csproj @@ -64,6 +64,8 @@ Properties\GlobalAssemblyInfo.cs + + diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index e2fbb4f19d..2742dd1802 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -179,7 +179,14 @@ private bool ViewMetadata() return true; } - private bool LaunchURL(Uri u) + /// + /// Launch a URL in the system browser. + /// + /// URL to launch + /// + /// True. + /// + public static bool LaunchURL(Uri u) { // I'm getting error output on Linux, because this runs xdg-open which // calls chromium-browser which prints a bunch of stuff about plugins that diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index a2b70444de..ae6fe10635 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -256,6 +256,9 @@ public ModListScreen(KSPManager mgr, bool dbg) new ConsoleMenuOption("Select KSP install...", "", "Switch to a different game instance", true, SelectInstall), + new ConsoleMenuOption("Authentication tokens...", "", + "Edit authentication tokens sent to download servers", + true, EditAuthTokens), null, new ConsoleMenuOption("Help", helpKey, "Tips & tricks", @@ -436,6 +439,12 @@ private bool SelectInstall() return true; } + private bool EditAuthTokens() + { + LaunchSubScreen(new AuthTokenScreen()); + return true; + } + private void RefreshList() { moduleList.SetData(GetAllMods(true)); diff --git a/Core/Net/AutoUpdate.cs b/Core/Net/AutoUpdate.cs index 38623e30f4..270710f3cf 100644 --- a/Core/Net/AutoUpdate.cs +++ b/Core/Net/AutoUpdate.cs @@ -63,8 +63,10 @@ private static bool CanWrite(string path) /// /// Report whether it's possible to run the auto-updater. /// Checks whether we can overwrite the running ckan.exe. + /// Windows doesn't let us check this because it locks the EXE + /// for a running process, so assume we can always overwrite on Windows. /// - public static readonly bool CanUpdate = CanWrite(exePath); + public static readonly bool CanUpdate = Platform.IsWindows || CanWrite(exePath); /// /// Our metadata is considered fetched if we have a latest version, release notes, @@ -86,12 +88,15 @@ public void FetchLatestReleaseInfo() try { - fetchedCkanUrl = RetrieveUrl(response, 0); // Check whether the release includes the auto updater foreach (var asset in response.assets) { string url = asset.browser_download_url.ToString(); - if (url.EndsWith("AutoUpdater.exe")) + if (url.EndsWith("ckan.exe")) + { + fetchedCkanUrl = new Tuple(new Uri(url), (long)asset.size); + } + else if (url.EndsWith("AutoUpdater.exe")) { fetchedUpdaterUrl = new Tuple(new Uri(url), (long)asset.size); break; diff --git a/Core/Net/NetAsyncDownloader.cs b/Core/Net/NetAsyncDownloader.cs index 13d14afd3d..ae6bc963d1 100644 --- a/Core/Net/NetAsyncDownloader.cs +++ b/Core/Net/NetAsyncDownloader.cs @@ -54,7 +54,7 @@ public NetAsyncDownloaderDownloadPart(Net.DownloadTarget target, string path = n { log.InfoFormat("Using auth token for {0}", this.url.Host); // Send our auth token to the GitHub API (or whoever else needs one) - agent.Headers.Add("Authentication", $"token {token}"); + agent.Headers.Add("Authorization", $"token {token}"); } } } diff --git a/Core/Win32Registry.cs b/Core/Win32Registry.cs index 42f5677796..334550b2df 100644 --- a/Core/Win32Registry.cs +++ b/Core/Win32Registry.cs @@ -110,11 +110,22 @@ public static IEnumerable GetAuthTokenHosts() /// Set an auth token in the registry /// /// Host for which to set the token - /// Token to set + /// Token to set, or null to delete public static void SetAuthToken(string host, string token) { ConstructKey(authTokenKeyNoPrefix); - Microsoft.Win32.Registry.SetValue(authTokenKey, host, token); + if (!string.IsNullOrEmpty(host)) + { + if (string.IsNullOrEmpty(token)) + { + RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(authTokenKeyNoPrefix, true); + key.DeleteValue(host); + } + else + { + Microsoft.Win32.Registry.SetValue(authTokenKey, host, token); + } + } } private static string StripPrefixKey(string keyname) diff --git a/GUI/SettingsDialog.Designer.cs b/GUI/SettingsDialog.Designer.cs index de51834fb6..c022fa3f77 100644 --- a/GUI/SettingsDialog.Designer.cs +++ b/GUI/SettingsDialog.Designer.cs @@ -35,30 +35,35 @@ private void InitializeComponent() this.UpRepoButton = new System.Windows.Forms.Button(); this.DeleteRepoButton = new System.Windows.Forms.Button(); this.ReposListBox = new System.Windows.Forms.ListBox(); + this.AuthTokensGroupBox = new System.Windows.Forms.GroupBox(); + this.AuthTokensListBox = new System.Windows.Forms.ListBox(); + this.NewAuthTokenButton = new System.Windows.Forms.Button(); + this.DeleteAuthTokenButton = new System.Windows.Forms.Button(); this.CacheGroupBox = new System.Windows.Forms.GroupBox(); this.ClearCKANCacheButton = new System.Windows.Forms.Button(); this.CKANCacheLabel = new System.Windows.Forms.Label(); - this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.AutoUpdateGroupBox = new System.Windows.Forms.GroupBox(); this.HideEpochsCheckbox = new System.Windows.Forms.CheckBox(); this.RefreshOnStartupCheckbox = new System.Windows.Forms.CheckBox(); this.CheckUpdateOnLaunchCheckbox = new System.Windows.Forms.CheckBox(); this.InstallUpdateButton = new System.Windows.Forms.Button(); this.LatestVersionLabel = new System.Windows.Forms.Label(); - this.label4 = new System.Windows.Forms.Label(); + this.LatestVersionLabelLabel = new System.Windows.Forms.Label(); this.LocalVersionLabel = new System.Windows.Forms.Label(); - this.label3 = new System.Windows.Forms.Label(); + this.LocalVersionLabelLabel = new System.Windows.Forms.Label(); this.CheckForUpdatesButton = new System.Windows.Forms.Button(); this.RepositoryGroupBox.SuspendLayout(); + this.AuthTokensGroupBox.SuspendLayout(); this.CacheGroupBox.SuspendLayout(); - this.groupBox2.SuspendLayout(); + this.AutoUpdateGroupBox.SuspendLayout(); this.SuspendLayout(); // // NewRepoButton // this.NewRepoButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.NewRepoButton.Location = new System.Drawing.Point(6, 224); + this.NewRepoButton.Location = new System.Drawing.Point(6, 94); this.NewRepoButton.Name = "NewRepoButton"; - this.NewRepoButton.Size = new System.Drawing.Size(56, 26); + this.NewRepoButton.Size = new System.Drawing.Size(56, 23); this.NewRepoButton.TabIndex = 6; this.NewRepoButton.Text = "New"; this.NewRepoButton.UseVisualStyleBackColor = true; @@ -74,18 +79,18 @@ private void InitializeComponent() this.RepositoryGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.RepositoryGroupBox.Location = new System.Drawing.Point(12, 12); this.RepositoryGroupBox.Name = "RepositoryGroupBox"; - this.RepositoryGroupBox.Size = new System.Drawing.Size(476, 261); + this.RepositoryGroupBox.Size = new System.Drawing.Size(476, 127); this.RepositoryGroupBox.TabIndex = 12; this.RepositoryGroupBox.TabStop = false; - this.RepositoryGroupBox.Text = "Metadata repositories"; + this.RepositoryGroupBox.Text = "Metadata Repositories"; // // DownRepoButton // this.DownRepoButton.Enabled = false; this.DownRepoButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.DownRepoButton.Location = new System.Drawing.Point(130, 224); + this.DownRepoButton.Location = new System.Drawing.Point(130, 94); this.DownRepoButton.Name = "DownRepoButton"; - this.DownRepoButton.Size = new System.Drawing.Size(56, 26); + this.DownRepoButton.Size = new System.Drawing.Size(56, 23); this.DownRepoButton.TabIndex = 11; this.DownRepoButton.Text = "Down"; this.DownRepoButton.UseVisualStyleBackColor = true; @@ -95,9 +100,9 @@ private void InitializeComponent() // this.UpRepoButton.Enabled = false; this.UpRepoButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.UpRepoButton.Location = new System.Drawing.Point(68, 224); + this.UpRepoButton.Location = new System.Drawing.Point(68, 94); this.UpRepoButton.Name = "UpRepoButton"; - this.UpRepoButton.Size = new System.Drawing.Size(56, 26); + this.UpRepoButton.Size = new System.Drawing.Size(56, 23); this.UpRepoButton.TabIndex = 10; this.UpRepoButton.Text = "Up"; this.UpRepoButton.UseVisualStyleBackColor = true; @@ -107,9 +112,9 @@ private void InitializeComponent() // this.DeleteRepoButton.Enabled = false; this.DeleteRepoButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.DeleteRepoButton.Location = new System.Drawing.Point(414, 224); + this.DeleteRepoButton.Location = new System.Drawing.Point(414, 94); this.DeleteRepoButton.Name = "DeleteRepoButton"; - this.DeleteRepoButton.Size = new System.Drawing.Size(56, 26); + this.DeleteRepoButton.Size = new System.Drawing.Size(56, 23); this.DeleteRepoButton.TabIndex = 9; this.DeleteRepoButton.Text = "Delete"; this.DeleteRepoButton.UseVisualStyleBackColor = true; @@ -120,10 +125,55 @@ private void InitializeComponent() this.ReposListBox.FormattingEnabled = true; this.ReposListBox.Location = new System.Drawing.Point(6, 19); this.ReposListBox.Name = "ReposListBox"; - this.ReposListBox.Size = new System.Drawing.Size(464, 199); + this.ReposListBox.Size = new System.Drawing.Size(464, 72); this.ReposListBox.TabIndex = 9; this.ReposListBox.SelectedIndexChanged += new System.EventHandler(this.ReposListBox_SelectedIndexChanged); // + // AuthTokensGroupBox + // + this.AuthTokensGroupBox.Controls.Add(this.AuthTokensListBox); + this.AuthTokensGroupBox.Controls.Add(this.NewAuthTokenButton); + this.AuthTokensGroupBox.Controls.Add(this.DeleteAuthTokenButton); + this.AuthTokensGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.AuthTokensGroupBox.Location = new System.Drawing.Point(12, 145); + this.AuthTokensGroupBox.Name = "AuthTokensGroupBox"; + this.AuthTokensGroupBox.Size = new System.Drawing.Size(476, 127); + this.AuthTokensGroupBox.TabIndex = 13; + this.AuthTokensGroupBox.TabStop = false; + this.AuthTokensGroupBox.Text = "Authentication Tokens"; + // + // AuthTokensListBox + // + this.AuthTokensListBox.FormattingEnabled = true; + this.AuthTokensListBox.Location = new System.Drawing.Point(6, 19); + this.AuthTokensListBox.Name = "AuthTokensListBox"; + this.AuthTokensListBox.Size = new System.Drawing.Size(464, 72); + this.AuthTokensListBox.TabIndex = 14; + this.AuthTokensListBox.SelectedIndexChanged += new System.EventHandler(this.AuthTokensListBox_SelectedIndexChanged); + // + // NewAuthTokenButton + // + this.NewAuthTokenButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.NewAuthTokenButton.Location = new System.Drawing.Point(6, 94); + this.NewAuthTokenButton.Name = "NewAuthTokenButton"; + this.NewAuthTokenButton.Size = new System.Drawing.Size(56, 23); + this.NewAuthTokenButton.TabIndex = 15; + this.NewAuthTokenButton.Text = "New"; + this.NewAuthTokenButton.UseVisualStyleBackColor = true; + this.NewAuthTokenButton.Click += new System.EventHandler(this.NewAuthTokenButton_Click); + // + // DeleteAuthTokenButton + // + this.DeleteAuthTokenButton.Enabled = false; + this.DeleteAuthTokenButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.DeleteAuthTokenButton.Location = new System.Drawing.Point(414, 94); + this.DeleteAuthTokenButton.Name = "DeleteAuthTokenButton"; + this.DeleteAuthTokenButton.Size = new System.Drawing.Size(56, 23); + this.DeleteAuthTokenButton.TabIndex = 16; + this.DeleteAuthTokenButton.Text = "Delete"; + this.DeleteAuthTokenButton.UseVisualStyleBackColor = true; + this.DeleteAuthTokenButton.Click += new System.EventHandler(this.DeleteAuthTokenButton_Click); + // // CacheGroupBox // this.CacheGroupBox.Controls.Add(this.ClearCKANCacheButton); @@ -134,7 +184,7 @@ private void InitializeComponent() this.CacheGroupBox.Size = new System.Drawing.Size(476, 49); this.CacheGroupBox.TabIndex = 10; this.CacheGroupBox.TabStop = false; - this.CacheGroupBox.Text = "CKAN Cache"; + this.CacheGroupBox.Text = "Cache"; // // ClearCKANCacheButton // @@ -156,27 +206,27 @@ private void InitializeComponent() this.CKANCacheLabel.TabIndex = 0; this.CKANCacheLabel.Text = "There are currently N files in the cache, taking up M MB"; // - // groupBox2 + // AutoUpdateGroupBox // - this.groupBox2.Controls.Add(this.HideEpochsCheckbox); - this.groupBox2.Controls.Add(this.RefreshOnStartupCheckbox); + this.AutoUpdateGroupBox.Controls.Add(this.HideEpochsCheckbox); + this.AutoUpdateGroupBox.Controls.Add(this.RefreshOnStartupCheckbox); if (AutoUpdate.CanUpdate) { - this.groupBox2.Controls.Add(this.CheckUpdateOnLaunchCheckbox); - this.groupBox2.Controls.Add(this.InstallUpdateButton); - this.groupBox2.Controls.Add(this.LatestVersionLabel); - this.groupBox2.Controls.Add(this.label4); - this.groupBox2.Controls.Add(this.LocalVersionLabel); - this.groupBox2.Controls.Add(this.label3); - this.groupBox2.Controls.Add(this.CheckForUpdatesButton); + this.AutoUpdateGroupBox.Controls.Add(this.CheckUpdateOnLaunchCheckbox); + this.AutoUpdateGroupBox.Controls.Add(this.InstallUpdateButton); + this.AutoUpdateGroupBox.Controls.Add(this.LatestVersionLabel); + this.AutoUpdateGroupBox.Controls.Add(this.LatestVersionLabelLabel); + this.AutoUpdateGroupBox.Controls.Add(this.LocalVersionLabel); + this.AutoUpdateGroupBox.Controls.Add(this.LocalVersionLabelLabel); + this.AutoUpdateGroupBox.Controls.Add(this.CheckForUpdatesButton); } - this.groupBox2.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.groupBox2.Location = new System.Drawing.Point(12, 334); - this.groupBox2.Name = "groupBox2"; - this.groupBox2.Size = new System.Drawing.Size(476, 105); - this.groupBox2.TabIndex = 11; - this.groupBox2.TabStop = false; - this.groupBox2.Text = "CKAN auto-update"; + this.AutoUpdateGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.AutoUpdateGroupBox.Location = new System.Drawing.Point(12, 334); + this.AutoUpdateGroupBox.Name = "AutoUpdateGroupBox"; + this.AutoUpdateGroupBox.Size = new System.Drawing.Size(476, 105); + this.AutoUpdateGroupBox.TabIndex = 11; + this.AutoUpdateGroupBox.TabStop = false; + this.AutoUpdateGroupBox.Text = "Auto-Updates"; // // HideEpochsCheckbox // @@ -232,14 +282,14 @@ private void InitializeComponent() this.LatestVersionLabel.TabIndex = 4; this.LatestVersionLabel.Text = "???"; // - // label4 + // LatestVersionLabelLabel // - this.label4.AutoSize = true; - this.label4.Location = new System.Drawing.Point(7, 43); - this.label4.Name = "label4"; - this.label4.Size = new System.Drawing.Size(76, 13); - this.label4.TabIndex = 3; - this.label4.Text = "Latest version:"; + this.LatestVersionLabelLabel.AutoSize = true; + this.LatestVersionLabelLabel.Location = new System.Drawing.Point(7, 43); + this.LatestVersionLabelLabel.Name = "LatestVersionLabelLabel"; + this.LatestVersionLabelLabel.Size = new System.Drawing.Size(76, 13); + this.LatestVersionLabelLabel.TabIndex = 3; + this.LatestVersionLabelLabel.Text = "Latest version:"; // // LocalVersionLabel // @@ -250,14 +300,14 @@ private void InitializeComponent() this.LocalVersionLabel.TabIndex = 2; this.LocalVersionLabel.Text = "v0.0.0"; // - // label3 + // LocalVersionLabelLabel // - this.label3.AutoSize = true; - this.label3.Location = new System.Drawing.Point(7, 20); - this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(73, 13); - this.label3.TabIndex = 1; - this.label3.Text = "Local version:"; + this.LocalVersionLabelLabel.AutoSize = true; + this.LocalVersionLabelLabel.Location = new System.Drawing.Point(7, 20); + this.LocalVersionLabelLabel.Name = "LocalVersionLabelLabel"; + this.LocalVersionLabelLabel.Size = new System.Drawing.Size(73, 13); + this.LocalVersionLabelLabel.TabIndex = 1; + this.LocalVersionLabelLabel.Text = "Local version:"; // // CheckForUpdatesButton // @@ -275,19 +325,21 @@ private void InitializeComponent() this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(495, 442); - this.Controls.Add(this.groupBox2); + this.Controls.Add(this.AutoUpdateGroupBox); this.Controls.Add(this.CacheGroupBox); this.Controls.Add(this.RepositoryGroupBox); + this.Controls.Add(this.AuthTokensGroupBox); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.Name = "SettingsDialog"; this.Text = "Settings"; this.Load += new System.EventHandler(this.SettingsDialog_Load); this.RepositoryGroupBox.ResumeLayout(false); + this.AuthTokensGroupBox.ResumeLayout(false); this.CacheGroupBox.ResumeLayout(false); this.CacheGroupBox.PerformLayout(); - this.groupBox2.ResumeLayout(false); - this.groupBox2.PerformLayout(); + this.AutoUpdateGroupBox.ResumeLayout(false); + this.AutoUpdateGroupBox.PerformLayout(); this.ResumeLayout(false); } @@ -303,11 +355,15 @@ private void InitializeComponent() private System.Windows.Forms.ListBox ReposListBox; private System.Windows.Forms.Button UpRepoButton; private System.Windows.Forms.Button DownRepoButton; - private System.Windows.Forms.GroupBox groupBox2; + private System.Windows.Forms.GroupBox AuthTokensGroupBox; + private System.Windows.Forms.ListBox AuthTokensListBox; + private System.Windows.Forms.Button NewAuthTokenButton; + private System.Windows.Forms.Button DeleteAuthTokenButton; + private System.Windows.Forms.GroupBox AutoUpdateGroupBox; private System.Windows.Forms.Label LatestVersionLabel; - private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label LatestVersionLabelLabel; private System.Windows.Forms.Label LocalVersionLabel; - private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label LocalVersionLabelLabel; private System.Windows.Forms.Button CheckForUpdatesButton; private System.Windows.Forms.Button InstallUpdateButton; private System.Windows.Forms.CheckBox CheckUpdateOnLaunchCheckbox; diff --git a/GUI/SettingsDialog.cs b/GUI/SettingsDialog.cs index 60d958bade..6bb4d61580 100644 --- a/GUI/SettingsDialog.cs +++ b/GUI/SettingsDialog.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Windows.Forms; +using System.Drawing; using log4net; namespace CKAN @@ -29,6 +30,7 @@ private void SettingsDialog_Load(object sender, EventArgs e) public void UpdateDialog() { RefreshReposListBox(); + RefreshAuthTokensListBox(); LocalVersionLabel.Text = Meta.GetVersion(); @@ -225,6 +227,149 @@ private void DownRepoButton_Click(object sender, EventArgs e) RefreshReposListBox(); } + private void RefreshAuthTokensListBox() + { + AuthTokensListBox.Items.Clear(); + foreach (string host in Win32Registry.GetAuthTokenHosts()) + { + string token; + if (Win32Registry.TryGetAuthToken(host, out token)) + { + AuthTokensListBox.Items.Add(string.Format("{0} | {1}", host, token)); + } + } + } + + private void AuthTokensListBox_SelectedIndexChanged(object sender, EventArgs e) + { + DeleteAuthTokenButton.Enabled = AuthTokensListBox.SelectedItem != null; + } + + private void NewAuthTokenButton_Click(object sender, EventArgs e) + { + // Inspired by https://stackoverflow.com/a/17546909/2422988 + Form newAuthTokenPopup = new Form() + { + FormBorderStyle = FormBorderStyle.FixedToolWindow, + StartPosition = FormStartPosition.CenterParent, + ClientSize = new Size(300, 100), + Text = "Add Authentication Token" + }; + Label hostLabel = new Label() + { + AutoSize = true, + Location = new Point(3, 6), + Size = new Size(271, 13), + Text = "Host:" + }; + TextBox hostTextBox = new TextBox() + { + Location = new Point(45, 6), + Size = new Size(newAuthTokenPopup.ClientSize.Width - 40 - 10, 23), + Text = "" + }; + Label tokenLabel = new Label() + { + AutoSize = true, + Location = new Point(3, 35), + Size = new Size(271, 13), + Text = "Token:" + }; + TextBox tokenTextBox = new TextBox() + { + Location = new Point(45, 35), + Size = new Size(newAuthTokenPopup.ClientSize.Width - 40 - 10, 23), + Text = "" + }; + Button acceptButton = new Button() + { + DialogResult = DialogResult.OK, + Name = "okButton", + Size = new Size(75, 23), + Text = "&Accept", + Location = new Point((newAuthTokenPopup.ClientSize.Width - 80 - 80) / 2, 64) + }; + acceptButton.Click += (origin, evt) => + { + newAuthTokenPopup.DialogResult = validNewAuthToken(hostTextBox.Text, tokenTextBox.Text) + ? DialogResult.OK + : DialogResult.None; + }; + Button cancelButton = new Button() + { + DialogResult = DialogResult.Cancel, + Name = "cancelButton", + Size = new Size(75, 23), + Text = "&Cancel", + Location = new Point(acceptButton.Location.X + acceptButton.Size.Width + 5, 64) + }; + + newAuthTokenPopup.Controls.Add(hostLabel); + newAuthTokenPopup.Controls.Add(hostTextBox); + newAuthTokenPopup.Controls.Add(tokenLabel); + newAuthTokenPopup.Controls.Add(tokenTextBox); + newAuthTokenPopup.Controls.Add(acceptButton); + newAuthTokenPopup.Controls.Add(cancelButton); + newAuthTokenPopup.AcceptButton = acceptButton; + newAuthTokenPopup.CancelButton = cancelButton; + + switch (newAuthTokenPopup.ShowDialog(this)) + { + case DialogResult.Abort: + case DialogResult.Cancel: + case DialogResult.Ignore: + case DialogResult.No: + // User cancelled out, so do nothing + break; + + case DialogResult.OK: + case DialogResult.Yes: + Win32Registry.SetAuthToken(hostTextBox.Text, tokenTextBox.Text); + RefreshAuthTokensListBox(); + break; + } + } + + private static bool validNewAuthToken(string host, string token) + { + if (host.Length <= 0) + { + GUI.user.RaiseError("Host field is required."); + return false; + } + if (token.Length <= 0) + { + GUI.user.RaiseError("Token field is required."); + return false; + } + if (Uri.CheckHostName(host) == UriHostNameType.Unknown) + { + GUI.user.RaiseError("{0} is not a valid host name.", host); + return false; + } + string oldToken; + if (Win32Registry.TryGetAuthToken(host, out oldToken)) + { + GUI.user.RaiseError("{0} already has an authentication token.", host); + return false; + } + + return true; + } + + private void DeleteAuthTokenButton_Click(object sender, EventArgs e) + { + if (AuthTokensListBox.SelectedItem != null) + { + string item = AuthTokensListBox.SelectedItem as string; + string host = item?.Split('|')[0].Trim(); + + Win32Registry.SetAuthToken(host, null); + RefreshAuthTokensListBox(); + DeleteRepoButton.Enabled = false; + } + } + private void CheckForUpdatesButton_Click(object sender, EventArgs e) { try