]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(ui-server): add source-aware gateway access policy (#1891)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 14 Jun 2026 18:24:13 +0000 (20:24 +0200)
committerGitHub <noreply@github.com>
Sun, 14 Jun 2026 18:24:13 +0000 (20:24 +0200)
commit72bb3f08a9545d1d8eec1aeb9444f73b7ed1ae6c
tree5cea1e3f98c43a6eb87f19a8ae6f48df6569fb62
parentef750a0caa9b16abdd686f83e0f620abc115c7ca
feat(ui-server): add source-aware gateway access policy (#1891)

* feat(config): add UI server access policy schema

* feat(config): add UI server access policy defaults

* feat(ui-server): add source-aware access evaluation

* feat(ui-server): authorize requests before authentication

* feat(ui-server): gate HTTP requests by access policy

* feat(ui-server): gate MCP requests by access policy

* feat(ui-server): gate WebSocket upgrades by access policy

* chore(docker): align UI access policy for local compose

* docs(ui): document gateway access policy

* [autofix.ci] apply automated fixes

* refactor(ui-server): type access decisions and unify request prologue

- Replace stringly-typed denial reason with UIServerAccessDenialReason enum and
  DENIAL_MESSAGES rendering map; UIServerAccessDecision is a discriminated union
  narrowed by 'allowed'.
- Memoize the per-request access decision via WeakMap so the policy is evaluated
  once per request.
- Add runRequestPrologue template method on AbstractUIServer; rate-limit now
  accounts denied requests, closing pre-auth flood amplification.
- Validate accessPolicy.trustedProxies entries as IPv4/IPv6 literals at the
  schema layer; precompile the normalized set per UIServerConfiguration.
- Drop the dead httpServer.on('connect') listener in UIWebSocketServer; use
  StatusCodes consistently in UIMCPServer.
- README clarifies the binary loopback-vs-non-loopback model, single-hop XFF,
  TLS termination via reverse proxy only, and adds a migration note for the
  default requireTlsForNonLoopback: true.

* refactor(ui-server): split access policy module and tighten its surface

- Split UIServerUtils.ts into UIServerAccessPolicy (decision, forwarded
  parsing, host/origin allowlists) and UIServerNet (IP/host normalization,
  loopback, header tokenization). UIServerUtils retains auth tokens and WS
  subprotocol negotiation only.
- Make evaluateUIServerAccess private; tests and runtime callers go through
  resolveUIServerAccess so the decision cache is always exercised.
- Move accessDecisionCache and trustedProxiesCache from module scope to a
  per-AbstractUIServer instance cache injected into resolveUIServerAccess.
- Quote-aware splitHeaderList honors RFC 7239 / RFC 7230 double-quoted
  values; commas inside "…" are preserved.
- Replace the {error?, value?} dialects in forwarded-header parsers with a
  discriminated ParseOutcome<T>; close the Forwarded params type to
  by/for/host/proto.
- Drop the unreachable isDirectTLSRequest helper and its TLSSocket import.
- Set HTTP server requestTimeout=30s and headersTimeout=5s on
  AbstractUIServer to bound slow-loris exposure on rejected upgrades.
- Ensure UIMCPServer.handleMcpRequest releases the transport and the MCP
  server in every error branch (idempotent cleanup).
- Promote MockUpgradeSocket to UIServerTestUtils.ts; replace the duplicate
  test factory createAccessPolicyConfiguration with the canonical
  createMockUIServerConfiguration.
- Cover the Docker dual-stack case where an IPv4-mapped IPv6 remote address
  matches an IPv4 trustedProxies entry.
- README accessPolicy cell switches to per-field sub-bullets, aligned with
  the log and worker rows.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor(ui-server): drop @file headers from new policy modules

@file JSDoc is the test-suite convention (TEST_STYLE_GUIDE.md) and is
absent from the rest of src/. Align the new UIServerAccessPolicy and
UIServerNet modules with the existing source-tree style.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* docs(ui-server): tighten access policy comments and README

- Drop README migration note and the duplicate access policy paragraph;
  the configuration table cell already specifies the policy.
- Tighten the Docker section access policy paragraph.
- Trim narrative meta-commentary in UIServerAccessPolicy and UIServerNet
  JSDoc; let constant and type names carry the rest.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor(ui-server): address review feedback on access policy

- isHostAllowed: when the immediate peer is a trusted proxy and
  X-Forwarded-Host is present, match it (not the immediate Host) against
  allowedHosts, so reverse proxies that rewrite Host to an internal
  upstream name are not rejected.
- isOriginAllowed: compare allowedOrigins via parsed URL protocol+host
  rather than literal string equality, so entries with a trailing slash or
  with a protocol-default port match the canonical browser-emitted Origin.
- UIMCPServer: destroy the response and request sockets after a denied
  prologue, mirroring UIHttpServer.
- evaluateUIServerAccess: parse the Forwarded header once and pass the
  outcome to getForwardedProtocol and getForwardedClientAddress.
- hasDuplicateHeaders: count rawHeaders in a single pass.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): include prologue headers on WebSocket upgrade denial

The WebSocket upgrade denial path wrote only the HTTP/1.1 status line, so
rate-limit denials reached clients without the Retry-After header that
the HTTP and MCP transports already emit.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): align auth denial across transports

- UIMCPServer: destroy the response and request sockets after a denied
  authentication, mirroring UIHttpServer.
- UIWebSocketServer: include the WWW-Authenticate header on auth-denied
  upgrades so RFC 7235 clients receive the Basic challenge that the HTTP
  and MCP transports already emit.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): close denied connections cleanly and harden allowedOrigins

- AbstractUIServer: also call setTimeout() on the http server so the
  HTTP/2 transport (Http2Server lacks requestTimeout/headersTimeout) gets
  the same idle bound.
- UIHttpServer / UIMCPServer: emit Connection: close on prologue and auth
  denials and drop the explicit res.destroy()/req.destroy() calls; the
  body now flushes before the connection closes.
- UIWebSocketServer: destroy the upgrade socket from the write callback
  so the response bytes are flushed before teardown.
- UIServerAccessPolicySchema: refine allowedOrigins entries to reject
  paths, query strings, and fragments at config load instead of silently
  ignoring them at runtime.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): honor RFC 7239 host parameter and standardize upgrade rejection

- UIServerAccessPolicy: parse the Forwarded host parameter symmetrically
  with for and proto. Both Forwarded host=... and X-Forwarded-Host now
  feed isHostAllowed when the immediate peer is trusted, with a new
  AmbiguousForwardedHost denial reason when they conflict.
- UIWebSocketServer: build upgrade rejection responses through a single
  helper that always emits Connection: close and Content-Length: 0,
  matching the HTTP and MCP transports.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(test): wait on response finish event in gzip tests

Replace the fixed 50 ms waitForStreamFlush delay with await once(res,
'finish') in the UIHttpServer gzip tests. MockServerResponse already
emits 'finish' from end(), so the wait is now event-driven and stops
flaking on slow CI runners (notably Windows / Node 24.x).

Drop the now-unused waitForStreamFlush helper and GZIP_STREAM_FLUSH_DELAY_MS
constant.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): scope HTTP timeouts to slow-loris and tighten Forwarded parser

- AbstractUIServer: drop the connection-wide setTimeout call. requestTimeout
  and headersTimeout already cover slow-loris on HTTP/1.1, and HTTP/2 streams
  are per-request and not vulnerable to the same pattern. The dropped 30 s
  socket-inactivity cap was killing legitimate long-running responses on
  the deprecated HTTP transport.
- UIServerAccessPolicy: split Forwarded pairs with quote awareness so that
  RFC 7239 quoted-string values containing semicolons are not truncated.
  Mirrors the quote handling already in splitHeaderList.
- UIWebSocketServer: place Connection: close after the extraHeaders spread
  in buildUpgradeRejectionResponse to match the HTTP and MCP transports
  and keep the close header non-overridable by callers.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): omit Connection header on HTTP/2 and prioritize trusted-peer denial reason

- AbstractUIServer: add getConnectionCloseHeader() returning an empty
  header set when the underlying server is HTTP/2. HTTP/2 forbids the
  Connection header as a connection-specific header; Node otherwise emits
  an UnsupportedWarning and silently drops the value. Streams are closed
  by res.end() on that path. HTTP/1.1 keeps the explicit Connection: close
  to terminate keep-alive on denial.
- UIHttpServer, UIMCPServer: route prologue-denial and auth-denial
  responses through the helper instead of hardcoding Connection: close.
- UIServerAccessPolicy: hoist the trusted-peer check above the protocol
  and host ambiguity checks. An untrusted peer sending forwarded headers
  is now denied as ForwardedFromUntrustedPeer regardless of duplicate
  proto/host metadata, so operator logs reflect the trust violation
  rather than a parser-level symptom. Duplicate-headers retains absolute
  priority. Tests added for the protocol- and host-ambiguity paths from
  an untrusted peer.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): treat empty forwarded values as absent

Empty forwarded-header values were treated as present-with-empty-string,
which caused two misbehaviors when the immediate peer was a trusted proxy:

- An empty 'host=' parameter inside a Forwarded header (or an empty
  X-Forwarded-Host) shadowed the real Host header. The policy resolved
  the host to '' and denied the request as HostNotAllowed even though
  the actual Host was in allowedHosts.
- An empty X-Forwarded-Proto was rejected as AmbiguousForwardedProtocol
  (via splitHeaderList returning an empty array) instead of being
  treated as missing.

A nonEmpty() helper now normalizes empty/undefined header values to
undefined at parse time. parseSingleForwardedHeader skips empty
parameter values rather than recording them. The two getters fall
through to the next source when the upstream value is empty.

Tests cover the host fallback paths (Forwarded host= and
X-Forwarded-Host) and the protocol absent-vs-ambiguous distinction.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): accept RFC 7239 hidden-identity forms and tighten Forwarded parser

- UIServerAccessPolicy: recognize the RFC 7239 nodename forms 'unknown'
  and obfuscated identifiers ('_' followed by token characters) in the
  Forwarded 'for=' parameter and X-Forwarded-For header. Hidden-identity
  forms now resolve to ABSENT, so the access decision falls back to the
  trusted proxy's own remote address as the client identifier (used as
  the rate-limit bucket and the logged identity). Previously these
  values were rejected as InvalidForwardedClient even though the rest
  of the request (TLS, host, origin, trust) was valid.
- UIServerAccessPolicy: strip Forwarded parameter quotes only when the
  pair is balanced. The previous '/^"|"$/g' replace stripped the
  leading or trailing quote independently and silently accepted
  unbalanced inputs such as 'for="203.0.113.10'. The new
  '/^"(.*)"$/' anchors both ends in a single match.
- README: clarify that allowLoopbackProxy requires the loopback peer
  to also be listed in trustedProxies. allowLoopbackProxy alone has
  no effect because the trusted-peer check denies forwarded headers
  from peers absent from the trustedProxies list.
- Tests: positive path for allowLoopbackProxy=true; identity-hidden
  paths for Forwarded for=unknown, Forwarded for=_obfuscated, and
  X-Forwarded-For: unknown.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): normalize empty X-Forwarded-For and align MCP auth with continuation pattern

- UIServerAccessPolicy: normalize an empty X-Forwarded-For header to
  undefined so the access decision falls back to the trusted proxy's own
  remote address rather than denying the request as InvalidForwardedClient
  or as AmbiguousForwardedClient when a Forwarded for= parameter is also
  present. Restores symmetry with X-Forwarded-Host and X-Forwarded-Proto
  empty-value handling.
- UIMCPServer: rewrite the auth path as a callback continuation. The
  previous capture-then-check pattern relied on authenticate() being
  synchronous and on the last next() call winning. The new shape
  matches UIHttpServer and UIWebSocketServer and continues to handle
  MCP requests only after a successful authentication callback.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): align MCP error responses and treat empty forwarded headers as absent

- UIMCPServer: emit Connection: close on the 404 not-found and on the
  centralized sendErrorResponse helper (BAD_REQUEST, METHOD_NOT_ALLOWED,
  INTERNAL_SERVER_ERROR), aligning these paths with the post-prologue
  and auth denial responses. The connection-close header is omitted on
  HTTP/2 by the existing getConnectionCloseHeader() helper.
- UIServerAccessPolicy: hasForwardedHeaders now ignores empty header
  values, restoring symmetry with the downstream forwarded-header
  parsers that already map empty values to absent. Empty X-Forwarded-*
  headers from an untrusted peer no longer trigger
  ForwardedFromUntrustedPeer; an empty X-Forwarded-Proto from a
  non-loopback untrusted peer now denies as TlsRequired (no proxy
  proof of TLS). Empty headers from a loopback peer no longer block
  the request.
- README: clarify that requireTlsForNonLoopback honors X-Forwarded-Proto
  / Forwarded: proto= from a trusted proxy and does not detect native
  direct TLS; note that a compromised trustedProxies entry can bypass
  per-client rate limiting by varying X-Forwarded-For.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* chore(ui-server): trim redundant access-policy comments and JSDoc

- Drop module-level JSDoc on UIServerAccessPolicy and UIServerNet to
  match the codebase convention (no module-level JSDoc on peer files).
- Drop redundant inline comments whose content is already documented in
  the README or self-evident from the surrounding code: the empty
  X-Forwarded-For malformed branch, the allowedOrigins fallback, and
  the loopback-aliases derivation.
- Trim the verbose 'Discriminated by ...' prose on the UIServerAccessDecision
  and ParseOutcome types and remove the JSDoc on resolveUIServerAccess
  (function name and signature are self-explanatory; memoization is
  visible from the cache lookup).

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor(ui-server): extract splitQuoted and pickForwardedValue helpers

- UIServerNet: factor the quote-aware splitter into splitQuoted(value,
  delimiter); splitHeaderList becomes a thin wrapper. The previously
  duplicated state machine in UIServerAccessPolicy (splitForwardedPairs)
  is replaced by a direct splitQuoted call on ';'.
- UIServerAccessPolicy: factor the X-Forwarded-* / Forwarded ambiguity
  pick into pickForwardedValue. The three getForwarded* helpers now
  share a single source of truth for the present/absent/ambiguous
  decision; transport-specific tail logic (multi-hop X-Forwarded-For
  rejection, X-Forwarded-Proto multi-value rejection, lowercasing)
  stays local.
- UIServerAccessPolicy: drop the unreachable string-trim fallback in
  normalizeIPAddress (every input that fails normalizeHost also fails
  the downstream IP checks), narrow isSecureForwardedProtocol's
  parameter to string | undefined (no caller passes null), and skip
  the redundant pre-pass through normalizeHost in isSameHost (the
  IP-address branch already calls normalizeHost internally).
- UIServerAccessPolicy: replace Reflect.get with direct property
  access on IncomingMessage.headersDistinct and rawHeaders (typed
  members on Node 18+).
- Tests: extend the createMockIncomingMessage and the policy test's
  createAccessPolicyRequest factories with headersDistinct and
  rawHeaders defaults so direct property access works against the
  mocks.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor(ui-server): unify denial rendering and switch authenticate to boolean

- AbstractUIServer: introduce renderDenial(res, payload) and
  getUnauthorizedDenial() so HTTP and MCP route every prologue, 401,
  and post-handler error response through the same code path. The
  helper centralizes Content-Type, Connection-close (HTTP/2-aware),
  and 'res.headersSent' guarding.
- AbstractUIServer: change authenticate(req) to return a boolean
  instead of invoking a continuation with an Error. The CPS shape was
  fully synchronous in practice — every caller did
  'if (err != null)' — and the BaseError allocation was dead. The
  three transports collapse to 'if (!this.authenticate(req)) { … }'.
- AbstractUIServer + UIServerUtils: replace hand-typed reason phrases
  with getReasonPhrase() from http-status-codes; the helper functions
  isValidBasicAuth / isValidProtocolBasicAuth and
  getUsernameAndPasswordFromAuthorizationToken drop their continuation
  parameter accordingly.
- UIServerSecurity: replace the chunk-counter createBodySizeLimiter()
  with a shared readLimitedBody(req, maxBytes): Promise<Buffer> helper
  and a typed PayloadTooLargeError. UIHttpServer reads the request
  body via the helper instead of an event-based loop, and UIMCPServer
  detects oversized payloads via 'instanceof PayloadTooLargeError'
  rather than string-matching the error message.
- UIMCPServer: route sendErrorResponse through renderDenial so all
  4xx/5xx error responses share the same headers and Connection
  semantics; preserve the path-filter-before-authenticate ordering
  with a comment so unknown paths return 404 without revealing
  whether authentication would have succeeded.
- UIWebSocketServer: use getUnauthorizedDenial() for the upgrade
  rejection on auth failure instead of inlining the WWW-Authenticate
  string and the 'Unauthorized' literal.
- Tests: align UIServerUtils.test, UIServerSecurity.test, and
  UIMCPServer.test with the new APIs (boolean signatures, new
  PayloadTooLargeError, new readLimitedBody).

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor(ui-server): centralize access-policy defaults and validate allowedHosts entries

- ConfigurationSchema: introduce UI_SERVER_ACCESS_POLICY_DEFAULTS as the
  single source of truth for the access-policy field defaults
  (requireTlsForNonLoopback=true, allowLoopbackProxy=false, and the
  empty allowlists). The defaults previously lived inline in
  evaluateUIServerAccess.
- UIServerAccessPolicy: consume UI_SERVER_ACCESS_POLICY_DEFAULTS instead
  of the hardcoded literals so a default change propagates uniformly
  across the policy code, the schema, and the README.
- ConfigurationSchema: validate allowedHosts entries via normalizeHost
  so paths, queries, fragments, and other malformed Host values are
  rejected at config load. The previous z.string().min(1) accepted
  any non-empty string.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ui-server): factor access-policy test fixtures and address constants

- UIServerTestUtils: introduce TRUSTED_PROXY_IP, EXTERNAL_CLIENT_IP, and
  GATEWAY_HOST constants from RFC 5737 / RFC 3849 reserved ranges;
  factor the recurrent gateway access-policy configurations into
  createGatewayConfigWithTrustedProxy() and
  createGatewayConfigWithoutTrustedProxies() so subsequent test
  additions inherit a single source of truth for the gateway shape.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ui-server): cover missing denial reasons and tighten weak assertions

- UIServerAccessPolicy.test: cover the previously untested denial
  reasons AmbiguousForwardedProtocol (multi-value X-Forwarded-Proto),
  AmbiguousForwardedHeader (multi-entry Forwarded), and
  AmbiguousForwardedParameter (duplicate parameter inside one entry);
  cover the InvalidForwardedClient branch for non-IP, non-hidden 'for='
  values; cover AmbiguousForwardedClient when 'Forwarded: for=unknown'
  collides with X-Forwarded-For; cover the headersDistinct path of
  hasDuplicateHeaders.
- UIServerAccessPolicy.test: cover the malformed Origin URL branch in
  isOriginAllowed and the allowedHosts-fallback branch when
  allowedOrigins is empty; pin the design choices for whitespace-padded
  Host normalization, uppercase X-Forwarded-Proto values, IPv4-mapped
  IPv6 loopback proxies under allowLoopbackProxy, and the policy's
  intentional non-detection of req.socket.encrypted without forwarded
  protocol headers.
- UIServerAccessPolicy.test: tighten the previously weak assertions on
  the accessPolicy-undefined paths (now check the denial reason and
  the resolved client address); rename the misleading
  'should reject empty-but-present X-Forwarded-For' test to reflect
  what it actually exercises (no parseable addresses); replace the
  trivial cache-isolation assertion with a real
  cacheA.decisions.has(req) / cacheB.decisions.has(req) check.
- UIMCPServer.test: drop the two malformed-host tests that re-tested
  policy logic already covered by UIServerAccessPolicy.test; the
  Connection: close tests in the same describe still anchor the
  transport-level rendering contract.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ui-server): close coverage gaps and tighten weak assertions identified in second-pass audit

- ConfigurationSchema.test: validate the allowedHosts schema refine
  rejects malformed entries (excess colons, non-numeric port, port out
  of range, empty input) and accepts canonical hostnames and IP
  literals; lock the canonical UI_SERVER_ACCESS_POLICY_DEFAULTS map
  shape so a default change cannot drift silently.
- UIServerNet.test: cover normalizeHost directly (rejection branch is
  the boundary of the allowedHosts schema refine; previously only
  exercised indirectly through transport tests).
- UIServerSecurity.test: assert readLimitedBody propagates upstream
  stream errors (security-relevant fail-closed behavior).
- UIServerAccessPolicy.test: rename the misleading
  for=unknown collision test to reflect the actual code path
  (collision is generic, not unknown-specific); drop the duplicate
  encrypted-proxy plaintext rejection (the encrypted flag is ignored
  by the policy by design and a dedicated test already pins it);
  tighten the whitespace-padded Host test to use an explicit
  allowedHosts list so the trim path is genuinely exercised rather
  than the loopback alias fallback.
- UIServerTestUtils: drop the dead EXTERNAL_CLIENT_IP export and
  correct the RFC reference comment (RFC 5737 IPv4 / RFC 2606
  example.com — RFC 3849 covers IPv6 and is irrelevant here).
- UIWebSocketServer: add a brief comment documenting why
  buildUpgradeRejectionResponse stays separate from
  AbstractUIServer.renderDenial — pre-handshake WS rejections write
  raw HTTP/1.1 to a Duplex socket while renderDenial targets
  ServerResponse.
- src/utils/index: re-export UI_SERVER_ACCESS_POLICY_DEFAULTS so the
  defaults can be asserted from the schema test layer.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): tighten X-Forwarded-Host validation and host charset

- UIServerAccessPolicy: getForwardedHost rejects multi-value
  X-Forwarded-Host as AmbiguousForwardedHost, mirroring the symmetric
  guards in getForwardedProtocol and getForwardedClientAddress. The PR
  description's claim that 'multi-value X-Forwarded-* are rejected'
  now applies uniformly across the three forwarded dimensions.
- UIServerNet: normalizeHost validates the hostname charset against a
  conservative [a-z0-9._-]+ allowlist on the dot path. Inputs with
  commas, spaces, or unbalanced brackets are rejected at config-load
  time when used as allowedHosts entries, and at runtime as a
  defense-in-depth check on the Host header.
- UIServerNet: isValidPort rejects port 0 (RFC 6335 reserved). Port 0
  in a client-side Host header has no useful meaning; tightening to
  1..65535 matches common practice and the prior config-bind path is
  unaffected.
- README: requireTlsForNonLoopback wording corrected — non-loopback
  requests without forwarded protocol headers are denied as
  tls-required, not merely 'not detected', even when the underlying
  socket is encrypted.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): empty Forwarded header, 405 Allow, wildcard misconfig warn

- UIServerAccessPolicy: parseSingleForwardedHeader normalises an empty
  Forwarded header value (e.g., 'Forwarded: ') to ABSENT instead of
  rejecting it as AmbiguousForwardedHeader. The PR description claim
  that 'empty forwarded values are treated as absent' now applies to
  the header value itself, not only to parameters inside the header.
- UIMCPServer: sendErrorResponse accepts an optional headers argument
  so 405 Method Not Allowed responses advertise 'Allow: GET, POST,
  DELETE' per RFC 9110 §15.5.6.
- AbstractUIServer: warn at startup when the UI server is bound to a
  wildcard host ('', '0.0.0.0', '::') with an empty
  accessPolicy.allowedHosts; the previously silent lockout (every
  request denied as host-not-allowed) now produces an actionable log
  line pointing at the configuration to adjust.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(ui-server): broaden misconfig warning and log rate-limit denials

- AbstractUIServer: warnIfMisconfigured now also fires when the UI
  server is bound to a non-loopback specific host with
  requireTlsForNonLoopback=true and no accessPolicy.trustedProxies;
  the previously silent path (every plaintext request denied as
  tls-required) now produces an actionable startup log line. Wildcard
  binding with empty allowedHosts retains its dedicated warning.
- AbstractUIServer: rate-limit denials now emit a warn log symmetric
  to the access-policy 403 path so operators can detect probing.
  Previously the 429 branch returned silently, which made abusive
  clients invisible from the logs.
- README: drop the 'even when the underlying socket is encrypted'
  caveat from the requireTlsForNonLoopback description; the simulator
  only listens via plaintext or h2c, so the scenario described was
  unreachable. The runtime behaviour is unchanged and the test that
  pins the design choice stays as defense in depth.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
---------

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
28 files changed:
README.md
docker/config.json
docker/docker-compose.yml
src/assets/config-template.json
src/charging-station/ui-server/AbstractUIServer.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIMCPServer.ts
src/charging-station/ui-server/UIServerAccessPolicy.ts [new file with mode: 0644]
src/charging-station/ui-server/UIServerFactory.ts
src/charging-station/ui-server/UIServerNet.ts [new file with mode: 0644]
src/charging-station/ui-server/UIServerSecurity.ts
src/charging-station/ui-server/UIServerUtils.ts
src/charging-station/ui-server/UIWebSocketServer.ts
src/utils/Configuration.ts
src/utils/ConfigurationSchema.ts
src/utils/index.ts
tests/charging-station/ui-server/UIHttpServer.test.ts
tests/charging-station/ui-server/UIMCPServer.test.ts
tests/charging-station/ui-server/UIServerAccessPolicy.test.ts [new file with mode: 0644]
tests/charging-station/ui-server/UIServerNet.test.ts [new file with mode: 0644]
tests/charging-station/ui-server/UIServerSecurity.test.ts
tests/charging-station/ui-server/UIServerTestConstants.ts
tests/charging-station/ui-server/UIServerTestUtils.ts
tests/charging-station/ui-server/UIServerUtils.test.ts
tests/charging-station/ui-server/UIWebSocketServer.test.ts
tests/utils/Configuration.test.ts
tests/utils/ConfigurationSchema.test.ts
tests/utils/ConfigurationValidation.test.ts