fix: add clientId as qp for websocket for MC

This commit is contained in:
Andrew Welker
2025-12-29 08:57:52 -06:00
parent 53e7a30224
commit 78c9381108
3 changed files with 149 additions and 33 deletions

View File

@@ -64,6 +64,12 @@ namespace PepperDash.Essentials.WebSocketServer
[JsonProperty("userAppUrl")]
public string UserAppUrl { get; set; }
/// <summary>
/// Gets or sets the WebSocketUrl with clientId query parameter
/// </summary>
[JsonProperty("webSocketUrl")]
public string WebSocketUrl { get; set; }
/// <summary>
/// Gets or sets the EnableDebug

View File

@@ -68,6 +68,12 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
/// <summary>
/// Stores queues of pending client IDs per token for legacy clients (FIFO)
/// This ensures thread-safety when multiple legacy clients use the same token
/// </summary>
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> legacyClientIdQueues = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
/// <summary>
/// Gets the collection of UI clients
/// </summary>
@@ -730,49 +736,95 @@ namespace PepperDash.Essentials.WebSocketServer
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
{
// Try to retrieve a pending client ID that was registered during the join request
// Use the composite key: token-clientId
string clientId = null;
string registrationKey = null;
// Find a registration for this token
var matchingRegistrations = pendingClientRegistrations.Keys
.Where(k => k.StartsWith($"{key}-"))
.OrderBy(k => k) // Process in order for FIFO behavior
.ToList();
if (matchingRegistrations.Any())
// Dequeue the next clientId for legacy client support (FIFO per token)
// New clients will override this ID in OnOpen with the validated query parameter value
var clientId = "pending";
if (legacyClientIdQueues.TryGetValue(key, out var queue) && queue.TryDequeue(out var dequeuedId))
{
registrationKey = matchingRegistrations.First();
if (pendingClientRegistrations.TryRemove(registrationKey, out clientId))
{
this.LogVerbose("Retrieved pending clientId {clientId} for token {key}", clientId, key);
}
}
if (clientId == null)
{
// Fallback: generate a new client ID if none was pending
clientId = $"{Utilities.GetNextClientId()}";
this.LogWarning("No pending clientId found for token {key}, generated new ID {clientId}", key, clientId);
clientId = dequeuedId;
this.LogVerbose("Dequeued legacy clientId {clientId} for token {token}", clientId, key);
}
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, clientId);
this.LogInformation("Constructing UiClient with key {key} and temporary ID (will be set from query param)", key);
c.Controller = _parent;
c.RoomKey = roomKey;
c.TokenKey = key; // Store the URL token key for filtering
c.Server = this; // Give UiClient access to server for ID registration
uiClients.AddOrUpdate(clientId, c, (id, existingClient) =>
// Don't add to uiClients yet - will be added in OnOpen after ID is set from query param
c.ConnectionClosed += (o, a) =>
{
this.LogWarning("replacing client with duplicate id {id}", id);
return c;
});
// UiClients[key].SetClient(c);
c.ConnectionClosed += (o, a) => uiClients.TryRemove(a.ClientId, out _);
uiClients.TryRemove(a.ClientId, out _);
// Clean up any pending registrations for this token
var keysToRemove = pendingClientRegistrations.Keys
.Where(k => k.StartsWith($"{key}-"))
.ToList();
foreach (var k in keysToRemove)
{
pendingClientRegistrations.TryRemove(k, out _);
}
// Clean up legacy queue if empty
if (legacyClientIdQueues.TryGetValue(key, out var legacyQueue) && legacyQueue.IsEmpty)
{
legacyClientIdQueues.TryRemove(key, out _);
}
};
return c;
}
/// <summary>
/// Registers a UiClient with its validated client ID after WebSocket connection
/// </summary>
/// <param name="client">The UiClient to register</param>
/// <param name="clientId">The validated client ID</param>
/// <param name="tokenKey">The token key for validation</param>
/// <returns>True if registration successful, false if validation failed</returns>
public bool RegisterUiClient(UiClient client, string clientId, string tokenKey)
{
var registrationKey = $"{tokenKey}-{clientId}";
// Verify this clientId was generated during a join request for this token
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
{
this.LogWarning("Client attempted to connect with unregistered or expired clientId {clientId} for token {token}", clientId, tokenKey);
return false;
}
// Registration is valid - add to active clients
uiClients.AddOrUpdate(clientId, client, (id, existingClient) =>
{
this.LogWarning("Replacing existing client with duplicate id {id}", id);
return client;
});
this.LogInformation("Successfully registered UiClient with ID {clientId} for token {token}", clientId, tokenKey);
return true;
}
/// <summary>
/// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
/// </summary>
/// <param name="client">The UiClient to register</param>
public void RegisterLegacyUiClient(UiClient client)
{
if (string.IsNullOrEmpty(client.Id))
{
this.LogError("Cannot register client with null or empty ID");
return;
}
uiClients.AddOrUpdate(client.Id, client, (id, existingClient) =>
{
this.LogWarning("Replacing existing client with duplicate id {id} (legacy flow)", id);
return client;
});
this.LogInformation("Successfully registered UiClient with ID {clientId} using legacy flow", client.Id);
}
/// <summary>
/// Prints out the session data for each path
/// </summary>
@@ -1079,15 +1131,23 @@ namespace PepperDash.Essentials.WebSocketServer
});
}
// Generate a client ID for this join request and store it with a composite key
// Generate a client ID for this join request
var clientId = $"{Utilities.GetNextClientId()}";
// Store registration with composite key: token-clientId so BuildUiClient can verify it
// Store in pending registrations for new clients that send clientId via query param
var registrationKey = $"{token}-{clientId}";
pendingClientRegistrations.TryAdd(registrationKey, clientId);
// Also enqueue for legacy clients (thread-safe FIFO per token)
var queue = legacyClientIdQueues.GetOrAdd(token, _ => new ConcurrentQueue<string>());
queue.Enqueue(clientId);
this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token);
// Construct WebSocket URL with clientId query parameter
var wsProtocol = "ws";
var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
// Construct the response object
JoinResponse jRes = new JoinResponse
{
@@ -1101,6 +1161,7 @@ namespace PepperDash.Essentials.WebSocketServer
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port),
WebSocketUrl = wsUrl,
EnableDebug = false,
DeviceInterfaceSupport = deviceInterfaces
};

View File

@@ -46,6 +46,11 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public MobileControlSystemController Controller { get; set; }
/// <summary>
/// Gets or sets the server instance for client registration
/// </summary>
public MobileControlWebsocketServer Server { get; set; }
/// <summary>
/// Gets or sets the room key that this client is associated with
/// </summary>
@@ -104,6 +109,50 @@ namespace PepperDash.Essentials.WebSocketServer
Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
Log.Level = LogLevel.Trace;
// Get clientId from query parameter
var queryString = Context.QueryString;
var clientId = queryString["clientId"];
if (!string.IsNullOrEmpty(clientId))
{
// New behavior: Validate and register with the server using provided clientId
if (Server == null || !Server.RegisterUiClient(this, clientId, TokenKey))
{
this.LogError("Failed to register client with ID {clientId}. Invalid or expired registration.", clientId);
Context.WebSocket.Close(CloseStatusCode.PolicyViolation, "Invalid or expired clientId");
return;
}
// Update this client's ID to the validated one
Id = clientId;
Key = $"uiclient-{TokenKey}-{RoomKey}-{clientId}";
this.LogInformation("Client {clientId} successfully connected and registered (new flow)", clientId);
}
else
{
// Legacy behavior: Use clientId from Token.Id (generated in HandleJoinRequest)
this.LogInformation("Client connected without clientId query parameter. Using legacy registration flow.");
// Id is already set from Token in constructor, use it
if (string.IsNullOrEmpty(Id))
{
this.LogError("Legacy client has no ID from token. Connection will be closed.");
Context.WebSocket.Close(CloseStatusCode.PolicyViolation, "No client ID available");
return;
}
Key = $"uiclient-{TokenKey}-{RoomKey}-{Id}";
// Register directly to active clients (legacy flow)
if (Server != null)
{
Server.RegisterLegacyUiClient(this);
}
this.LogInformation("Client {clientId} registered using legacy flow", Id);
}
if (Controller == null)
{
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");