Skip to content

Commit

Permalink
feat: add support for web-based first-time setup
Browse files Browse the repository at this point in the history
  • Loading branch information
anna-is-cute committed Sep 5, 2024
1 parent cfbfe2c commit afa3b00
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 26 deletions.
20 changes: 19 additions & 1 deletion Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using gfoidl.Base64;
using Heliosphere.Exceptions;
using Heliosphere.Model.Generated;
using Heliosphere.Ui;
Expand Down Expand Up @@ -100,6 +101,7 @@ public class Plugin : IDalamudPlugin {
private Stopwatch LimitTimer { get; } = Stopwatch.StartNew();

internal bool IntegrityFailed { get; private set; }
internal string? FirstTimeSetupKey { get; private set; }

internal ICache<string, IDalamudTextureWrap?> CoverImages { get; } = new ConcurrentLruBuilder<string, IDalamudTextureWrap?>()
.WithConcurrencyLevel(1)
Expand Down Expand Up @@ -262,7 +264,7 @@ public Plugin() {
Task.Run(async () => await this.State.UpdatePackages());

if (this.Interface.Reason == PluginLoadReason.Installer && !this.Config.FirstTimeSetupComplete) {
this.PluginUi.FirstTimeSetupWindow.Visible = true;
this.DoFirstTimeSetup();
}
}

Expand Down Expand Up @@ -348,9 +350,25 @@ private void OpenFirstTimeSetup() {
return;
}

this.DoFirstTimeSetup();
}

internal void DoFirstTimeSetup() {
var bytes = new byte[8];
Random.Shared.NextBytes(bytes);
var key = Base64.Url.Encode(bytes);
this.FirstTimeSetupKey = key;
this.PluginUi.FirstTimeSetupWindow.Visible = true;
}

internal void EndFirstTimeSetup() {
this.Config.FirstTimeSetupComplete = true;
this.SaveConfig();

this.FirstTimeSetupKey = null;
this.PluginUi.FirstTimeSetupWindow.Visible = false;
}

/// <summary>
/// Attempt to add a download to the download queue. This can fail (and
/// therefore return null) if a download for the same version is already in
Expand Down
77 changes: 77 additions & 0 deletions Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,65 @@ await this.Plugin.AddDownloadAsync(new DownloadTask {
};
break;
}
case "/first-time" when method == "post": {
if (this.Plugin.FirstTimeSetupKey is not { } key) {
statusCode = 400;
break;
}

var info = ReadJson<FirstTimeSetup>(req);
if (info == null) {
statusCode = 400;
break;
}

// TODO: do we care about timing attacks
if (info.Key != key) {
statusCode = 401;
break;
}

statusCode = 204;

if (info.Options == null) {
statusCode = 200;
response = new {
Plugin.Version,
Options = new FirstTimeSetupConfigOptions {
AutoUpdate = this.Plugin.Config.AutoUpdate,
ShowPreviews = this.Plugin.Config.Penumbra.ShowImages,
ShowInPenumbra = this.Plugin.Config.OpenPenumbraAfterInstall,
TitlePrefix = this.Plugin.Config.TitlePrefix,
PenumbraFolder = this.Plugin.Config.PenumbraFolder,
OneClick = this.Plugin.Config.OneClick,
},
};
break;
}

var oneClickWasEnabled = this.Plugin.Config.OneClick;

this.Plugin.Config.AutoUpdate = info.Options.AutoUpdate;
this.Plugin.Config.Penumbra.ShowImages = info.Options.ShowPreviews;
this.Plugin.Config.OpenPenumbraAfterInstall = info.Options.ShowInPenumbra;
this.Plugin.Config.TitlePrefix = info.Options.TitlePrefix;
this.Plugin.Config.PenumbraFolder = info.Options.PenumbraFolder;
this.Plugin.Config.OneClick = info.Options.OneClick;

if (!oneClickWasEnabled && info.Options.OneClick) {
// one-click was enabled
var password = this.Plugin.PluginUi.Settings.GenerateOneClickKey();
statusCode = 200;
response = new {
OneClickPassword = password,
};
}

// NOTE: this calls saveconfig
this.Plugin.EndFirstTimeSetup();

break;
}
default: {
if (method == "options") {
statusCode = 200;
Expand Down Expand Up @@ -620,3 +679,21 @@ internal class InstallInfo {
public Guid VersionId { get; set; }
public string? DownloadCode { get; set; }
}

[Serializable]
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
internal class FirstTimeSetup {
public string Key { get; set; }
public FirstTimeSetupConfigOptions? Options { get; set; }
}

[Serializable]
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
internal class FirstTimeSetupConfigOptions {
public bool AutoUpdate { get; set; }
public bool ShowPreviews { get; set; }
public string TitlePrefix { get; set; }
public string PenumbraFolder { get; set; }
public bool OneClick { get; set; }
public bool ShowInPenumbra { get; set; }
}
30 changes: 22 additions & 8 deletions Ui/FirstTimeSetupWindow.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using Heliosphere.Util;
using ImGuiNET;

Expand Down Expand Up @@ -25,18 +26,31 @@ private void Draw() {
// TODO: SetNextWindowSize

using var end = new OnDispose(ImGui.End);
if (!ImGui.Begin($"{Plugin.Name} first-time setup", ref this.Visible)) {
if (!ImGui.Begin($"{Plugin.Name} first-time setup")) {
return;
}

var anyChanged = false;
anyChanged |= ImGuiHelper.BooleanYesNo(
"Do you want mods to automatically update when you log in?",
ref this.Plugin.Config.AutoUpdate
);
using var popTextWrapPos = new OnDispose(ImGui.PopTextWrapPos);
ImGui.PushTextWrapPos();

if (anyChanged) {
this.Plugin.SaveConfig();
ImGuiHelper.TextUnformattedCentred("Welcome to Heliosphere!", PluginUi.TitleSize);

ImGui.Spacing();

ImGui.TextUnformatted("To get everything set up, open the first-time setup window by clicking the button below. It will open in your default web browser.");

if (ImGuiHelper.CentredWideButton("Open first-time setup")) {
var url = new UriBuilder("https://heliosphere.app/setup") {
Fragment = this.Plugin.FirstTimeSetupKey,
};

Process.Start(new ProcessStartInfo(url.Uri.ToString()) {
UseShellExecute = true,
});
}

if (ImGui.SmallButton("Skip (not recommended)")) {
this.Plugin.EndFirstTimeSetup();
}
}
}
2 changes: 1 addition & 1 deletion Ui/PluginUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal enum Tab {
private Manager Manager { get; }
private DownloadHistory DownloadHistory { get; }
private LatestUpdate LatestUpdate { get; }
private Settings Settings { get; }
internal Settings Settings { get; }
internal DownloadStatusWindow StatusWindow { get; }
internal BreakingChangeWindow BreakingChangeWindow { get; }
internal FirstTimeSetupWindow FirstTimeSetupWindow { get; }
Expand Down
38 changes: 22 additions & 16 deletions Ui/Tabs/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,24 +202,10 @@ void DrawLimitCombo(string title, string id, ref Configuration.SpeedLimit limit)
? "Generate code"
: "Generate new code";
if (ImGui.Button(label)) {
var salt = new byte[8];
Random.Shared.NextBytes(salt);

var password = new byte[16];
Random.Shared.NextBytes(password);

var hash = new Argon2id(password) {
Iterations = 3,
MemorySize = 65536,
DegreeOfParallelism = 4,
Salt = salt,
}.GetBytes(128);

this.Plugin.Config.OneClickSalt = salt;
this.Plugin.Config.OneClickHash = Base64.Default.Encode(hash);
var password = this.GenerateOneClickKey();
anyChanged = true;

ImGui.SetClipboardText(Base64.Default.Encode(password));
ImGui.SetClipboardText(password);
this.Plugin.NotificationManager.AddNotification(new Notification {
Type = NotificationType.Info,
Content = "Code copied to clipboard. Paste it on the Heliosphere website.",
Expand Down Expand Up @@ -335,4 +321,24 @@ ref this.Plugin.Config.OneClickCollectionId
this.Plugin.SaveConfig();
}
}

internal string GenerateOneClickKey() {
var salt = new byte[8];
Random.Shared.NextBytes(salt);

var password = new byte[16];
Random.Shared.NextBytes(password);

var hash = new Argon2id(password) {
Iterations = 3,
MemorySize = 65536,
DegreeOfParallelism = 4,
Salt = salt,
}.GetBytes(128);

this.Plugin.Config.OneClickSalt = salt;
this.Plugin.Config.OneClickHash = Base64.Default.Encode(hash);

return Base64.Default.Encode(password);
}
}

0 comments on commit afa3b00

Please sign in to comment.