From a18f7800d17c07879a237181761d3176e04079a0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 23 Jun 2026 15:33:46 -0600 Subject: [PATCH] fix: improve port forwarding logic and error handling in RoutingFeedback session --- .../DebugSessionRequestHandler.cs | 7 +- .../RoutingFeedbackSessionRequestHandler.cs | 110 ++++++++---------- .../Web/RoutingFeedbackWebsocket.cs | 43 +++---- 3 files changed, 70 insertions(+), 90 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index af4e3d38..c6303190 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -58,8 +58,12 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler Debug.WebsocketSink.StartServerAndSetPort(port); Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); } + else + { + port = Debug.WebsocketSink.Port; + } - // Attempt to get the CS LAN IP and forward the port + // Always ensure port forwarding is active — it may have been removed by timeout try { var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( @@ -188,6 +192,7 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler if (Debug.WebsocketSink.HasActiveConnections) { Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward"); + StartPortForwardTimeout(port, csIp); return; } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs index 4b4981bb..2ca432c3 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs @@ -10,9 +10,9 @@ using Serilog.Events; namespace PepperDash.Essentials.Core.Web.RequestHandlers; /// -/// Handles HTTP requests to start and retrieve the routing feedback WebSocket session URL. -/// GET returns the current URL (starting the server if necessary). -/// POST stops the server. +/// Handles HTTP requests to start and stop the routing feedback WebSocket session. +/// GET starts the server and returns connection URLs. POST stops the session. +/// Automatically configures port forwarding for Crestron processors with a CS LAN adapter. /// public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler { @@ -20,11 +20,6 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler private CTimer _portForwardTimeoutTimer; private readonly object _timerLock = new object(); - /// - /// Gets the singleton RoutingFeedbackWebsocket instance. - /// - public static RoutingFeedbackWebsocket Instance => _instance; - /// /// Constructor /// @@ -34,7 +29,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler } /// - /// Handles GET requests: starts the routing feedback WebSocket if not running and returns the URL. + /// Starts the routing feedback WebSocket server and returns the connection URL. /// protected override void HandleGet(HttpCwsContext context) { @@ -44,7 +39,6 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); var port = 0; - string csIp = null; if (!_instance.IsRunning) { @@ -52,8 +46,13 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler port = new Random().Next(65335, 65434); _instance.StartServerAndSetPort(port); } + else + { + port = _instance.Port; + } - // Attempt to get the CS LAN IP and forward the port + // Always ensure port forwarding is active — it may have been removed by timeout + string csIp = null; try { var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( @@ -91,10 +90,8 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler { context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = Encoding.UTF8; context.Response.Write( - JsonConvert.SerializeObject(new { error = "Failed to start routing feedback WebSocket server." }), + JsonConvert.SerializeObject(new { error = "Failed to start routing feedback WebSocket server. Check logs for details." }), false); context.Response.End(); return; @@ -109,20 +106,21 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler }; Debug.LogMessage(LogEventLevel.Information, "Routing Feedback Session URL: {0}", url); - Debug.LogMessage(LogEventLevel.Information, "Fallback Routing Feedback Session URL: {0}", data.fallbackUrl); + if (data.fallbackUrl != null) + Debug.LogMessage(LogEventLevel.Information, "Routing Feedback Fallback URL: {0}", data.fallbackUrl); - var json = JsonConvert.SerializeObject(data); + var res = JsonConvert.SerializeObject(data); - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; context.Response.ContentType = "application/json"; context.Response.ContentEncoding = Encoding.UTF8; - context.Response.Write(json, false); + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(res, false); context.Response.End(); } - catch (Exception ex) + catch (Exception e) { - Debug.LogError(ex, "Error handling routing feedback session GET: {message}", ex.Message); + Debug.LogMessage(LogEventLevel.Error, "Error handling routing feedback session request: {0}", e); context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; context.Response.End(); @@ -130,58 +128,51 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler } /// - /// Handles POST requests: stops the routing feedback WebSocket server. + /// Stops the routing feedback WebSocket session and removes port forwarding. /// protected override void HandlePost(HttpCwsContext context) { + CancelPortForwardTimeout(); + + var port = _instance.Port; + + _instance.StopServer(); + + // Remove port forwarding if CS LAN exists try { - CancelPortForwardTimeout(); + var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( + EthernetAdapterType.EthernetCSAdapter); + var csIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); - var port = _instance.Port; - _instance.StopServer(); + var result = CrestronEthernetHelper.RemovePortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); - // Remove port forwarding if CS LAN exists - try + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) { - var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( - EthernetAdapterType.EthernetCSAdapter); - var csIp = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); - - var result = CrestronEthernetHelper.RemovePortForwarding( - (ushort)port, (ushort)port, csIp, - CrestronEthernetHelper.ePortMapTransport.TCP); - - if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) - { - Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for port {0}: {1}", port, result); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port); - } + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for routing port {0}: {1}", port, result); } - catch (ArgumentException) + else { - // No CS LAN adapter + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for routing port {0} removed", port); } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding: {0}", ex.Message); - } - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.End(); + } + catch (ArgumentException) + { + // No CS LAN adapter } catch (Exception ex) { - Debug.LogError(ex, "Error handling routing feedback session POST: {message}", ex.Message); - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for routing: {0}", ex.Message); } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.End(); + + Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket Session Stopped"); } private void StartPortForwardTimeout(int port, string csIp) @@ -194,6 +185,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler if (_instance.HasActiveConnections) { Debug.LogMessage(LogEventLevel.Debug, "Routing feedback websocket has active connections; keeping port forward"); + StartPortForwardTimeout(port, csIp); return; } @@ -211,7 +203,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler } else { - Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed due to timeout", port); + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for routing port {0} removed due to timeout", port); } } catch (Exception ex) diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index 32e2eebf..14139323 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Timers; using Crestron.SimplSharp; using Newtonsoft.Json; @@ -12,7 +11,6 @@ using Newtonsoft.Json.Serialization; using PepperDash.Core; using Serilog.Events; using WebSocketSharp; -using WebSocketSharp.Net; using WebSocketSharp.Server; namespace PepperDash.Essentials.Core.Web; @@ -114,11 +112,11 @@ public class RoutingFeedbackWebsocket : IKeyed _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12; _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - _httpsServer.AddWebSocketService(_path, () => new RoutingFeedbackClient(this)); - _httpsServer.OnGet += HandleHttpGet; + _httpsServer.AddWebSocketService(_path); _httpsServer.Log.Level = LogLevel.Warn; _httpsServer.Start(); + RoutingFeedbackClient.Owner = this; SubscribeToRoutingEvents(); Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket ready at {url}", this, Url); @@ -340,23 +338,6 @@ public class RoutingFeedbackWebsocket : IKeyed service.Sessions.Broadcast(message); } - private void HandleHttpGet(object sender, HttpRequestEventArgs e) - { - var res = e.Response; - res.ContentType = "text/html"; - res.ContentEncoding = Encoding.UTF8; - res.StatusCode = 200; - - const string html = @" -Essentials Routing Feedback - -

Certificate Accepted

-

You may close this tab and return to the configuration app.

-"; - - res.WriteContent(Encoding.UTF8.GetBytes(html)); - } - private static X509Certificate2 LoadCert(string certPath, string certPassword) { return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); @@ -408,32 +389,34 @@ public class RoutingFeedbackWebsocket : IKeyed /// public class RoutingFeedbackClient : WebSocketBehavior { - private readonly RoutingFeedbackWebsocket _owner; + /// + /// Static reference to the owning instance. + /// Set before the server starts accepting connections. + /// + internal static RoutingFeedbackWebsocket Owner { get; set; } /// /// Initializes a new instance of the class. /// - /// The owning instance. - public RoutingFeedbackClient(RoutingFeedbackWebsocket owner) + public RoutingFeedbackClient() { - _owner = owner; } /// protected override void OnOpen() { base.OnOpen(); - Debug.LogMessage(LogEventLevel.Information, "Routing feedback client connected from: {url}", _owner, Context.WebSocket.Url); + Debug.LogMessage(LogEventLevel.Information, "Routing feedback client connected from: {url}", Owner, Context.WebSocket.Url); // Send full state snapshot to the newly connected client try { - var snapshot = _owner.GetSnapshotMessage(); + var snapshot = Owner.GetSnapshotMessage(); Send(snapshot); } catch (Exception ex) { - Debug.LogError(ex, "Error sending routing snapshot to client: {message}", _owner, ex.Message); + Debug.LogError(ex, "Error sending routing snapshot to client: {message}", Owner, ex.Message); } } @@ -441,13 +424,13 @@ public class RoutingFeedbackClient : WebSocketBehavior protected override void OnClose(CloseEventArgs e) { base.OnClose(e); - Debug.LogMessage(LogEventLevel.Debug, "Routing feedback client disconnected: {code} {reason}", _owner, e.Code, e.Reason); + Debug.LogMessage(LogEventLevel.Debug, "Routing feedback client disconnected: {code} {reason}", Owner, e.Code, e.Reason); } /// protected override void OnError(WebSocketSharp.ErrorEventArgs e) { base.OnError(e); - Debug.LogError(e.Exception, "Routing feedback client error: {message}", _owner, e.Message); + Debug.LogError(e.Exception, "Routing feedback client error: {message}", Owner, e.Message); } }