using System; using System.IO; using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Net.Security; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; namespace OversightService.Services { public class ApiClient { /// /// Constructs the full URL for API requests. /// private static readonly HttpClient httpClient = CreateHttpClient(); private readonly AppConfig _config; private static string jwtToken = null; // 🔹 Store the JWT token globally public ApiClient(AppConfig config) { _config = config ?? throw new ArgumentNullException(nameof(config)); } private void Log(string message) { try { var logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PSG-Oversight"); Directory.CreateDirectory(logDir); var logPath = Path.Combine(logDir, "api_client.log"); File.AppendAllText(logPath, $"[{DateTime.Now}] {message}\n"); } catch { } } private string BuildUrl(string route) { return $"{_config.ServerUrl.TrimEnd('/')}/{route.TrimStart('/')}"; } /// /// Creates an HttpClient that bypasses SSL validation (for testing purposes only). /// public static HttpClient CreateHttpClient() { HttpClientHandler handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, certChain, sslPolicyErrors) => { return true; // 🔥 Ignores all SSL certificate warnings (TEMPORARY!) } }; return new HttpClient(handler); } /// /// Sets the JWT token to be used for subsequent requests. /// public static void SetJwtToken(string token) { jwtToken = token; httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", jwtToken); } /// /// Gets the JWT token to be used for subsequent requests. /// public static string GetJwtToken() { return jwtToken; } /// /// Authenticates with the API using credentials from config.json. /// public async Task AuthenticateAsync(string username, string password) { try { string encryptedUsername = EncryptionHelper.EncryptData(username); string encryptedPassword = EncryptionHelper.EncryptData(password); var payload = new { username = encryptedUsername, password = encryptedPassword }; string jsonContent = JsonConvert.SerializeObject(payload); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); HttpResponseMessage response = await httpClient.PostAsync(BuildUrl("/api/auth/login"), content); string rawResponse = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Log($"❌ Authentication failed: {response.StatusCode} - {rawResponse}"); return null; } var loginResponse = JsonConvert.DeserializeObject(rawResponse); if (!string.IsNullOrEmpty(loginResponse?.Token)) { SetJwtToken(loginResponse.Token); } return loginResponse; } catch (Exception ex) { Log($"❌ Exception during login: {ex.Message}"); return null; } } /// /// Checks connectivity to the server using the stored JWT token. /// public async Task CheckConnectivity() { string testUrl = "https://localhost:8443/api/system/ping-status"; // Your HTTPS URL with port 8443 try { if (string.IsNullOrEmpty(jwtToken)) { throw new Exception("No JWT token stored. Please authenticate first."); } var response = await SendWithAutoReauthAsync(() => httpClient.GetAsync(testUrl) ); if (response.IsSuccessStatusCode) { return true; } else { Log($"❌ Server Unreachable: {response.StatusCode}"); return false; } } catch (Exception ex) { Log($"❌ Connection Error: {ex.Message}"); return false; } } /// /// Sends the collected data to the server. /// public async Task StoreSystemInfoAsync(string encryptedData) { try { if (string.IsNullOrEmpty(jwtToken)) return "❌ Error: Not authenticated. Please log in first."; var payload = new { data = encryptedData }; string jsonContent = JsonConvert.SerializeObject(payload); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await SendWithAutoReauthAsync(() => httpClient.PostAsync(BuildUrl("/api/system-info"), content) ); if (response.IsSuccessStatusCode) return await response.Content.ReadAsStringAsync(); else return $"❌ Error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}"; } catch (Exception ex) { return $"❌ Exception: {ex.Message}"; } } /// /// Refreshes the current JWT token without requiring username/password. /// public async Task RefreshTokenAsync() { try { if (string.IsNullOrEmpty(jwtToken)) { Log("⚠️ No token to refresh."); return false; } HttpResponseMessage response = await httpClient.PostAsync(BuildUrl("/api/auth/refresh"), null); string rawResponse = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Log($"❌ Token refresh failed: {response.StatusCode} - {rawResponse}"); return false; } // Parse the response to get the new token if provided in body // Note: Your server sets it via cookie, but may also return it in response try { var refreshResponse = JsonConvert.DeserializeObject>(rawResponse); if (refreshResponse?.ContainsKey("token") == true) { SetJwtToken(refreshResponse["token"]); } } catch { // Token might only be in cookie, which is fine } Log("✅ Token refreshed successfully."); return true; } catch (Exception ex) { Log($"❌ Exception during token refresh: {ex.Message}"); return false; } } /// /// Sends the collected data to the server with automatic token refresh and re-auth. /// private async Task SendWithAutoReauthAsync(Func> requestFunc) { var response = await requestFunc(); if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { Log("⚠️ Token expired or invalid. Attempting token refresh..."); // Try refreshing the token first (faster than full re-auth) bool refreshed = await RefreshTokenAsync(); if (refreshed) { // Retry original request after refresh response = await requestFunc(); if (response.IsSuccessStatusCode) { return response; } } // If refresh failed or didn't work, fall back to full re-authentication Log("⚠️ Token refresh failed. Attempting full re-auth..."); var loginResponse = await AuthenticateAsync(_config.Auth.Username, _config.Auth.Password); if (loginResponse == null || string.IsNullOrEmpty(loginResponse.Token)) { Log("❌ Re-authentication failed."); return response; // Return original 401 response } // Retry original request after reauth response = await requestFunc(); } return response; } /// /// Sends Windows update history (patch compliance data) to the backend API /// public async Task PostPatchComplianceAsync(object patchData) { try { if (string.IsNullOrEmpty(jwtToken)) { Log("❌ Not authenticated. Cannot send patch compliance data."); return false; } var jsonContent = JsonConvert.SerializeObject(patchData); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await SendWithAutoReauthAsync(() => httpClient.PostAsync(BuildUrl("/api/patch-compliance"), content) ); if (response.IsSuccessStatusCode) { Log("✅ Patch compliance data sent successfully."); return true; } else { Log($"❌ Failed to send patch compliance data: {response.StatusCode}"); return false; } } catch (Exception ex) { Log($"❌ Exception sending patch compliance data: {ex.Message}"); return false; } } } }