diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs
index d9680a3..148c966 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs
@@ -38,6 +38,7 @@ namespace WelsonJS.Launcher
{
e.Cancel = true;
this.Hide();
+ this.ShowInTaskbar = false;
notifyIcon1.Visible = true;
}
base.OnFormClosing(e);
@@ -47,6 +48,7 @@ namespace WelsonJS.Launcher
{
this.Show();
this.WindowState = FormWindowState.Normal;
+ this.ShowInTaskbar = true;
this.Focus();
notifyIcon1.Visible = false;
}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs
index 8ace81e..a7248c1 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs
@@ -2,6 +2,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Windows.Forms;
using WelsonJS.Launcher.Tools;
@@ -9,14 +10,25 @@ namespace WelsonJS.Launcher
{
internal static class Program
{
+ static Mutex mutex;
public static ResourceServer resourceServer;
[STAThread]
static void Main()
{
+ mutex = new Mutex(true, "WelsonJS.Launcher.Mutex", out bool isMutexNotExists);
+ if (!isMutexNotExists)
+ {
+ MessageBox.Show("WelsonJS Launcher already running.");
+ return;
+ }
+
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
+
+ mutex.ReleaseMutex();
+ mutex.Dispose();
}
public static void RunCommandPrompt(string workingDirectory, string entryFileName, string scriptName, bool isConsoleApplication = false, bool isInteractiveServiceAapplication = false)
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Tools/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Tools/ResourceServer.cs
index bfea1b8..2f76b0e 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/Tools/ResourceServer.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Tools/ResourceServer.cs
@@ -105,7 +105,7 @@ namespace WelsonJS.Launcher.Tools
const string devtoolsPrefix = "devtools/";
if (path.StartsWith(devtoolsPrefix, StringComparison.OrdinalIgnoreCase))
{
- await ServeDevTools(context, path.Substring(devtoolsPrefix.Length - 1));
+ await ServeDevTools(context, path.Substring(devtoolsPrefix.Length));
return;
}
@@ -125,6 +125,14 @@ namespace WelsonJS.Launcher.Tools
return;
}
+ // Serve TFA request
+ const string tfaPrefix = "tfa/";
+ if (path.StartsWith(tfaPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ ServeTfaRequest(context, path.Substring(tfaPrefix.Length));
+ return;
+ }
+
// Serve a resource
ServeResource(context, GetResource(_resourceName), "text/html");
}
@@ -170,7 +178,7 @@ namespace WelsonJS.Launcher.Tools
{
using (HttpClient client = new HttpClient())
{
- string url = "http://localhost:9222" + endpoint;
+ string url = "http://localhost:9222/" + endpoint;
string data = await client.GetStringAsync(url);
ServeResource(context, data, "application/json");
@@ -251,6 +259,24 @@ namespace WelsonJS.Launcher.Tools
}
}
+ private void ServeTfaRequest(HttpListenerContext context, string endpoint)
+ {
+ Tfa _tfa = new Tfa();
+
+ if (endpoint.Equals("pubkey"))
+ {
+ ServeResource(context, _tfa.GetPubKey(), "text/plain", 200);
+ return;
+ }
+
+ ServeResource(context);
+ }
+
+ private void ServeResource(HttpListenerContext context)
+ {
+ ServeResource(context, "Not Found", "application/xml", 404);
+ }
+
private void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200)
{
string xmlHeader = "";
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Tools/Tfa.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Tools/Tfa.cs
new file mode 100644
index 0000000..615e668
--- /dev/null
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Tools/Tfa.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Collections.Generic;
+
+namespace WelsonJS.Launcher.Tools
+{
+ public class Tfa
+ {
+ private const string Base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+ public int GetOtp(string key)
+ {
+ byte[] binaryKey = DecodeBase32(key.Replace(" ", ""));
+ long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 30;
+ byte[] timestampBytes = BitConverter.GetBytes(timestamp);
+ Array.Reverse(timestampBytes); // Ensure big-endian order
+
+ using (var hmac = new HMACSHA1(binaryKey))
+ {
+ byte[] hash = hmac.ComputeHash(timestampBytes);
+ int offset = hash[hash.Length - 1] & 0xF;
+
+ int otp = ((hash[offset] & 0x7F) << 24) |
+ ((hash[offset + 1] & 0xFF) << 16) |
+ ((hash[offset + 2] & 0xFF) << 8) |
+ (hash[offset + 3] & 0xFF);
+
+ return otp % 1000000; // Ensure 6-digit OTP
+ }
+ }
+
+ public string GetPubKey()
+ {
+ using (var rng = RandomNumberGenerator.Create())
+ {
+ var key = new char[16];
+ var randomBytes = new byte[16];
+ rng.GetBytes(randomBytes);
+
+ for (int i = 0; i < 16; i++)
+ {
+ key[i] = Base32Chars[randomBytes[i] % Base32Chars.Length];
+ }
+
+ return string.Join(" ", Enumerable.Range(0, 4).Select(i => new string(key, i * 4, 4)));
+ }
+ }
+
+ private static byte[] DecodeBase32(string key)
+ {
+ int buffer = 0, bitsLeft = 0;
+ var binaryKey = new List();
+
+ foreach (char c in key)
+ {
+ int value = Base32Chars.IndexOf(c);
+ if (value < 0) continue; // Ignore invalid characters
+
+ buffer = (buffer << 5) + value;
+ bitsLeft += 5;
+ if (bitsLeft >= 8)
+ {
+ bitsLeft -= 8;
+ binaryKey.Add((byte)((buffer >> bitsLeft) & 0xFF));
+ }
+ }
+ return binaryKey.ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
index d037bbf..3525d39 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj
@@ -101,6 +101,7 @@
GlobalSettingsForm.cs
+
EnvForm.cs