using Laservall.Solidworks.Server.Handlers; using System; using System.Diagnostics; using System.Net; using System.Threading; using System.Threading.Tasks; namespace Laservall.Solidworks.Server { internal sealed class BomHttpServer : IDisposable { private HttpListener _listener; private CancellationTokenSource _cts; private Task _listenTask; private readonly RequestRouter _router; private readonly AuthTokenMiddleware _auth; private readonly HeartbeatMonitor _heartbeat; private readonly IBomDataProvider _dataProvider; private readonly BomStreamHandler _bomStreamHandler; private readonly BomDataHandler _bomDataHandler; private readonly SaveHandler _saveHandler; private readonly ExportHandler _exportHandler; private readonly SettingsHandler _settingsHandler; private int _port; private bool _disposed; public int Port => _port; public string BaseUrl => $"http://127.0.0.1:{_port}"; public string BrowserUrl => $"{BaseUrl}/#token={_auth.Token}"; public bool IsRunning => _listener != null && _listener.IsListening; public BomHttpServer(IBomDataProvider dataProvider) { _dataProvider = dataProvider; _auth = new AuthTokenMiddleware(); _router = new RequestRouter(); _heartbeat = new HeartbeatMonitor(TimeSpan.FromSeconds(30), OnHeartbeatTimeout); _bomStreamHandler = new BomStreamHandler(dataProvider); _bomDataHandler = new BomDataHandler(dataProvider); _saveHandler = new SaveHandler(dataProvider); _exportHandler = new ExportHandler(() => _bomDataHandler.LastLoadResult ?? _bomStreamHandler.LastLoadResult, dataProvider); _settingsHandler = new SettingsHandler(dataProvider); RegisterRoutes(); } public void Start() { if (IsRunning) return; _port = PortFinder.FindAvailablePort(); _cts = new CancellationTokenSource(); _listener = new HttpListener(); _listener.Prefixes.Add($"http://127.0.0.1:{_port}/"); _listener.Start(); _heartbeat.Start(); _listenTask = Task.Run(() => AcceptLoop(_cts.Token)); } public void Stop() { if (!IsRunning) return; _heartbeat.Stop(); _cts?.Cancel(); try { _listener?.Stop(); } catch { } try { _listener?.Close(); } catch { } _listener = null; } public void OpenInBrowser() { if (!IsRunning) return; try { Process.Start(new ProcessStartInfo { FileName = BrowserUrl, UseShellExecute = true }); } catch { } } private void RegisterRoutes() { _router.Get("/", async (ctx, ct) => { await StaticFileHandler.ServeIndex(ctx, _auth.Token, ct); }); _router.Get("/api/ping", async (ctx, ct) => { _heartbeat.Ping(); ctx.Response.StatusCode = 200; ctx.Response.ContentType = "application/json; charset=utf-8"; byte[] body = System.Text.Encoding.UTF8.GetBytes("{\"ok\":true}"); ctx.Response.ContentLength64 = body.Length; await ctx.Response.OutputStream.WriteAsync(body, 0, body.Length, ct); ctx.Response.Close(); }); _router.Get("/api/bom/stream", async (ctx, ct) => { await _bomStreamHandler.HandleStream(ctx, ct); }); _router.Get("/api/bom/data", async (ctx, ct) => { await _bomDataHandler.HandleGetData(ctx, ct); }); _router.Post("/api/bom/save", async (ctx, ct) => { await _saveHandler.HandleSave(ctx, ct); }); _router.Get("/api/bom/export", async (ctx, ct) => { await _exportHandler.HandleExport(ctx, ct); }); _router.Get("/api/settings", async (ctx, ct) => { await _settingsHandler.HandleGet(ctx, ct); }); _router.Post("/api/settings", async (ctx, ct) => { await _settingsHandler.HandlePost(ctx, ct); }); _router.Post("/api/shutdown", async (ctx, ct) => { ctx.Response.StatusCode = 200; ctx.Response.ContentType = "application/json; charset=utf-8"; byte[] body = System.Text.Encoding.UTF8.GetBytes("{\"ok\":true}"); ctx.Response.ContentLength64 = body.Length; await ctx.Response.OutputStream.WriteAsync(body, 0, body.Length, ct); ctx.Response.Close(); // Delay stop so response is sent first _ = Task.Delay(200).ContinueWith(_ => Stop()); }); } private async Task AcceptLoop(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { var context = await _listener.GetContextAsync(); _ = HandleRequestAsync(context, ct); } catch (HttpListenerException) when (ct.IsCancellationRequested) { break; } catch (ObjectDisposedException) { break; } catch { } } } private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken ct) { try { SetCorsHeaders(context.Response); if (context.Request.HttpMethod == "OPTIONS") { context.Response.StatusCode = 204; context.Response.Close(); return; } if (!_auth.IsAuthorized(context.Request)) { AuthTokenMiddleware.RejectUnauthorized(context.Response); return; } await _router.RouteAsync(context, ct); } catch (Exception) { try { context.Response.StatusCode = 500; context.Response.Close(); } catch { } } } private static void SetCorsHeaders(HttpListenerResponse response) { response.Headers.Set("Access-Control-Allow-Origin", "*"); response.Headers.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.Headers.Set("Access-Control-Allow-Headers", "Authorization, Content-Type"); } private void OnHeartbeatTimeout() { Stop(); } public void Dispose() { if (_disposed) return; _disposed = true; Stop(); _cts?.Dispose(); _heartbeat?.Dispose(); } } }