using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace ZB.MOM.WW.ScadaBridge.CLI; public class ManagementHttpClient : IDisposable { private readonly HttpClient _httpClient; /// /// Initializes a new instance of the class. /// /// The base URL for the management API. /// The username for HTTP Basic authentication. /// The password for HTTP Basic authentication. public ManagementHttpClient(string baseUrl, string username, string password) : this(new HttpClient(), baseUrl, username, password) { } /// /// Test-only constructor that accepts a pre-built (typically /// over a stub ) so the request/response handling can /// be exercised without a live server. /// /// The HTTP client to use for requests. /// The base URL for the management API. /// The username for HTTP Basic authentication. /// The password for HTTP Basic authentication. internal ManagementHttpClient(HttpClient httpClient, string baseUrl, string username, string password) { _httpClient = httpClient; _httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"); var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); } /// /// Sends a management command to the management API. /// /// The command name to execute. /// The command payload. /// The request timeout. /// A management response containing status and data. public async Task SendCommandAsync(string commandName, object payload, TimeSpan timeout) { using var cts = new CancellationTokenSource(timeout); var body = JsonSerializer.Serialize(new { command = commandName, payload }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var content = new StringContent(body, Encoding.UTF8, "application/json"); HttpResponseMessage httpResponse; try { httpResponse = await _httpClient.PostAsync("management", content, cts.Token); } catch (TaskCanceledException) { return new ManagementResponse(504, null, "Request timed out.", "TIMEOUT"); } catch (HttpRequestException ex) { return new ManagementResponse(0, null, $"Connection failed: {ex.Message}", "CONNECTION_FAILED"); } var responseBody = await httpResponse.Content.ReadAsStringAsync(cts.Token); if (httpResponse.IsSuccessStatusCode) { return new ManagementResponse((int)httpResponse.StatusCode, responseBody, null, null); } // Parse error response string? error = null; string? code = null; try { using var doc = JsonDocument.Parse(responseBody); error = doc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : responseBody; code = doc.RootElement.TryGetProperty("code", out var c) ? c.GetString() : null; } catch { error = responseBody; } return new ManagementResponse((int)httpResponse.StatusCode, null, error, code); } /// /// Issues a plain HTTP GET against a REST endpoint (e.g. the audit /// /api/audit/query endpoint introduced by Audit Log #23 M8) and returns the /// response body. Unlike , this does not wrap the call /// in the POST /management command envelope — the audit endpoints are plain /// REST resources. Authentication (HTTP Basic) and the base address are shared. /// /// Path relative to the base URL, with query string. /// The request timeout. /// A management response containing status and data. public async Task SendGetAsync(string relativePath, TimeSpan timeout) { using var cts = new CancellationTokenSource(timeout); HttpResponseMessage httpResponse; try { httpResponse = await _httpClient.GetAsync(relativePath, cts.Token); } catch (TaskCanceledException) { return new ManagementResponse(504, null, "Request timed out.", "TIMEOUT"); } catch (HttpRequestException ex) { return new ManagementResponse(0, null, $"Connection failed: {ex.Message}", "CONNECTION_FAILED"); } var responseBody = await httpResponse.Content.ReadAsStringAsync(cts.Token); if (httpResponse.IsSuccessStatusCode) { return new ManagementResponse((int)httpResponse.StatusCode, responseBody, null, null); } string? error = null; string? code = null; try { using var doc = JsonDocument.Parse(responseBody); error = doc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : responseBody; code = doc.RootElement.TryGetProperty("code", out var c) ? c.GetString() : null; } catch { error = responseBody; } return new ManagementResponse((int)httpResponse.StatusCode, null, error, code); } /// /// Issues a plain HTTP GET and returns the raw /// so the caller can stream the response body without buffering it in memory — used /// by audit export, where the response can be many megabytes. The caller owns /// disposing the returned message. The /// option ensures the body is not pre-buffered. /// /// Path relative to the base URL, with query string. /// A cancellation token that can be used to cancel the operation. /// The raw HTTP response message for streaming. public async Task SendGetStreamAsync(string relativePath, CancellationToken cancellationToken) => await _httpClient.GetAsync(relativePath, HttpCompletionOption.ResponseHeadersRead, cancellationToken); /// /// Disposes the underlying HTTP client. /// public void Dispose() => _httpClient.Dispose(); } public record ManagementResponse(int StatusCode, string? JsonData, string? Error, string? ErrorCode);