feat: return correct config for CS processors

When MC is running on a processor with a control subnet and a user
attempts to access the UI via the control subnet, the return _config.local.json
needs to reflect the CS LAN IP address for the processor rather than the LAN IP Address.

In order to accomplish this, there will now be 2 config files written to disk on startup:
1. the original file, /user/programX/mcUserApp/_local-config/_config.local.json
2. A file with the correct CS IP Address /user/programX/mcUserApp/_local-config/_config.cs.json

When a user requests the _config.local.json, the processor will compare the remote IP address
with the CS LAN IP Address and determine if they're in the same subnet, the assumption being
that if the IP Addresses are in the same subnet, then the app or end user is on the CS LAN and needs the CS config.
If the addresses are in the same subnet, then the contents of the _config.cs.json will be returned.
This commit is contained in:
Andrew Welker 2025-06-06 14:48:22 -05:00
parent ee44ff1d5f
commit 4d29fe1976
2 changed files with 1333 additions and 1263 deletions

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Exnsions for IPAddress to provide additional functionality such as getting broadcast address, network address, and checking if two addresses are in the same subnet.
/// </summary>
public static class IPAddressExtensions
{
/// <summary>
/// Get the broadcast address for a given IP address and subnet mask.
/// </summary>
/// <param name="address">Address to check</param>
/// <param name="subnetMask">Subnet mask in a.b.c.d format</param>
/// <returns>Broadcast address</returns>
/// <remarks>
/// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the broadcast address will be 192.168.1.255
/// </remarks>
/// <exception cref="ArgumentException"></exception>
public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask)
{
byte[] ipAdressBytes = address.GetAddressBytes();
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
for (int i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255));
}
return new IPAddress(broadcastAddress);
}
/// <summary>
/// Get the network address for a given IP address and subnet mask.
/// </summary>
/// <param name="address">Address to check</param>
/// <param name="subnetMask">Subnet mask in a.b.c.d</param>
/// <returns>Network Address</returns>
/// /// <remarks>
/// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the network address will be 192.168.1.0
/// </remarks>
/// <exception cref="ArgumentException"></exception>
public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask)
{
byte[] ipAdressBytes = address.GetAddressBytes();
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
for (int i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
}
return new IPAddress(broadcastAddress);
}
/// <summary>
/// Determine if two IP addresses are in the same subnet.
/// </summary>
/// <param name="address2">Address to check</param>
/// <param name="address">Second address to check</param>
/// <param name="subnetMask">Subnet mask to use to compare the 2 IP Address</param>
/// <returns>True if addresses are in the same subnet</returns>
/// <remarks>
/// If the input IP addresses are 192.168.1.100 and 192.168.1.200, and the subnet mask is 255.255.255.0, this will return true.
/// If the input IP addresses are 10.1.1.100 and 192.168.1.100, and the subnet mask is 255.255.255.0, this will return false.
/// </remarks>
public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask)
{
IPAddress network1 = address.GetNetworkAddress(subnetMask);
IPAddress network2 = address2.GetNetworkAddress(subnetMask);
return network1.Equals(network2);
}
}
}

View file

@ -60,47 +60,9 @@ namespace PepperDash.Essentials.WebSocketServer
private string lanIpAddress => CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter)); private string lanIpAddress => CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter));
private string csIpAddress private System.Net.IPAddress csIpAddress;
{
get
{
short csAdapterId = -1;
try private System.Net.IPAddress csSubnetMask;
{
CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
}
catch (ArgumentException)
{
return "";
}
return CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
}
}
private string csSubnet
{
get
{
short csAdapterId = -1;
try
{
CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
}
catch (ArgumentException)
{
return "";
}
var subnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
this.LogDebug("cs subnet mask: {subnetMask}", subnetMask);
return subnetMask;
}
}
/// <summary> /// <summary>
/// The path for the WebSocket messaging /// The path for the WebSocket messaging
@ -162,8 +124,6 @@ namespace PepperDash.Essentials.WebSocketServer
// Set the default port to be 50000 plus the slot number of the program // Set the default port to be 50000 plus the slot number of the program
Port = 50000 + (int)Global.ControlSystem.ProgramNumber; Port = 50000 + (int)Global.ControlSystem.ProgramNumber;
this.LogDebug("CS Subnet: {csSubnet}", csSubnet);
if (customPort != 0) if (customPort != 0)
{ {
Port = customPort; Port = customPort;
@ -173,9 +133,6 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
try try
{ {
CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
Debug.LogMessage(LogEventLevel.Information, "Automatically forwarding port {0} to CS LAN", Port); Debug.LogMessage(LogEventLevel.Information, "Automatically forwarding port {0} to CS LAN", Port);
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter); var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
@ -198,6 +155,23 @@ namespace PepperDash.Essentials.WebSocketServer
} }
} }
try
{
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
var csSubnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
var csIpAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
this.csSubnetMask = System.Net.IPAddress.Parse(csSubnetMask);
this.csIpAddress = System.Net.IPAddress.Parse(csIpAddress);
}
catch (ArgumentException)
{
if (parent.Config.DirectServer.AutomaticallyForwardPortToCSLAN == false)
{
Debug.LogMessage(LogEventLevel.Information, "This processor does not have a CS LAN", this);
}
}
UiClients = new Dictionary<string, UiClientContext>(); UiClients = new Dictionary<string, UiClientContext>();
@ -382,6 +356,8 @@ namespace PepperDash.Essentials.WebSocketServer
// Write the LAN application configuration file. Used when a request comes in for the application config from the LAN // Write the LAN application configuration file. Used when a request comes in for the application config from the LAN
var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter); var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter);
this.LogDebug("LAN Adapter ID: {lanAdapterId}", lanAdapterId);
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId); var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
var config = GetApplicationConfig(processorIp); var config = GetApplicationConfig(processorIp);
@ -408,6 +384,8 @@ namespace PepperDash.Essentials.WebSocketServer
return; return;
} }
this.LogDebug("CS LAN Adapter ID: {csAdapterId}. Adding CS Config", csAdapterId);
using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigCsFileName}", FileMode.Create, FileAccess.ReadWrite))) using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigCsFileName}", FileMode.Create, FileAccess.ReadWrite)))
{ {
// Write the CS application configuration file. Used when a request comes in for the application config from the CS // Write the CS application configuration file. Used when a request comes in for the application config from the CS
@ -1121,7 +1099,13 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogVerbose("Attempting to serve file: {filePath}", filePath); this.LogVerbose("Attempting to serve file: {filePath}", filePath);
var remoteIp = req.RemoteEndPoint.Address;
// Check if the request is coming from the CS LAN and if so, send the CS config instead of the LAN config
if (csSubnetMask != null && csIpAddress != null && remoteIp.IsInSameSubnet(csIpAddress, csSubnetMask) && filePath.Contains(appConfigFileName))
{
filePath = filePath.Replace(appConfigFileName, appConfigCsFileName);
}
byte[] contents; byte[] contents;
if (File.Exists(filePath)) if (File.Exists(filePath))