a9b23556eb
A caller addressing a standby central node directly (or hitting Traefik inside its /health/active probe window after a failover) used to get 503 STANDBY_NODE. The standby now relays the request verbatim to the active peer over HTTP (IStandbyRequestForwarder / HttpStandbyRequestForwarder) and replays the response; method scripts and Route.To() still execute only on the active node. - ScadaBridge:InboundApi:StandbyForwardPeerEndpoints (static list of the OTHER central node's HTTP base URL; empty = old 503 behaviour) + StandbyForwardTimeout (60s), both startup-validated. - Peers tried in order: peer 503 stamped X-ScadaBridge-Standby, connection failure or timeout → next peer; all fail → 503 ACTIVE_NODE_UNREACHABLE. A 503 without the marker is the active node's own answer and is replayed. - One-hop loop guard via X-ScadaBridge-Forwarded; 413 body cap now runs BEFORE the active-node check so a standby never buffers/relays an oversized body. - AuditWriteMiddleware skips its own ApiInbound row for a relayed request so a call is audited once, on the node that executed it. - Rig + env2 central nodes configured; spec (Component-InboundAPI.md → Standby forwarding), Component-Host.md options row, CLAUDE.md updated. - 20 new tests (filter decision + relay mechanics + validator). Claude-Session: https://claude.ai/code/session_019ZxXT5sxZs5XrtdwYzJDPV
49 lines
2.8 KiB
C#
49 lines
2.8 KiB
C#
using ZB.MOM.WW.Configuration;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
|
|
|
/// <summary>
|
|
/// Validates <see cref="InboundApiOptions"/> at startup. <see cref="InboundApiOptions.DefaultMethodTimeout"/>
|
|
/// bounds each inbound-API method execution and <see cref="InboundApiOptions.MaxRequestBodyBytes"/> caps the
|
|
/// accepted <c>POST /api/{methodName}</c> body before it is buffered — a zero/negative timeout would cancel
|
|
/// every request immediately (or never), and a non-positive body cap would reject every request with HTTP 413.
|
|
/// Registered with <c>ValidateOnStart()</c> so a bad <c>ScadaBridge:InboundApi</c> section fails fast at boot
|
|
/// with a clear, key-naming message rather than degrading the endpoint at runtime.
|
|
/// <para>
|
|
/// <see cref="InboundApiOptions.ApiKeyPepper"/> is intentionally NOT validated here: an empty pepper is a
|
|
/// legitimate dev default, and production pepper strength is owned by the shared auth-key library / startup
|
|
/// validator, not by this options validator.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class InboundApiOptionsValidator : OptionsValidatorBase<InboundApiOptions>
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void Validate(ValidationBuilder builder, InboundApiOptions options)
|
|
{
|
|
builder.RequireThat(options.DefaultMethodTimeout > TimeSpan.Zero,
|
|
$"ScadaBridge:InboundApi:DefaultMethodTimeout must be a positive duration " +
|
|
$"(was {options.DefaultMethodTimeout}); it bounds each inbound-API method execution.");
|
|
|
|
builder.RequireThat(options.MaxRequestBodyBytes > 0,
|
|
$"ScadaBridge:InboundApi:MaxRequestBodyBytes must be greater than zero " +
|
|
$"(was {options.MaxRequestBodyBytes}); it caps the accepted request body before buffering.");
|
|
|
|
builder.RequireThat(options.ApiMethodCacheTtl > TimeSpan.Zero,
|
|
$"ScadaBridge:InboundApi:ApiMethodCacheTtl must be a positive duration " +
|
|
$"(was {options.ApiMethodCacheTtl}); it bounds how long a resolved ApiMethod row is cached.");
|
|
|
|
builder.RequireThat(options.StandbyForwardTimeout > TimeSpan.Zero,
|
|
$"ScadaBridge:InboundApi:StandbyForwardTimeout must be a positive duration " +
|
|
$"(was {options.StandbyForwardTimeout}); it bounds each standby→active relay attempt.");
|
|
|
|
foreach (var peer in options.StandbyForwardPeerEndpoints)
|
|
{
|
|
var ok = Uri.TryCreate(peer, UriKind.Absolute, out var uri)
|
|
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
|
builder.RequireThat(ok,
|
|
$"ScadaBridge:InboundApi:StandbyForwardPeerEndpoints entry '{peer}' must be an absolute " +
|
|
$"http/https URL (e.g. http://central-b:5000); it is the peer a standby relays inbound API calls to.");
|
|
}
|
|
}
|
|
}
|