mbproxy: fix dashboard review findings, add named BCD tags + fleet config
Reviewed the new SignalR dashboard and fixed its two top findings: a stored XSS on the connection-detail page (unescaped tag name / direction / timestamp rendered into innerHTML) and FC03/FC04 cache hits bypassing the debug-view capture, which left cached tags frozen while their age climbed. Also adds an optional human-friendly Name to BCD tags surfaced on the debug view, and loads the real fleet config from tags.txt (12 named BCD tags, PLC Z28061) so the published appsettings.json is deploy-ready. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,21 +25,33 @@
|
||||
// Remove — removes specific addresses from the effective set for that PLC.
|
||||
// Effective set = (Global ∪ Add) − Remove, resolved per PDU.
|
||||
"BcdTags": {
|
||||
// Fleet-wide BCD tag list from tags.txt — applies to every PLC.
|
||||
// Address = Modbus PDU-decimal = (4xxxx Modbus address − 40001), which also
|
||||
// equals the DirectLOGIC V-memory address converted octal → decimal
|
||||
// (e.g. 41549 / V3014 → 41549 − 40001 = 1548 ; octal 3014 = 1548).
|
||||
// A 32-bit tag is ONE entry at its low/base address; it covers Address &
|
||||
// Address+1 (CDAB: low word at Address, high word at Address+1).
|
||||
// Name (optional) is a human-friendly label shown on the connection-detail
|
||||
// debug view; it has no effect on rewriting. tags.txt's "Data Direction" is
|
||||
// informational — the proxy rewrites BCD on whichever FC touches the address.
|
||||
// CacheTtlMs (optional, per entry) opts a tag into the Phase-11 response cache;
|
||||
// omitted / 0 = uncached (the default for every tag).
|
||||
"Global": [
|
||||
// V2000 (octal) = decimal address 1024. 16-bit BCD counter.
|
||||
{ "Address": 1024, "Width": 16 },
|
||||
// ── 16-bit setpoints — BCD16, HMI-written ────────────────────────────
|
||||
{ "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537
|
||||
{ "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540
|
||||
{ "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010
|
||||
{ "Address": 1545, "Width": 16, "Name": "Right ChlorineSP" }, // 41546 · V3011
|
||||
{ "Address": 1546, "Width": 16, "Name": "Left HydrogenSP" }, // 41547 · V3012
|
||||
{ "Address": 1547, "Width": 16, "Name": "Right HydrogenSP" }, // 41548 · V3013
|
||||
{ "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014
|
||||
{ "Address": 1549, "Width": 16, "Name": "Right AirSP" }, // 41550 · V3015
|
||||
|
||||
// V2040 (octal) = decimal address 1056. 32-bit BCD total at 1056/1057.
|
||||
{ "Address": 1056, "Width": 32 },
|
||||
|
||||
// V2100 (octal) = decimal address 1088. 16-bit BCD setpoint.
|
||||
//
|
||||
// Phase 11: CacheTtlMs (optional) opts this tag into the response cache. With
|
||||
// CacheTtlMs > 0 set, upstream clients reading this register will see values up
|
||||
// to CacheTtlMs MILLISECONDS OLD — explicit acknowledgement of the staleness
|
||||
// window is required by enabling it. Default (omitted or 0) = cache disabled
|
||||
// for this tag. The cache is OFF by default for every tag.
|
||||
{ "Address": 1088, "Width": 16 /* , "CacheTtlMs": 1000 */ }
|
||||
// ── 32-bit runtimes — BCD32, read; CDAB pair spans Address & Address+1 ─
|
||||
{ "Address": 4616, "Width": 32, "Name": "MTA Runtime Left (min)" }, // 44617/44618 · V11010
|
||||
{ "Address": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012
|
||||
{ "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022
|
||||
{ "Address": 4628, "Width": 32, "Name": "FRR Runtime Right (min)" } // 44629/44630 · V11024
|
||||
]
|
||||
},
|
||||
|
||||
@@ -52,26 +64,12 @@
|
||||
// port will cause a backend connect failure and an immediate upstream disconnect.
|
||||
"Plcs": [
|
||||
{
|
||||
"Name": "Line1-Mixer", // Human-readable name (shown on status page and in logs)
|
||||
"ListenPort": 5020, // Port the proxy listens on (upstream clients connect here)
|
||||
"Host": "10.0.1.1", // PLC IP address or hostname
|
||||
"Port": 502, // PLC Modbus TCP port (almost always 502)
|
||||
"BcdTags": {
|
||||
// Additional 32-bit tag specific to this PLC only.
|
||||
"Add": [
|
||||
{ "Address": 1200, "Width": 32 }
|
||||
],
|
||||
// Remove address 1056 from the Global list for this PLC
|
||||
// (this mixer doesn't use the 32-bit BCD total).
|
||||
"Remove": [ 1056 ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Line1-Conveyor",
|
||||
"ListenPort": 5021,
|
||||
"Host": "10.0.1.2",
|
||||
"Port": 502
|
||||
// No BcdTags override — uses the Global set as-is.
|
||||
"Name": "Z28061", // Human-readable name (shown on status page and in logs)
|
||||
"ListenPort": 5020, // Port the proxy listens on (upstream clients connect here)
|
||||
"Host": "10.210.192.5", // PLC IP address or hostname
|
||||
"Port": 502 // PLC Modbus TCP port (almost always 502)
|
||||
// No BcdTags override — uses the Global set as-is. Per-PLC overrides are
|
||||
// available: "BcdTags": { "Add": [ ... ], "Remove": [ ... ] }.
|
||||
}
|
||||
// Add one entry per PLC. Ports must be unique per host. Typical fleet: 54 PLCs.
|
||||
],
|
||||
|
||||
@@ -29,21 +29,33 @@
|
||||
// Remove — removes specific addresses from the effective set for that PLC.
|
||||
// Effective set = (Global ∪ Add) − Remove, resolved per PDU.
|
||||
"BcdTags": {
|
||||
// Fleet-wide BCD tag list from tags.txt — applies to every PLC.
|
||||
// Address = Modbus PDU-decimal = (4xxxx Modbus address − 40001), which also
|
||||
// equals the DirectLOGIC V-memory address converted octal → decimal
|
||||
// (e.g. 41549 / V3014 → 41549 − 40001 = 1548 ; octal 3014 = 1548).
|
||||
// A 32-bit tag is ONE entry at its low/base address; it covers Address &
|
||||
// Address+1 (CDAB: low word at Address, high word at Address+1).
|
||||
// Name (optional) is a human-friendly label shown on the connection-detail
|
||||
// debug view; it has no effect on rewriting. tags.txt's "Data Direction" is
|
||||
// informational — the proxy rewrites BCD on whichever FC touches the address.
|
||||
// CacheTtlMs (optional, per entry) opts a tag into the Phase-11 response cache;
|
||||
// omitted / 0 = uncached (the default for every tag).
|
||||
"Global": [
|
||||
// V2000 (octal) = decimal address 1024. 16-bit BCD counter.
|
||||
{ "Address": 1024, "Width": 16 },
|
||||
// ── 16-bit setpoints — BCD16, HMI-written ────────────────────────────
|
||||
{ "Address": 1536, "Width": 16, "Name": "Left ArgonSP" }, // 41537
|
||||
{ "Address": 1539, "Width": 16, "Name": "Right ArgonSP" }, // 41540
|
||||
{ "Address": 1544, "Width": 16, "Name": "Left ChlorineSP" }, // 41545 · V3010
|
||||
{ "Address": 1545, "Width": 16, "Name": "Right ChlorineSP" }, // 41546 · V3011
|
||||
{ "Address": 1546, "Width": 16, "Name": "Left HydrogenSP" }, // 41547 · V3012
|
||||
{ "Address": 1547, "Width": 16, "Name": "Right HydrogenSP" }, // 41548 · V3013
|
||||
{ "Address": 1548, "Width": 16, "Name": "Left AirSP" }, // 41549 · V3014
|
||||
{ "Address": 1549, "Width": 16, "Name": "Right AirSP" }, // 41550 · V3015
|
||||
|
||||
// V2040 (octal) = decimal address 1056. 32-bit BCD total at 1056/1057.
|
||||
{ "Address": 1056, "Width": 32 },
|
||||
|
||||
// V2100 (octal) = decimal address 1088. 16-bit BCD setpoint.
|
||||
//
|
||||
// Phase 11: CacheTtlMs (optional) opts this tag into the response cache. With
|
||||
// CacheTtlMs > 0 set, upstream clients reading this register will see values up
|
||||
// to CacheTtlMs MILLISECONDS OLD — explicit acknowledgement of the staleness
|
||||
// window is required by enabling it. Default (omitted or 0) = cache disabled
|
||||
// for this tag. The cache is OFF by default for every tag.
|
||||
{ "Address": 1088, "Width": 16 /* , "CacheTtlMs": 1000 */ }
|
||||
// ── 32-bit runtimes — BCD32, read; CDAB pair spans Address & Address+1 ─
|
||||
{ "Address": 4616, "Width": 32, "Name": "MTA Runtime Left (min)" }, // 44617/44618 · V11010
|
||||
{ "Address": 4618, "Width": 32, "Name": "MTA Runtime Right (min)" }, // 44619/44620 · V11012
|
||||
{ "Address": 4626, "Width": 32, "Name": "FRR Runtime Left (min)" }, // 44627/44628 · V11022
|
||||
{ "Address": 4628, "Width": 32, "Name": "FRR Runtime Right (min)" } // 44629/44630 · V11024
|
||||
]
|
||||
},
|
||||
|
||||
@@ -56,26 +68,12 @@
|
||||
// port will cause a backend connect failure and an immediate upstream disconnect.
|
||||
"Plcs": [
|
||||
{
|
||||
"Name": "Line1-Mixer", // Human-readable name (shown on status page and in logs)
|
||||
"ListenPort": 5020, // Port the proxy listens on (upstream clients connect here)
|
||||
"Host": "10.0.1.1", // PLC IP address or hostname
|
||||
"Port": 502, // PLC Modbus TCP port (almost always 502)
|
||||
"BcdTags": {
|
||||
// Additional 32-bit tag specific to this PLC only.
|
||||
"Add": [
|
||||
{ "Address": 1200, "Width": 32 }
|
||||
],
|
||||
// Remove address 1056 from the Global list for this PLC
|
||||
// (this mixer doesn't use the 32-bit BCD total).
|
||||
"Remove": [ 1056 ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Line1-Conveyor",
|
||||
"ListenPort": 5021,
|
||||
"Host": "10.0.1.2",
|
||||
"Port": 502
|
||||
// No BcdTags override — uses the Global set as-is.
|
||||
"Name": "Z28061", // Human-readable name (shown on status page and in logs)
|
||||
"ListenPort": 5020, // Port the proxy listens on (upstream clients connect here)
|
||||
"Host": "10.210.192.5", // PLC IP address or hostname
|
||||
"Port": 502 // PLC Modbus TCP port (almost always 502)
|
||||
// No BcdTags override — uses the Global set as-is. Per-PLC overrides are
|
||||
// available: "BcdTags": { "Add": [ ... ], "Remove": [ ... ] }.
|
||||
}
|
||||
// Add one entry per PLC. Ports must be unique per host. Typical fleet: 54 PLCs.
|
||||
],
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
framework-dependent\ ~1.6 MB — requires the .NET 10 + ASP.NET Core runtime
|
||||
preinstalled on the target.
|
||||
|
||||
Each folder also receives a current appsettings.json — the platform-appropriate
|
||||
install template (Windows or Linux, selected by -Rid) — so every publish-out
|
||||
flavour is a complete, deployable folder.
|
||||
|
||||
The runtime is selected with -Rid (default win-x64). The binary is Mbproxy.exe on
|
||||
Windows RIDs and Mbproxy on Linux/macOS RIDs.
|
||||
|
||||
@@ -70,6 +74,25 @@ Write-Host "`n=== Publishing framework-dependent ($Rid, ~1.6 MB) ===" -Foregroun
|
||||
& dotnet publish $csproj -c Release -r $Rid -p:SelfContained=false -p:PublishSingleFile=true -o $frameworkDependentOut --nologo
|
||||
if ($LASTEXITCODE -ne 0) { throw "framework-dependent publish failed (exit $LASTEXITCODE)" }
|
||||
|
||||
# ── Ship the platform-appropriate config template as appsettings.json ──────────
|
||||
# dotnet publish already copies it via the Mbproxy.csproj <Content> link, but that
|
||||
# link uses PreserveNewest — an incremental (non-Clean) run can leave a stale
|
||||
# config behind. Copy it explicitly so every publish-out flavour is guaranteed a
|
||||
# current appsettings.json, and so the config's source is obvious.
|
||||
$configTemplate = if ($Rid -like 'win-*') {
|
||||
Join-Path $repoRoot 'install\mbproxy.config.template.json'
|
||||
} else {
|
||||
Join-Path $repoRoot 'install\mbproxy.linux.config.template.json'
|
||||
}
|
||||
if (-not (Test-Path $configTemplate)) { throw "Cannot find config template: $configTemplate" }
|
||||
|
||||
Write-Host "`n=== Config (appsettings.json) ===" -ForegroundColor Cyan
|
||||
foreach ($flavour in 'self-contained','framework-dependent') {
|
||||
$dest = Join-Path $OutputDir "$flavour\appsettings.json"
|
||||
Copy-Item -LiteralPath $configTemplate -Destination $dest -Force
|
||||
Write-Host (" {0,-22} <- {1}" -f $flavour, $configTemplate)
|
||||
}
|
||||
|
||||
function Format-Size {
|
||||
param([long]$Bytes)
|
||||
if ($Bytes -ge 1MB) { '{0:N1} MB' -f ($Bytes / 1MB) }
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
# framework-dependent/ ~1.6 MB — requires the .NET 10 + ASP.NET Core runtime
|
||||
# preinstalled on the target.
|
||||
#
|
||||
# Each folder also receives a current appsettings.json — the platform-appropriate
|
||||
# install template (Windows or Linux, selected by -r RID) — so every publish-out
|
||||
# flavour is a complete, deployable folder.
|
||||
#
|
||||
# Both builds use the Release configuration and inherit the publish settings in
|
||||
# src/Mbproxy/Mbproxy.csproj (those settings are gated on an explicit RID, which
|
||||
# is supplied here). The framework-dependent build overrides SelfContained=false.
|
||||
@@ -68,6 +72,28 @@ echo "=== Publishing framework-dependent ($rid, ~1.6 MB) ==="
|
||||
dotnet publish "$csproj" -c Release -r "$rid" \
|
||||
-p:SelfContained=false -p:PublishSingleFile=true -o "$framework_dependent_out" --nologo
|
||||
|
||||
# Ship the platform-appropriate config template as appsettings.json.
|
||||
# dotnet publish already copies it via the Mbproxy.csproj <Content> link, but that
|
||||
# link uses PreserveNewest — an incremental (non-clean) run can leave a stale config
|
||||
# behind. Copy it explicitly so every publish-out flavour is guaranteed a current
|
||||
# appsettings.json, and so the config's source is obvious.
|
||||
if [[ "$rid" == win-* ]]; then
|
||||
config_template="$repo_root/install/mbproxy.config.template.json"
|
||||
else
|
||||
config_template="$repo_root/install/mbproxy.linux.config.template.json"
|
||||
fi
|
||||
if [[ ! -f "$config_template" ]]; then
|
||||
echo "Cannot find config template: $config_template" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Config (appsettings.json) ==="
|
||||
for flavour in self-contained framework-dependent; do
|
||||
cp -f "$config_template" "$output_dir/$flavour/appsettings.json"
|
||||
printf ' %-22s <- %s\n' "$flavour" "$config_template"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "=== Result ($rid) ==="
|
||||
for flavour in self-contained framework-dependent; do
|
||||
|
||||
Reference in New Issue
Block a user