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