6565import java .util .zip .Inflater ;
6666import java .util .zip .InflaterInputStream ;
6767import org .apache .commons .lang3 .StringUtils ;
68+ import org .apache .http .conn .DnsResolver ;
6869import org .apache .jmeter .protocol .http .control .AuthManager ;
6970import org .apache .jmeter .protocol .http .control .Authorization ;
7071import org .apache .jmeter .protocol .http .control .Cookie ;
@@ -188,6 +189,15 @@ public class HTTP2JettyClient {
188189 private static final String ATTR_SKIP_H2C_UPGRADE = "bzm.skipH2cUpgrade" ;
189190 private static final String ATTR_ORIGIN_KEY = "bzm.http3.origin" ;
190191 private static final String ATTR_REQUEST_HEADERS_SERIALIZED = "bzm.request.headers.serialized" ;
192+ /**
193+ * JMeter's deprecated "BASIC_DIGEST" Auth Manager mechanism, still selectable in the GUI and
194+ * still present in older plans. HC4 keeps honouring it: {@code AuthManager.setupCredentials}
195+ * registers the credentials without binding them to a scheme, so they answer either challenge,
196+ * and the preemptive auth cache treats the row as Basic. Referenced by name so this file does
197+ * not have to carry a deprecation suppression, the same way the surrounding code compares
198+ * mechanisms by {@code name()}.
199+ */
200+ private static final String BASIC_DIGEST_MECHANISM = "BASIC_DIGEST" ;
191201 private static final String PROP_SKIP_REDUNDANT_MANUAL_DECODE =
192202 "blazemeter.http.skipManualDecodeWhenAdvertised" ;
193203 private static final Path DEBUG_LOG_PATH = resolveDebugLogPath ();
@@ -327,13 +337,25 @@ public class HTTP2JettyClient {
327337 * QUIC one too - no transport {@code doStart} propagates the bind address to it.
328338 */
329339 private final List <ClientConnector > connectors = new ArrayList <>();
340+ /**
341+ * The sampler's DNS Cache Manager, or {@code null} when the plan has none. Held so
342+ * {@link #configureHttpClient} can install {@link JMeterDnsSocketAddressResolver} on every
343+ * protocol-variant client before any of them is started.
344+ */
345+ private final DnsResolver dnsResolver ;
330346
331347 public HTTP2JettyClient (boolean http1UpgradeRequired , String name ) {
332348 this (http1UpgradeRequired , name , null );
333349 }
334350
335351 public HTTP2JettyClient (boolean http1UpgradeRequired , String name ,
336352 HTTP2ClientProfileConfig profileConfig ) {
353+ this (http1UpgradeRequired , name , profileConfig , null );
354+ }
355+
356+ public HTTP2JettyClient (boolean http1UpgradeRequired , String name ,
357+ HTTP2ClientProfileConfig profileConfig , DnsResolver dnsResolver ) {
358+ this .dnsResolver = dnsResolver ;
337359 loadProperties (profileConfig );
338360 lowLevelDebug (PLUGIN_BUILD_TAG );
339361
@@ -3358,6 +3380,7 @@ public Thread newThread(Runnable r) {
33583380
33593381 private void configureHttpClient (HttpClient client , ClientConnector connector ) {
33603382 client .setUserAgentField (null );
3383+ configureDnsResolution (client );
33613384 connector .setByteBufferPool (this .bufferPool );
33623385 client .setMaxRequestsQueuedPerDestination (maxRequestsQueuedPerDestination );
33633386 client .setMaxConnectionsPerDestination (maxConnectionsPerDestination );
@@ -3376,6 +3399,23 @@ private void configureHttpClient(HttpClient client, ClientConnector connector) {
33763399 }
33773400 }
33783401
3402+ /**
3403+ * Routes host name resolution through the plan's DNS Cache Manager, when there is one.
3404+ *
3405+ * <p>With no manager configured nothing is set and {@code HttpClient.doStart} installs its own
3406+ * {@code SocketAddressResolver.Async}, which is the same default {@code HTTPHC4Impl} falls back
3407+ * to ({@code SystemDefaultDnsResolver}). Under a proxy this still resolves the proxy host rather
3408+ * than the target host, because Jetty resolves {@code HttpDestination.resolveOrigin()} - again
3409+ * matching HC4, which connects to the proxy hop of the route.
3410+ */
3411+ private void configureDnsResolution (HttpClient client ) {
3412+ if (dnsResolver == null ) {
3413+ return ;
3414+ }
3415+ client .setSocketAddressResolver (new JMeterDnsSocketAddressResolver (dnsResolver ,
3416+ client ::getExecutor , client ::getScheduler , client .getAddressResolutionTimeout ()));
3417+ }
3418+
33793419 private static void addConnectionLogging (HttpClient client ) {
33803420 client .addBean (new Connection .Listener () {
33813421 @ Override
@@ -3682,24 +3722,44 @@ private void setAuthManager(HTTP2Sampler sampler) {
36823722 private boolean isSupportedMechanism (Authorization auth ) {
36833723 String authName = auth .getMechanism ().name ();
36843724 return authName .equals (AuthManager .Mechanism .BASIC .name ())
3685- || authName .equals (AuthManager .Mechanism .DIGEST .name ());
3725+ || authName .equals (AuthManager .Mechanism .DIGEST .name ())
3726+ || authName .equals (BASIC_DIGEST_MECHANISM );
3727+ }
3728+
3729+ /**
3730+ * Whether the row may answer a {@code Basic} challenge, which {@code BASIC_DIGEST} rows may.
3731+ */
3732+ private static boolean answersBasicChallenge (Authorization auth ) {
3733+ String authName = auth .getMechanism ().name ();
3734+ return authName .equals (AuthManager .Mechanism .BASIC .name ())
3735+ || authName .equals (BASIC_DIGEST_MECHANISM );
3736+ }
3737+
3738+ /**
3739+ * Whether the row may answer a {@code Digest} challenge, which {@code BASIC_DIGEST} rows may.
3740+ */
3741+ private static boolean answersDigestChallenge (Authorization auth ) {
3742+ String authName = auth .getMechanism ().name ();
3743+ return authName .equals (AuthManager .Mechanism .DIGEST .name ())
3744+ || authName .equals (BASIC_DIGEST_MECHANISM );
36863745 }
36873746
36883747 private void addAuthenticationToJettyClient (Authorization auth ) {
36893748 String authName = auth .getMechanism ().name ();
3690- if (authName .equals (AuthManager .Mechanism .BASIC .name ())
3691- && BzmHttpPluginProperties .getPropDefault ("httpJettyClient.auth.preemptive" , false )) {
3749+ boolean preemptive =
3750+ BzmHttpPluginProperties .getPropDefault ("httpJettyClient.auth.preemptive" , false );
3751+ if (preemptive && answersBasicChallenge (auth )) {
36923752 BasicAuthentication .BasicResult result =
36933753 new BasicAuthentication .BasicResult (URI .create (auth .getURL ()), auth .getUser (),
36943754 auth .getPass ());
36953755 // Results are keyed by URI (Map.put replaces); safe to re-register every sample.
36963756 forEachAuthenticationStore (store -> store .addAuthenticationResult (result ));
3697- return ;
3698- }
3699-
3700- String fingerprint = authFingerprint ( auth );
3701- if (! registeredAuthFingerprints . add ( fingerprint )) {
3702- return ;
3757+ if ( authName . equals ( AuthManager . Mechanism . BASIC . name ())) {
3758+ return ;
3759+ }
3760+ // A BASIC_DIGEST row falls through: sending Basic up front is what HC4's auth cache does,
3761+ // but the credentials must still be able to answer whichever challenge the server sends
3762+ // back, which is the whole point of the mechanism.
37033763 }
37043764
37053765 URI uri = URI .create (auth .getURL ());
@@ -3708,10 +3768,26 @@ private void addAuthenticationToJettyClient(Authorization auth) {
37083768 // Blank JMeter realm must match any challenge realm; "" would only match "".
37093769 realm = Authentication .ANY_REALM ;
37103770 }
3711- AbstractAuthentication authentication =
3712- authName .equals (AuthManager .Mechanism .BASIC .name ())
3713- ? new BasicAuthentication (uri , realm , auth .getUser (), auth .getPass ())
3714- : new DigestAuthentication (uri , realm , auth .getUser (), auth .getPass ());
3771+ if (answersBasicChallenge (auth )) {
3772+ registerAuthentication (auth ,
3773+ new BasicAuthentication (uri , realm , auth .getUser (), auth .getPass ()));
3774+ }
3775+ if (answersDigestChallenge (auth )) {
3776+ registerAuthentication (auth ,
3777+ new DigestAuthentication (uri , realm , auth .getUser (), auth .getPass ()));
3778+ }
3779+ }
3780+
3781+ /**
3782+ * Adds one Jetty authentication to every protocol-variant store, once per distinct row.
3783+ *
3784+ * <p>The fingerprint carries the Jetty authentication type because a single {@code BASIC_DIGEST}
3785+ * row produces two of them, and both have to get through.
3786+ */
3787+ private void registerAuthentication (Authorization auth , AbstractAuthentication authentication ) {
3788+ if (!registeredAuthFingerprints .add (authFingerprint (auth ) + '|' + authentication .getType ())) {
3789+ return ;
3790+ }
37153791 forEachAuthenticationStore (store -> store .addAuthentication (authentication ));
37163792 }
37173793
@@ -4034,7 +4110,7 @@ private void addPreemptiveAuthorizationHeader(Request request, URL url,
40344110 StreamSupport .stream (authManager .getAuthObjects ().spliterator (), false )
40354111 .map (j -> (Authorization ) j .getObjectValue ())
40364112 .filter (auth -> auth != null
4037- && AuthManager . Mechanism . BASIC . equals (auth . getMechanism () )
4113+ && answersBasicChallenge (auth )
40384114 && !StringUtils .isEmpty (auth .getURL ()))
40394115 .filter (auth -> url .toString ().startsWith (auth .getURL ()))
40404116 .findFirst ()
0 commit comments