mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-01-11 19:44:52 +00:00
fix: add clientId as qp for websocket for MC
This commit is contained in:
@@ -64,6 +64,12 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
[JsonProperty("userAppUrl")]
|
[JsonProperty("userAppUrl")]
|
||||||
public string UserAppUrl { get; set; }
|
public string UserAppUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the WebSocketUrl with clientId query parameter
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("webSocketUrl")]
|
||||||
|
public string WebSocketUrl { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the EnableDebug
|
/// Gets or sets the EnableDebug
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
|
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>
|
/// <summary>
|
||||||
/// Gets the collection of UI clients
|
/// Gets the collection of UI clients
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -730,49 +736,95 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
|
|
||||||
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
|
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
|
||||||
{
|
{
|
||||||
// Try to retrieve a pending client ID that was registered during the join request
|
// Dequeue the next clientId for legacy client support (FIFO per token)
|
||||||
// Use the composite key: token-clientId
|
// New clients will override this ID in OnOpen with the validated query parameter value
|
||||||
string clientId = null;
|
var clientId = "pending";
|
||||||
string registrationKey = null;
|
if (legacyClientIdQueues.TryGetValue(key, out var queue) && queue.TryDequeue(out var dequeuedId))
|
||||||
|
|
||||||
// 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())
|
|
||||||
{
|
{
|
||||||
registrationKey = matchingRegistrations.First();
|
clientId = dequeuedId;
|
||||||
if (pendingClientRegistrations.TryRemove(registrationKey, out clientId))
|
this.LogVerbose("Dequeued legacy clientId {clientId} for token {token}", clientId, key);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
|
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.Controller = _parent;
|
||||||
c.RoomKey = roomKey;
|
c.RoomKey = roomKey;
|
||||||
c.TokenKey = key; // Store the URL token key for filtering
|
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);
|
uiClients.TryRemove(a.ClientId, out _);
|
||||||
return c;
|
// Clean up any pending registrations for this token
|
||||||
});
|
var keysToRemove = pendingClientRegistrations.Keys
|
||||||
// UiClients[key].SetClient(c);
|
.Where(k => k.StartsWith($"{key}-"))
|
||||||
c.ConnectionClosed += (o, a) => uiClients.TryRemove(a.ClientId, out _);
|
.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;
|
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>
|
/// <summary>
|
||||||
/// Prints out the session data for each path
|
/// Prints out the session data for each path
|
||||||
/// </summary>
|
/// </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()}";
|
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}";
|
var registrationKey = $"{token}-{clientId}";
|
||||||
pendingClientRegistrations.TryAdd(registrationKey, 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);
|
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
|
// Construct the response object
|
||||||
JoinResponse jRes = new JoinResponse
|
JoinResponse jRes = new JoinResponse
|
||||||
{
|
{
|
||||||
@@ -1101,6 +1161,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
|
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
|
||||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||||
Port),
|
Port),
|
||||||
|
WebSocketUrl = wsUrl,
|
||||||
EnableDebug = false,
|
EnableDebug = false,
|
||||||
DeviceInterfaceSupport = deviceInterfaces
|
DeviceInterfaceSupport = deviceInterfaces
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public MobileControlSystemController Controller { get; set; }
|
public MobileControlSystemController Controller { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the server instance for client registration
|
||||||
|
/// </summary>
|
||||||
|
public MobileControlWebsocketServer Server { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the room key that this client is associated with
|
/// Gets or sets the room key that this client is associated with
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -104,6 +109,50 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
|
Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
|
||||||
Log.Level = LogLevel.Trace;
|
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)
|
if (Controller == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");
|
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");
|
||||||
|
|||||||
Reference in New Issue
Block a user