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:
@@ -542,4 +542,75 @@ public sealed class ResponseCacheMultiplexerTests
|
||||
l.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CacheHit_RecordsServedRead_IntoArmedDebugCapture()
|
||||
{
|
||||
// C3 regression guard: a cache hit bypasses the BCD pipeline, so without the
|
||||
// cache-entry observation replay the connection-detail debug view would freeze
|
||||
// for the whole TTL on a cached tag. A hit must re-record the served read into
|
||||
// the (armed) capture so the debug view reflects what the client receives.
|
||||
int backendPort = PickFreePort();
|
||||
await using var backend = new StubBackend(backendPort) { RegisterValue = 0x1234 };
|
||||
|
||||
using var cache = new ResponseCache(maxEntriesPerPlc: 64, evictionIntervalMs: 5000);
|
||||
var tag = BcdTag.Create(100, 16, cacheTtlMs: 5000);
|
||||
|
||||
// A detail-page viewer has armed this PLC's debug-view capture.
|
||||
var capture = new TagValueCapture([tag]);
|
||||
capture.Arm();
|
||||
|
||||
var frozen = new[] { tag }.ToDictionary(t => t.Address).ToFrozenDictionary();
|
||||
var ctx = new PerPlcContext
|
||||
{
|
||||
PlcName = "PLC1",
|
||||
TagMap = new BcdTagMap(frozen),
|
||||
Counters = new ProxyCounters(),
|
||||
Logger = NullLogger.Instance,
|
||||
Cache = cache,
|
||||
Capture = capture,
|
||||
};
|
||||
var plc = new PlcOptions { Name = "PLC1", ListenPort = 0, Host = "127.0.0.1", Port = backendPort };
|
||||
await using var mux = BuildMux(plc, ctx);
|
||||
|
||||
var (c, p, l) = await ConnectClientAsync(mux, plc.Name);
|
||||
try
|
||||
{
|
||||
// First read — cache miss. The pipeline records the observation and the
|
||||
// entry is stored with the per-tag observations attached.
|
||||
await c.SendAsync(BuildFc03(0x0001, 100, 1), SocketFlags.None);
|
||||
_ = await ReadOneFrameAsync(c, TestContext.Current.CancellationToken);
|
||||
|
||||
var afterMiss = capture.Snapshot().Single(o => o.Address == 100);
|
||||
afterMiss.UpdatedAtUtc.ShouldNotBeNull("the cache-miss read must record an observation");
|
||||
afterMiss.DecodedValue.ShouldBe(1234);
|
||||
afterMiss.RawLow.ShouldBe((ushort)0x1234);
|
||||
|
||||
// Clear the capture's slots (models the debug view holding no fresh data),
|
||||
// then re-arm. Only the cache-hit replay can repopulate slot 100 now — the
|
||||
// backend is not contacted again.
|
||||
capture.Disarm();
|
||||
capture.Arm();
|
||||
capture.Snapshot().Single(o => o.Address == 100).UpdatedAtUtc
|
||||
.ShouldBeNull("Disarm must clear the slot");
|
||||
|
||||
// Second read — cache hit. No backend round-trip; the pipeline is bypassed.
|
||||
await c.SendAsync(BuildFc03(0x0002, 100, 1), SocketFlags.None);
|
||||
_ = await ReadOneFrameAsync(c, TestContext.Current.CancellationToken);
|
||||
backend.RequestCount.ShouldBe(1, "the second read must be served from the cache");
|
||||
|
||||
var afterHit = capture.Snapshot().Single(o => o.Address == 100);
|
||||
afterHit.UpdatedAtUtc.ShouldNotBeNull(
|
||||
"a cache hit must re-record the served read so the debug view does not freeze");
|
||||
afterHit.DecodedValue.ShouldBe(1234, "the replayed observation carries the decoded value");
|
||||
afterHit.RawLow.ShouldBe((ushort)0x1234, "the replayed observation carries the raw BCD nibbles");
|
||||
afterHit.Direction.ShouldBe(CaptureDirection.Read);
|
||||
}
|
||||
finally
|
||||
{
|
||||
c.Dispose();
|
||||
await p.DisposeAsync();
|
||||
l.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user