Skip to content

Extend New-IshSession on PowerShell 7+ with automatic User-Agent fallback to avoid cloud WAF bot-management blocking #275

Description

@ddemeyer

Context

More customers run Tridion Docs behind containerized/managed infrastructure on AWS, Azure or GCP, increasingly fronted by a WAF. All three clouds ship managed bot-detection rule groups that inspect User-Agent on every request: AWS WAF Bot Control, Azure Front Door/App Gateway WAF bot manager rules, GCP Cloud Armor adaptive protection/bot management. HttpClient sends no User-Agent unless set explicitly, and ISHRemote never has. No complaints yet, but it's a matter of time as more customers enable these rule groups — cheap to fix now against a live repro, expensive as a reactive support incident later.

Measured against a live environment (ish.example.com/health, AWS WAF fronted):

User-Agent sent Result
(none) 200 OK
ISHRemote/8.3.0 (bare custom token) 403 Forbidden
curl/8.0.1, PostmanRuntime/7.36.0, python-requests/2.31.0 403 Forbidden
Mozilla/5.0 (compatible; ISHRemote/8.3.0; +https://github.com/rws/ISHRemote) 200 OK

Counter-intuitively, this rule set doesn't punish a missing header — only headers matching known non-browser tool signatures. A bare Product/Version token gets bucketed with curl/python-requests and blocked. Only the RFC-sanctioned crawler self-identification convention (same format Googlebot/Bingbot use) got through. So "always send ISHRemote/x.y.z" is worse than today's behavior, and "never send anything" is unsafe against WAF configs that punish a missing header instead. Both failure directions exist; the fix must be adaptive, not a single hardcoded choice.

Fallback mechanism

Default behavior is unchanged: no User-Agent header sent unless a WAF has already proven that blocks the connection.

New-IshSession already makes exactly one "prove the credentials work" call per protocol before returning the session — a connectionconfiguration.xml download over HttpClient, and (for WcfSoapWithOpenIdConnect) an Application25.GetVersion() SOAP call that exists specifically to confirm the connection is alive. Today either failing with 403 fails New-IshSession outright.

New behavior: on 403 from that first call, retry it once with Mozilla/5.0 (compatible; ISHRemote/{version}; +https://github.com/rws/ISHRemote). On success, that header sticks for the session's lifetime — every subsequent request carries it, and a Write-Verbose logs the fallback and the value in use. On repeat failure, New-IshSession fails exactly as today.

No new parameter, no configuration, nothing for existing scripts to learn.

Non-goals

  • WcfSoapWithWsTrust is out of scope. Legacy/maintain-only, removed in 16.0.0, doesn't use the endpoint-behavior/message-inspector pattern today unlike WcfSoapWithOpenIdConnect.
  • Not a per-call retry. Only the two existing "prove the connection" probe calls in New-IshSession are guarded, not every subsequent SOAP/OpenAPI call. A WAF rule inconsistent per-operation won't be caught — closing that would mean retrofitting retry logic into per-cmdlet exception handling, which is duplicated across dozens of cmdlet files rather than centralized. Not worth it for a corner case.

Implementation details

Scope: IshSession.cs, InfoShareWcfSoapWithOpenIdConnectConnection.cs, one new file. OpenApiWithOpenIdConnect and its OIDC discovery/token calls are covered for free — they share the same HttpClient as connectionconfiguration.xml.

  1. IshSession.cs

    • Add a private mutable holder _userAgentState (class with a single string Value property, default null) — must be a reference type read live by the WCF inspector, not captured once at construction.
    • LoadConnectionConfiguration(Uri): on !IsSuccessStatusCode and StatusCode == Forbidden, set _userAgentState.Value to the fallback string, set it on _httpClient.DefaultRequestHeaders.UserAgent, WriteVerbose the fallback and value, retry GetAsync once. Only on repeat failure throw the existing ArgumentException.
    • CreateInfoShareWcfSoapWithOpenIdConnectConnection(): wrap application25Proxy.GetVersion() in try/catch. On a 403-shaped failure — unwrap CommunicationException/ProtocolExceptionWebExceptionHttpStatusCode.Forbidden (WCF's exception wrapping isn't fully consistent across .NET targets) — set _userAgentState.Value to the fallback, WriteVerbose the fallback and value, retry GetVersion() once on the same already-open channel (inspector reads mutable state at send time, no channel rebuild needed).
    • CreateOpenApiWithOpenIdConnectConnection(): no dedicated retry — shares _httpClient, inherits whatever LoadConnectionConfiguration resolved.
  2. New file Connection/InfoShareWcfSoapUserAgentClientMessageInspector.cs

    • InfoShareWcfSoapUserAgentClientMessageInspector : IClientMessageInspector — in BeforeSendRequest, if _userAgentState.Value != null, set HttpRequestMessageProperty.Headers[HttpRequestHeader.UserAgent] on the outgoing message.
    • InfoShareWcfSoapUserAgentEndpointBehavior : IEndpointBehavior — standard passthrough wiring ApplyClientBehavior to add the inspector.
    • Constructed once per IshSession, holding a reference to the same _userAgentState so a later flip is visible to every channel immediately.
  3. InfoShareWcfSoapWithOpenIdConnectConnection.cs

    • Mechanical: add one .EndpointBehaviors.Add(userAgentBehavior) next to each of the ~18 existing .EndpointBehaviors.Add(bearerCredentials) call sites, so the header that got the probe through also rides along on every real cmdlet call afterward (confirmed this WAF evaluates per-request, not per-session/cookie).
  4. Version string: reuse IshSession.ClientIshVersion/ClientVersion (already computed from assembly file version) for the fallback string.

Acceptance criteria

  • New-IshSession against a non-WAF'd environment is byte-identical to today (no User-Agent sent, verified via capture/server log).
  • New-IshSession against an environment blocking bare-token UAs succeeds transparently, fallback header set, for both OpenApiWithOpenIdConnect and WcfSoapWithOpenIdConnect.
  • Fallback triggers a Write-Verbose entry stating the fallback and the User-Agent value in use.
  • Once triggered, every subsequent cmdlet call in that session carries the fallback header (spot-check Get-IshUser post-session-creation).
  • Release notes entry

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions