fix: improve port forwarding logic and error handling in RoutingFeedback session

This commit is contained in:
Neil Dorin 2026-06-23 15:33:46 -06:00
parent 00c7128778
commit a18f7800d1
3 changed files with 70 additions and 90 deletions

View file

@ -58,8 +58,12 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler
Debug.WebsocketSink.StartServerAndSetPort(port); Debug.WebsocketSink.StartServerAndSetPort(port);
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); 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 try
{ {
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(
@ -188,6 +192,7 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler
if (Debug.WebsocketSink.HasActiveConnections) if (Debug.WebsocketSink.HasActiveConnections)
{ {
Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward"); Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward");
StartPortForwardTimeout(port, csIp);
return; return;
} }

View file

@ -10,9 +10,9 @@ using Serilog.Events;
namespace PepperDash.Essentials.Core.Web.RequestHandlers; namespace PepperDash.Essentials.Core.Web.RequestHandlers;
/// <summary> /// <summary>
/// Handles HTTP requests to start and retrieve the routing feedback WebSocket session URL. /// Handles HTTP requests to start and stop the routing feedback WebSocket session.
/// GET returns the current URL (starting the server if necessary). /// GET starts the server and returns connection URLs. POST stops the session.
/// POST stops the server. /// Automatically configures port forwarding for Crestron processors with a CS LAN adapter.
/// </summary> /// </summary>
public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
{ {
@ -20,11 +20,6 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
private CTimer _portForwardTimeoutTimer; private CTimer _portForwardTimeoutTimer;
private readonly object _timerLock = new object(); private readonly object _timerLock = new object();
/// <summary>
/// Gets the singleton RoutingFeedbackWebsocket instance.
/// </summary>
public static RoutingFeedbackWebsocket Instance => _instance;
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
@ -34,7 +29,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
protected override void HandleGet(HttpCwsContext context) protected override void HandleGet(HttpCwsContext context)
{ {
@ -44,7 +39,6 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var port = 0; var port = 0;
string csIp = null;
if (!_instance.IsRunning) if (!_instance.IsRunning)
{ {
@ -52,8 +46,13 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
port = new Random().Next(65335, 65434); port = new Random().Next(65335, 65434);
_instance.StartServerAndSetPort(port); _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 try
{ {
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(
@ -91,10 +90,8 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
{ {
context.Response.StatusCode = 500; context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error"; context.Response.StatusDescription = "Internal Server Error";
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write( 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); false);
context.Response.End(); context.Response.End();
return; return;
@ -109,20 +106,21 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
}; };
Debug.LogMessage(LogEventLevel.Information, "Routing Feedback Session URL: {0}", url); 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.ContentType = "application/json";
context.Response.ContentEncoding = Encoding.UTF8; 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(); 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.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error"; context.Response.StatusDescription = "Internal Server Error";
context.Response.End(); context.Response.End();
@ -130,15 +128,14 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
} }
/// <summary> /// <summary>
/// Handles POST requests: stops the routing feedback WebSocket server. /// Stops the routing feedback WebSocket session and removes port forwarding.
/// </summary> /// </summary>
protected override void HandlePost(HttpCwsContext context) protected override void HandlePost(HttpCwsContext context)
{
try
{ {
CancelPortForwardTimeout(); CancelPortForwardTimeout();
var port = _instance.Port; var port = _instance.Port;
_instance.StopServer(); _instance.StopServer();
// Remove port forwarding if CS LAN exists // Remove port forwarding if CS LAN exists
@ -155,11 +152,11 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
{ {
Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for port {0}: {1}", port, result); Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for routing port {0}: {1}", port, result);
} }
else else
{ {
Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port); Debug.LogMessage(LogEventLevel.Information, "Port forwarding for routing port {0} removed", port);
} }
} }
catch (ArgumentException) catch (ArgumentException)
@ -168,20 +165,14 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding: {0}", ex.Message); Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for routing: {0}", ex.Message);
} }
context.Response.StatusCode = 200; context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK"; context.Response.StatusDescription = "OK";
context.Response.End(); context.Response.End();
}
catch (Exception ex) Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket Session Stopped");
{
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();
}
} }
private void StartPortForwardTimeout(int port, string csIp) private void StartPortForwardTimeout(int port, string csIp)
@ -194,6 +185,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
if (_instance.HasActiveConnections) if (_instance.HasActiveConnections)
{ {
Debug.LogMessage(LogEventLevel.Debug, "Routing feedback websocket has active connections; keeping port forward"); Debug.LogMessage(LogEventLevel.Debug, "Routing feedback websocket has active connections; keeping port forward");
StartPortForwardTimeout(port, csIp);
return; return;
} }
@ -211,7 +203,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler
} }
else 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) catch (Exception ex)

View file

@ -4,7 +4,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Security.Authentication; using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Timers; using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -12,7 +11,6 @@ using Newtonsoft.Json.Serialization;
using PepperDash.Core; using PepperDash.Core;
using Serilog.Events; using Serilog.Events;
using WebSocketSharp; using WebSocketSharp;
using WebSocketSharp.Net;
using WebSocketSharp.Server; using WebSocketSharp.Server;
namespace PepperDash.Essentials.Core.Web; namespace PepperDash.Essentials.Core.Web;
@ -114,11 +112,11 @@ public class RoutingFeedbackWebsocket : IKeyed
_httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12; _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
_httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
_httpsServer.AddWebSocketService<RoutingFeedbackClient>(_path, () => new RoutingFeedbackClient(this)); _httpsServer.AddWebSocketService<RoutingFeedbackClient>(_path);
_httpsServer.OnGet += HandleHttpGet;
_httpsServer.Log.Level = LogLevel.Warn; _httpsServer.Log.Level = LogLevel.Warn;
_httpsServer.Start(); _httpsServer.Start();
RoutingFeedbackClient.Owner = this;
SubscribeToRoutingEvents(); SubscribeToRoutingEvents();
Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket ready at {url}", this, Url); Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket ready at {url}", this, Url);
@ -340,23 +338,6 @@ public class RoutingFeedbackWebsocket : IKeyed
service.Sessions.Broadcast(message); 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 = @"<!DOCTYPE html>
<html><head><title>Essentials Routing Feedback</title></head>
<body style=""font-family:sans-serif;padding:2rem;text-align:center"">
<h2>Certificate Accepted</h2>
<p>You may close this tab and return to the configuration app.</p>
</body></html>";
res.WriteContent(Encoding.UTF8.GetBytes(html));
}
private static X509Certificate2 LoadCert(string certPath, string certPassword) private static X509Certificate2 LoadCert(string certPath, string certPassword)
{ {
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
@ -408,32 +389,34 @@ public class RoutingFeedbackWebsocket : IKeyed
/// </summary> /// </summary>
public class RoutingFeedbackClient : WebSocketBehavior public class RoutingFeedbackClient : WebSocketBehavior
{ {
private readonly RoutingFeedbackWebsocket _owner; /// <summary>
/// Static reference to the owning <see cref="RoutingFeedbackWebsocket"/> instance.
/// Set before the server starts accepting connections.
/// </summary>
internal static RoutingFeedbackWebsocket Owner { get; set; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RoutingFeedbackClient"/> class. /// Initializes a new instance of the <see cref="RoutingFeedbackClient"/> class.
/// </summary> /// </summary>
/// <param name="owner">The owning <see cref="RoutingFeedbackWebsocket"/> instance.</param> public RoutingFeedbackClient()
public RoutingFeedbackClient(RoutingFeedbackWebsocket owner)
{ {
_owner = owner;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnOpen() protected override void OnOpen()
{ {
base.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 // Send full state snapshot to the newly connected client
try try
{ {
var snapshot = _owner.GetSnapshotMessage(); var snapshot = Owner.GetSnapshotMessage();
Send(snapshot); Send(snapshot);
} }
catch (Exception ex) 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) protected override void OnClose(CloseEventArgs e)
{ {
base.OnClose(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);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnError(WebSocketSharp.ErrorEventArgs e) protected override void OnError(WebSocketSharp.ErrorEventArgs e)
{ {
base.OnError(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);
} }
} }