From ed004b9681aeb1eb9b18e062b02915f4f3136859 Mon Sep 17 00:00:00 2001 From: Eric S Date: Sun, 14 Sep 2025 01:52:15 +0200 Subject: [PATCH 1/4] Adds services --- .gitignore | 3 +- App.xaml | 9 - App.xaml.cs | 13 - MainWindow.xaml | 12 - MainWindow.xaml.cs | 23 -- Services/HttpService.cs | 108 +++++++- Services/RedirectChecker.cs | 176 ++++++++++++ Services/SslChecker.cs | 96 ++++++- Tray/TrayIconManager.cs | 44 ++- UI/App.xaml | 23 ++ UI/App.xaml.cs | 40 +++ UI/CustomMessageBox.xaml | 41 +++ UI/CustomMessageBox.xaml.cs | 34 +++ UI/MainWindow.xaml | 175 ++++++++++++ UI/MainWindow.xaml.cs | 526 ++++++++++++++++++++++++++++++++++++ UI/Theme.xaml | 402 +++++++++++++++++++++++++++ msOps.csproj | 16 +- 17 files changed, 1678 insertions(+), 63 deletions(-) delete mode 100644 App.xaml delete mode 100644 App.xaml.cs delete mode 100644 MainWindow.xaml delete mode 100644 MainWindow.xaml.cs create mode 100644 Services/RedirectChecker.cs create mode 100644 UI/App.xaml create mode 100644 UI/App.xaml.cs create mode 100644 UI/CustomMessageBox.xaml create mode 100644 UI/CustomMessageBox.xaml.cs create mode 100644 UI/MainWindow.xaml create mode 100644 UI/MainWindow.xaml.cs create mode 100644 UI/Theme.xaml diff --git a/.gitignore b/.gitignore index 15def66..2f3e0e6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ artifacts/ # Others *.DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db +.claude \ No newline at end of file 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/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/Services/HttpService.cs b/Services/HttpService.cs index e47fd09..a7e58aa 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 msOps; public class HttpService { + private readonly HttpClient _httpClient; + + public HttpService() + { + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "msOps/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..332d2c8 --- /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 msOps; + +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", + "msOps-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/SslChecker.cs b/Services/SslChecker.cs index 6e1e1e2..71b59dc 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 msOps; 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..df8051f 100644 --- a/Tray/TrayIconManager.cs +++ b/Tray/TrayIconManager.cs @@ -1,6 +1,46 @@ -namespace msOps; +using System; +using System.Drawing; +using System.Windows; +using System.Windows.Forms; -public class TrayIconManager +namespace msOps.Tray { + public class TrayIconManager : IDisposable + { + private NotifyIcon? _notifyIcon; + public void Initialize() + { + _notifyIcon = new NotifyIcon + { + Icon = SystemIcons.Application, + Visible = true, + Text = "msOps Helper" + }; + + var contextMenu = new ContextMenuStrip(); + contextMenu.Items.Add("Open", null, OnOpenClicked); + contextMenu.Items.Add("Exit", null, OnExitClicked); + + _notifyIcon.ContextMenuStrip = contextMenu; + } + + private void OnOpenClicked(object? sender, EventArgs e) + { + var window = new msOps.MainWindow(); + window.Show(); + window.Activate(); + } + + private void OnExitClicked(object? sender, EventArgs e) + { + System.Windows.Application.Current.Shutdown(); + } + + public void Dispose() + { + _notifyIcon?.Dispose(); + GC.SuppressFinalize(this); + } + } } diff --git a/UI/App.xaml b/UI/App.xaml new file mode 100644 index 0000000..f3e796e --- /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..029b05a --- /dev/null +++ b/UI/App.xaml.cs @@ -0,0 +1,40 @@ +using System.Windows; +using msOps.Tray; + +namespace msOps +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : System.Windows.Application + { + private TrayIconManager? _trayIconManager; + + protected override void OnStartup(StartupEventArgs e) + { + try + { + base.OnStartup(e); + + _trayIconManager = new TrayIconManager(); + _trayIconManager.Initialize(); + + var mainWindow = new MainWindow(); + this.MainWindow = mainWindow; + mainWindow.Show(); + } + catch (Exception ex) + { + System.Windows.MessageBox.Show($"Startup Error: {ex.Message}\n\nStack Trace: {ex.StackTrace}", "Application Error"); + this.Shutdown(1); + } + } + + protected override void OnExit(ExitEventArgs e) + { + _trayIconManager?.Dispose(); + base.OnExit(e); + } + } +} + diff --git a/UI/CustomMessageBox.xaml b/UI/CustomMessageBox.xaml new file mode 100644 index 0000000..6d02daa --- /dev/null +++ b/UI/CustomMessageBox.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + +