View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.transport.jdk;
20  
21  import javax.net.ssl.SSLContext;
22  import javax.net.ssl.SSLEngine;
23  import javax.net.ssl.SSLParameters;
24  import javax.net.ssl.X509ExtendedTrustManager;
25  import javax.net.ssl.X509TrustManager;
26  
27  import java.io.BufferedInputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.lang.reflect.InvocationTargetException;
31  import java.lang.reflect.Method;
32  import java.net.Authenticator;
33  import java.net.ConnectException;
34  import java.net.InetAddress;
35  import java.net.InetSocketAddress;
36  import java.net.PasswordAuthentication;
37  import java.net.ProxySelector;
38  import java.net.Socket;
39  import java.net.URI;
40  import java.net.URISyntaxException;
41  import java.net.UnknownHostException;
42  import java.net.http.HttpClient;
43  import java.net.http.HttpRequest;
44  import java.net.http.HttpResponse;
45  import java.nio.file.Files;
46  import java.nio.file.Path;
47  import java.nio.file.StandardCopyOption;
48  import java.security.cert.X509Certificate;
49  import java.time.Duration;
50  import java.time.Instant;
51  import java.time.ZoneId;
52  import java.time.ZonedDateTime;
53  import java.time.format.DateTimeFormatter;
54  import java.time.format.DateTimeParseException;
55  import java.util.Base64;
56  import java.util.Collections;
57  import java.util.HashMap;
58  import java.util.Locale;
59  import java.util.Map;
60  import java.util.concurrent.Semaphore;
61  import java.util.function.Function;
62  import java.util.function.Supplier;
63  import java.util.regex.Matcher;
64  
65  import com.github.mizosoft.methanol.Methanol;
66  import org.eclipse.aether.ConfigurationProperties;
67  import org.eclipse.aether.RepositorySystemSession;
68  import org.eclipse.aether.repository.AuthenticationContext;
69  import org.eclipse.aether.repository.RemoteRepository;
70  import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
71  import org.eclipse.aether.spi.connector.transport.GetTask;
72  import org.eclipse.aether.spi.connector.transport.PeekTask;
73  import org.eclipse.aether.spi.connector.transport.PutTask;
74  import org.eclipse.aether.spi.connector.transport.TransportTask;
75  import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
76  import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
77  import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
78  import org.eclipse.aether.spi.io.PathProcessor;
79  import org.eclipse.aether.transfer.NoTransporterException;
80  import org.eclipse.aether.util.ConfigUtils;
81  import org.slf4j.Logger;
82  import org.slf4j.LoggerFactory;
83  
84  import static java.nio.charset.StandardCharsets.ISO_8859_1;
85  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.ACCEPT_ENCODING;
86  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CACHE_CONTROL;
87  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_LENGTH;
88  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE;
89  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE_PATTERN;
90  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.IF_UNMODIFIED_SINCE;
91  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.LAST_MODIFIED;
92  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.MULTIPLE_CHOICES;
93  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.NOT_FOUND;
94  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.PRECONDITION_FAILED;
95  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.RANGE;
96  import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.USER_AGENT;
97  import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_HTTP_VERSION;
98  import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_MAX_CONCURRENT_REQUESTS;
99  import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_HTTP_VERSION;
100 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_MAX_CONCURRENT_REQUESTS;
101 
102 /**
103  * JDK Transport using {@link HttpClient}.
104  * <p>
105  * Known issues:
106  * <ul>
107  *     <li>Does not support {@link ConfigurationProperties#REQUEST_TIMEOUT}, see <a href="https://bugs.openjdk.org/browse/JDK-8258397">JDK-8258397</a></li>
108  * </ul>
109  * <p>
110  * Related: <a href="https://dev.to/kdrakon/httpclient-can-t-connect-to-a-tls-proxy-118a">No TLS proxy supported</a>.
111  *
112  * @since 2.0.0
113  */
114 final class JdkTransporter extends AbstractTransporter implements HttpTransporter {
115     private static final Logger LOGGER = LoggerFactory.getLogger(JdkTransporter.class);
116 
117     private static final DateTimeFormatter RFC7231 = DateTimeFormatter.ofPattern(
118                     "EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)
119             .withZone(ZoneId.of("GMT"));
120 
121     private static final long MODIFICATION_THRESHOLD = 60L * 1000L;
122 
123     private final ChecksumExtractor checksumExtractor;
124 
125     private final PathProcessor pathProcessor;
126 
127     private final URI baseUri;
128 
129     private final HttpClient client;
130 
131     private final Map<String, String> headers;
132 
133     private final int connectTimeout;
134 
135     private final int requestTimeout;
136 
137     private final Boolean expectContinue;
138 
139     private final Semaphore maxConcurrentRequests;
140 
141     private boolean preemptivePutAuth;
142 
143     private boolean preemptiveAuth;
144 
145     private PasswordAuthentication serverAuthentication;
146 
147     JdkTransporter(
148             RepositorySystemSession session,
149             RemoteRepository repository,
150             int javaVersion,
151             ChecksumExtractor checksumExtractor,
152             PathProcessor pathProcessor)
153             throws NoTransporterException {
154         this.checksumExtractor = checksumExtractor;
155         this.pathProcessor = pathProcessor;
156         try {
157             URI uri = new URI(repository.getUrl()).parseServerAuthority();
158             if (uri.isOpaque()) {
159                 throw new URISyntaxException(repository.getUrl(), "URL must not be opaque");
160             }
161             if (uri.getRawFragment() != null || uri.getRawQuery() != null) {
162                 throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query");
163             }
164             String path = uri.getPath();
165             if (path == null) {
166                 path = "/";
167             }
168             if (!path.startsWith("/")) {
169                 path = "/" + path;
170             }
171             if (!path.endsWith("/")) {
172                 path = path + "/";
173             }
174             this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path);
175         } catch (URISyntaxException e) {
176             throw new NoTransporterException(repository, e.getMessage(), e);
177         }
178 
179         HashMap<String, String> headers = new HashMap<>();
180         String userAgent = ConfigUtils.getString(
181                 session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT);
182         if (userAgent != null) {
183             headers.put(USER_AGENT, userAgent);
184         }
185         @SuppressWarnings("unchecked")
186         Map<Object, Object> configuredHeaders = (Map<Object, Object>) ConfigUtils.getMap(
187                 session,
188                 Collections.emptyMap(),
189                 ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
190                 ConfigurationProperties.HTTP_HEADERS);
191         if (configuredHeaders != null) {
192             configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null));
193         }
194         headers.put(CACHE_CONTROL, "no-cache, no-store");
195 
196         this.connectTimeout = ConfigUtils.getInteger(
197                 session,
198                 ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
199                 ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(),
200                 ConfigurationProperties.CONNECT_TIMEOUT);
201         this.requestTimeout = ConfigUtils.getInteger(
202                 session,
203                 ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
204                 ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(),
205                 ConfigurationProperties.REQUEST_TIMEOUT);
206         String expectContinueConf = ConfigUtils.getString(
207                 session,
208                 null,
209                 ConfigurationProperties.HTTP_EXPECT_CONTINUE + "." + repository.getId(),
210                 ConfigurationProperties.HTTP_EXPECT_CONTINUE);
211         if (javaVersion > 19) {
212             this.expectContinue = expectContinueConf == null ? null : Boolean.parseBoolean(expectContinueConf);
213         } else {
214             this.expectContinue = null;
215             if (expectContinueConf != null) {
216                 LOGGER.warn(
217                         "Configuration for Expect-Continue set but is ignored on Java versions below 20 (current java version is {}) due https://bugs.openjdk.org/browse/JDK-8286171",
218                         javaVersion);
219             }
220         }
221         final String httpsSecurityMode = ConfigUtils.getString(
222                 session,
223                 ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT,
224                 ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(),
225                 ConfigurationProperties.HTTPS_SECURITY_MODE);
226 
227         if (!ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT.equals(httpsSecurityMode)
228                 && !ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode)) {
229             throw new IllegalArgumentException("Unsupported '" + httpsSecurityMode + "' HTTPS security mode.");
230         }
231         final boolean insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode);
232 
233         this.maxConcurrentRequests = new Semaphore(ConfigUtils.getInteger(
234                 session,
235                 DEFAULT_MAX_CONCURRENT_REQUESTS,
236                 CONFIG_PROP_MAX_CONCURRENT_REQUESTS + "." + repository.getId(),
237                 CONFIG_PROP_MAX_CONCURRENT_REQUESTS));
238 
239         this.preemptiveAuth = ConfigUtils.getBoolean(
240                 session,
241                 ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_AUTH,
242                 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH + "." + repository.getId(),
243                 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH);
244         this.preemptivePutAuth = ConfigUtils.getBoolean(
245                 session,
246                 ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_PUT_AUTH,
247                 ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH + "." + repository.getId(),
248                 ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH);
249 
250         this.headers = headers;
251         this.client = createClient(session, repository, insecure);
252     }
253 
254     private URI resolve(TransportTask task) {
255         return baseUri.resolve(task.getLocation());
256     }
257 
258     private ConnectException enhance(ConnectException connectException) {
259         ConnectException result = new ConnectException("Connection to " + baseUri.toASCIIString() + " refused");
260         result.initCause(connectException);
261         return result;
262     }
263 
264     @Override
265     public int classify(Throwable error) {
266         if (error instanceof HttpTransporterException
267                 && ((HttpTransporterException) error).getStatusCode() == NOT_FOUND) {
268             return ERROR_NOT_FOUND;
269         }
270         return ERROR_OTHER;
271     }
272 
273     @Override
274     protected void implPeek(PeekTask task) throws Exception {
275         HttpRequest.Builder request =
276                 HttpRequest.newBuilder().uri(resolve(task)).method("HEAD", HttpRequest.BodyPublishers.noBody());
277         headers.forEach(request::setHeader);
278 
279         prepare(request);
280         try {
281             HttpResponse<Void> response = send(request.build(), HttpResponse.BodyHandlers.discarding());
282             if (response.statusCode() >= MULTIPLE_CHOICES) {
283                 throw new HttpTransporterException(response.statusCode());
284             }
285         } catch (ConnectException e) {
286             throw enhance(e);
287         }
288     }
289 
290     @Override
291     protected void implGet(GetTask task) throws Exception {
292         boolean resume = task.getResumeOffset() > 0L && task.getDataPath() != null;
293         HttpResponse<InputStream> response = null;
294 
295         try {
296             while (true) {
297                 HttpRequest.Builder request =
298                         HttpRequest.newBuilder().uri(resolve(task)).GET();
299                 headers.forEach(request::setHeader);
300 
301                 if (resume) {
302                     long resumeOffset = task.getResumeOffset();
303                     long lastModified = pathProcessor.lastModified(task.getDataPath(), 0L);
304                     request.header(RANGE, "bytes=" + resumeOffset + '-');
305                     request.header(
306                             IF_UNMODIFIED_SINCE,
307                             RFC7231.format(Instant.ofEpochMilli(lastModified - MODIFICATION_THRESHOLD)));
308                     request.header(ACCEPT_ENCODING, "identity");
309                 }
310 
311                 prepare(request);
312                 try {
313                     response = send(request.build(), HttpResponse.BodyHandlers.ofInputStream());
314                     if (response.statusCode() >= MULTIPLE_CHOICES) {
315                         if (resume && response.statusCode() == PRECONDITION_FAILED) {
316                             closeBody(response);
317                             resume = false;
318                             continue;
319                         }
320                         try {
321                             JdkRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> {
322                                 throw new HttpTransporterException(statusCode);
323                             });
324                         } finally {
325                             closeBody(response);
326                         }
327                     }
328                 } catch (ConnectException e) {
329                     closeBody(response);
330                     throw enhance(e);
331                 }
332                 break;
333             }
334 
335             long offset = 0L,
336                     length = response.headers().firstValueAsLong(CONTENT_LENGTH).orElse(-1L);
337             if (resume) {
338                 String range = response.headers().firstValue(CONTENT_RANGE).orElse(null);
339                 if (range != null) {
340                     Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
341                     if (!m.matches()) {
342                         throw new IOException("Invalid Content-Range header for partial download: " + range);
343                     }
344                     offset = Long.parseLong(m.group(1));
345                     length = Long.parseLong(m.group(2)) + 1L;
346                     if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
347                         throw new IOException("Invalid Content-Range header for partial download from offset "
348                                 + task.getResumeOffset() + ": " + range);
349                     }
350                 }
351             }
352 
353             final boolean downloadResumed = offset > 0L;
354             final Path dataFile = task.getDataPath();
355             if (dataFile == null) {
356                 try (InputStream is = response.body()) {
357                     utilGet(task, is, true, length, downloadResumed);
358                 }
359             } else {
360                 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
361                     task.setDataPath(tempFile.getPath(), downloadResumed);
362                     if (downloadResumed && Files.isRegularFile(dataFile)) {
363                         try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(dataFile))) {
364                             Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
365                         }
366                     }
367                     try (InputStream is = response.body()) {
368                         utilGet(task, is, true, length, downloadResumed);
369                     }
370                     tempFile.move();
371                 } finally {
372                     task.setDataPath(dataFile);
373                 }
374             }
375             if (task.getDataPath() != null) {
376                 String lastModifiedHeader = response.headers()
377                         .firstValue(LAST_MODIFIED)
378                         .orElse(null); // note: Wagon also does first not last
379                 if (lastModifiedHeader != null) {
380                     try {
381                         pathProcessor.setLastModified(
382                                 task.getDataPath(),
383                                 ZonedDateTime.parse(lastModifiedHeader, RFC7231)
384                                         .toInstant()
385                                         .toEpochMilli());
386                     } catch (DateTimeParseException e) {
387                         // fall through
388                     }
389                 }
390             }
391             Map<String, String> checksums = checksumExtractor.extractChecksums(headerGetter(response));
392             if (checksums != null && !checksums.isEmpty()) {
393                 checksums.forEach(task::setChecksum);
394             }
395         } finally {
396             closeBody(response);
397         }
398     }
399 
400     private static Function<String, String> headerGetter(HttpResponse<?> response) {
401         return s -> response.headers().firstValue(s).orElse(null);
402     }
403 
404     private void closeBody(HttpResponse<InputStream> streamHttpResponse) throws IOException {
405         if (streamHttpResponse != null) {
406             InputStream body = streamHttpResponse.body();
407             if (body != null) {
408                 body.close();
409             }
410         }
411     }
412 
413     @Override
414     protected void implPut(PutTask task) throws Exception {
415         HttpRequest.Builder request = HttpRequest.newBuilder().uri(resolve(task));
416         if (expectContinue != null) {
417             request = request.expectContinue(expectContinue);
418         }
419         headers.forEach(request::setHeader);
420         try (PathProcessor.TempFile tempFile = pathProcessor.newTempFile()) {
421             utilPut(task, Files.newOutputStream(tempFile.getPath()), true);
422             request.PUT(HttpRequest.BodyPublishers.ofFile(tempFile.getPath()));
423 
424             prepare(request);
425             try {
426                 HttpResponse<Void> response = send(request.build(), HttpResponse.BodyHandlers.discarding());
427                 if (response.statusCode() >= MULTIPLE_CHOICES) {
428                     throw new HttpTransporterException(response.statusCode());
429                 }
430             } catch (ConnectException e) {
431                 throw enhance(e);
432             }
433         }
434     }
435 
436     private void prepare(HttpRequest.Builder requestBuilder) {
437         if (serverAuthentication != null
438                 && (preemptiveAuth
439                         || (preemptivePutAuth && requestBuilder.build().method().equals("PUT")))) {
440             // https://stackoverflow.com/a/58612586
441             requestBuilder.setHeader(
442                     "Authorization",
443                     getBasicAuthValue(serverAuthentication.getUserName(), serverAuthentication.getPassword()));
444         }
445     }
446 
447     static String getBasicAuthValue(String username, char[] password) {
448         StringBuilder sb = new StringBuilder(128);
449         sb.append(username).append(':').append(password);
450         // Java's HTTP client uses ISO-8859-1 for Basic auth encoding
451         return "Basic " + Base64.getEncoder().encodeToString(sb.toString().getBytes(ISO_8859_1));
452     }
453 
454     private <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
455             throws Exception {
456         maxConcurrentRequests.acquire();
457         try {
458             return client.send(request, responseBodyHandler);
459         } finally {
460             maxConcurrentRequests.release();
461         }
462     }
463 
464     @Override
465     protected void implClose() {
466         if (client != null) {
467             JdkTransporterCloser.closer(client).run();
468         }
469     }
470 
471     private HttpClient createClient(RepositorySystemSession session, RemoteRepository repository, boolean insecure)
472             throws RuntimeException {
473 
474         HashMap<Authenticator.RequestorType, PasswordAuthentication> authentications = new HashMap<>();
475         SSLContext sslContext = null;
476         try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) {
477             if (repoAuthContext != null) {
478                 sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class);
479 
480                 String username = repoAuthContext.get(AuthenticationContext.USERNAME);
481                 String password = repoAuthContext.get(AuthenticationContext.PASSWORD);
482                 serverAuthentication = new PasswordAuthentication(username, password.toCharArray());
483                 authentications.put(Authenticator.RequestorType.SERVER, serverAuthentication);
484             }
485         }
486 
487         if (sslContext == null) {
488             try {
489                 if (insecure) {
490                     sslContext = SSLContext.getInstance("TLS");
491                     X509ExtendedTrustManager tm = new X509ExtendedTrustManager() {
492                         @Override
493                         public void checkClientTrusted(X509Certificate[] chain, String authType) {}
494 
495                         @Override
496                         public void checkServerTrusted(X509Certificate[] chain, String authType) {}
497 
498                         @Override
499                         public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {}
500 
501                         @Override
502                         public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {}
503 
504                         @Override
505                         public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
506 
507                         @Override
508                         public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
509 
510                         @Override
511                         public X509Certificate[] getAcceptedIssuers() {
512                             return null;
513                         }
514                     };
515                     sslContext.init(null, new X509TrustManager[] {tm}, null);
516                 } else {
517                     sslContext = SSLContext.getDefault();
518                 }
519             } catch (Exception e) {
520                 if (e instanceof RuntimeException) {
521                     throw (RuntimeException) e;
522                 } else {
523                     throw new IllegalStateException("SSL Context setup failure", e);
524                 }
525             }
526         }
527 
528         HttpClient.Builder builder = Methanol.newBuilder()
529                 .version(HttpClient.Version.valueOf(ConfigUtils.getString(
530                         session,
531                         DEFAULT_HTTP_VERSION,
532                         CONFIG_PROP_HTTP_VERSION + "." + repository.getId(),
533                         CONFIG_PROP_HTTP_VERSION)))
534                 .followRedirects(HttpClient.Redirect.NORMAL)
535                 .connectTimeout(Duration.ofMillis(connectTimeout))
536                 // this only considers the time until the response header is received, see
537                 // https://bugs.openjdk.org/browse/JDK-8208693
538                 // but better than nothing
539                 .requestTimeout(Duration.ofMillis(requestTimeout))
540                 .sslContext(sslContext);
541 
542         if (insecure) {
543             SSLParameters sslParameters = sslContext.getDefaultSSLParameters();
544             sslParameters.setEndpointIdentificationAlgorithm(null);
545             builder.sslParameters(sslParameters);
546         }
547 
548         setLocalAddress(builder, () -> getHttpLocalAddress(session, repository));
549 
550         if (repository.getProxy() != null) {
551             ProxySelector proxy = ProxySelector.of(new InetSocketAddress(
552                     repository.getProxy().getHost(), repository.getProxy().getPort()));
553 
554             builder.proxy(proxy);
555             try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) {
556                 if (proxyAuthContext != null) {
557                     String username = proxyAuthContext.get(AuthenticationContext.USERNAME);
558                     String password = proxyAuthContext.get(AuthenticationContext.PASSWORD);
559 
560                     authentications.put(
561                             Authenticator.RequestorType.PROXY,
562                             new PasswordAuthentication(username, password.toCharArray()));
563                 }
564             }
565         }
566 
567         if (!authentications.isEmpty()) {
568             builder.authenticator(new Authenticator() {
569                 @Override
570                 protected PasswordAuthentication getPasswordAuthentication() {
571                     return authentications.get(getRequestorType());
572                 }
573             });
574         }
575 
576         return builder.build();
577     }
578 
579     private static InetAddress getHttpLocalAddress(RepositorySystemSession session, RemoteRepository repository) {
580         String bindAddress = ConfigUtils.getString(
581                 session,
582                 null,
583                 ConfigurationProperties.HTTP_LOCAL_ADDRESS + "." + repository.getId(),
584                 ConfigurationProperties.HTTP_LOCAL_ADDRESS);
585         if (bindAddress == null) {
586             return null;
587         }
588         try {
589             return InetAddress.getByName(bindAddress);
590         } catch (UnknownHostException uhe) {
591             throw new IllegalArgumentException(
592                     "Given bind address (" + bindAddress + ") cannot be resolved for remote repository " + repository,
593                     uhe);
594         }
595     }
596 
597     private static void setLocalAddress(HttpClient.Builder builder, Supplier<InetAddress> addressSupplier) {
598         try {
599             final InetAddress address = addressSupplier.get();
600             if (address == null) {
601                 return;
602             }
603 
604             final Method mtd = builder.getClass().getDeclaredMethod("localAddress", InetAddress.class);
605             if (!mtd.canAccess(builder)) {
606                 mtd.setAccessible(true);
607             }
608             mtd.invoke(builder, address);
609         } catch (final NoSuchMethodException nsme) {
610             // skip, not yet in the API
611         } catch (InvocationTargetException e) {
612             throw new IllegalStateException(e.getTargetException());
613         } catch (IllegalAccessException e) {
614             throw new IllegalStateException(e);
615         }
616     }
617 }