Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/ReleaseNotes-ISHRemote-8.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The below text describes the delta compared to fielded release ISHRemote v8.2.
* Migrated all 58 `*.Tests.ps1` files from Pester v5 to Pester v6 (`Should -Be` to `Should-Be`, `Should -BeExactly` to `Should-BeString -CaseSensitive`, `Should -Not -BeNullOrEmpty` to `Should-NotBeNull`, `Should -Throw "msg"` to `Should-Throw -ExceptionMessage "msg"`, etc.). CI install gates updated to `-MinimumVersion 6.0.0`. Classic `Should -Not -Throw` retained as there is no `Should-NotThrow` equivalent in Pester 6. Hardened the library for parallel test execution by replacing the process-wide `TrisoftCmdletLogger` singleton with per-cmdlet `ILogger` routing and adding a double-checked lock on `IshSession._ishTypeFieldSetup` to eliminate Collection was modified races under `Run.Parallel = $true`. CI Pester invocations now use `New-PesterConfiguration` (with `Run.Parallel = $false`) so parallel mode can be toggled in one place when ready. See #242, #265, #266.
* Fixed `New-IshSession` (protocol `WcfSoapWithOpenIdConnect`, PowerShell 7.2+/.NET 6.0+) throwing `FileLoadException: Could not load file or assembly 'Microsoft.IdentityModel.Tokens, Version=8.14.0.0, ...'. The located assembly's manifest definition does not match the assembly reference.` on machines where a different build of `Microsoft.IdentityModel.Tokens` (and related `Duende.IdentityModel.OidcClient`) is registered in the Global Assembly Cache (GAC) — observed on machines with Microsoft Intune Management Extension installed. `AppDomainModuleAssemblyInitializer` now force-loads ISHRemote's own bundled copies of `Duende.IdentityModel`, `Duende.IdentityModel.OidcClient`, `Microsoft.IdentityModel.Abstractions/.Logging/.Tokens/.Tokens.Saml/.Xml` as early as possible during module import, and `SessionCmdlet.BeginProcessing` now reports the full forced list over `-Verbose`. Root cause for the `Duende.IdentityModel.OidcClient` variant: `InfoShareOpenIdConnectSystemBrowser` was `public` and implemented `Duende.IdentityModel.OidcClient.Browser.IBrowser`, which put it in `Trisoft.ISHRemote.dll`'s exported types, forcing PowerShell's own binary-module cmdlet discovery (`Assembly.GetExportedTypes()`) to resolve `Duende.IdentityModel.OidcClient` before `IModuleAssemblyInitializer.OnImport()` ever ran — see Breaking Changes - Code. A new `TestPrerequisite.Tests.ps1` check asserts no assembly is ever loaded from the GAC on PowerShell Core. See #272. Thanks @ddemeyer
* Fixed cmdlets over protocol `WcfSoapWithOpenIdConnect` occasionally throwing `An unsecured or incorrectly secured fault was received from the other party` on the first SOAP call after a channel fault, requiring the user to re-run the same cmdlet for it to succeed (the existing #201/#219 rebuild-on-next-call logic only kicked in on a second, separate call). Each `Get*25Channel()` method in `InfoShareWcfSoapWithOpenIdConnectConnection` now returns the channel wrapped in a new `RetryOnFaultProxy<T>` (`System.Reflection.DispatchProxy`) that catches `CommunicationException`/`FaultException` on the actual SOAP call, rebuilds the channel via the existing rebuild logic, and retries exactly once within the same cmdlet invocation before propagating any further failure — with the original exception type/stack trace preserved. Requires a new `net48`-only NuGet dependency, `System.Reflection.DispatchProxy` (built into the BCL on `net6.0`/`net10.0`). See #273. Thanks @ddemeyer
* Extended `New-IshSession` with an automatic `User-Agent` fallback on PowerShell 7+ so it no longer gets blocked outright by cloud WAF bot-management rule groups (AWS WAF Bot Control, Azure Front Door/App Gateway WAF bot manager rules, GCP Cloud Armor adaptive protection/bot management) that inspect the `User-Agent` header on every request. Measured behavior against a live WAF-fronted environment: sending no `User-Agent` at all (today's default) or a bare `Product/Version` token (e.g. `ISHRemote/8.3.0`) both get bucketed with known non-browser tooling (`curl`, `python-requests`, ...) and blocked with `HTTP 403 Forbidden`. When WAF blocks the connection on the first `403` from the `connectionconfiguration.xml` probe, `New-IshSession` retries once with `Mozilla/5.0 (compatible; ISHRemote/{version}; +https://github.com/rws/ISHRemote)`; on success that header sticks for the session's lifetime and every subsequent request carries it. Windows PowerShell 5.1 (`net48`) will warn instead of a raw WCF exception, recommending you to ask your Tridion Docs administrator to adjust the WAF bot-management rule for this endpoint, or switch to PowerShell 7+ where the fallback is applied automatically. See #275. Thanks @ddemeyer



Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright (c) 2014 All Rights Reserved by the SDL Group.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if !NET48
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
#endif

namespace Trisoft.ISHRemote.Connection
{
/// <summary>
/// Mutable holder for a lazily-decided User-Agent value. Reference type on purpose: <see cref="Objects.Public.IshSession"/>
/// creates exactly one instance and hands it to every <see cref="InfoShareWcfSoapWithOpenIdConnectConnection"/>
/// endpoint behavior at construction time. If a cloud WAF (AWS WAF Bot Control, Azure Front Door/App Gateway
/// WAF bot manager, GCP Cloud Armor) later blocks a bare/no User-Agent request, <see cref="Objects.Public.IshSession"/> flips
/// <see cref="Value"/> once - every already-constructed InfoShareWcfSoapUserAgentClientMessageInspector
/// observes the change immediately on its next outgoing message, without rebuilding any channel. A plain
/// <c>string</c> field could not do this: strings are immutable, so passing one at construction time only ever
/// shares a snapshot, never the later mutation. See #275.
/// </summary>
/// <remarks>
/// Declared unconditionally (not under <c>#if !NET48</c>) because <see cref="Objects.Public.IshSession"/> - which builds for
/// net48/net6.0/net10.0 alike - owns and mutates the single instance from its HttpClient-based
/// LoadConnectionConfiguration fallback regardless of target framework. Only the WCF endpoint-behavior wiring
/// below that reads it is net6.0+/net10.0-only, because on net48 the WcfSoapWithOpenIdConnect SOAP channels are
/// built with ChannelFactory.CreateChannelWithIssuedToken(...), which does not go through EndpointBehaviors/
/// IClientMessageInspector at all - see IshSession.CreateInfoShareWcfSoapWithOpenIdConnectConnection for the
/// resulting net48 limitation (SOAP calls cannot carry the fallback header; a Write-Warning is emitted instead).
/// </remarks>
internal sealed class UserAgentState
{
/// <summary>
/// Null (so header omitted, today's default behavior) until a 403 is observed against a User-Agent-sensitive
/// WAF rule, at which point this becomes the RFC-sanctioned crawler self-identification convention
/// (same format Googlebot/Bingbot use) for the remaining lifetime of the owning <see cref="Objects.Public.IshSession"/>.
/// </summary>
public string Value { get; set; }
}

#if !NET48
/// <summary>
/// Sets the outgoing SOAP message's HTTP <c>User-Agent</c> header whenever <see cref="UserAgentState.Value"/> is
/// non-null. Added as a <see cref="System.ServiceModel.Dispatcher.IClientMessageInspector"/> next to the existing
/// <c>bearerCredentials</c> endpoint behavior wiring in <see cref="InfoShareWcfSoapWithOpenIdConnectConnection"/>.
/// See #275.
/// </summary>
internal sealed class InfoShareWcfSoapUserAgentClientMessageInspector : IClientMessageInspector
{
private readonly UserAgentState _userAgentState;

internal InfoShareWcfSoapUserAgentClientMessageInspector(UserAgentState userAgentState)
{
_userAgentState = userAgentState;
}

public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
if (_userAgentState.Value != null)
{
object propertyObject;
HttpRequestMessageProperty httpRequestMessageProperty;
if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name, out propertyObject))
{
httpRequestMessageProperty = (HttpRequestMessageProperty)propertyObject;
}
else
{
httpRequestMessageProperty = new HttpRequestMessageProperty();
request.Properties[HttpRequestMessageProperty.Name] = httpRequestMessageProperty;
}
httpRequestMessageProperty.Headers[HttpRequestHeader.UserAgent] = _userAgentState.Value;
}
return null;
}

public void AfterReceiveReply(ref Message reply, object correlationState)
{
// no-op, only outgoing requests need the User-Agent header
}
}

/// <summary>
/// Standard passthrough <see cref="IEndpointBehavior"/> wiring <see cref="InfoShareWcfSoapUserAgentClientMessageInspector"/>
/// into a WCF client channel's <see cref="ClientRuntime"/>. Constructed once per <see cref="Objects.Public.IshSession"/>/
/// <see cref="InfoShareWcfSoapWithOpenIdConnectConnection"/> holding a reference to the same <see cref="UserAgentState"/>
/// so a later flip is visible to every channel immediately. See #275.
/// </summary>
internal sealed class InfoShareWcfSoapUserAgentEndpointBehavior : IEndpointBehavior
{
private readonly UserAgentState _userAgentState;

internal InfoShareWcfSoapUserAgentEndpointBehavior(UserAgentState userAgentState)
{
_userAgentState = userAgentState;
}

public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
// no-op
}

public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.ClientMessageInspectors.Add(new InfoShareWcfSoapUserAgentClientMessageInspector(_userAgentState));
}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
// no-op, client-side only
}

public void Validate(ServiceEndpoint endpoint)
{
// no-op
}
}
#endif
}
Loading
Loading