() {
+ @Override
+ public void completed(final TransportSecurityLayer transportSecurityLayer) {
+ // If the upgrade succeeded, call the callback with the original connection
+ if (callback != null) {
+ callback.completed(connection);
+ }
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ // If the upgrade failed, call the callback with the exception
+ if (callback != null) {
+ callback.failed(ex);
+ }
+ }
+
+ @Override
+ public void cancelled() {
+ // If the upgrade was cancelled, call the callback with an exception indicating cancellation
+ if (callback != null) {
+ callback.failed(new CancellationException("Upgrade was cancelled"));
+ }
+ }
+ });
+ } else {
+ // If no TLS strategy is available, call the callback with the original connection
+ if (callback != null) {
+ callback.completed(connection);
+ }
+ }
+ }
+
+ /**
+ * Calculates the delay before the next connection attempt based on the attempt index and the configured connection
+ *
+ * attempt delay parameters.
+ *
+ * @param attemptIndex the index of the connection attempt, starting from 0
+ * @return the duration to wait before the next connection attempt
+ */
+ private Duration calculateDelay(final int attemptIndex) {
+ final Duration delay;
+ final Duration attemptDelay = connectionAttemptDelay.toDuration();
+ final Duration maximumAttemptDelay = maximumConnectionAttemptDelay.toDuration();
+ final Duration minimumAttemptDelay = minimumConnectionAttemptDelay.toDuration();
+
+ if (attemptIndex == 0) {
+ delay = attemptDelay;
+ } else {
+ delay = attemptDelay.multipliedBy(2).compareTo(maximumAttemptDelay) <= 0 ?
+ attemptDelay.multipliedBy(2) : maximumAttemptDelay;
+ }
+ return delay.compareTo(minimumAttemptDelay) >= 0 ? delay : minimumAttemptDelay;
+ }
+
+ /**
+ * Initiates an orderly shutdown in which previously submitted tasks are executed,
+ * but no new tasks will be accepted. If the provided CloseMode is GRACEFUL,
+ * the scheduler will wait for currently executing tasks to complete before shutting down.
+ * Otherwise, it will attempt to cancel currently executing tasks.
+ *
+ * @param closeMode The mode to use for shutting down the scheduler.
+ */
+ public void shutdown(final CloseMode closeMode) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Shutdown ScheduledExecutorService {}", closeMode);
+ }
+ if (closeMode == CloseMode.GRACEFUL) {
+ scheduler.shutdown();
+ } else {
+ scheduler.shutdownNow();
+ }
+ }
+
+ /**
+ * Initiates an orderly shutdown in which previously submitted tasks are executed,
+ * but no new tasks will be accepted. The scheduler will wait for currently executing tasks
+ * to complete before shutting down.
+ */
+ public void shutdown() {
+ shutdown(CloseMode.GRACEFUL);
+ }
+
+ /**
+ * Decrements the number of scheduled tasks and shuts down the scheduler if there are no more tasks left to execute.
+ * This method is intended to be used when all tasks have been completed, and the scheduler should be shut down
+ * gracefully. It is executed asynchronously using {@link CompletableFuture#runAsync(Runnable)}, so it will not block
+ * the calling thread.
+ */
+ private void shutdownSchedulerIfTasksCompleted() {
+ if (scheduledTasks.decrementAndGet() <= 0) {
+ this.shutdown();
+ }
+ }
+}
+
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorBuilder.java
new file mode 100644
index 000000000..c8c6f6d38
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorBuilder.java
@@ -0,0 +1,277 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.ssl.ConscryptClientTlsStrategy;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.ReflectionUtils;
+import org.apache.hc.core5.util.Timeout;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * A builder for creating instances of {@link HappyEyeballsV2AsyncClientConnectionOperator}.
+ *
+ *
This builder provides a fluent API for configuring various options of the
+ * {@link HappyEyeballsV2AsyncClientConnectionOperator}. Once all the desired options have been set,
+ * the {@link #build()} method can be called to create an instance of the connection operator.
+ *
+ *
The following options can be configured using this builder:
+ *
+ * - The TLS strategy to be used for establishing TLS connections
+ * - The connection operator to be used for creating connections
+ * - The DNS resolver to be used for resolving hostnames
+ * - The timeout for establishing a connection
+ * - The delay between resolution of hostnames and connection establishment attempts
+ * - The minimum and maximum delays between connection establishment attempts
+ * - The delay between subsequent connection establishment attempts
+ * - The number of connections to be established with the first address family in the list
+ * - The preferred address family to be used for establishing connections
+ *
+ *
+ * If no options are explicitly set using this builder, default options will be used for each option.
+ *
+ *
This class is not thread-safe.
+ *
+ * @see HappyEyeballsV2AsyncClientConnectionOperator
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperatorBuilder {
+
+ private AsyncClientConnectionOperator connectionOperator;
+ private DnsResolver dnsResolver;
+ private TlsStrategy tlsStrategy;
+ private Timeout timeout;
+ private Timeout minimumConnectionAttemptDelay;
+ private Timeout maximumConnectionAttemptDelay;
+ private Timeout connectionAttemptDelay;
+ private Timeout resolutionDelay;
+ private int firstAddressFamilyCount;
+ private HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily addressFamily;
+ private ScheduledExecutorService scheduler;
+
+
+ public static HappyEyeballsV2AsyncClientConnectionOperatorBuilder create() {
+ return new HappyEyeballsV2AsyncClientConnectionOperatorBuilder();
+ }
+
+
+ HappyEyeballsV2AsyncClientConnectionOperatorBuilder() {
+ super();
+ }
+
+
+ private boolean systemProperties;
+
+ /**
+ * Use system properties when creating and configuring default
+ * implementations.
+ */
+ public final HappyEyeballsV2AsyncClientConnectionOperatorBuilder useSystemProperties() {
+ this.systemProperties = true;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AsyncClientConnectionOperator} to use for establishing connections.
+ *
+ * @param connectionOperator the {@link AsyncClientConnectionOperator} to use
+ * @return this {@link HappyEyeballsV2AsyncClientConnectionOperatorBuilder} instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withConnectionOperator(
+ final AsyncClientConnectionOperator connectionOperator) {
+ this.connectionOperator = connectionOperator;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DnsResolver} to use for resolving host names to IP addresses.
+ *
+ * @param dnsResolver the {@link DnsResolver} to use
+ * @return this builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withDnsResolver(final DnsResolver dnsResolver) {
+ this.dnsResolver = dnsResolver;
+ return this;
+ }
+
+ /**
+ * Sets the {@link TlsStrategy} to use for creating TLS connections.
+ *
+ * @param tlsStrategy the {@link TlsStrategy} to use
+ * @return this {@link HappyEyeballsV2AsyncClientConnectionOperatorBuilder} instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withTlsStrategyLookup(final TlsStrategy tlsStrategy) {
+ this.tlsStrategy = tlsStrategy;
+ return this;
+ }
+
+ /**
+ * Set the timeout to use for connection attempts.
+ *
+ * @param timeout the timeout to use for connection attempts
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withTimeout(final Timeout timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * Sets the minimum delay between connection attempts. The actual delay may be longer if a resolution delay has been
+ * specified, in which case the minimum connection attempt delay is added to the resolution delay.
+ *
+ * @param minimumConnectionAttemptDelay the minimum delay between connection attempts
+ * @return this builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withMinimumConnectionAttemptDelay(
+ final Timeout minimumConnectionAttemptDelay) {
+ this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay;
+ return this;
+ }
+
+ /**
+ * Sets the maximum delay between two connection attempts.
+ *
+ * @param maximumConnectionAttemptDelay the maximum delay between two connection attempts
+ * @return the builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withMaximumConnectionAttemptDelay(
+ final Timeout maximumConnectionAttemptDelay) {
+ this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay;
+ return this;
+ }
+
+ /**
+ * Sets the delay between two connection attempts.
+ *
+ * @param connectionAttemptDelay the delay between two connection attempts
+ * @return the builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withConnectionAttemptDelay(
+ final Timeout connectionAttemptDelay) {
+ this.connectionAttemptDelay = connectionAttemptDelay;
+ return this;
+ }
+
+ /**
+ * Sets the delay before attempting to resolve the next address in the list.
+ *
+ * @param resolutionDelay the delay before attempting to resolve the next address in the list
+ * @return the builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withResolutionDelay(final Timeout resolutionDelay) {
+ this.resolutionDelay = resolutionDelay;
+ return this;
+ }
+
+ /**
+ * Sets the number of first address families to try before falling back to the other address families.
+ *
+ * @param firstAddressFamilyCount the number of first address families to try
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withFirstAddressFamilyCount(
+ final int firstAddressFamilyCount) {
+ this.firstAddressFamilyCount = firstAddressFamilyCount;
+ return this;
+ }
+
+ /**
+ * Sets the preferred address family to use for connections.
+ *
+ * @param addressFamily the preferred address family
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withAddressFamily(final HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily addressFamily) {
+ this.addressFamily = addressFamily;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ScheduledExecutorService} for the {@link HappyEyeballsV2AsyncClientConnectionOperator}.
+ *
+ * @param scheduler The ScheduledExecutorService to set.
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withScheduler(final ScheduledExecutorService scheduler) {
+ this.scheduler = scheduler;
+ return this;
+ }
+
+ /**
+ * Builds a {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+ *
+ * @return the {@link HappyEyeballsV2AsyncClientConnectionOperator} instance built with the specified parameters.
+ * @throws IllegalArgumentException if the connection operator is null.
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperator build() {
+ final TlsStrategy tlsStrategyCopy;
+ if (tlsStrategy != null) {
+ tlsStrategyCopy = tlsStrategy;
+ } else {
+ if (ReflectionUtils.determineJRELevel() <= 8 && ConscryptClientTlsStrategy.isSupported()) {
+ if (systemProperties) {
+ tlsStrategyCopy = ConscryptClientTlsStrategy.getSystemDefault();
+ } else {
+ tlsStrategyCopy = ConscryptClientTlsStrategy.getDefault();
+ }
+ } else {
+ if (systemProperties) {
+ tlsStrategyCopy = DefaultClientTlsStrategy.getSystemDefault();
+ } else {
+ tlsStrategyCopy = DefaultClientTlsStrategy.getDefault();
+ }
+ }
+ }
+
+
+ connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+
+ return new HappyEyeballsV2AsyncClientConnectionOperator(
+ RegistryBuilder.create()
+ .register(URIScheme.HTTPS.getId(), tlsStrategyCopy)
+ .build(),
+ connectionOperator,
+ dnsResolver,
+ timeout,
+ resolutionDelay,
+ minimumConnectionAttemptDelay,
+ maximumConnectionAttemptDelay,
+ connectionAttemptDelay,
+ firstAddressFamilyCount,
+ addressFamily,
+ scheduler
+ );
+ }
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/InetAddressComparator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/InetAddressComparator.java
new file mode 100644
index 000000000..da6119247
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/InetAddressComparator.java
@@ -0,0 +1,619 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * This class implements a comparator for {@link InetAddress} instances based on the Happy Eyeballs V2 algorithm.
+ *
+ * The comparator is used to sort a list of IP addresses based on their reachability and preference.
+ *
+ *
+ * The Happy Eyeballs algorithm is a mechanism for reducing connection latency when connecting to IPv6-capable
+ *
+ * servers over networks where both IPv6 and IPv4 are available. The algorithm attempts to establish connections
+ *
+ * using IPv6 and IPv4 in parallel, and selects the first connection to complete successfully.
+ *
+ *
+ * This comparator implements the Happy Eyeballs V2 rules defined in RFC 8305. The following rules are used for
+ *
+ * comparing two IP addresses:
+ *
+ *
+ * - Rule 1: Avoid unusable destinations.
+ * - Rule 2: Prefer matching scope.
+ * - Rule 3: Avoid deprecated addresses.
+ * - Rule 4: Prefer higher precedence.
+ * - Rule 5: Prefer matching label.
+ * - Rule 6: Prefer smaller address.
+ * - Rule 7: Prefer home network.
+ * - Rule 8: Prefer public network.
+ * - Rule 9: Prefer stable privacy addresses.
+ * - Rule 10: Prefer temporary addresses.
+ *
+ *
+ * @since 5.3
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Internal
+class InetAddressComparator implements Comparator {
+
+ /**
+ * Singleton instance of the comparator.
+ */
+ public static final InetAddressComparator INSTANCE = new InetAddressComparator();
+
+ /**
+ * Compares two IP addresses based on the Happy Eyeballs algorithm rules.
+ *
+ * The method first orders the addresses based on their precedence, and then compares them based on other rules,
+ *
+ * including avoiding unusable destinations, preferring matching scope, preferring global scope, preferring
+ *
+ * IPv6 addresses, and preferring smaller address prefixes.
+ *
+ * @param addr1 the first address to be compared
+ * @param addr2 the second address to be compared
+ * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater
+ * than the second.
+ */
+ @Override
+ public int compare(final InetAddress addr1, final InetAddress addr2) {
+ if (addr1 == null && addr2 == null) {
+ return 0;
+ }
+ if (addr1 == null) {
+ return -1;
+ }
+ if (addr2 == null) {
+ return 1;
+ }
+
+ try {
+ // Get the list of network interfaces
+ final List networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+
+
+ // Rule 1: Avoid unusable destinations.
+ final boolean add1IsReachable;
+ final boolean add2IsReachable;
+
+ // Check if address 1 is reachable on any network interface
+ add1IsReachable = isAddressReachable(addr1, networkInterfaces);
+
+ // Check if address 2 is reachable on any network interface
+ add2IsReachable = isAddressReachable(addr2, networkInterfaces);
+
+ if (add1IsReachable && !add2IsReachable) {
+ return -1;
+ } else if (!add1IsReachable && add2IsReachable) {
+ return 1;
+ }
+
+ } catch (final Exception e) {
+ return 0;
+ }
+
+ // Rule 2: Prefer matching scope.
+ final InetAddress localhost;
+ final InetAddress sourceAddr;
+ try {
+ localhost = InetAddress.getLocalHost();
+ sourceAddr = InetAddress.getByName(localhost.getHostAddress());
+ } catch (final UnknownHostException e) {
+ return 0;
+ }
+
+ final int scope1 = getScope(addr1);
+ final int scope2 = getScope(addr2);
+ final int scopeSource = getScope(sourceAddr);
+
+ if (scope1 == scope2) {
+ return 0;
+ } else if (scope1 == scopeSource) {
+ return -1;
+ } else if (scope2 == scopeSource) {
+ return 1;
+ }
+
+ //Rule 3: Avoid deprecated addresses.
+ final boolean add1IsDeprecated = isDeprecated(addr1);
+ final boolean add2IsDeprecated = isDeprecated(addr2);
+
+ if (add1IsDeprecated && !add2IsDeprecated) {
+ return 1;
+ } else if (!add1IsDeprecated && add2IsDeprecated) {
+ return -1;
+ }
+
+
+ // Rule 4: Prefer home addresses.
+ final boolean isHomeAddr11 = isHomeAddress(addr1);
+ final boolean isCareOfAddr11 = isCareOfAddress(addr1);
+ final boolean isHomeAddr21 = isHomeAddress(addr2);
+ final boolean isCareOfAddr21 = isCareOfAddress(addr2);
+
+ if (isHomeAddr11 && isCareOfAddr11 && !isHomeAddr21) {
+ return -1;
+ } else if (isHomeAddr21 && isCareOfAddr21 && !isHomeAddr11) {
+ return 1;
+ }
+
+ //Rule 5: Prefer matching label.
+ final int label1 = getLabel(addr1);
+ final int label2 = getLabel(addr2);
+
+ if (label1 < label2) {
+ return -1;
+ } else if (label1 > label2) {
+ return 1;
+ }
+
+ // Rule 6: Prefer higher precedence.
+ final int add1Precedence = getPrecedence(addr1);
+ final int add2Precedence = getPrecedence(addr2);
+
+ if (add1Precedence > add2Precedence) {
+ return -1;
+ } else if (add1Precedence < add2Precedence) {
+ return 1;
+ }
+
+ // Rule 7: Prefer native transport.
+ final boolean addr1Encapsulated = isEncapsulated(addr1);
+ final boolean addr2Encapsulated = isEncapsulated(addr2);
+
+ if (addr1Encapsulated && !addr2Encapsulated) {
+ return 1;
+ } else if (!addr1Encapsulated && addr2Encapsulated) {
+ return -1;
+ }
+
+
+ // Rule 8: Prefer smaller scope.
+ if (scope1 < scope2) {
+ return -1;
+ } else if (scope1 > scope2) {
+ return 1;
+ }
+
+ // Rule 9: Use longest matching prefix.
+ final int prefixLen1 = commonPrefixLen(addr1, addr2);
+ final int prefixLen2 = commonPrefixLen(addr2, addr1);
+
+ if (prefixLen1 > prefixLen2) {
+ return -1;
+ } else if (prefixLen1 < prefixLen2) {
+ return 1;
+ }
+
+ // Rule 10: Otherwise, leave the order unchanged.
+ return 0;
+ }
+
+
+ private static int getScope(final InetAddress addr) {
+ if (isIpv6Address(addr)) {
+ if (addr.isMulticastAddress()) {
+ return getIpv6MulticastScope(addr);
+ } else if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+ return 0x02;
+ } else if (addr.isSiteLocalAddress()) {
+ return 0x05;
+ } else {
+ return 0x0e;
+ }
+ } else if (isIpv4Address(addr)) {
+ if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+ return 0x02;
+ } else {
+ return 0x0e;
+ }
+ } else {
+ return 0x01;
+ }
+ }
+
+
+ /**
+ * Checks whether the given IPv6 address is a deprecated address.
+ *
+ * @param addr the IPv6 address to check.
+ * @return {@code true} if the given address is deprecated, {@code false} otherwise.
+ */
+ private boolean isDeprecated(final InetAddress addr) {
+ if (addr instanceof Inet4Address) {
+ return false;
+ } else if (addr instanceof Inet6Address) {
+ final Inet6Address ipv6Addr = (Inet6Address) addr;
+ final byte[] addressBytes = ipv6Addr.getAddress();
+
+ // Check if the IPv6 address is IPv4-mapped
+ if (addressBytes[0] == 0 && addressBytes[1] == 0 && addressBytes[2] == 0 && addressBytes[3] == 0 &&
+ addressBytes[4] == 0 && addressBytes[5] == 0 && addressBytes[6] == 0 && addressBytes[7] == 0 &&
+ addressBytes[8] == 0 && addressBytes[9] == 0 && addressBytes[10] == (byte) 0xFF && addressBytes[11] == (byte) 0xFF) {
+ return true;
+ }
+
+ // Check if the IPv6 address is a link-local address
+ if ((addressBytes[0] & 0xFF) == 0xFE && (addressBytes[1] & 0xC0) == 0x80) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines the label to use for an IP address based on its type and properties.
+ *
+ * @param addr the IP address to determine the label for
+ * @return an integer value representing the label to use for the IP address
+ */
+ private static int getLabel(final InetAddress addr) {
+ if (isIpv4Address(addr)) {
+ return 4;
+ } else if (isIpv6Address(addr)) {
+ if (addr.isLoopbackAddress()) {
+ return 0;
+ } else if (isIpv6Address6To4(addr)) {
+ return 2;
+ } else if (isIpv6AddressTeredo(addr)) {
+ return 5;
+ } else if (isIpv6AddressULA(addr)) {
+ return 13;
+ } else if (((Inet6Address) addr).isIPv4CompatibleAddress()) {
+ return 3;
+ } else if (addr.isSiteLocalAddress()) {
+ return 11;
+ } else if (isIpv6Address6Bone(addr)) {
+ return 12;
+ } else {
+ // All other IPv6 addresses, including global unicast addresses.
+ return 1;
+ }
+ } else {
+ // This should never happen.
+ return 1;
+ }
+ }
+
+ /**
+ * Returns true if the given InetAddress is an IPv6 address.
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the given InetAddress is an IPv6 address; false otherwise.
+ */
+ private static boolean isIpv6Address(final InetAddress addr) {
+ return addr instanceof Inet6Address;
+ }
+
+ /**
+ * Returns true if the given InetAddress is an IPv4 address.
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the given InetAddress is an IPv4 address; false otherwise.
+ */
+ private static boolean isIpv4Address(final InetAddress addr) {
+ return addr instanceof Inet4Address;
+ }
+
+ /**
+ * Returns true if the given InetAddress is an IPv6 6to4 address.
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the given InetAddress is an IPv6 6to4 address; false otherwise.
+ */
+ private static boolean isIpv6Address6To4(final InetAddress addr) {
+ if (!isIpv6Address(addr)) {
+ return false;
+ }
+ final byte[] byteAddr = addr.getAddress();
+ return byteAddr[0] == 0x20 && byteAddr[1] == 0x02;
+ }
+
+ /**
+ * Returns true if the given InetAddress is an IPv6 Teredo address.
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the given InetAddress is an IPv6 Teredo address; false otherwise.
+ */
+ private static boolean isIpv6AddressTeredo(final InetAddress addr) {
+ if (!isIpv6Address(addr)) {
+ return false;
+ }
+ final byte[] byteAddr = addr.getAddress();
+ return byteAddr[0] == 0x20 && byteAddr[1] == 0x01 && byteAddr[2] == 0x00
+ && byteAddr[3] == 0x00;
+ }
+
+ /**
+ * Returns true if the given InetAddress is an IPv6 Unique Local Address (ULA).
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the given InetAddress is a ULA; false otherwise.
+ */
+ private static boolean isIpv6AddressULA(final InetAddress addr) {
+ return isIpv6Address(addr) && (addr.getAddress()[0] & 0xfe) == 0xfc;
+ }
+
+ /**
+ * Returns true if the given InetAddress is an IPv6 6bone address.
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the given InetAddress is an IPv6 6bone address; false otherwise.
+ */
+ private static boolean isIpv6Address6Bone(final InetAddress addr) {
+ if (!isIpv6Address(addr)) {
+ return false;
+ }
+ final byte[] byteAddr = addr.getAddress();
+ return byteAddr[0] == 0x3f && byteAddr[1] == (byte) 0xfe;
+ }
+
+ /**
+ * Returns the scope of the given IPv6 multicast address.
+ *
+ * @param addr The IPv6 multicast address.
+ * @return The scope of the given IPv6 multicast address.
+ */
+ private static int getIpv6MulticastScope(final InetAddress addr) {
+ return !isIpv6Address(addr) ? 0 : (addr.getAddress()[1] & 0x0f);
+ }
+
+ /**
+ * Determines the precedence of an IP address based on its type and scope.
+ * Precedence is used to determine which of two candidate IP addresses
+ * should be used as the destination address in an IP packet, according to
+ * the rules defined in RFC 6724.
+ *
+ * @param addr The IP address to determine the precedence of.
+ * @return The precedence of the IP address, as an integer. The possible
+ * values are:
+ *
+ * - 1 - multicast address
+ * - 2 - IPv4 address
+ * - 3 - global unicast address
+ * - 4 - IPv4-mapped IPv6 address
+ * - 5 - unique local address
+ * - 6 - link-local address
+ * - 7 - site-local address
+ *
+ */
+ private int getPrecedence(final InetAddress addr) {
+ if (addr instanceof Inet6Address) {
+ final byte[] addrBytes = addr.getAddress();
+ if (addrBytes[0] == (byte) 0xFF) {
+ return 1; // multicast
+ } else if (isIPv4MappedIPv6Address(addrBytes)) {
+ return 4; // IPv4-mapped IPv6 address
+ } else if (isULA(addrBytes)) {
+ return 5; // unique local address
+ } else if (isLinkLocal(addrBytes)) {
+ return 6; // link-local address
+ } else if (isSiteLocal(addrBytes)) {
+ return 7; // site-local address
+ } else {
+ return 3; // global address
+ }
+ } else {
+ return 2; // IPv4 address
+ }
+ }
+
+ /**
+ * Checks whether the given byte array represents an IPv4-mapped IPv6 address.
+ *
+ * @param addr the byte array to check
+ * @return true if the byte array represents an IPv4-mapped IPv6 address, false otherwise
+ */
+ private static boolean isIPv4MappedIPv6Address(final byte[] addr) {
+ return addr.length == 16 && addr[0] == 0x00 && addr[1] == 0x00 && addr[2] == 0x00
+ && addr[3] == 0x00 && addr[4] == 0x00 && addr[5] == 0x00 && addr[6] == 0x00
+ && addr[7] == 0x00 && addr[8] == 0x00 && addr[9] == 0x00 && addr[10] == (byte) 0xFF
+ && addr[11] == (byte) 0xFF;
+ }
+
+ /**
+ * Returns true if the given byte array represents an IPv6 Unique Local Address (ULA) range, false otherwise.
+ *
+ * @param addr the byte array to check
+ * @return true if the byte array represents a ULA range, false otherwise
+ */
+ boolean isULA(final byte[] addr) {
+ return addr.length == 16 && ((addr[0] & 0xFE) == (byte) 0xFC);
+ }
+
+ /**
+ * Returns true if the given byte array represents an IPv6 link-local address, false otherwise.
+ *
+ * @param addr the byte array to check
+ * @return true if the byte array represents a link-local address, false otherwise
+ */
+ private boolean isLinkLocal(final byte[] addr) {
+ return addr.length == 16 && (addr[0] & 0xFF) == 0xFE && (addr[1] & 0xC0) == 0x80;
+ }
+
+ /**
+ * Returns true if the given byte array represents an IPv6 site-local address, false otherwise.
+ *
+ * @param addr the byte array to check
+ * @return true if the byte array represents a site-local address, false otherwise
+ */
+ private boolean isSiteLocal(final byte[] addr) {
+ return addr.length == 16 && (addr[0] & 0xFF) == 0xFE && (addr[1] & 0xC0) == 0xC0;
+ }
+
+
+ /**
+ * Checks if the given address is reachable on any network interface or if it is a link-local, loopback,
+ * or site-local address.
+ *
+ * @param address the address to check
+ * @param networkInterfaces a list of network interfaces to check
+ * @return {@code true} if the address is reachable or a link-local, loopback, or site-local address;
+ * {@code false} otherwise
+ */
+ private static boolean isAddressReachable(final InetAddress address, final List networkInterfaces) {
+ // Check if the address is a link-local, loopback, or site-local address
+ if (address.isLinkLocalAddress() || address.isLoopbackAddress() || address.isSiteLocalAddress()) {
+ return true;
+ }
+ // Check if the address is reachable on any network interface
+ for (final NetworkInterface networkInterface : networkInterfaces) {
+ final Enumeration addresses = networkInterface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ final InetAddress addr = addresses.nextElement();
+ if (addr.equals(address)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the given address is a "home" address. A home address is defined as a global unicast IPv6 address
+ * with the high-order bit of the first octet set to zero, or a private IPv4 address (i.e., an address in
+ * the ranges 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16).
+ *
+ * @param addr the address to check
+ * @return {@code true} if the address is a "home" address; {@code false} otherwise
+ */
+ private static boolean isHomeAddress(final InetAddress addr) {
+ if (addr instanceof Inet6Address) {
+ // Check if the address is a global unicast address with the
+ // high-order bit of the first octet set to zero.
+ final byte[] bytes = addr.getAddress();
+ return (bytes[0] & 0x80) == 0x00;
+ } else if (addr instanceof Inet4Address) {
+ // Check if the address is a private address.
+ final byte[] bytes = addr.getAddress();
+ return ((bytes[0] & 0xFF) == 10)
+ || (((bytes[0] & 0xFF) == 172) && ((bytes[1] & 0xF0) == 0x10))
+ || (((bytes[0] & 0xFF) == 192) && ((bytes[1] & 0xFF) == 168));
+ }
+ return false;
+ }
+
+ /**
+ * Determines if the given InetAddress is a care-of address. A care-of address is an
+ * IPv6 address that is assigned to a mobile node while it is away from its home network.
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the InetAddress is a care-of address, false otherwise.
+ */
+ private static boolean isCareOfAddress(final InetAddress addr) {
+ if (addr instanceof Inet6Address) {
+ final byte[] bytes = addr.getAddress();
+ return (bytes[0] & 0xfe) == 0xfc; // IPv6 Unique Local Addresses (ULA) range
+ }
+ return false;
+ }
+
+ /**
+ * Determines if the given InetAddress is an encapsulated address. An encapsulated address
+ * is either an IPv4-mapped IPv6 address or an IPv4-compatible IPv6 address.
+ *
+ * @param addr The InetAddress to check.
+ * @return True if the InetAddress is an encapsulated address, false otherwise.
+ */
+ private static boolean isEncapsulated(final InetAddress addr) {
+ if (isIpv6Address(addr)) {
+ final String addrStr = addr.getHostAddress();
+ // Check if the IPv6 address is in the format of "::ffff:x.x.x.x"
+ if (addrStr.startsWith("::ffff:")) {
+ return true;
+ }
+ } else if (isIpv4Address(addr)) {
+ // Check if the IPv4 address is in the format of "x.x.x.x"
+ final byte[] byteAddr = addr.getAddress();
+ if (byteAddr[0] == 0 && byteAddr[1] == 0 && byteAddr[2] == 0 && byteAddr[3] != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Calculates the length of the common prefix between two IP addresses.
+ *
+ * @param addr1 The first IP address to compare.
+ * @param addr2 The second IP address to compare.
+ * @return The length of the common prefix between the two IP addresses.
+ */
+ private static int commonPrefixLen(final InetAddress addr1, final InetAddress addr2) {
+ byte[] bytes1 = addr1.getAddress();
+ byte[] bytes2 = addr2.getAddress();
+
+ // If the second address is IPv4, convert it to IPv6 format.
+ if (bytes2.length == 4) {
+ bytes2 = Arrays.copyOf(bytes2, 16);
+ }
+
+ // If the addresses are IPv6, truncate them to the first 64 bits.
+ if (bytes1.length > 8) {
+ bytes1 = Arrays.copyOf(bytes1, 8);
+ bytes2 = Arrays.copyOf(bytes2, 8);
+ }
+
+ int prefixLen = 0;
+ for (int i = 0; i < bytes1.length; i++) {
+ int bits = 8;
+ for (int j = 7; j >= 0; j--) {
+ if ((bytes1[i] & (1 << j)) == (bytes2[i] & (1 << j))) {
+ prefixLen += 1;
+ } else {
+ bits--;
+ }
+ if (bits == 0) {
+ break;
+ }
+ }
+ if (bits != 8) {
+ break;
+ }
+ }
+ return prefixLen;
+ }
+
+}
\ No newline at end of file
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java
index b56c24628..2f5a504dd 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java
@@ -163,6 +163,15 @@ public PoolingAsyncClientConnectionManager(
poolConcurrencyPolicy, poolReusePolicy, timeToLive);
}
+ PoolingAsyncClientConnectionManager(
+ final PoolConcurrencyPolicy poolConcurrencyPolicy,
+ final PoolReusePolicy poolReusePolicy,
+ final TimeValue timeToLive,
+ final AsyncClientConnectionOperator connectionOperator) {
+ this(connectionOperator,
+ poolConcurrencyPolicy, poolReusePolicy, timeToLive);
+ }
+
@Internal
protected PoolingAsyncClientConnectionManager(
final AsyncClientConnectionOperator connectionOperator,
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java
index 6f72c6bb0..b41e1f5ec 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java
@@ -30,8 +30,10 @@
import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.ssl.ConscryptClientTlsStrategy;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.core5.function.Resolver;
@@ -86,6 +88,19 @@ public class PoolingAsyncClientConnectionManagerBuilder {
private Resolver connectionConfigResolver;
private Resolver tlsConfigResolver;
+ /**
+ * A flag indicating whether to use the Happy Eyeballs v2 fast fallback strategy
+ * for establishing connections. If true, the connection manager will use
+ * {@link HappyEyeballsV2AsyncClientConnectionOperator} to establish connections
+ * that attempt to connect to multiple IP addresses in parallel, selecting the
+ * first successful connection.
+ *
+ * When false, the default connection operator will be used, which establishes
+ * connections sequentially.
+ * @since 5.3
+ */
+ private boolean fastFallback;
+
public static PoolingAsyncClientConnectionManagerBuilder create() {
return new PoolingAsyncClientConnectionManagerBuilder();
}
@@ -229,6 +244,20 @@ public final PoolingAsyncClientConnectionManagerBuilder useSystemProperties() {
return this;
}
+ /**
+ * Enables or disables the fast fallback option for connection management.
+ * When fast fallback is enabled, the connection manager will attempt to
+ * quickly switch to alternative connection options if the initial connection fails.
+ *
+ * @param fastFallback true to enable fast fallback, false to disable it
+ * @return this builder instance for method chaining
+ * @since 5.3
+ */
+ public final PoolingAsyncClientConnectionManagerBuilder useFastFallback(final boolean fastFallback) {
+ this.fastFallback = fastFallback;
+ return this;
+ }
+
public PoolingAsyncClientConnectionManager build() {
final TlsStrategy tlsStrategyCopy;
if (tlsStrategy != null) {
@@ -248,15 +277,32 @@ public PoolingAsyncClientConnectionManager build() {
}
}
}
- final PoolingAsyncClientConnectionManager poolingmgr = new PoolingAsyncClientConnectionManager(
- RegistryBuilder.create()
- .register(URIScheme.HTTPS.getId(), tlsStrategyCopy)
- .build(),
- poolConcurrencyPolicy,
- poolReusePolicy,
- null,
- schemePortResolver,
- dnsResolver);
+ final PoolingAsyncClientConnectionManager poolingmgr;
+ if (fastFallback) {
+ final HappyEyeballsV2AsyncClientConnectionOperator connectionOperator = new HappyEyeballsV2AsyncClientConnectionOperator(
+ RegistryBuilder.create()
+ .register(URIScheme.HTTPS.getId(), tlsStrategyCopy)
+ .build(),
+ DefaultSchemePortResolver.INSTANCE,
+ SystemDefaultDnsResolver.INSTANCE);
+
+ poolingmgr = new PoolingAsyncClientConnectionManager(
+ poolConcurrencyPolicy,
+ poolReusePolicy,
+ null,
+ connectionOperator);
+ } else {
+ poolingmgr = new PoolingAsyncClientConnectionManager(
+ RegistryBuilder.create()
+ .register(URIScheme.HTTPS.getId(), tlsStrategyCopy)
+ .build(),
+ poolConcurrencyPolicy,
+ poolReusePolicy,
+ null,
+ schemePortResolver,
+ dnsResolver);
+ }
+
poolingmgr.setConnectionConfigResolver(connectionConfigResolver);
poolingmgr.setTlsConfigResolver(tlsConfigResolver);
if (maxConnTotal > 0) {
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2AsyncClientConnectionOperatorExample.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2AsyncClientConnectionOperatorExample.java
new file mode 100644
index 000000000..ec8e869e6
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2AsyncClientConnectionOperatorExample.java
@@ -0,0 +1,219 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http.examples;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
+import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.impl.nio.HappyEyeballsV2AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.ssl.ConscryptClientTlsStrategy;
+import org.apache.hc.core5.concurrent.DefaultThreadFactory;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.message.StatusLine;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
+import org.apache.hc.core5.pool.PoolReusePolicy;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Example class demonstrating how to use the Happy Eyeballs V2 connection operator in the Apache
+ * HttpComponents asynchronous HTTP client.
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperatorExample {
+
+ public static void main(final String[] args) throws ExecutionException, InterruptedException, IOException {
+
+ // Create a custom AsyncConnectionFactory
+ final HappyEyeballsV2AsyncClientConnectionOperator connectionOperator = new HappyEyeballsV2AsyncClientConnectionOperator(
+ RegistryBuilder.create().register(URIScheme.HTTPS.getId(), ConscryptClientTlsStrategy.getDefault()).build(),
+ DefaultSchemePortResolver.INSTANCE,
+ new MultipleHostsDnsResolver(new String[]{"ipv6.google.com", "ipv4.google.com", "facebook.com", "yahoo.com"}, new int[]{443, 443, 443, 443}));
+
+
+ final PoolingAsyncClientConnectionManager connectionManager = new CustomPoolingAsyncClientConnectionManager(
+ connectionOperator,
+ PoolConcurrencyPolicy.LAX,
+ PoolReusePolicy.LIFO,
+ TimeValue.ofMinutes(1));
+
+ connectionManager.setConnectionConfigResolver(httpRoute -> ConnectionConfig.custom()
+ .setConnectTimeout(Timeout.ofSeconds(2))
+ .setSocketTimeout(Timeout.ofSeconds(1))
+ .setValidateAfterInactivity(TimeValue.ofSeconds(10))
+ .setTimeToLive(TimeValue.ofMinutes(1))
+ .build());
+
+ // Create a custom AsyncClient
+ final CloseableHttpAsyncClient client = HttpAsyncClients.custom()
+ .setConnectionManager(connectionManager)
+ .setThreadFactory(new DefaultThreadFactory("async-client", true))
+ .build();
+
+ // Start the CloseableHttpAsyncClient
+ client.start();
+
+
+ final SimpleHttpRequest request = SimpleRequestBuilder.get()
+ .setHttpHost(new HttpHost("www.google.com"))
+ .setPath("/")
+ .build();
+
+ System.out.println("Executing request " + request);
+ final Future future = client.execute(
+ SimpleRequestProducer.create(request),
+ SimpleResponseConsumer.create(),
+ new FutureCallback() {
+
+ @Override
+ public void completed(final SimpleHttpResponse response) {
+ System.out.println(request + "->" + new StatusLine(response));
+ System.out.println(response.getBody());
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ System.out.println(request + "->" + ex);
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println(request + " cancelled");
+ }
+
+ });
+ future.get();
+
+ System.out.println("Shutting down");
+ client.close(CloseMode.GRACEFUL);
+
+ }
+
+ /**
+ * Custom implementation of PoolingAsyncClientConnectionManager that uses the specified connection operator
+ * and pool configuration.
+ */
+ private static class CustomPoolingAsyncClientConnectionManager extends PoolingAsyncClientConnectionManager {
+
+ CustomPoolingAsyncClientConnectionManager(
+ final AsyncClientConnectionOperator connectionOperator,
+ final PoolConcurrencyPolicy poolConcurrencyPolicy,
+ final PoolReusePolicy poolReusePolicy,
+ final TimeValue timeToLive) {
+ super(connectionOperator, poolConcurrencyPolicy, poolReusePolicy, timeToLive);
+ }
+
+ }
+
+ /**
+ * A DNS resolver that resolves hostnames against multiple hosts and ports.
+ */
+ static class MultipleHostsDnsResolver implements DnsResolver {
+
+ /**
+ * The array of hostnames to use for resolving.
+ */
+ private final String[] hosts;
+
+ /**
+ * The array of ports to use for resolving.
+ */
+ private final int[] ports;
+
+ public MultipleHostsDnsResolver(final String[] hosts, final int[] ports) {
+ this.hosts = hosts;
+ this.ports = ports;
+ }
+
+ public static final SystemDefaultDnsResolver INSTANCE = new SystemDefaultDnsResolver();
+
+ @Override
+ public InetAddress[] resolve(final String host) throws UnknownHostException {
+ final List addresses = new ArrayList<>();
+ for (int i = 0; i < hosts.length; i++) {
+ final InetAddress[] inetAddresses = InetAddress.getAllByName(hosts[i]);
+ for (final InetAddress inetAddress : inetAddresses) {
+ addresses.add(new InetSocketAddress(inetAddress, ports[i]));
+ }
+ }
+
+ // Convert InetSocketAddress objects to InetAddress objects
+ final List result = new ArrayList<>();
+ for (final InetSocketAddress address : addresses) {
+ result.add(address.getAddress());
+ }
+ return result.toArray(new InetAddress[0]);
+ }
+
+ @Override
+ public String resolveCanonicalHostname(final String host) throws UnknownHostException {
+ if (host == null) {
+ return null;
+ }
+ final InetAddress in = InetAddress.getByName(host);
+ final String canonicalServer = in.getCanonicalHostName();
+ if (in.getHostAddress().contentEquals(canonicalServer)) {
+ return host;
+ }
+ return canonicalServer;
+ }
+
+ }
+
+}
+
+
+
+
+
+
+
+
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2RulesTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2RulesTest.java
new file mode 100644
index 000000000..d888844b2
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2RulesTest.java
@@ -0,0 +1,680 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http.examples;
+
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * This class contains unit tests to verify the implementation of the Happy Eyeballs V2 connection
+ * operator, which applies a set of rules to determine the optimal order in which to connect to
+ * multiple IP addresses for a given hostname. These tests verify that the implementation follows
+ * the specified rules correctly and returns the expected results.
+ */
+public class HappyEyeballsV2RulesTest {
+
+
+ public static void main(final String[] args) throws Exception {
+
+ System.out.println("---- Running HappyEyeballsV2RulesTest ----");
+
+ rule1();
+ rule2();
+ rule3();
+ rule4();
+ rule5();
+ rule6();
+ rule7();
+ rule8();
+ rule9();
+
+
+ System.out.println("---- All tests passed. ----");
+ }
+
+ public static void rule1() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("192.0.2.1");
+ final InetAddress addr2 = InetAddress.getByName("2001:db8::1");
+ final InetAddress addr3 = InetAddress.getByName("www.google.com");
+
+ final List addresses = Arrays.asList(addr3, addr2, addr1);
+ addresses.sort((addr11, addr22) -> {
+ // Get the list of network interfaces
+ final List networkInterfaces;
+ try {
+ networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ } catch (final SocketException e) {
+ return 0;
+ }
+
+ // Rule 1: Avoid unusable destinations.
+ final boolean add1IsReachable;
+ final boolean add2IsReachable;
+ try {
+ // Check if address 1 is reachable on any network interface
+ add1IsReachable = isAddressReachable(addr1, networkInterfaces);
+ } catch (final IOException e) {
+ return -1;
+ }
+ try {
+ // Check if address 2 is reachable on any network interface
+ add2IsReachable = isAddressReachable(addr2, networkInterfaces);
+ } catch (final IOException e) {
+ return 1;
+ }
+
+ if (add1IsReachable && !add2IsReachable) {
+ return -1;
+ } else if (!add1IsReachable && add2IsReachable) {
+ return 1;
+ }
+
+ return 0;
+
+ });
+
+ if (!addresses.get(0).equals(addr3)) {
+ throw new Exception("Rule 1 not satisfied. Expected addr3 as the first address.");
+ }
+
+ if (!addresses.get(1).equals(addr2)) {
+ throw new Exception("Rule 1 not satisfied. Expected addr2 as the second address.");
+ }
+
+ if (!addresses.get(2).equals(addr1)) {
+ throw new Exception("Rule 1 not satisfied. Expected addr1 as the third address.");
+ }
+
+ System.out.println("Rule 1 satisfied.");
+ }
+
+
+ public static void rule2() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("192.0.2.1");
+ final InetAddress addr2 = InetAddress.getByName("2001:db8::1");
+ final InetAddress addr3 = InetAddress.getByName("www.google.com");
+
+ final InetAddress localhost;
+ final InetAddress sourceAddr;
+ try {
+ localhost = InetAddress.getLocalHost();
+ sourceAddr = InetAddress.getByName(localhost.getHostAddress());
+ } catch (final UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+
+ final List addresses = Arrays.asList(addr3, addr1, addr2);
+ addresses.sort((addr11, addr21) -> {
+ final int scope1 = getScope(addr11);
+ final int scope2 = getScope(addr21);
+ final int scopeSource = getScope(sourceAddr);
+
+ if (scope1 == scope2) {
+ return 0;
+ } else if (scope1 == scopeSource) {
+ return -1;
+ } else if (scope2 == scopeSource) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ // Check if the result is correct
+ if (!addresses.get(0).equals(addr3)) {
+ throw new Exception("Rule 2 not satisfied. Expected www.google.com as the first address.");
+ }
+
+ if (!addresses.get(1).equals(addr1)) {
+ throw new Exception("Rule 2 not satisfied. Expected addr1 as the second address.");
+ }
+
+ if (!addresses.get(2).equals(addr2)) {
+ throw new Exception("Rule 2 not satisfied. Expected addr2 as the third address.");
+ }
+ System.out.println("Rule 2 satisfied.");
+ }
+
+ public static void rule3() throws Exception {
+ final InetAddress srcAddr1 = InetAddress.getByName("192.0.2.1");
+ final InetAddress dstAddr1 = InetAddress.getByName("www.google.com");
+ final InetAddress srcAddr2 = InetAddress.getByName("2001:db8::1");
+ final InetAddress dstAddr2 = InetAddress.getByName("2001:db8::2");
+
+ final List addresses = Arrays.asList(dstAddr2, dstAddr1, srcAddr2, srcAddr1);
+ addresses.sort((addr1, addr2) -> {
+ final boolean addr1IsDeprecated = isDeprecated(addr1);
+ final boolean addr2IsDeprecated = isDeprecated(addr2);
+ final boolean addr1IsSource = isSourceAddress(addr1, srcAddr1, srcAddr2);
+ final boolean addr2IsSource = isSourceAddress(addr2, srcAddr1, srcAddr2);
+
+ if (addr1IsDeprecated && !addr2IsDeprecated) {
+ return 1;
+ } else if (!addr1IsDeprecated && addr2IsDeprecated) {
+ return -1;
+ } else if (addr1IsSource && !addr2IsSource) {
+ return -1;
+ } else if (!addr1IsSource && addr2IsSource) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ if (!addresses.get(0).equals(srcAddr2)) {
+ throw new Exception("Rule 3 not satisfied. Expected srcAddr2 as the first address.");
+ }
+
+ if (!addresses.get(1).equals(srcAddr1)) {
+ throw new Exception("Rule 3 not satisfied. Expected srcAddr1 as the second address.");
+ }
+
+ if (!addresses.get(2).equals(dstAddr2)) {
+ throw new Exception("Rule 3 not satisfied. Expected dstAddr2 as the third address.");
+ }
+
+ if (!addresses.get(3).equals(dstAddr1)) {
+ throw new Exception("Rule 3 not satisfied. Expected srcAddr2 as the fourth address.");
+ }
+
+ System.out.println("Rule 3 satisfied.");
+ }
+
+ public static void rule4() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("192.0.2.1");
+ final InetAddress addr2 = InetAddress.getByName("192.168.0.1");
+ final InetAddress addr3 = InetAddress.getByName("2001:db8::1");
+ final InetAddress addr4 = InetAddress.getByName("2001:db8::2");
+
+ final List addresses = Arrays.asList(addr3, addr1, addr2, addr4);
+
+ addresses.sort((addr11, addr21) -> {
+ // Rule 4: Prefer home addresses.
+ final boolean isHomeAddr11 = isHomeAddress(addr11);
+ final boolean isCareOfAddr11 = isCareOfAddress(addr11);
+ final boolean isHomeAddr21 = isHomeAddress(addr21);
+ final boolean isCareOfAddr21 = isCareOfAddress(addr21);
+
+ if (isHomeAddr11 && isCareOfAddr11 && !isHomeAddr21) {
+ return -1;
+ } else if (isHomeAddr21 && isCareOfAddr21 && !isHomeAddr11) {
+ return 1;
+ }
+ return 0;
+ });
+
+
+ if (!addresses.get(0).equals(addr3)) {
+ throw new Exception("Rule 4 not satisfied. Expected addr1 as the first address.");
+ }
+
+ System.out.println("Rule 4 satisfied.");
+ }
+
+ public static void rule5() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("192.0.2.1");
+ final InetAddress addr2 = InetAddress.getByName("2001:db8::1");
+ final InetAddress addr3 = InetAddress.getByName("www.google.com");
+
+ final List addresses = Arrays.asList(addr3, addr1, addr2);
+
+ addresses.sort((addr11, addr21) -> {
+ //Rule 5: Prefer matching label.
+ final int label1 = getLabel(addr11);
+ final int label2 = getLabel(addr21);
+
+ if (label1 < label2) {
+ return -1;
+ } else if (label1 > label2) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ if (!addresses.get(0).equals(addr2)) {
+ throw new Exception("Rule 5 not satisfied. Expected 2001:db8::1 as the first address.");
+ }
+
+ if (!addresses.get(1).equals(addr3)) {
+ throw new Exception("Rule 5 not satisfied. Expected www.google.com as the second address.");
+ }
+
+ if (!addresses.get(2).equals(addr1)) {
+ throw new Exception("Rule 5 not satisfied. Expected 192.0.2.1 as the third address.");
+ }
+
+ System.out.println("Rule 5 satisfied.");
+ }
+
+ public static void rule6() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("2001:db8:0:0:1::");
+ final InetAddress addr2 = InetAddress.getByName("2001:db8:0:0:2::");
+
+ final List addresses = Arrays.asList(addr2, addr1);
+
+ addresses.sort((addr11, addr21) -> {
+ // Rule 6: Prefer higher precedence.
+ final int add1Precedence = getPrecedence(addr11);
+ final int add2Precedence = getPrecedence(addr21);
+
+ if (add1Precedence > add2Precedence) {
+ return -1;
+ } else if (add1Precedence < add2Precedence) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ if (!addresses.get(0).equals(addr2)) {
+ throw new Exception("Rule 6 not satisfied. Expected addr2 as the first address.");
+ }
+
+ System.out.println("Rule 6 satisfied.");
+ }
+
+
+ public static void rule7() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("2001:db8:0:0:1::");
+ final InetAddress addr2 = InetAddress.getByName("2001:db8:0:0:2::");
+
+ final List addresses = Arrays.asList(addr2, addr1);
+
+ addresses.sort((addr11, addr21) -> {
+ // Rule 7: Prefer native transport.
+ final boolean addr1Encapsulated = isEncapsulated(addr11);
+ final boolean addr2Encapsulated = isEncapsulated(addr21);
+
+ if (addr1Encapsulated && !addr2Encapsulated) {
+ return 1;
+ } else if (!addr1Encapsulated && addr2Encapsulated) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ if (!addresses.get(0).equals(addr2)) {
+ throw new Exception("Rule 7 not satisfied. Expected 2001:db8:0:0:2:: as the first address.");
+ }
+
+ if (!addresses.get(1).equals(addr1)) {
+ throw new Exception("Rule 7 not satisfied. Expected 2001:db8:0:0:1:: as the second address.");
+ }
+ System.out.println("Rule 7 satisfied.");
+ }
+
+ public static void rule8() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("2001:db8:0:0:0:0:0:1");
+ final InetAddress addr2 = InetAddress.getByName("2001:db8:0:1:0:0:0:1");
+ final InetAddress addr3 = InetAddress.getByName("2001:db8:0:2:0:0:0:1");
+ final InetAddress addr4 = InetAddress.getByName("::1");
+ final InetAddress addr5 = InetAddress.getByName("fe80::1%lo0");
+
+ final List addresses = Arrays.asList(addr4, addr5, addr3, addr1, addr2);
+
+ addresses.sort((addr11, addr21) -> {
+ // Rule 8: Prefer smaller scope.
+ final int scope1 = getScope(addr11);
+ final int scope2 = getScope(addr21);
+
+ if (scope1 < scope2) {
+ return -1;
+ } else if (scope1 > scope2) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ if (!addresses.get(0).equals(addr4)) {
+ throw new Exception("Rule 8 not satisfied. Expected fe80::1%lo0 as the first address.");
+ }
+
+ if (!addresses.get(1).equals(addr5)) {
+ throw new Exception("Rule 8 not satisfied. Expected ::1 as the second address.");
+ }
+
+ if (!addresses.get(2).equals(addr3)) {
+ throw new Exception("Rule 8 not satisfied. Expected 2001:db8:0:0:0:0:0:1 as the third address.");
+ }
+
+ if (!addresses.get(3).equals(addr1)) {
+ throw new Exception("Rule 8 not satisfied. Expected 2001:db8:0:1:0:0:0:1 as the fourth address.");
+ }
+
+ if (!addresses.get(4).equals(addr2)) {
+ throw new Exception("Rule 8 not satisfied. Expected 2001:db8:0:2:0:0:0:1 as the fifth address.");
+ }
+
+ System.out.println("Rule 8 satisfied.");
+ }
+
+ public static void rule9() throws Exception {
+ final InetAddress addr1 = InetAddress.getByName("2001:db8:0:0:1::");
+ final InetAddress addr2 = InetAddress.getByName("2001:db8:0:0:1:0:0:2");
+ final InetAddress addr3 = InetAddress.getByName("2001:db8:0:0:1:0:0:3");
+ final InetAddress addr4 = InetAddress.getByName("2001:db8:0:0:2:0:0:4");
+
+ final List addresses = Arrays.asList(addr4, addr1, addr2, addr3);
+
+ addresses.sort((addr11, addr21) -> {
+ // Rule 9: Use longest matching prefix.
+ final int prefixLen1 = commonPrefixLen(addr11, addr21);
+ final int prefixLen2 = commonPrefixLen(addr21, addr11);
+
+ if (prefixLen1 > prefixLen2) {
+ return -1;
+ } else if (prefixLen1 < prefixLen2) {
+ return 1;
+ }
+ return 0;
+ });
+
+ if (!addresses.get(0).equals(addr4)) {
+ throw new Exception("Rule 9 not satisfied. Expected addr1 as the first address.");
+ }
+
+ System.out.println("Rule 9 satisfied.");
+ }
+
+
+ private static int getPrecedence(final InetAddress addr) {
+ if (addr instanceof Inet6Address) {
+ final byte[] addrBytes = addr.getAddress();
+ if (addrBytes[0] == (byte) 0xFF) {
+ return 1; // multicast
+ } else if (isIPv4MappedIPv6Address(addrBytes)) {
+ return 4; // IPv4-mapped IPv6 address
+ } else if (isULA(addrBytes)) {
+ return 5; // unique local address
+ } else if (isLinkLocal(addrBytes)) {
+ return 6; // link-local address
+ } else if (isSiteLocal(addrBytes)) {
+ return 7; // site-local address
+ } else {
+ return 3; // global address
+ }
+ } else {
+ return 2; // IPv4 address
+ }
+ }
+
+ private static boolean isIPv4MappedIPv6Address(final byte[] addr) {
+ return addr.length == 16 && addr[0] == 0x00 && addr[1] == 0x00 && addr[2] == 0x00
+ && addr[3] == 0x00 && addr[4] == 0x00 && addr[5] == 0x00 && addr[6] == 0x00
+ && addr[7] == 0x00 && addr[8] == 0x00 && addr[9] == 0x00 && addr[10] == (byte) 0xFF
+ && addr[11] == (byte) 0xFF;
+ }
+
+ private static boolean isULA(final byte[] addr) {
+ return addr.length == 16 && ((addr[0] & 0xFE) == (byte) 0xFC);
+ }
+
+ private static boolean isLinkLocal(final byte[] addr) {
+ return addr.length == 16 && (addr[0] & 0xFF) == 0xFE && (addr[1] & 0xC0) == 0x80;
+ }
+
+ private static boolean isSiteLocal(final byte[] addr) {
+ return addr.length == 16 && (addr[0] & 0xFF) == 0xFE && (addr[1] & 0xC0) == 0xC0;
+ }
+
+
+ private static boolean isEncapsulated(final InetAddress addr) {
+ if (isIpv6Address(addr)) {
+ final String addrStr = addr.getHostAddress();
+ // Check if the IPv6 address is in the format of "::ffff:x.x.x.x"
+ if (addrStr.startsWith("::ffff:")) {
+ return true;
+ }
+ } else if (isIpv4Address(addr)) {
+ // Check if the IPv4 address is in the format of "x.x.x.x"
+ final byte[] byteAddr = addr.getAddress();
+ if (byteAddr[0] == 0 && byteAddr[1] == 0 && byteAddr[2] == 0 && byteAddr[3] != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ private static boolean isDeprecated(final InetAddress addr) {
+ if (addr instanceof Inet4Address) {
+ return false;
+ } else if (addr instanceof Inet6Address) {
+ final Inet6Address ipv6Addr = (Inet6Address) addr;
+ final byte[] addressBytes = ipv6Addr.getAddress();
+
+ // Check if the IPv6 address is IPv4-mapped
+ if (addressBytes[0] == 0 && addressBytes[1] == 0 && addressBytes[2] == 0 && addressBytes[3] == 0 &&
+ addressBytes[4] == 0 && addressBytes[5] == 0 && addressBytes[6] == 0 && addressBytes[7] == 0 &&
+ addressBytes[8] == 0 && addressBytes[9] == 0 && addressBytes[10] == (byte) 0xFF && addressBytes[11] == (byte) 0xFF) {
+ return true;
+ }
+
+ // Check if the IPv6 address is a link-local address
+ if ((addressBytes[0] & 0xFF) == 0xFE && (addressBytes[1] & 0xC0) == 0x80) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isAddressReachable(final InetAddress address, final List networkInterfaces) throws IOException {
+ // Check if the address is a link-local, loopback, or site-local address
+ if (address.isLinkLocalAddress() || address.isLoopbackAddress() || address.isSiteLocalAddress()) {
+ return true;
+ }
+ // Check if the address is reachable on any network interface
+ for (final NetworkInterface networkInterface : networkInterfaces) {
+ final Enumeration addresses = networkInterface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ final InetAddress addr = addresses.nextElement();
+ if (addr.equals(address)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+ private static int getLabel(final InetAddress addr) {
+ if (isIpv4Address(addr)) {
+ return 4;
+ } else if (isIpv6Address(addr)) {
+ if (addr.isLoopbackAddress()) {
+ return 0;
+ } else if (isIpv6Address6To4(addr)) {
+ return 2;
+ } else if (isIpv6AddressTeredo(addr)) {
+ return 5;
+ } else if (isIpv6AddressULA(addr)) {
+ return 13;
+ } else if (((Inet6Address) addr).isIPv4CompatibleAddress()) {
+ return 3;
+ } else if (addr.isSiteLocalAddress()) {
+ return 11;
+ } else if (isIpv6Address6Bone(addr)) {
+ return 12;
+ } else {
+ // All other IPv6 addresses, including global unicast addresses.
+ return 1;
+ }
+ } else {
+ // This should never happen.
+ return 1;
+ }
+ }
+
+ private static boolean isIpv6Address(final InetAddress addr) {
+ return addr instanceof Inet6Address;
+ }
+
+ private static boolean isIpv4Address(final InetAddress addr) {
+ return addr instanceof Inet4Address;
+ }
+
+ private static boolean isIpv6Address6To4(final InetAddress addr) {
+ if (!isIpv6Address(addr)) {
+ return false;
+ }
+ final byte[] byteAddr = addr.getAddress();
+ return byteAddr[0] == 0x20 && byteAddr[1] == 0x02;
+ }
+
+ private static boolean isIpv6AddressTeredo(final InetAddress addr) {
+ if (!isIpv6Address(addr)) {
+ return false;
+ }
+ final byte[] byteAddr = addr.getAddress();
+ return byteAddr[0] == 0x20 && byteAddr[1] == 0x01 && byteAddr[2] == 0x00
+ && byteAddr[3] == 0x00;
+ }
+
+ private static boolean isIpv6AddressULA(final InetAddress addr) {
+ return isIpv6Address(addr) && (addr.getAddress()[0] & 0xfe) == 0xfc;
+ }
+
+ private static boolean isIpv6Address6Bone(final InetAddress addr) {
+ if (!isIpv6Address(addr)) {
+ return false;
+ }
+ final byte[] byteAddr = addr.getAddress();
+ return byteAddr[0] == 0x3f && byteAddr[1] == (byte) 0xfe;
+ }
+
+ private static int getIpv6MulticastScope(final InetAddress addr) {
+ return !isIpv6Address(addr) ? 0 : (addr.getAddress()[1] & 0x0f);
+ }
+
+
+ private static int getScope(final InetAddress addr) {
+ if (isIpv6Address(addr)) {
+ if (addr.isMulticastAddress()) {
+ return getIpv6MulticastScope(addr);
+ } else if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+ return 0x02;
+ } else if (addr.isSiteLocalAddress()) {
+ return 0x05;
+ } else {
+ return 0x0e;
+ }
+ } else if (isIpv4Address(addr)) {
+ if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+ return 0x02;
+ } else {
+ return 0x0e;
+ }
+ } else {
+ return 0x01;
+ }
+ }
+
+ private static boolean isHomeAddress(final InetAddress addr) {
+ if (addr instanceof Inet6Address) {
+ // Check if the address is a global unicast address with the
+ // high-order bit of the first octet set to zero.
+ final byte[] bytes = addr.getAddress();
+ return (bytes[0] & 0x80) == 0x00;
+ } else if (addr instanceof Inet4Address) {
+ // Check if the address is a private address.
+ final byte[] bytes = addr.getAddress();
+ return ((bytes[0] & 0xFF) == 10)
+ || (((bytes[0] & 0xFF) == 172) && ((bytes[1] & 0xF0) == 0x10))
+ || (((bytes[0] & 0xFF) == 192) && ((bytes[1] & 0xFF) == 168));
+ }
+ return false;
+ }
+
+ private static boolean isCareOfAddress(final InetAddress addr) {
+ if (addr instanceof Inet6Address) {
+ final byte[] bytes = addr.getAddress();
+ return (bytes[0] & 0xfe) == 0xfc; // IPv6 Unique Local Addresses (ULA) range
+ }
+ return false;
+ }
+
+ private static boolean isSourceAddress(final InetAddress addr, final InetAddress srcAddr1, final InetAddress srcAddr2) {
+ // Check if the address matches either srcAddr1 or srcAddr2
+ return addr.equals(srcAddr1) || addr.equals(srcAddr2);
+ }
+
+ private static int commonPrefixLen(final InetAddress addr1, final InetAddress addr2) {
+ byte[] bytes1 = addr1.getAddress();
+ byte[] bytes2 = addr2.getAddress();
+
+ if (bytes2.length == 4) {
+ bytes2 = Arrays.copyOf(bytes2, 16);
+ }
+
+ if (bytes1.length > 8) {
+ bytes1 = Arrays.copyOf(bytes1, 8);
+ bytes2 = Arrays.copyOf(bytes2, 8);
+ }
+
+ int prefixLen = 0;
+ for (int i = 0; i < bytes1.length; i++) {
+ int bits = 8;
+ for (int j = 7; j >= 0; j--) {
+ if ((bytes1[i] & (1 << j)) == (bytes2[i] & (1 << j))) {
+ prefixLen += 1;
+ } else {
+ bits--;
+ }
+ if (bits == 0) {
+ break;
+ }
+ }
+ if (bits != 8) {
+ break;
+ }
+ }
+ return prefixLen;
+ }
+}
+
+
+
+
+
+
+
+
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorTest.java
new file mode 100644
index 000000000..2d84368f7
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorTest.java
@@ -0,0 +1,408 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.nio;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
+import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
+import org.apache.hc.client5.http.ssl.ConscryptClientTlsStrategy;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.BasicHttpContext;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+
+public class HappyEyeballsV2AsyncClientConnectionOperatorTest {
+
+ private AsyncClientConnectionOperator happyEyeballsV2AsyncClientConnectionOperator;
+
+ private ConnectionInitiator connectionInitiator;
+
+ private DnsResolver dnsResolver;
+
+ private SocketAddress socketAddress;
+
+ private AsyncClientConnectionOperator connectionOperator;
+
+
+ @BeforeEach
+ public void setup() {
+
+ dnsResolver = Mockito.mock(DnsResolver.class);
+ connectionOperator = Mockito.mock(AsyncClientConnectionOperator.class);
+
+ happyEyeballsV2AsyncClientConnectionOperator = new HappyEyeballsV2AsyncClientConnectionOperator
+ (RegistryBuilder.create().register(URIScheme.HTTPS.getId(), ConscryptClientTlsStrategy.getDefault()).build(),
+ connectionOperator,
+ dnsResolver,
+ Timeout.ofSeconds(1),
+ Timeout.ofSeconds(1),
+ Timeout.ofSeconds(1),
+ Timeout.ofSeconds(1),
+ Timeout.ofSeconds(1),
+ 1,
+ HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily.IPv4,
+ null);
+
+ connectionInitiator = mock(ConnectionInitiator.class);
+ socketAddress = mock(SocketAddress.class);
+
+
+ }
+
+ @DisplayName("Test that application prioritizes IPv6 over IPv4 when both are available")
+ @Test
+ public void testIPv6ConnectionIsAttemptedBeforeIPv4() throws UnknownHostException, ExecutionException, InterruptedException {
+ final HttpContext context = new BasicHttpContext();
+ final HttpHost host = new HttpHost("somehost");
+ final InetAddress ip1 = InetAddress.getByAddress(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xFF, (byte) 0xFF, 0x7F, 0, 0, 0x1}); // IPv6 address ::ffff:127.0.0.1
+ final InetAddress ip2 = InetAddress.getByAddress(new byte[]{127, 0, 0, 2});
+ final TlsConfig tlsConfig = TlsConfig.custom()
+ .build();
+
+ Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[]{ip1, ip2});
+ Mockito.when(connectionOperator.connect(any(), any(), any(), any(), any(), any()))
+ .thenAnswer((Answer>) invocation -> {
+ // Extract the callback from the arguments
+ final FutureCallback callback =
+ invocation.getArgument(5);
+
+ // Create a CompletableFuture for the connection result
+ final CompletableFuture result = new CompletableFuture<>();
+
+ // Invoke the callback's completed() method with a mock connection
+ callback.completed(Mockito.mock(ManagedAsyncClientConnection.class));
+
+ // Return the CompletableFuture
+ return result;
+ });
+
+
+ final Future future = happyEyeballsV2AsyncClientConnectionOperator.connect(
+ connectionInitiator,
+ host,
+ socketAddress,
+ Timeout.ofMilliseconds(123),
+ tlsConfig,
+ context,
+ new FutureCallback() {
+ @Override
+ public void completed(final ManagedAsyncClientConnection managedAsyncClientConnection) {
+ System.out.println("Executing request " + managedAsyncClientConnection);
+ }
+
+ @Override
+ public void failed(final Exception e) {
+ System.out.println("Exception " + e);
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println("Cancelled");
+ }
+ }
+ );
+
+ future.get();
+ assertTrue(future.isDone());
+ assertFalse(future.isCancelled());
+
+ }
+
+ @DisplayName("Test asynchronous behavior with valid input and expect correct output")
+ @Test
+ public void testAsyncBehavior_withValidInput_expectCorrectOutput() throws Exception {
+ final HttpContext context = new BasicHttpContext();
+ final HttpHost host = new HttpHost("somehost");
+ final InetAddress ip1 = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
+ final InetAddress ip2 = InetAddress.getByAddress(new byte[]{127, 0, 0, 2});
+ final TlsConfig tlsConfig = TlsConfig.custom().build();
+
+ Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[]{ip1, ip2});
+
+ // Create a mock connection
+ final ManagedAsyncClientConnection connection = Mockito.mock(ManagedAsyncClientConnection.class);
+
+ // Create a future that will complete when the connection is established
+ final CompletableFuture future = new CompletableFuture<>();
+
+ // Make the connection mock return the future when connect() is called
+ Mockito.when(connectionOperator.connect(any(), any(), any(), any(), any(), any()))
+ .thenAnswer((Answer>) invocation -> {
+ // Extract the callback from the arguments
+ final FutureCallback callback =
+ invocation.getArgument(5);
+
+ // Create a CompletableFuture for the connection result
+ final CompletableFuture result = new CompletableFuture<>();
+
+ // Invoke the callback's completed() method with a mock connection
+ callback.completed(connection);
+
+ // Return the CompletableFuture
+ return result;
+ });
+
+ // Call connect() on the operator
+ final Future result = happyEyeballsV2AsyncClientConnectionOperator.connect(
+ connectionInitiator,
+ host,
+ socketAddress,
+ Timeout.ofMilliseconds(123),
+ tlsConfig,
+ context,
+ new FutureCallback() {
+ @Override
+ public void completed(final ManagedAsyncClientConnection managedAsyncClientConnection) {
+ System.out.println("Executing request " + managedAsyncClientConnection);
+ }
+
+ @Override
+ public void failed(final Exception e) {
+ System.out.println("Exception " + e);
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println("Cancelled");
+ }
+ });
+
+ // Verify that the connection is not yet established
+ assertFalse(result.isDone());
+
+ // Complete the future with the connection mock
+ future.complete(connection);
+
+ // Wait for the connection to be established
+ final ManagedAsyncClientConnection actualConnection = result.get();
+
+ // Verify that the correct connection was returned
+ assertSame(connection, actualConnection);
+ }
+
+
+ @Test
+ @DisplayName("Verify Asynchronous Behavior of Request Processing")
+ public void verifyAsynchronousBehaviorOfRequestProcessing() throws Exception {
+ final HttpContext context = new BasicHttpContext();
+ final HttpHost host = new HttpHost("somehost");
+ final InetAddress ip1 = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
+ final InetAddress ip2 = InetAddress.getByAddress(new byte[]{127, 0, 0, 2});
+ final TlsConfig tlsConfig = TlsConfig.custom().build();
+
+ Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[]{ip1, ip2});
+
+ // Create a mock connection
+ final ManagedAsyncClientConnection connection = Mockito.mock(ManagedAsyncClientConnection.class);
+
+ // Create a future that will complete when the connection is established
+ final CompletableFuture future = new CompletableFuture<>();
+
+ // Make the connection mock return the future when connect() is called
+ Mockito.when(connectionOperator.connect(any(), any(), any(), any(), any(), any()))
+ .thenAnswer((Answer>) invocation -> {
+ // Extract the callback from the arguments
+ final FutureCallback callback =
+ invocation.getArgument(5);
+
+ // Create a CompletableFuture for the connection result
+ final CompletableFuture result = new CompletableFuture<>();
+
+ // Invoke the callback's completed() method with a mock connection
+ callback.completed(connection);
+
+ // Return the CompletableFuture
+ return result;
+ });
+
+ // Call connect() on the operator
+ final Future result = happyEyeballsV2AsyncClientConnectionOperator.connect(
+ connectionInitiator,
+ host,
+ socketAddress,
+ Timeout.ofMilliseconds(123),
+ tlsConfig,
+ context,
+ new FutureCallback() {
+ @Override
+ public void completed(final ManagedAsyncClientConnection managedAsyncClientConnection) {
+ System.out.println("Executing request " + managedAsyncClientConnection);
+ }
+
+ @Override
+ public void failed(final Exception e) {
+ System.out.println("Exception " + e);
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println("Cancelled");
+ }
+ });
+
+ // Verify that the connection is not yet established
+ assertFalse(result.isDone());
+
+ // Complete the future with the connection mock
+ future.complete(connection);
+
+ // Wait for the connection to be established
+ final ManagedAsyncClientConnection actualConnection = result.get();
+
+ // Verify that the correct connection was returned
+ assertSame(connection, actualConnection);
+
+ // Check that the failed and cancelled callbacks were not called
+ assertFalse(future.isCompletedExceptionally());
+ assertFalse(result.isCancelled());
+ }
+
+ @Test
+ @DisplayName("Test successful connection using only IPv4")
+ public void testIPv4SuccessfulConnection() throws Exception {
+ final HttpContext context = new BasicHttpContext();
+ final HttpHost host = new HttpHost("ipv4host");
+ final InetAddress ip1 = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
+ final TlsConfig tlsConfig = TlsConfig.custom().build();
+
+ Mockito.when(dnsResolver.resolve("ipv4host")).thenReturn(new InetAddress[]{ip1});
+ Mockito.when(connectionOperator.connect(any(), any(), any(), any(), any(), any()))
+ .thenAnswer(invocation -> {
+ final FutureCallback callback = invocation.getArgument(5);
+ final CompletableFuture result = new CompletableFuture<>();
+ callback.completed(Mockito.mock(ManagedAsyncClientConnection.class));
+ return result;
+ });
+
+ final Future future = happyEyeballsV2AsyncClientConnectionOperator.connect(
+ connectionInitiator,
+ host,
+ socketAddress,
+ Timeout.ofMilliseconds(123),
+ tlsConfig,
+ context,
+ new FutureCallback() {
+ @Override
+ public void completed(final ManagedAsyncClientConnection managedAsyncClientConnection) {
+ System.out.println("Executing request " + managedAsyncClientConnection);
+ }
+
+ @Override
+ public void failed(final Exception e) {
+ System.out.println("Exception " + e);
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println("Cancelled");
+ }
+ });
+
+ future.get();
+ assertTrue(future.isDone());
+ assertFalse(future.isCancelled());
+ Mockito.verify(connectionOperator, times(1)).connect(any(), any(), any(), any(), any(), any());
+ }
+
+ @Test
+ @DisplayName("Test failed connection attempt")
+ public void testFailedConnectionAttempt() throws Exception {
+ final HttpContext context = new BasicHttpContext();
+ final HttpHost host = new HttpHost("failedhost");
+ final InetAddress ip1 = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
+ final TlsConfig tlsConfig = TlsConfig.custom().build();
+
+ Mockito.when(dnsResolver.resolve("failedhost")).thenReturn(new InetAddress[]{ip1});
+ Mockito.when(connectionOperator.connect(any(), any(), any(), any(), any(), any()))
+ .thenAnswer(invocation -> {
+ final FutureCallback callback = invocation.getArgument(5);
+ final CompletableFuture result = new CompletableFuture<>();
+ callback.failed(new IOException("Failed to connect"));
+ return result;
+ });
+
+ final CompletableFuture future = new CompletableFuture<>();
+ happyEyeballsV2AsyncClientConnectionOperator.connect(
+ connectionInitiator,
+ host,
+ socketAddress,
+ Timeout.ofMilliseconds(123),
+ tlsConfig,
+ context,
+ new FutureCallback() {
+ @Override
+ public void completed(final ManagedAsyncClientConnection managedAsyncClientConnection) {
+ System.out.println("Executing request " + managedAsyncClientConnection);
+ future.complete(managedAsyncClientConnection);
+ }
+
+ @Override
+ public void failed(final Exception e) {
+ System.out.println("Exception " + e);
+ future.completeExceptionally(e);
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println("Cancelled");
+ future.cancel(true);
+ }
+ });
+
+ assertThrows(ExecutionException.class, () -> future.get());
+ assertTrue(future.isCompletedExceptionally());
+ assertFalse(future.isCancelled());
+ Mockito.verify(connectionOperator, times(1)).connect(any(), any(), any(), any(), any(), any());
+ }
+}