using Newtonsoft.Json; using System.Net; using System.Text; namespace AiQ_GUI { public enum CAMTYPE { GOOD, BAD, BIZARRE } public class FakeCamera { public const string JSONLoc = "C:\\Users\\BradleyBorn\\OneDrive - MAV Systems Ltd\\Desktop\\AIQ_GUI_TEST\\FakeCameraGood\\"; public static bool Snapshot = false; private HttpListener listener; public FakeCamera(int port = 8080) { listener = new HttpListener(); listener.Prefixes.Add($"http://*:{port}/"); listener.Prefixes.Add("http://localhost:8080/"); } public async Task StartAsync(CAMTYPE camType) { try { listener.Start(); MainForm.Instance.AddToActionsList("Started server successfully"); } catch (Exception ex) { MainForm.Instance.AddToActionsList($"Failed to start server: {ex.Message}"); return; } while (true) { HttpListenerContext? context = await listener.GetContextAsync(); _ = HandleRequestAsync(context, camType); } } public void Stop() { if (listener.IsListening) { listener.Stop(); listener.Close(); } } public static bool ValidateCredentials(string username, string password, CAMTYPE camType) { string filePath = camType switch { CAMTYPE.GOOD => $"{JSONLoc}Versions.json", CAMTYPE.BAD => $"{JSONLoc}Versions_Bad.json", CAMTYPE.BIZARRE => $"{JSONLoc}Versions_Bizarre.json", _ => throw new ArgumentOutOfRangeException(nameof(camType), "Invalid camera type") }; string versJson = File.ReadAllText(filePath); Versions vers = JsonConvert.DeserializeObject(versJson); TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1); int secondsSinceEpoch = (int)t.TotalSeconds; string PASS = Lics.GeneratePassword(vers.MAC, vers.version, secondsSinceEpoch); return username == "developer" && password == PASS; } private static async Task HTTPReplySetup(HttpListenerContext context, string ResponseText, int StatusCode, string ContentType) { try { context.Response.StatusCode = StatusCode; context.Response.ContentType = ContentType; byte[] responseBytes = Encoding.UTF8.GetBytes(ResponseText); context.Response.ContentLength64 = responseBytes.Length; await context.Response.OutputStream.WriteAsync(responseBytes); } finally { context.Response.OutputStream.Close(); } } private static async Task HTTPReplySetup(HttpListenerContext context, byte[] content, int statusCode, string contentType) { context.Response.StatusCode = statusCode; context.Response.ContentType = contentType; context.Response.ContentLength64 = content.Length; await context.Response.OutputStream.WriteAsync(content); await context.Response.OutputStream.FlushAsync(); context.Response.OutputStream.Close(); } private static async Task HandleRequestAsync(HttpListenerContext context, CAMTYPE camType) { string path = context.Request.Url?.AbsolutePath ?? "/"; string Method = context.Request.HttpMethod; string headers = context.Request.Headers.ToString(); string Content = context.Request.InputStream?.ToString() ?? ""; // Define endpoints that do NOT require authentication string[] publicEndpoints = ["/api/versions", "/api/diagnostics"]; bool requiresAuth = !publicEndpoints.Contains(path); if (requiresAuth) { string authHeader = context.Request.Headers["Authorization"]; if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ")) { context.Response.AddHeader("WWW-Authenticate", "Basic realm=\"FakeCamera\""); await HTTPReplySetup(context, "", 401, "text/plain"); return; } try { string encodedCredentials = authHeader.Substring("Basic ".Length).Trim(); string decodedCredentials = Encoding.UTF8.GetString(Convert.FromBase64String(encodedCredentials)); string[] parts = decodedCredentials.Split(':', 2); if (parts.Length != 2) { throw new Exception("Invalid Basic auth format. Expected username:password."); } string username = parts[0]; string password = parts[1]; if (!ValidateCredentials(username, password, camType)) { throw new Exception("Invalid credentials"); } } catch { context.Response.AddHeader("WWW-Authenticate", "Basic realm=\"FakeCamera\""); await HTTPReplySetup(context, "", 401, "text/plain"); return; } } try { if (Method == "GET") { if (path.Equals("/Infrared/led-controls")) // Works { string[] allowedPowers = ["LOW", "MID", "HIGH", "SAFE", "OFF"]; string power = context.Request.QueryString["power"]?.ToUpper(); if (allowedPowers.Contains(power)) await HTTPReplySetup(context, "Power levels set successfully", 200, "text/plain"); else await HTTPReplySetup(context, "Power level not accepted", 400, "text/plain"); } else if (path.Equals("/api/versions")) { string versionsJson = File.ReadAllText(camType switch { CAMTYPE.GOOD => $"{JSONLoc}Versions.json", CAMTYPE.BAD => $"{JSONLoc}Versions_Bad.json", CAMTYPE.BIZARRE => $"{JSONLoc}Versions_Bizarre.json", _ => throw new ArgumentOutOfRangeException() }); Versions vers = JsonConvert.DeserializeObject(versionsJson); vers.timeStamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; versionsJson = JsonConvert.SerializeObject(vers); await HTTPReplySetup(context, versionsJson, 200, "application/json"); } else if (path.Equals("/api/diagnostics")) { string filePath = camType switch { CAMTYPE.GOOD => $"{JSONLoc}Diagnostics.json", CAMTYPE.BAD => $"{JSONLoc}Diagnostics_Bad.json", CAMTYPE.BIZARRE => $"{JSONLoc}Diagnostics_Bizarre.json", _ => throw new ArgumentOutOfRangeException() }; string diagsJson = File.ReadAllText(filePath); if (camType == CAMTYPE.GOOD) { Diags diags = JsonConvert.DeserializeObject(diagsJson); diags.timeStamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; diagsJson = JsonConvert.SerializeObject(diags); } await HTTPReplySetup(context, diagsJson, 200, "application/json"); } else if (path.Equals("/api/zoomLock")) // Fixed { string enable = context.Request.QueryString["enable"]; if (!string.IsNullOrEmpty(enable) && enable.Equals("true", StringComparison.OrdinalIgnoreCase)) // FIXED Use http://localhost:8080/api/zoomLock?enable=true { await HTTPReplySetup(context, "Zoom lock enabled.", 200, "text/plain"); } else if (!string.IsNullOrEmpty(enable) && enable.Equals("false", StringComparison.OrdinalIgnoreCase)) // FIXED Use http://localhost:8080/api/zoomLock?enable=false { await HTTPReplySetup(context, "Zoom lock disabled.", 200, "text/plain"); } else { await HTTPReplySetup(context, "Missing or invalid 'enable' parameter.", 400, "text/plain"); } } else if (path.Equals("/Infrared-camera-factory-reset") || path.Equals("/Colour-camera-factory-reset")) // WORKS { await HTTPReplySetup(context, "Factory reset OK.", 200, "text/plain"); } else if (path.Equals("/Infrared-camera-control") || path.Equals("/Colour-camera-control")) // Fixed { string viscaReply = "9041FF9051FF"; if (!RegexCache.VISCARegex().IsMatch(context.Request.QueryString["commandHex"])) viscaReply = "9060FF"; await HTTPReplySetup(context, viscaReply, 200, "text/plain"); } else if (path.Equals("/SightingCreator-plate-positions")) //Works { string trimJson = File.ReadAllText($"{JSONLoc}Trim.json"); await HTTPReplySetup(context, trimJson, 200, "application/json"); } else if (path.Equals("/Infrared-snapshot")) // Fixed { string jpegPath; if (!Snapshot) { jpegPath = $"{JSONLoc}IR_Open_image.jpg"; // Light Image Snapshot = true; } else { jpegPath = $"{JSONLoc}IR_Tight_image.jpg"; // Dark Image } byte[] jpegBytes = File.ReadAllBytes(jpegPath); await HTTPReplySetup(context, jpegBytes, 200, "image/jpeg"); } else if (path.Equals("/Colour-snapshot")) // Fixed { byte[] jpegBytes = File.ReadAllBytes($"{JSONLoc}OV_image.jpg"); await HTTPReplySetup(context, jpegBytes, 200, "image/jpeg"); } else if (path.Equals("/api/fetch-config")) { ConfigObject config = JsonConvert.DeserializeObject(Content); if (config.Id == "GLOBAL--NetworkConfig") { // TODO - Return success message? } else { await HTTPReplySetup(context, "ID not valid.", 200, "text/plain"); } } else if (path.Equals("/api/RaptorOCR-auto-license")) // Works { string vaxtorJson = "{ \"protectionKeyId\":\"123456789012345678\" }"; await HTTPReplySetup(context, vaxtorJson, 200, "application/json"); } else { await HTTPReplySetup(context, $"GET request for {path} not supported", 400, "text/plain"); } } else if (Method == "POST") { if (path.Equals("/api/update-config")) { ConfigObject fo = JsonConvert.DeserializeObject(Content); if (fo.Id == "GLOBAL--NetworkConfig" || fo.Id == "SightingCreator" || fo.Id == "RaptorOCR") { await HTTPReplySetup(context, "Update successful.", 200, "text/plain"); } else if (fo.Id == "Internal Config") { string SerialNumber = ""; string ModelNumber = ""; fo.Fields.ForEach(field => { if (field.Property == "propSerialNumber") SerialNumber = field.Value; else if (field.Property == "propMavModelNumber") ModelNumber = field.Value; }); // Update JSON with new SerialNumber and ModelNumber beofre sending back to the client using StreamReader r = new($"{JSONLoc}InternalConfig.json"); string InternalConfig_JSON = r.ReadToEnd(); InternalConfig_JSON = InternalConfig_JSON.Replace("SSSSSS", SerialNumber).Replace("MMMMMM", ModelNumber); HTTPReplySetup(context, InternalConfig_JSON, 200, "application/json"); } else { await HTTPReplySetup(context, "ID not valid.", 200, "text/plain"); } } else { await HTTPReplySetup(context, $"POST request for {path} not supported.", 400, "text/plain"); } } else { await HTTPReplySetup(context, "Bad Request.", 400, "text/plain"); } } catch (Exception ex) { await HTTPReplySetup(context, $"Server error: {ex.Message}", 500, "text/plain"); MainForm.Instance.AddToActionsList($"Server error handling {Method} {path}: {ex.Message}"); } } public class ConfigObject { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("fields")] public List Fields { get; set; } } public class Field { [JsonProperty("property")] public string Property { get; set; } [JsonProperty("value")] public string Value { get; set; } } } }