diff --git a/App.xaml b/App.xaml
deleted file mode 100644
index a620f35..0000000
--- a/App.xaml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
diff --git a/App.xaml.cs b/App.xaml.cs
deleted file mode 100644
index 5f61e58..0000000
--- a/App.xaml.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.Configuration;
-using System.Data;
-using System.Windows;
-
-namespace msOps;
-
-///
-/// Interaction logic for App.xaml
-///
-public partial class App : Application
-{
-}
-
diff --git a/Assets/App.ico b/Assets/App.ico
new file mode 100644
index 0000000..b6a9a35
Binary files /dev/null and b/Assets/App.ico differ
diff --git a/MainWindow.xaml b/MainWindow.xaml
deleted file mode 100644
index 594b873..0000000
--- a/MainWindow.xaml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
deleted file mode 100644
index c5f2725..0000000
--- a/MainWindow.xaml.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Text;
-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;
-
-namespace msOps;
-
-///
-/// Interaction logic for MainWindow.xaml
-///
-public partial class MainWindow : Window
-{
- public MainWindow()
- {
- InitializeComponent();
- }
-}
\ No newline at end of file
diff --git a/NetKit.csproj b/NetKit.csproj
new file mode 100644
index 0000000..8dff929
--- /dev/null
+++ b/NetKit.csproj
@@ -0,0 +1,33 @@
+
+
+
+ WinExe
+ net8.0-windows
+ enable
+ enable
+ true
+ true
+ NetKit
+ NetKit
+
+ Assets\App.ico
+
+
+
+
+
+
+
+
+ App.xaml
+
+
+ MainWindow.xaml
+
+
+
+
+
+
+
+
diff --git a/README.MD b/README.MD
index 0dba219..2ff40b7 100644
--- a/README.MD
+++ b/README.MD
@@ -1,69 +1,27 @@
-# DevOps Helper – Windows System Tray App
+# NetKit
-A lightweight Windows system tray application built with **C# (.NET + WPF)** to speed up common DevOps tasks.
-Initial features:
-- Run fast HTTP(S) requests against a domain (using `HttpClient`).
-- Check SSL certificate expiry natively in C# (no OpenSSL/WSL required).
+A tiny Windows tray app for quick network checks. It runs in the system tray and pops open with a global hotkey.
----
+## Features
+- HTTP GET/POST with headers and body
+- SSL/TLS certificate details and expiry
+- Redirect chain viewer
+- DNS lookups (A, AAAA, CNAME, MX, TXT, NS, PTR)
-## 🚀 Plan & Setup
+## Download & Run
+- Windows 10/11.
+- Download the latest Release ZIP, extract, and run `NetKit.exe`.
+- A tray icon appears. Press `Win + ' (apostrophe)` to show/hide the window or double click the tray icon.
+- To stop the app just right-click the tray icon and click on Close.
-### 1. Tech Stack
-- **C# (.NET 8)** – modern runtime.
-- **WPF** – flexible, stylable UI (borderless, rounded corners).
-- **NotifyIcon** (via WinForms interop) – system tray integration.
-- **HttpClient** – native HTTP(S) requests (faster & cleaner than `curl.exe`).
+## Notes
+- Settings are saved to `%AppData%\\NetKit\\settings.json`.
----
+## Build From Source
+- Prerequisite: .NET 8 SDK.
+- Restore/build: `dotnet restore` then `dotnet build -c Release`.
-### 2. Project Bootstrap
-1. Create a new **WPF App (.NET)** in Visual Studio or via CLI:
- ```bash
- dotnet new wpf -n msOps
----
+## Optional single-file build (can be larger and slower to start for WPF):
+ - `dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true`
-### 3. System Tray & Hotkey
-- Add tray icon context menu: **Open UI** | **Exit**.
-- Register a **global hotkey** (e.g. `Ctrl+Alt+Space`) → opens the UI instantly.
-
----
-
-### 4. Popup UI
-- Create a **borderless WPF window** styled like a MacOS panel:
- - Rounded corners, shadows.
- - Input field for domain.
- - Toggle for `http` / `https`.
- - Buttons for **Curl** and **SSL Check**.
-
----
-
-### 5. Features
-- **HTTP Request**
- - Use `HttpClient` to send requests directly from C#.
- - Capture and display headers or response.
-
-- **SSL Expiry Check**
- - Use `TcpClient` + `SslStream` to fetch the server certificate.
- - Display expiry date + remaining days.
-
----
-
-### 6. Quality of Life
-- Persist last used domain (e.g. in `Properties.Settings`).
-- Optionally auto-run on Windows startup.
-- Error handling for bad domains / network issues.
-
----
-
-### 7. Polish
-- Improve styling (minimal, clean, MacOS-like).
-- Add keyboard navigation.
-- Add tray notifications (e.g. SSL expiring soon).
-
----
-
-### 8. Future Extensions
-- Add utilities like **ping** or **DNS lookup**.
-- Integrate with cloud CLIs (AWS, Azure, GCP).
-- Support “favorite domains” or quick actions.
\ No newline at end of file
+ ##
\ No newline at end of file
diff --git a/Services/DnsLookup.cs b/Services/DnsLookup.cs
new file mode 100644
index 0000000..0ab4df0
--- /dev/null
+++ b/Services/DnsLookup.cs
@@ -0,0 +1,534 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading.Tasks;
+using DnsClient;
+
+namespace NetKit;
+
+public class DnsResult
+{
+ public bool IsSuccess { get; set; }
+ public string Error { get; set; } = string.Empty;
+ public string Domain { get; set; } = string.Empty;
+ public string RecordType { get; set; } = string.Empty;
+ public List Records { get; set; } = new();
+ public TimeSpan QueryTime { get; set; }
+ public string DnsServer { get; set; } = string.Empty;
+}
+
+public class DnsRecord
+{
+ public string Type { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public string Value { get; set; } = string.Empty;
+ public int TTL { get; set; }
+ public int Priority { get; set; } // For MX records
+}
+
+public class DnsLookup : IDisposable
+{
+ private readonly LookupClient _dnsClient;
+ private readonly SemaphoreSlim _concurrencySemaphore;
+ private bool _disposed;
+ private CancellationTokenSource? _shutdownCts;
+
+ public DnsLookup()
+ {
+ // Configure DNS client with timeouts and limits
+ var options = new LookupClientOptions
+ {
+ Timeout = TimeSpan.FromSeconds(10), // Prevent hanging queries
+ Retries = 2,
+ UseCache = true,
+ CacheFailedResults = true
+ };
+
+ _dnsClient = new LookupClient(options);
+
+ // Limit concurrent DNS operations to prevent resource exhaustion
+ _concurrencySemaphore = new SemaphoreSlim(5, 5);
+ _shutdownCts = new CancellationTokenSource();
+ }
+
+ public async Task LookupAsync(string domain, string recordType = "A")
+ {
+ var result = new DnsResult
+ {
+ Domain = domain,
+ RecordType = recordType
+ };
+
+ var startTime = DateTime.UtcNow;
+
+ // Check if we're disposed/shutting down
+ if (_disposed || _shutdownCts?.Token.IsCancellationRequested == true)
+ {
+ result.Error = "DNS lookup cancelled - service is shutting down";
+ return result;
+ }
+
+ // Acquire semaphore to limit concurrent operations
+ if (!await _concurrencySemaphore.WaitAsync(TimeSpan.FromSeconds(5), _shutdownCts?.Token ?? CancellationToken.None).ConfigureAwait(false))
+ {
+ result.Error = "DNS lookup timeout - too many concurrent operations";
+ return result;
+ }
+
+ try
+ {
+ if (string.IsNullOrWhiteSpace(domain))
+ {
+ result.Error = "Domain cannot be empty";
+ return result;
+ }
+
+ // Validate domain length to prevent memory issues
+ if (domain.Length > 255)
+ {
+ result.Error = "Domain name too long (max 255 characters)";
+ return result;
+ }
+
+ // Clean up domain (remove protocol if present)
+ domain = domain.Replace("http://", "").Replace("https://", "").Split('/')[0];
+
+ result.DnsServer = GetDnsServers().FirstOrDefault() ?? "System Default";
+
+ switch (recordType.ToUpper())
+ {
+ case "A":
+ await LookupARecords(domain, result);
+ break;
+ case "AAAA":
+ await LookupAAAARecords(domain, result);
+ break;
+ case "CNAME":
+ await LookupCNAMERecords(domain, result);
+ break;
+ case "MX":
+ await LookupMXRecords(domain, result);
+ break;
+ case "TXT":
+ await LookupTXTRecords(domain, result);
+ break;
+ case "NS":
+ await LookupNSRecords(domain, result);
+ break;
+ case "PTR":
+ await LookupPTRRecords(domain, result);
+ break;
+ case "ALL":
+ await LookupAllRecords(domain, result);
+ break;
+ default:
+ result.Error = $"Unsupported record type: {recordType}";
+ return result;
+ }
+
+ result.QueryTime = DateTime.UtcNow - startTime;
+ result.IsSuccess = result.Records.Count > 0 || string.IsNullOrEmpty(result.Error);
+ }
+ catch (OperationCanceledException)
+ {
+ result.Error = "DNS lookup was cancelled";
+ result.QueryTime = DateTime.UtcNow - startTime;
+ }
+ catch (ObjectDisposedException)
+ {
+ result.Error = "DNS service is disposed";
+ result.QueryTime = DateTime.UtcNow - startTime;
+ }
+ catch (Exception ex)
+ {
+ result.Error = $"DNS lookup failed: {ex.Message}";
+ result.QueryTime = DateTime.UtcNow - startTime;
+ }
+ finally
+ {
+ // Always release the semaphore
+ _concurrencySemaphore?.Release();
+ }
+
+ return result;
+ }
+
+ private async Task LookupARecords(string domain, DnsResult result)
+ {
+ try
+ {
+ // Use ConfigureAwait(false) for better performance in async contexts
+ var addresses = await Dns.GetHostAddressesAsync(domain).ConfigureAwait(false);
+ foreach (var address in addresses.Where(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork))
+ {
+ result.Records.Add(new DnsRecord
+ {
+ Type = "A",
+ Name = domain,
+ Value = address.ToString(),
+ TTL = 0 // .NET doesn't provide TTL info easily
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ result.Error = ex.Message;
+ }
+ }
+
+ private async Task LookupAAAARecords(string domain, DnsResult result)
+ {
+ try
+ {
+ var addresses = await Dns.GetHostAddressesAsync(domain).ConfigureAwait(false);
+ foreach (var address in addresses.Where(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6))
+ {
+ result.Records.Add(new DnsRecord
+ {
+ Type = "AAAA",
+ Name = domain,
+ Value = address.ToString(),
+ TTL = 0
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ result.Error = ex.Message;
+ }
+ }
+
+ private async Task LookupCNAMERecords(string domain, DnsResult result)
+ {
+ try
+ {
+ var hostEntry = await Dns.GetHostEntryAsync(domain).ConfigureAwait(false);
+ if (hostEntry.HostName != domain && !string.IsNullOrEmpty(hostEntry.HostName))
+ {
+ result.Records.Add(new DnsRecord
+ {
+ Type = "CNAME",
+ Name = domain,
+ Value = hostEntry.HostName,
+ TTL = 0
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ result.Error = ex.Message;
+ }
+ }
+
+ private async Task LookupMXRecords(string domain, DnsResult result)
+ {
+ try
+ {
+ _shutdownCts?.Token.ThrowIfCancellationRequested();
+
+ var queryResult = await _dnsClient.QueryAsync(domain, QueryType.MX, cancellationToken: _shutdownCts?.Token ?? CancellationToken.None).ConfigureAwait(false);
+
+ const int maxRecords = 50; // Limit to prevent memory issues
+ var recordCount = 0;
+
+ foreach (var record in queryResult.Answers.MxRecords())
+ {
+ if (recordCount >= maxRecords)
+ {
+ result.Error = $"Too many MX records (showing first {maxRecords})";
+ break;
+ }
+
+ // Validate record data to prevent issues
+ var exchangeValue = record.Exchange?.Value ?? "Invalid Exchange";
+ if (exchangeValue.Length > 500)
+ {
+ exchangeValue = exchangeValue.Substring(0, 497) + "...";
+ }
+
+ result.Records.Add(new DnsRecord
+ {
+ Type = "MX",
+ Name = domain,
+ Value = exchangeValue,
+ TTL = Math.Max(0, Math.Min(record.TimeToLive, int.MaxValue)),
+ Priority = Math.Max(0, Math.Min((int)record.Preference, 65535))
+ });
+
+ recordCount++;
+ }
+
+ if (!result.Records.Any() && string.IsNullOrEmpty(result.Error))
+ {
+ result.Error = "No MX records found";
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ result.Error = "MX lookup was cancelled";
+ }
+ catch (Exception ex)
+ {
+ result.Error = $"MX lookup failed: {ex.Message}";
+ }
+ }
+
+ private async Task LookupTXTRecords(string domain, DnsResult result)
+ {
+ try
+ {
+ _shutdownCts?.Token.ThrowIfCancellationRequested();
+
+ var queryResult = await _dnsClient.QueryAsync(domain, QueryType.TXT, cancellationToken: _shutdownCts?.Token ?? CancellationToken.None).ConfigureAwait(false);
+
+ const int maxRecords = 25; // Lower limit for TXT as they can be very large
+ var recordCount = 0;
+
+ foreach (var record in queryResult.Answers.TxtRecords())
+ {
+ if (recordCount >= maxRecords)
+ {
+ result.Error = $"Too many TXT records (showing first {maxRecords})";
+ break;
+ }
+
+ // TXT records can be extremely large, limit them
+ var txtValue = string.Join(" ", record.Text);
+ if (txtValue.Length > 2000) // Prevent huge TXT records from consuming memory
+ {
+ txtValue = txtValue.Substring(0, 1997) + "...";
+ }
+
+ result.Records.Add(new DnsRecord
+ {
+ Type = "TXT",
+ Name = domain,
+ Value = txtValue,
+ TTL = Math.Max(0, Math.Min(record.TimeToLive, int.MaxValue))
+ });
+
+ recordCount++;
+ }
+
+ if (!result.Records.Any() && string.IsNullOrEmpty(result.Error))
+ {
+ result.Error = "No TXT records found";
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ result.Error = "TXT lookup was cancelled";
+ }
+ catch (Exception ex)
+ {
+ result.Error = $"TXT lookup failed: {ex.Message}";
+ }
+ }
+
+ private async Task LookupNSRecords(string domain, DnsResult result)
+ {
+ try
+ {
+ var queryResult = await _dnsClient.QueryAsync(domain, QueryType.NS).ConfigureAwait(false);
+
+ foreach (var record in queryResult.Answers.NsRecords())
+ {
+ result.Records.Add(new DnsRecord
+ {
+ Type = "NS",
+ Name = domain,
+ Value = record.NSDName.Value,
+ TTL = Math.Max(0, Math.Min(record.TimeToLive, int.MaxValue))
+ });
+ }
+
+ if (!result.Records.Any())
+ {
+ result.Error = "No NS records found";
+ }
+ }
+ catch (Exception ex)
+ {
+ result.Error = $"NS lookup failed: {ex.Message}";
+ }
+ }
+
+ private async Task LookupPTRRecords(string domain, DnsResult result)
+ {
+ try
+ {
+ // Assume domain is an IP address for PTR lookup
+ if (IPAddress.TryParse(domain, out var ipAddress))
+ {
+ var hostEntry = await Dns.GetHostEntryAsync(ipAddress).ConfigureAwait(false);
+ result.Records.Add(new DnsRecord
+ {
+ Type = "PTR",
+ Name = domain,
+ Value = hostEntry.HostName,
+ TTL = 0
+ });
+ }
+ else
+ {
+ result.Error = "PTR lookup requires an IP address";
+ }
+ }
+ catch (Exception ex)
+ {
+ result.Error = ex.Message;
+ }
+ }
+
+ private async Task LookupAllRecords(string domain, DnsResult result)
+ {
+ // Run all lookups concurrently for better performance
+ var tasks = new List>
+ {
+ LookupRecordTypeAsync(domain, "A"),
+ LookupRecordTypeAsync(domain, "AAAA"),
+ LookupRecordTypeAsync(domain, "CNAME"),
+ LookupRecordTypeAsync(domain, "MX"),
+ LookupRecordTypeAsync(domain, "TXT"),
+ LookupRecordTypeAsync(domain, "NS"),
+ LookupRecordTypeAsync(domain, "PTR") // Try reverse lookup too if it's an IP
+ };
+
+ var results = await Task.WhenAll(tasks);
+
+ // Combine all successful results
+ foreach (var lookupResult in results)
+ {
+ if (lookupResult.Records.Count > 0)
+ {
+ result.Records.AddRange(lookupResult.Records);
+ }
+ }
+
+ // Clear error if we got some results
+ if (result.Records.Count > 0)
+ {
+ result.Error = string.Empty;
+ }
+ else
+ {
+ result.Error = "No records found for any record type";
+ }
+ }
+
+ private async Task LookupRecordTypeAsync(string domain, string recordType)
+ {
+ var tempResult = new DnsResult
+ {
+ Domain = domain,
+ RecordType = recordType
+ };
+
+ try
+ {
+ switch (recordType.ToUpper())
+ {
+ case "A":
+ await LookupARecords(domain, tempResult);
+ break;
+ case "AAAA":
+ await LookupAAAARecords(domain, tempResult);
+ break;
+ case "CNAME":
+ await LookupCNAMERecords(domain, tempResult);
+ break;
+ case "MX":
+ await LookupMXRecords(domain, tempResult);
+ break;
+ case "TXT":
+ await LookupTXTRecords(domain, tempResult);
+ break;
+ case "NS":
+ await LookupNSRecords(domain, tempResult);
+ break;
+ case "PTR":
+ await LookupPTRRecords(domain, tempResult);
+ break;
+ }
+ }
+ catch
+ {
+ // Ignore individual lookup failures for ALL mode
+ }
+
+ return tempResult;
+ }
+
+ private List GetDnsServers()
+ {
+ var dnsServers = new List();
+
+ try
+ {
+ var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
+ foreach (var networkInterface in networkInterfaces)
+ {
+ if (networkInterface.OperationalStatus == OperationalStatus.Up)
+ {
+ var ipProperties = networkInterface.GetIPProperties();
+ foreach (var dnsAddress in ipProperties.DnsAddresses)
+ {
+ if (!dnsServers.Contains(dnsAddress.ToString()))
+ {
+ dnsServers.Add(dnsAddress.ToString());
+ }
+ }
+ }
+ }
+ }
+ catch
+ {
+ // Fallback to common DNS servers
+ dnsServers.AddRange(new[] { "8.8.8.8", "1.1.1.1" });
+ }
+
+ return dnsServers;
+ }
+
+ public void Shutdown()
+ {
+ try
+ {
+ // Signal all operations to cancel
+ _shutdownCts?.Cancel();
+ }
+ catch
+ {
+ // Ignore cancellation exceptions during shutdown
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ try
+ {
+ // Cancel any ongoing operations
+ Shutdown();
+
+ // Wait briefly for operations to complete
+ Task.Delay(1000).Wait();
+
+ // Dispose resources (LookupClient doesn't implement IDisposable)
+ _concurrencySemaphore?.Dispose();
+ _shutdownCts?.Dispose();
+ }
+ catch
+ {
+ // Ignore exceptions during disposal
+ }
+ finally
+ {
+ _disposed = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/GlobalHotkey.cs b/Services/GlobalHotkey.cs
new file mode 100644
index 0000000..6651633
--- /dev/null
+++ b/Services/GlobalHotkey.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+using System.Windows.Interop;
+
+namespace NetKit;
+
+public class GlobalHotkey : IDisposable
+{
+ private const int WM_HOTKEY = 0x0312;
+ private const int MOD_CONTROL = 0x0002;
+ private const int MOD_ALT = 0x0001;
+ private const int MOD_WIN = 0x0008;
+
+ private readonly int _id;
+ private readonly IntPtr _hWnd;
+ private bool _disposed = false;
+ private bool _isRegistered = false;
+
+ private int _currentModifiers = MOD_WIN;
+ private int _currentVirtualKey = 0xDE;
+
+ public event Action? HotkeyPressed;
+
+ [DllImport("user32.dll")]
+ private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vlc);
+
+ [DllImport("user32.dll")]
+ private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
+
+ public GlobalHotkey(IntPtr windowHandle, int hotkeyId = 1)
+ {
+ _hWnd = windowHandle;
+ _id = hotkeyId;
+ }
+
+ public bool Register()
+ {
+ return Register(_currentModifiers, _currentVirtualKey);
+ }
+
+ public bool Register(int modifiers, int virtualKey)
+ {
+ try
+ {
+ // Unregister existing hotkey if registered
+ if (_isRegistered)
+ {
+ UnregisterHotKey(_hWnd, _id);
+ _isRegistered = false;
+ }
+
+ _currentModifiers = modifiers;
+ _currentVirtualKey = virtualKey;
+
+ var success = RegisterHotKey(_hWnd, _id, modifiers, virtualKey);
+ _isRegistered = success;
+ return success;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public bool UpdateHotkey(bool useCtrl, bool useAlt, bool useWin, int virtualKey)
+ {
+ int modifiers = 0;
+ if (useCtrl) modifiers |= MOD_CONTROL;
+ if (useAlt) modifiers |= MOD_ALT;
+ if (useWin) modifiers |= MOD_WIN;
+
+ return Register(modifiers, virtualKey);
+ }
+
+ public void Unregister()
+ {
+ try
+ {
+ if (_isRegistered)
+ {
+ UnregisterHotKey(_hWnd, _id);
+ _isRegistered = false;
+ }
+ }
+ catch
+ {
+ // Ignore unregister errors
+ }
+ }
+
+ public bool ProcessHotkey(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
+ {
+ if (msg == WM_HOTKEY && wParam.ToInt32() == _id)
+ {
+ HotkeyPressed?.Invoke();
+ handled = true;
+ return true;
+ }
+ return false;
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Unregister();
+ _disposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/HttpService.cs b/Services/HttpService.cs
index e47fd09..66fe915 100644
--- a/Services/HttpService.cs
+++ b/Services/HttpService.cs
@@ -1,6 +1,112 @@
-namespace msOps;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+
+namespace NetKit;
public class HttpService
{
+ private readonly HttpClient _httpClient;
+
+ public HttpService()
+ {
+ _httpClient = new HttpClient();
+ _httpClient.DefaultRequestHeaders.Add("User-Agent", "NetKit/1.0");
+ }
+
+ private string EnsureProtocol(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ return url;
+
+ url = url.Trim();
+
+ if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ return $"https://{url}";
+ }
+
+ return url;
+ }
+
+ public async Task GetAsync(string url)
+ {
+ try
+ {
+ url = EnsureProtocol(url);
+ var response = await _httpClient.GetAsync(url);
+ var content = await response.Content.ReadAsStringAsync();
+
+ return new HttpResult
+ {
+ IsSuccess = response.IsSuccessStatusCode,
+ StatusCode = (int)response.StatusCode,
+ StatusText = response.ReasonPhrase ?? "",
+ Headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)),
+ ContentHeaders = response.Content.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)),
+ Content = content,
+ ResponseTime = TimeSpan.Zero // We'll add timing later
+ };
+ }
+ catch (Exception ex)
+ {
+ return new HttpResult
+ {
+ IsSuccess = false,
+ Error = ex.Message
+ };
+ }
+ }
+ public async Task PostAsync(string url, object? data = null)
+ {
+ try
+ {
+ url = EnsureProtocol(url);
+
+ var content = data != null
+ ? new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json")
+ : new StringContent("");
+
+ var response = await _httpClient.PostAsync(url, content);
+ var responseContent = await response.Content.ReadAsStringAsync();
+
+ return new HttpResult
+ {
+ IsSuccess = response.IsSuccessStatusCode,
+ StatusCode = (int)response.StatusCode,
+ StatusText = response.ReasonPhrase ?? "",
+ Headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)),
+ ContentHeaders = response.Content.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)),
+ Content = responseContent,
+ ResponseTime = TimeSpan.Zero
+ };
+ }
+ catch (Exception ex)
+ {
+ return new HttpResult
+ {
+ IsSuccess = false,
+ Error = ex.Message
+ };
+ }
+ }
+
+ public void Dispose()
+ {
+ _httpClient?.Dispose();
+ }
+}
+
+public class HttpResult
+{
+ public bool IsSuccess { get; set; }
+ public int StatusCode { get; set; }
+ public string StatusText { get; set; } = "";
+ public Dictionary Headers { get; set; } = new();
+ public Dictionary ContentHeaders { get; set; } = new();
+ public string Content { get; set; } = "";
+ public TimeSpan ResponseTime { get; set; }
+ public string? Error { get; set; }
}
diff --git a/Services/RedirectChecker.cs b/Services/RedirectChecker.cs
new file mode 100644
index 0000000..4ab014e
--- /dev/null
+++ b/Services/RedirectChecker.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace NetKit;
+
+public class RedirectResult
+{
+ public bool IsSuccess { get; set; }
+ public string Error { get; set; } = string.Empty;
+ public List RedirectChain { get; set; } = new();
+ public string FinalUrl { get; set; } = string.Empty;
+ public int TotalRedirects { get; set; }
+ public TimeSpan TotalTime { get; set; }
+}
+
+public class RedirectStep
+{
+ public string FromUrl { get; set; } = string.Empty;
+ public string ToUrl { get; set; } = string.Empty;
+ public int StatusCode { get; set; }
+ public string StatusText { get; set; } = string.Empty;
+ public TimeSpan ResponseTime { get; set; }
+ public Dictionary Headers { get; set; } = new();
+}
+
+public class RedirectChecker : IDisposable
+{
+ private readonly HttpClient _httpClient;
+ private bool _disposed;
+
+ public RedirectChecker()
+ {
+ var handler = new HttpClientHandler()
+ {
+ AllowAutoRedirect = false // We want to handle redirects manually
+ };
+
+ _httpClient = new HttpClient(handler)
+ {
+ Timeout = TimeSpan.FromSeconds(30)
+ };
+
+ _httpClient.DefaultRequestHeaders.Add("User-Agent",
+ "NetKit-RedirectChecker/1.0 (Windows)");
+ }
+
+ public async Task CheckRedirectsAsync(string url, int maxRedirects = 10)
+ {
+ var result = new RedirectResult();
+ var startTime = DateTime.UtcNow;
+
+ try
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ result.Error = "URL cannot be empty";
+ return result;
+ }
+
+ // Ensure URL has a protocol
+ if (!url.StartsWith("http://") && !url.StartsWith("https://"))
+ {
+ url = "https://" + url;
+ }
+
+ var currentUrl = url;
+ var redirectCount = 0;
+
+ while (redirectCount < maxRedirects)
+ {
+ var stepStartTime = DateTime.UtcNow;
+
+ try
+ {
+ using var response = await _httpClient.GetAsync(currentUrl);
+ var stepTime = DateTime.UtcNow - stepStartTime;
+
+ var step = new RedirectStep
+ {
+ FromUrl = currentUrl,
+ StatusCode = (int)response.StatusCode,
+ StatusText = response.StatusCode.ToString(),
+ ResponseTime = stepTime
+ };
+
+ // Capture important headers
+ foreach (var header in response.Headers)
+ {
+ step.Headers[header.Key] = string.Join(", ", header.Value);
+ }
+
+ // Check for redirect status codes
+ if ((int)response.StatusCode >= 300 && (int)response.StatusCode < 400)
+ {
+ var locationHeader = response.Headers.Location;
+ if (locationHeader != null)
+ {
+ var nextUrl = locationHeader.IsAbsoluteUri
+ ? locationHeader.ToString()
+ : new Uri(new Uri(currentUrl), locationHeader).ToString();
+
+ step.ToUrl = nextUrl;
+ result.RedirectChain.Add(step);
+
+ currentUrl = nextUrl;
+ redirectCount++;
+ continue;
+ }
+ else
+ {
+ result.Error = $"Redirect response {response.StatusCode} without Location header";
+ return result;
+ }
+ }
+ else
+ {
+ // Final destination reached
+ step.ToUrl = currentUrl;
+ result.RedirectChain.Add(step);
+ result.FinalUrl = currentUrl;
+ result.TotalRedirects = redirectCount;
+ result.TotalTime = DateTime.UtcNow - startTime;
+ result.IsSuccess = true;
+ return result;
+ }
+ }
+ catch (HttpRequestException ex)
+ {
+ result.Error = $"HTTP error at step {redirectCount + 1}: {ex.Message}";
+ return result;
+ }
+ catch (TaskCanceledException ex)
+ {
+ result.Error = ex.InnerException is TimeoutException
+ ? "Request timeout"
+ : "Request canceled";
+ return result;
+ }
+ }
+
+ // Max redirects reached - still show the chain
+ result.TotalRedirects = redirectCount;
+ result.TotalTime = DateTime.UtcNow - startTime;
+ result.IsSuccess = true; // We want to show the results even if max reached
+ result.FinalUrl = currentUrl;
+
+ // Add a final step showing the redirect limit was reached
+ result.RedirectChain.Add(new RedirectStep
+ {
+ FromUrl = currentUrl,
+ ToUrl = "Maximum redirect limit reached",
+ StatusCode = 0,
+ StatusText = "ERR_TOO_MANY_REDIRECTS",
+ ResponseTime = TimeSpan.Zero
+ });
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ result.Error = $"Unexpected error: {ex.Message}";
+ return result;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _httpClient?.Dispose();
+ _disposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/SettingsService.cs b/Services/SettingsService.cs
new file mode 100644
index 0000000..ca07bbe
--- /dev/null
+++ b/Services/SettingsService.cs
@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace NetKit;
+
+public class HotkeySettings
+{
+ public bool UseCtrl { get; set; }
+ public bool UseAlt { get; set; }
+ public bool UseWin { get; set; } = true;
+ public int VirtualKeyCode { get; set; } = 0xDE; // Default to apostrophe
+ public string KeyDisplay { get; set; } = "'";
+}
+
+public class AppSettings
+{
+ public HotkeySettings Hotkey { get; set; } = new();
+}
+
+public class SettingsService
+{
+ private readonly string _settingsPath;
+ private AppSettings _settings;
+
+ public AppSettings Settings => _settings;
+
+ public SettingsService()
+ {
+ var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ var netKitFolder = Path.Combine(appDataPath, "NetKit");
+
+ if (!Directory.Exists(netKitFolder))
+ {
+ Directory.CreateDirectory(netKitFolder);
+ }
+
+ _settingsPath = Path.Combine(netKitFolder, "settings.json");
+ _settings = LoadSettings();
+ }
+
+ private AppSettings LoadSettings()
+ {
+ try
+ {
+ if (File.Exists(_settingsPath))
+ {
+ var json = File.ReadAllText(_settingsPath);
+ var settings = JsonSerializer.Deserialize(json);
+ return settings ?? new AppSettings();
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to load settings: {ex.Message}");
+ }
+
+ return new AppSettings();
+ }
+
+ public void SaveSettings()
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ });
+ File.WriteAllText(_settingsPath, json);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to save settings: {ex.Message}");
+ }
+ }
+
+ public void UpdateHotkey(bool useCtrl, bool useAlt, bool useWin, int virtualKeyCode, string keyDisplay)
+ {
+ _settings.Hotkey.UseCtrl = useCtrl;
+ _settings.Hotkey.UseAlt = useAlt;
+ _settings.Hotkey.UseWin = useWin;
+ _settings.Hotkey.VirtualKeyCode = virtualKeyCode;
+ _settings.Hotkey.KeyDisplay = keyDisplay;
+ SaveSettings();
+ }
+
+ public string GetHotkeyDisplayText()
+ {
+ var modifiers = new List();
+
+ if (_settings.Hotkey.UseCtrl) modifiers.Add("Ctrl");
+ if (_settings.Hotkey.UseAlt) modifiers.Add("Alt");
+ if (_settings.Hotkey.UseWin) modifiers.Add("Win");
+
+ var modifierText = modifiers.Count > 0 ? string.Join(" + ", modifiers) + " + " : "";
+ return $"{modifierText}{_settings.Hotkey.KeyDisplay}";
+ }
+}
\ No newline at end of file
diff --git a/Services/SslChecker.cs b/Services/SslChecker.cs
index 6e1e1e2..5d58a59 100644
--- a/Services/SslChecker.cs
+++ b/Services/SslChecker.cs
@@ -1,6 +1,100 @@
-namespace msOps;
+using System.Diagnostics;
+using System.Net.Http;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Security.Cryptography.X509Certificates;
+
+namespace NetKit;
public class SslChecker
{
+ public async Task CheckSslCertificateAsync(string hostname, int port = 443)
+ {
+ try
+ {
+ using var tcpClient = new TcpClient();
+ await tcpClient.ConnectAsync(hostname, port);
+
+ using var sslStream = new SslStream(tcpClient.GetStream());
+ await sslStream.AuthenticateAsClientAsync(hostname);
+
+ var certificate = sslStream.RemoteCertificate as X509Certificate2;
+ if (certificate == null)
+ {
+ return new SslCheckResult
+ {
+ IsValid = false,
+ Error = "No certificate found"
+ };
+ }
+
+ return new SslCheckResult
+ {
+ IsValid = true,
+ Subject = certificate.Subject,
+ Issuer = certificate.Issuer,
+ NotBefore = certificate.NotBefore,
+ NotAfter = certificate.NotAfter,
+ IsExpired = certificate.NotAfter < DateTime.Now,
+ DaysUntilExpiry = (certificate.NotAfter - DateTime.Now).Days,
+ Thumbprint = certificate.Thumbprint
+ };
+ }
+ catch (Exception ex)
+ {
+ return new SslCheckResult
+ {
+ IsValid = false,
+ Error = ex.Message
+ };
+ }
+ }
+
+ public async Task CheckHttpConnectionAsync(string hostname, int port = 80)
+ {
+ try
+ {
+ var stopwatch = Stopwatch.StartNew();
+
+ using var httpClient = new HttpClient();
+ httpClient.Timeout = TimeSpan.FromSeconds(10);
+ var url = $"http://{hostname}:{port}";
+ var response = await httpClient.GetAsync(url);
+
+ stopwatch.Stop();
+
+ var serverHeader = response.Headers.Server?.ToString() ?? "";
+
+ return new SslCheckResult
+ {
+ IsValid = true,
+ ResponseTimeMs = (int)stopwatch.ElapsedMilliseconds,
+ ServerHeader = serverHeader
+ };
+ }
+ catch (Exception ex)
+ {
+ return new SslCheckResult
+ {
+ IsValid = false,
+ Error = ex.Message
+ };
+ }
+ }
+}
+
+public class SslCheckResult
+{
+ public bool IsValid { get; set; }
+ public string? Subject { get; set; }
+ public string? Issuer { get; set; }
+ public DateTime NotBefore { get; set; }
+ public DateTime NotAfter { get; set; }
+ public bool IsExpired { get; set; }
+ public int DaysUntilExpiry { get; set; }
+ public string? Thumbprint { get; set; }
+ public string? Error { get; set; }
+ public int ResponseTimeMs { get; set; }
+ public string? ServerHeader { get; set; }
}
diff --git a/Tray/TrayIconManager.cs b/Tray/TrayIconManager.cs
index ee8ca05..b469111 100644
--- a/Tray/TrayIconManager.cs
+++ b/Tray/TrayIconManager.cs
@@ -1,6 +1,85 @@
-namespace msOps;
+using System;
+using System.Drawing;
+using System.Reflection;
+using System.Windows.Forms;
-public class TrayIconManager
+namespace NetKit;
+
+public class TrayIconManager : IDisposable
{
+ private NotifyIcon? _notifyIcon;
+
+ public event Action? ShowWindow;
+ public event Action? ExitApplication;
+
+ public TrayIconManager()
+ {
+ _notifyIcon = new NotifyIcon
+ {
+ Icon = LoadAppIcon() ?? SystemIcons.Application,
+ Visible = true,
+ Text = "NetKit (Win+' to toggle)"
+ };
+
+ var contextMenu = new ContextMenuStrip();
+ contextMenu.Items.Add("Show/Hide Window (Win+')", null, OnShowHideClicked);
+ contextMenu.Items.Add("-"); // Separator
+ contextMenu.Items.Add("Exit", null, OnExitClicked);
+
+ _notifyIcon.ContextMenuStrip = contextMenu;
+ _notifyIcon.DoubleClick += OnDoubleClick;
+ }
+
+ private static Icon? LoadAppIcon()
+ {
+ try
+ {
+ var exe = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
+ if (!string.IsNullOrEmpty(exe))
+ {
+ var icon = Icon.ExtractAssociatedIcon(exe);
+ return icon;
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+ return null;
+ }
+
+ public void UpdateHotkeyText(string hotkeyText)
+ {
+ if (_notifyIcon != null)
+ {
+ _notifyIcon.Text = $"NetKit ({hotkeyText} to toggle)";
+
+ // Update context menu item text too
+ if (_notifyIcon.ContextMenuStrip?.Items.Count > 0)
+ {
+ _notifyIcon.ContextMenuStrip.Items[0].Text = $"Show/Hide Window ({hotkeyText})";
+ }
+ }
+ }
+
+ private void OnShowHideClicked(object? sender, EventArgs e)
+ {
+ ShowWindow?.Invoke();
+ }
+
+ private void OnDoubleClick(object? sender, EventArgs e)
+ {
+ ShowWindow?.Invoke();
+ }
+
+ private void OnExitClicked(object? sender, EventArgs e)
+ {
+ ExitApplication?.Invoke();
+ }
+ public void Dispose()
+ {
+ _notifyIcon?.Dispose();
+ GC.SuppressFinalize(this);
+ }
}
diff --git a/UI/App.xaml b/UI/App.xaml
new file mode 100644
index 0000000..b33e520
--- /dev/null
+++ b/UI/App.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/App.xaml.cs b/UI/App.xaml.cs
new file mode 100644
index 0000000..a802344
--- /dev/null
+++ b/UI/App.xaml.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Windows;
+using System.Windows.Interop;
+
+namespace NetKit;
+
+///
+/// Interaction logic for App.xaml
+///
+public partial class App : System.Windows.Application
+{
+ private GlobalHotkey? _globalHotkey;
+ private SettingsService? _settingsService;
+ private TrayIconManager? _trayIconManager;
+ private MainWindow? _mainWindow;
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ try
+ {
+ base.OnStartup(e);
+
+ // Initialize services first
+ _settingsService = new SettingsService();
+
+ // Create main window but start hidden
+ _mainWindow = new MainWindow(_settingsService);
+ this.MainWindow = _mainWindow;
+ _mainWindow.Closed += (s, _) => { _mainWindow = null; };
+
+ // Initialize tray icon
+ _trayIconManager = new TrayIconManager();
+ _trayIconManager.ShowWindow += ShowOrCreateMainWindow;
+ _trayIconManager.ExitApplication += () => this.Shutdown();
+
+ // Set up global hotkey with a hidden helper window
+ SetupGlobalHotkey();
+
+ // Don't call Show() - window starts hidden
+ }
+ catch (Exception ex)
+ {
+ System.Windows.MessageBox.Show($"Startup Error: {ex.Message}\n\nStack Trace: {ex.StackTrace}", "Application Error");
+ this.Shutdown(1);
+ }
+ }
+
+ private void SetupGlobalHotkey()
+ {
+ try
+ {
+ // Create a hidden helper window just for the hotkey handle
+ var helperWindow = new Window
+ {
+ Width = 1,
+ Height = 1,
+ WindowStyle = WindowStyle.None,
+ ShowInTaskbar = false,
+ Visibility = Visibility.Hidden
+ };
+
+ // Force creation of window handle
+ helperWindow.Show();
+ var windowHandle = new WindowInteropHelper(helperWindow).Handle;
+ helperWindow.Hide();
+
+ // Initialize global hotkey
+ _globalHotkey = new GlobalHotkey(windowHandle);
+ var settings = _settingsService!.Settings.Hotkey;
+
+ // Connect hotkey to show/hide functionality
+ _globalHotkey.HotkeyPressed += ShowOrCreateMainWindow;
+
+ // Add hook for hotkey messages
+ var source = HwndSource.FromHwnd(windowHandle);
+ source?.AddHook(WndProc);
+
+ // Register the hotkey with saved settings
+ if (_globalHotkey.UpdateHotkey(settings.UseCtrl, settings.UseAlt, settings.UseWin, settings.VirtualKeyCode))
+ {
+ _trayIconManager?.UpdateHotkeyText(_settingsService.GetHotkeyDisplayText());
+ }
+ else
+ {
+ // If saved hotkey fails, try default Win+'
+ if (_globalHotkey.UpdateHotkey(false, false, true, 0xDE))
+ {
+ _settingsService.UpdateHotkey(false, false, true, 0xDE, "'");
+ _trayIconManager?.UpdateHotkeyText(_settingsService.GetHotkeyDisplayText());
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Hotkey registration failed: {ex.Message}");
+ }
+ }
+
+ private void ShowOrCreateMainWindow()
+ {
+ try
+ {
+ if (_mainWindow == null || !_mainWindow.IsLoaded)
+ {
+ _mainWindow = new MainWindow(_settingsService!);
+ _mainWindow.Closed += (s, _) => { _mainWindow = null; };
+ this.MainWindow = _mainWindow;
+ }
+
+ _mainWindow.ShowWindow();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to show/create MainWindow: {ex.Message}");
+ }
+ }
+
+ private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
+ {
+ _globalHotkey?.ProcessHotkey(hwnd, msg, wParam, lParam, ref handled);
+ return IntPtr.Zero;
+ }
+
+ public GlobalHotkey? GetGlobalHotkey() => _globalHotkey;
+ public SettingsService? GetSettingsService() => _settingsService;
+ public TrayIconManager? GetTrayIconManager() => _trayIconManager;
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ _globalHotkey?.Dispose();
+ _trayIconManager?.Dispose();
+ base.OnExit(e);
+ }
+}
diff --git a/UI/CustomMessageBox.xaml b/UI/CustomMessageBox.xaml
new file mode 100644
index 0000000..2ba4cad
--- /dev/null
+++ b/UI/CustomMessageBox.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/UI/CustomMessageBox.xaml.cs b/UI/CustomMessageBox.xaml.cs
new file mode 100644
index 0000000..a22f1df
--- /dev/null
+++ b/UI/CustomMessageBox.xaml.cs
@@ -0,0 +1,34 @@
+using System.Windows;
+
+namespace NetKit;
+
+public partial class CustomMessageBox : Window
+{
+ public CustomMessageBox(string message, string title = "NetKit")
+ {
+ InitializeComponent();
+ MessageText.Text = message;
+ Title = title;
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = true;
+ Close();
+ }
+
+ public static void Show(string message, string title = "NetKit")
+ {
+ var messageBox = new CustomMessageBox(message, title);
+ messageBox.ShowDialog();
+ }
+
+ public static void Show(Window owner, string message, string title = "NetKit")
+ {
+ var messageBox = new CustomMessageBox(message, title)
+ {
+ Owner = owner
+ };
+ messageBox.ShowDialog();
+ }
+}
\ No newline at end of file
diff --git a/UI/HotkeyConfigWindow.xaml b/UI/HotkeyConfigWindow.xaml
new file mode 100644
index 0000000..edf7ca5
--- /dev/null
+++ b/UI/HotkeyConfigWindow.xaml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/HotkeyConfigWindow.xaml.cs b/UI/HotkeyConfigWindow.xaml.cs
new file mode 100644
index 0000000..8312129
--- /dev/null
+++ b/UI/HotkeyConfigWindow.xaml.cs
@@ -0,0 +1,359 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace NetKit;
+
+public partial class HotkeyConfigWindow : Window
+{
+ private bool _capturing = false;
+ private readonly HashSet _pressedKeys = new();
+ private readonly SettingsService _settingsService;
+ private int _capturedModifiers = 0;
+ private int _capturedVirtualKey = 0;
+ private string _capturedKeyDisplay = "";
+
+ public bool HotkeyChanged { get; private set; } = false;
+
+ public HotkeyConfigWindow(SettingsService settingsService)
+ {
+ InitializeComponent();
+ _settingsService = settingsService;
+
+ // Set current hotkey display
+ CurrentHotkeyTextBox.Text = _settingsService.GetHotkeyDisplayText();
+
+ // Make window focusable and focus the capture box
+ Loaded += (s, e) =>
+ {
+ Focus();
+ CaptureBox.MouseLeftButtonDown += (sender, args) => StartCapturing();
+ };
+ }
+
+ private void StartCapturing()
+ {
+ _capturing = true;
+ _pressedKeys.Clear();
+ _capturedModifiers = 0;
+ _capturedVirtualKey = 0;
+ _capturedKeyDisplay = "";
+
+ CapturedHotkeyTextBox.Text = "Press keys now...";
+ CapturedHotkeyTextBox.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 255, 255));
+ CaptureBox.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 208, 132));
+
+ StatusTextBlock.Text = "";
+ Focus();
+ }
+
+ private void Window_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (!_capturing) return;
+
+ e.Handled = true;
+
+ // Add key to pressed keys set
+ _pressedKeys.Add(e.Key);
+
+ // Update display in real-time
+ UpdateCapturedDisplay();
+ }
+
+ private void Window_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (!_capturing) return;
+ e.Handled = true;
+ }
+
+ private void UpdateCapturedDisplay()
+ {
+ var modifiers = new List();
+ var regularKey = "";
+
+ _capturedModifiers = 0;
+ _capturedVirtualKey = 0;
+
+ // Process all pressed keys
+ foreach (var key in _pressedKeys)
+ {
+ switch (key)
+ {
+ case Key.LeftCtrl:
+ case Key.RightCtrl:
+ if (!modifiers.Contains("Ctrl"))
+ {
+ modifiers.Add("Ctrl");
+ _capturedModifiers |= 0x0002; // MOD_CONTROL
+ }
+ break;
+
+ case Key.LeftAlt:
+ case Key.RightAlt:
+ if (!modifiers.Contains("Alt"))
+ {
+ modifiers.Add("Alt");
+ _capturedModifiers |= 0x0001; // MOD_ALT
+ }
+ break;
+
+ case Key.LWin:
+ case Key.RWin:
+ if (!modifiers.Contains("Win"))
+ {
+ modifiers.Add("Win");
+ _capturedModifiers |= 0x0008; // MOD_WIN
+ }
+ break;
+
+ case Key.LeftShift:
+ case Key.RightShift:
+ if (!modifiers.Contains("Shift"))
+ {
+ modifiers.Add("Shift");
+ _capturedModifiers |= 0x0004; // MOD_SHIFT
+ }
+ break;
+
+ default:
+ // This is a regular key
+ if (string.IsNullOrEmpty(regularKey))
+ {
+ regularKey = GetKeyDisplayName(key);
+ _capturedVirtualKey = KeyToVirtualKey(key);
+ _capturedKeyDisplay = regularKey;
+ }
+ break;
+ }
+ }
+
+ // Build display text
+ var displayText = new StringBuilder();
+ if (modifiers.Count > 0)
+ {
+ displayText.Append(string.Join(" + ", modifiers));
+ if (!string.IsNullOrEmpty(regularKey))
+ {
+ displayText.Append(" + ");
+ displayText.Append(regularKey);
+ }
+ }
+ else if (!string.IsNullOrEmpty(regularKey))
+ {
+ displayText.Append(regularKey);
+ }
+ else
+ {
+ displayText.Append("Press keys...");
+ }
+
+ CapturedHotkeyTextBox.Text = displayText.ToString();
+
+ // Validate combination
+ if (_capturedModifiers != 0 && _capturedVirtualKey != 0)
+ {
+ StatusTextBlock.Text = "Valid hotkey combination captured!";
+ StatusTextBlock.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 208, 132));
+ ApplyButton.IsEnabled = true;
+ }
+ else if (_capturedModifiers == 0 && _capturedVirtualKey != 0)
+ {
+ StatusTextBlock.Text = "Warning: No modifier keys pressed. Add Ctrl, Alt, Win, or Shift.";
+ StatusTextBlock.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 165, 0));
+ ApplyButton.IsEnabled = false;
+ }
+ else
+ {
+ StatusTextBlock.Text = "";
+ ApplyButton.IsEnabled = false;
+ }
+ }
+
+ private string GetKeyDisplayName(Key key)
+ {
+ return key switch
+ {
+ Key.Space => "Space",
+ Key.Tab => "Tab",
+ Key.Enter => "Enter",
+ Key.Escape => "Esc",
+ Key.Back => "Backspace",
+ Key.Delete => "Delete",
+ Key.Insert => "Insert",
+ Key.Home => "Home",
+ Key.End => "End",
+ Key.PageUp => "Page Up",
+ Key.PageDown => "Page Down",
+ Key.Up => "↑",
+ Key.Down => "↓",
+ Key.Left => "←",
+ Key.Right => "→",
+ Key.F1 => "F1",
+ Key.F2 => "F2",
+ Key.F3 => "F3",
+ Key.F4 => "F4",
+ Key.F5 => "F5",
+ Key.F6 => "F6",
+ Key.F7 => "F7",
+ Key.F8 => "F8",
+ Key.F9 => "F9",
+ Key.F10 => "F10",
+ Key.F11 => "F11",
+ Key.F12 => "F12",
+ Key.D0 => "0",
+ Key.D1 => "1",
+ Key.D2 => "2",
+ Key.D3 => "3",
+ Key.D4 => "4",
+ Key.D5 => "5",
+ Key.D6 => "6",
+ Key.D7 => "7",
+ Key.D8 => "8",
+ Key.D9 => "9",
+ Key.OemTilde => "`",
+ Key.OemMinus => "-",
+ Key.OemPlus => "=",
+ Key.OemOpenBrackets => "[",
+ Key.OemCloseBrackets => "]",
+ Key.OemPipe => "\\",
+ Key.OemSemicolon => ";",
+ Key.OemQuotes => "'",
+ Key.OemComma => ",",
+ Key.OemPeriod => ".",
+ Key.OemQuestion => "/",
+ _ => key.ToString()
+ };
+ }
+
+ private int KeyToVirtualKey(Key key)
+ {
+ return key switch
+ {
+ Key.A => 0x41,
+ Key.B => 0x42,
+ Key.C => 0x43,
+ Key.D => 0x44,
+ Key.E => 0x45,
+ Key.F => 0x46,
+ Key.G => 0x47,
+ Key.H => 0x48,
+ Key.I => 0x49,
+ Key.J => 0x4A,
+ Key.K => 0x4B,
+ Key.L => 0x4C,
+ Key.M => 0x4D,
+ Key.N => 0x4E,
+ Key.O => 0x4F,
+ Key.P => 0x50,
+ Key.Q => 0x51,
+ Key.R => 0x52,
+ Key.S => 0x53,
+ Key.T => 0x54,
+ Key.U => 0x55,
+ Key.V => 0x56,
+ Key.W => 0x57,
+ Key.X => 0x58,
+ Key.Y => 0x59,
+ Key.Z => 0x5A,
+ Key.D0 => 0x30,
+ Key.D1 => 0x31,
+ Key.D2 => 0x32,
+ Key.D3 => 0x33,
+ Key.D4 => 0x34,
+ Key.D5 => 0x35,
+ Key.D6 => 0x36,
+ Key.D7 => 0x37,
+ Key.D8 => 0x38,
+ Key.D9 => 0x39,
+ Key.F1 => 0x70,
+ Key.F2 => 0x71,
+ Key.F3 => 0x72,
+ Key.F4 => 0x73,
+ Key.F5 => 0x74,
+ Key.F6 => 0x75,
+ Key.F7 => 0x76,
+ Key.F8 => 0x77,
+ Key.F9 => 0x78,
+ Key.F10 => 0x79,
+ Key.F11 => 0x7A,
+ Key.F12 => 0x7B,
+ Key.Space => 0x20,
+ Key.Tab => 0x09,
+ Key.Enter => 0x0D,
+ Key.Escape => 0x1B,
+ Key.Back => 0x08,
+ Key.Delete => 0x2E,
+ Key.Insert => 0x2D,
+ Key.Home => 0x24,
+ Key.End => 0x23,
+ Key.PageUp => 0x21,
+ Key.PageDown => 0x22,
+ Key.Up => 0x26,
+ Key.Down => 0x28,
+ Key.Left => 0x25,
+ Key.Right => 0x27,
+ Key.OemTilde => 0xC0,
+ Key.OemMinus => 0xBD,
+ Key.OemPlus => 0xBB,
+ Key.OemOpenBrackets => 0xDB,
+ Key.OemCloseBrackets => 0xDD,
+ Key.OemPipe => 0xDC,
+ Key.OemSemicolon => 0xBA,
+ Key.OemQuotes => 0xDE,
+ Key.OemComma => 0xBC,
+ Key.OemPeriod => 0xBE,
+ Key.OemQuestion => 0xBF,
+ _ => 0
+ };
+ }
+
+ private void ApplyButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_capturedModifiers == 0 || _capturedVirtualKey == 0)
+ {
+ StatusTextBlock.Text = "Please capture a valid hotkey first.";
+ StatusTextBlock.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 85, 85));
+ return;
+ }
+
+ HotkeyChanged = true;
+ DialogResult = true;
+ Close();
+ }
+
+ private void ResetButton_Click(object sender, RoutedEventArgs e)
+ {
+ _capturedModifiers = 0x0008; // MOD_WIN
+ _capturedVirtualKey = 0xDE; // Apostrophe
+ _capturedKeyDisplay = "'";
+
+ CapturedHotkeyTextBox.Text = "Win + '";
+ StatusTextBlock.Text = "Reset to default combination.";
+ StatusTextBlock.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 208, 132));
+ ApplyButton.IsEnabled = true;
+
+ _capturing = false;
+ CaptureBox.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(100, 100, 100));
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ public bool GetCapturedHotkey(out bool useCtrl, out bool useAlt, out bool useWin, out bool useShift, out int virtualKey, out string keyDisplay)
+ {
+ useCtrl = (_capturedModifiers & 0x0002) != 0;
+ useAlt = (_capturedModifiers & 0x0001) != 0;
+ useWin = (_capturedModifiers & 0x0008) != 0;
+ useShift = (_capturedModifiers & 0x0004) != 0;
+ virtualKey = _capturedVirtualKey;
+ keyDisplay = _capturedKeyDisplay;
+
+ return _capturedModifiers != 0 && _capturedVirtualKey != 0;
+ }
+}
\ No newline at end of file
diff --git a/UI/MainWindow.xaml b/UI/MainWindow.xaml
new file mode 100644
index 0000000..7c57c0d
--- /dev/null
+++ b/UI/MainWindow.xaml
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/MainWindow.xaml.cs b/UI/MainWindow.xaml.cs
new file mode 100644
index 0000000..3631f6b
--- /dev/null
+++ b/UI/MainWindow.xaml.cs
@@ -0,0 +1,745 @@
+using System.Text;
+using System.Text.Json;
+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 System.Windows.Shell;
+using System.Windows.Interop;
+
+namespace NetKit;
+
+///
+/// Interaction logic for MainWindow.xaml
+///
+public partial class MainWindow : Window
+{
+ private readonly HttpService _httpService;
+ private readonly SslChecker _sslChecker;
+ private readonly RedirectChecker _redirectChecker;
+ private readonly DnsLookup _dnsLookup;
+ private readonly SettingsService _settingsService;
+
+ public MainWindow(SettingsService settingsService)
+ {
+ try
+ {
+ InitializeComponent();
+ _httpService = new HttpService();
+ _sslChecker = new SslChecker();
+ _redirectChecker = new RedirectChecker();
+ _dnsLookup = new DnsLookup();
+ _settingsService = settingsService;
+
+
+ // Enable borderless window chrome with resize and caption area
+ var chrome = new WindowChrome
+ {
+ CaptionHeight = 42,
+ CornerRadius = new CornerRadius(0),
+ GlassFrameThickness = new Thickness(0),
+ ResizeBorderThickness = new Thickness(6),
+ UseAeroCaptionButtons = false
+ };
+ WindowChrome.SetWindowChrome(this, chrome);
+
+ // Try to set the window icon to the app's EXE icon
+ TrySetWindowIconFromExe();
+ }
+ catch (Exception ex)
+ {
+ CustomMessageBox.Show($"MainWindow Constructor Error: {ex.Message}\n\nStack Trace: {ex.StackTrace}", "Window Error");
+ throw;
+ }
+ }
+
+
+
+ public void ShowWindow()
+ {
+ if (Visibility == Visibility.Hidden)
+ {
+ Show();
+ WindowState = WindowState.Normal;
+ Activate();
+ Focus();
+ }
+ else
+ {
+ HideWindow();
+ }
+ }
+
+ private void HideWindow()
+ {
+ Hide();
+ }
+
+ private void MainWindow_Deactivated(object sender, EventArgs e)
+ {
+ // Hide window when it loses focus (click outside)
+ HideWindow();
+ }
+
+ private void ProtocolRadioButton_Changed(object sender, RoutedEventArgs e)
+ {
+ // Ensure UI elements are initialized before accessing them
+ if (SslPortTextBox == null) return;
+
+ if (SslRadioButton?.IsChecked == true)
+ {
+ SslPortTextBox.Text = "443";
+ }
+ else if (HttpRadioButton?.IsChecked == true)
+ {
+ SslPortTextBox.Text = "80";
+ }
+ }
+
+ private void HttpUrlTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ HttpGetButton_Click(sender, new RoutedEventArgs());
+ }
+ }
+
+ private void HttpPostDataTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ HttpPostButton_Click(sender, new RoutedEventArgs());
+ }
+ }
+
+ private void SslHostnameTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ SslCheckButton_Click(sender, new RoutedEventArgs());
+ }
+ }
+
+ private void SslPortTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ SslCheckButton_Click(sender, new RoutedEventArgs());
+ }
+ }
+
+ private void RedirectUrlTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ RedirectCheckButton_Click(sender, new RoutedEventArgs());
+ }
+ }
+
+ private void DnsHostnameTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ DnsLookupButton_Click(sender, new RoutedEventArgs());
+ }
+ }
+
+ private async void HttpGetButton_Click(object sender, RoutedEventArgs e)
+ {
+ var url = HttpUrlTextBox.Text;
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ CustomMessageBox.Show(this, "Please enter a URL");
+ return;
+ }
+
+ HttpStatusTextBlock.Text = "Making GET request...";
+ HttpSummaryTextBox.Text = "";
+ HttpResponseTextBox.Text = "";
+
+ // Collapse expanded view when making new request
+ HttpResponseTextBox.Visibility = Visibility.Collapsed;
+ ExpandButton.Content = "Show Full Response";
+
+ try
+ {
+ var result = await _httpService.GetAsync(url);
+
+ if (result.IsSuccess)
+ {
+ DisplayHttpResult(result);
+ HttpStatusTextBlock.Text = $"GET request completed - {result.StatusCode} {result.StatusText}";
+ HttpStatusTextBlock.Foreground = (SolidColorBrush)FindResource("SuccessBrush");
+ }
+ else if (!string.IsNullOrEmpty(result.Error))
+ {
+ HttpSummaryTextBox.Text = $"Error: {result.Error}";
+ HttpStatusTextBlock.Text = "GET request failed";
+ HttpStatusTextBlock.Foreground = (SolidColorBrush)FindResource("ErrorBrush");
+ }
+ else
+ {
+ DisplayHttpResult(result);
+ HttpStatusTextBlock.Text = $"GET request completed - {result.StatusCode} {result.StatusText}";
+ HttpStatusTextBlock.Foreground = new SolidColorBrush(Colors.Orange);
+ }
+ }
+ catch (Exception ex)
+ {
+ HttpSummaryTextBox.Text = $"Error: {ex.Message}";
+ HttpStatusTextBlock.Text = "GET request failed";
+ HttpStatusTextBlock.Foreground = (SolidColorBrush)FindResource("ErrorBrush");
+ }
+ }
+
+ private async void HttpPostButton_Click(object sender, RoutedEventArgs e)
+ {
+ var url = HttpUrlTextBox.Text;
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ CustomMessageBox.Show(this, "Please enter a URL");
+ return;
+ }
+
+ HttpStatusTextBlock.Text = "Making POST request...";
+ HttpSummaryTextBox.Text = "";
+ HttpResponseTextBox.Text = "";
+
+ // Collapse expanded view when making new request
+ HttpResponseTextBox.Visibility = Visibility.Collapsed;
+ ExpandButton.Content = "Show Full Response";
+
+ try
+ {
+ object? data = null;
+ var postDataText = HttpPostDataTextBox.Text;
+
+ if (!string.IsNullOrWhiteSpace(postDataText))
+ {
+ try
+ {
+ data = JsonSerializer.Deserialize