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.jetty;
20  
21  import javax.net.ssl.*;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.net.URI;
27  import java.net.URISyntaxException;
28  import java.nio.file.Files;
29  import java.nio.file.StandardCopyOption;
30  import java.nio.file.attribute.FileTime;
31  import java.security.cert.X509Certificate;
32  import java.time.format.DateTimeParseException;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.Map;
36  import java.util.concurrent.ExecutionException;
37  import java.util.concurrent.TimeUnit;
38  import java.util.concurrent.atomic.AtomicBoolean;
39  import java.util.concurrent.atomic.AtomicReference;
40  import java.util.regex.Matcher;
41  import java.util.regex.Pattern;
42  
43  import org.eclipse.aether.ConfigurationProperties;
44  import org.eclipse.aether.RepositorySystemSession;
45  import org.eclipse.aether.repository.AuthenticationContext;
46  import org.eclipse.aether.repository.RemoteRepository;
47  import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
48  import org.eclipse.aether.spi.connector.transport.GetTask;
49  import org.eclipse.aether.spi.connector.transport.PeekTask;
50  import org.eclipse.aether.spi.connector.transport.PutTask;
51  import org.eclipse.aether.spi.connector.transport.TransportTask;
52  import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
53  import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
54  import org.eclipse.aether.transfer.NoTransporterException;
55  import org.eclipse.aether.transfer.TransferCancelledException;
56  import org.eclipse.aether.util.ConfigUtils;
57  import org.eclipse.aether.util.FileUtils;
58  import org.eclipse.jetty.client.HttpClient;
59  import org.eclipse.jetty.client.HttpProxy;
60  import org.eclipse.jetty.client.api.Authentication;
61  import org.eclipse.jetty.client.api.Request;
62  import org.eclipse.jetty.client.api.Response;
63  import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
64  import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
65  import org.eclipse.jetty.client.util.BasicAuthentication;
66  import org.eclipse.jetty.client.util.InputStreamResponseListener;
67  import org.eclipse.jetty.http.HttpHeader;
68  import org.eclipse.jetty.http2.client.HTTP2Client;
69  import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2;
70  import org.eclipse.jetty.io.ClientConnector;
71  import org.eclipse.jetty.util.ssl.SslContextFactory;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * A transporter for HTTP/HTTPS.
77   *
78   * @since 2.0.0
79   */
80  final class JettyTransporter extends AbstractTransporter implements HttpTransporter {
81      private static final int MULTIPLE_CHOICES = 300;
82  
83      private static final int NOT_FOUND = 404;
84  
85      private static final int PRECONDITION_FAILED = 412;
86  
87      private static final long MODIFICATION_THRESHOLD = 60L * 1000L;
88  
89      private static final String ACCEPT_ENCODING = "Accept-Encoding";
90  
91      private static final String CONTENT_LENGTH = "Content-Length";
92  
93      private static final String CONTENT_RANGE = "Content-Range";
94  
95      private static final String LAST_MODIFIED = "Last-Modified";
96  
97      private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
98  
99      private static final String RANGE = "Range";
100 
101     private static final String USER_AGENT = "User-Agent";
102 
103     private static final Pattern CONTENT_RANGE_PATTERN =
104             Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*");
105 
106     private final URI baseUri;
107 
108     private final HttpClient client;
109 
110     private final int requestTimeout;
111 
112     private final Map<String, String> headers;
113 
114     private final boolean preemptiveAuth;
115 
116     private final boolean preemptivePutAuth;
117 
118     private final BasicAuthentication.BasicResult basicServerAuthenticationResult;
119 
120     private final BasicAuthentication.BasicResult basicProxyAuthenticationResult;
121 
122     JettyTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException {
123         try {
124             URI uri = new URI(repository.getUrl()).parseServerAuthority();
125             if (uri.isOpaque()) {
126                 throw new URISyntaxException(repository.getUrl(), "URL must not be opaque");
127             }
128             if (uri.getRawFragment() != null || uri.getRawQuery() != null) {
129                 throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query");
130             }
131             String path = uri.getPath();
132             if (path == null) {
133                 path = "/";
134             }
135             if (!path.startsWith("/")) {
136                 path = "/" + path;
137             }
138             if (!path.endsWith("/")) {
139                 path = path + "/";
140             }
141             this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path);
142         } catch (URISyntaxException e) {
143             throw new NoTransporterException(repository, e.getMessage(), e);
144         }
145 
146         HashMap<String, String> headers = new HashMap<>();
147         String userAgent = ConfigUtils.getString(
148                 session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT);
149         if (userAgent != null) {
150             headers.put(USER_AGENT, userAgent);
151         }
152         @SuppressWarnings("unchecked")
153         Map<Object, Object> configuredHeaders = (Map<Object, Object>) ConfigUtils.getMap(
154                 session,
155                 Collections.emptyMap(),
156                 ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
157                 ConfigurationProperties.HTTP_HEADERS);
158         if (configuredHeaders != null) {
159             configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null));
160         }
161 
162         this.headers = headers;
163 
164         this.requestTimeout = ConfigUtils.getInteger(
165                 session,
166                 ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
167                 ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(),
168                 ConfigurationProperties.REQUEST_TIMEOUT);
169         this.preemptiveAuth = ConfigUtils.getBoolean(
170                 session,
171                 ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_AUTH,
172                 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH + "." + repository.getId(),
173                 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH);
174         this.preemptivePutAuth = ConfigUtils.getBoolean(
175                 session,
176                 ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_PUT_AUTH,
177                 ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH + "." + repository.getId(),
178                 ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH);
179 
180         this.client = getOrCreateClient(session, repository);
181 
182         final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId();
183         this.basicServerAuthenticationResult =
184                 (BasicAuthentication.BasicResult) session.getData().get(instanceKey + ".serverAuth");
185         this.basicProxyAuthenticationResult =
186                 (BasicAuthentication.BasicResult) session.getData().get(instanceKey + ".proxyAuth");
187     }
188 
189     private URI resolve(TransportTask task) {
190         return baseUri.resolve(task.getLocation());
191     }
192 
193     @Override
194     public int classify(Throwable error) {
195         if (error instanceof HttpTransporterException
196                 && ((HttpTransporterException) error).getStatusCode() == NOT_FOUND) {
197             return ERROR_NOT_FOUND;
198         }
199         return ERROR_OTHER;
200     }
201 
202     @Override
203     protected void implPeek(PeekTask task) throws Exception {
204         Request request = client.newRequest(resolve(task))
205                 .timeout(requestTimeout, TimeUnit.MILLISECONDS)
206                 .method("HEAD");
207         request.headers(m -> headers.forEach(m::add));
208         if (preemptiveAuth) {
209             if (basicServerAuthenticationResult != null) {
210                 basicServerAuthenticationResult.apply(request);
211             }
212             if (basicProxyAuthenticationResult != null) {
213                 basicProxyAuthenticationResult.apply(request);
214             }
215         }
216         Response response = request.send();
217         if (response.getStatus() >= MULTIPLE_CHOICES) {
218             throw new HttpTransporterException(response.getStatus());
219         }
220     }
221 
222     @Override
223     protected void implGet(GetTask task) throws Exception {
224         boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null;
225         Response response;
226         InputStreamResponseListener listener;
227 
228         while (true) {
229             Request request = client.newRequest(resolve(task))
230                     .timeout(requestTimeout, TimeUnit.MILLISECONDS)
231                     .method("GET");
232             request.headers(m -> headers.forEach(m::add));
233             if (preemptiveAuth) {
234                 if (basicServerAuthenticationResult != null) {
235                     basicServerAuthenticationResult.apply(request);
236                 }
237                 if (basicProxyAuthenticationResult != null) {
238                     basicProxyAuthenticationResult.apply(request);
239                 }
240             }
241 
242             if (resume) {
243                 long resumeOffset = task.getResumeOffset();
244                 request.headers(h -> {
245                     h.add(RANGE, "bytes=" + resumeOffset + '-');
246                     h.addDateField(IF_UNMODIFIED_SINCE, task.getDataFile().lastModified() - MODIFICATION_THRESHOLD);
247                     h.remove(HttpHeader.ACCEPT_ENCODING);
248                     h.add(ACCEPT_ENCODING, "identity");
249                 });
250             }
251 
252             listener = new InputStreamResponseListener();
253             request.send(listener);
254             try {
255                 response = listener.get(requestTimeout, TimeUnit.MILLISECONDS);
256             } catch (ExecutionException e) {
257                 Throwable t = e.getCause();
258                 if (t instanceof Exception) {
259                     throw (Exception) t;
260                 } else {
261                     throw new RuntimeException(t);
262                 }
263             }
264             if (response.getStatus() >= MULTIPLE_CHOICES) {
265                 if (resume && response.getStatus() == PRECONDITION_FAILED) {
266                     resume = false;
267                     continue;
268                 }
269                 throw new HttpTransporterException(response.getStatus());
270             }
271             break;
272         }
273 
274         long offset = 0L, length = response.getHeaders().getLongField(CONTENT_LENGTH);
275         if (resume) {
276             String range = response.getHeaders().get(CONTENT_RANGE);
277             if (range != null) {
278                 Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
279                 if (!m.matches()) {
280                     throw new IOException("Invalid Content-Range header for partial download: " + range);
281                 }
282                 offset = Long.parseLong(m.group(1));
283                 length = Long.parseLong(m.group(2)) + 1L;
284                 if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
285                     throw new IOException("Invalid Content-Range header for partial download from offset "
286                             + task.getResumeOffset() + ": " + range);
287                 }
288             }
289         }
290 
291         final boolean downloadResumed = offset > 0L;
292         final File dataFile = task.getDataFile();
293         if (dataFile == null) {
294             try (InputStream is = listener.getInputStream()) {
295                 utilGet(task, is, true, length, downloadResumed);
296             }
297         } else {
298             try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile.toPath())) {
299                 task.setDataFile(tempFile.getPath().toFile(), downloadResumed);
300                 if (downloadResumed && Files.isRegularFile(dataFile.toPath())) {
301                     try (InputStream inputStream = Files.newInputStream(dataFile.toPath())) {
302                         Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
303                     }
304                 }
305                 try (InputStream is = listener.getInputStream()) {
306                     utilGet(task, is, true, length, downloadResumed);
307                 }
308                 tempFile.move();
309             } finally {
310                 task.setDataFile(dataFile);
311             }
312         }
313         if (task.getDataFile() != null && response.getHeaders().getDateField(LAST_MODIFIED) != -1) {
314             long lastModified =
315                     response.getHeaders().getDateField(LAST_MODIFIED); // note: Wagon also does first not last
316             if (lastModified != -1) {
317                 try {
318                     Files.setLastModifiedTime(task.getDataFile().toPath(), FileTime.fromMillis(lastModified));
319                 } catch (DateTimeParseException e) {
320                     // fall through
321                 }
322             }
323         }
324         Map<String, String> checksums = extractXChecksums(response);
325         if (checksums != null) {
326             checksums.forEach(task::setChecksum);
327             return;
328         }
329         checksums = extractNexus2Checksums(response);
330         if (checksums != null) {
331             checksums.forEach(task::setChecksum);
332         }
333     }
334 
335     private static Map<String, String> extractXChecksums(Response response) {
336         String value;
337         HashMap<String, String> result = new HashMap<>();
338         // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b
339         value = response.getHeaders().get("x-checksum-sha1");
340         if (value != null) {
341             result.put("SHA-1", value);
342         }
343         // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6
344         value = response.getHeaders().get("x-checksum-md5");
345         if (value != null) {
346             result.put("MD5", value);
347         }
348         if (!result.isEmpty()) {
349             return result;
350         }
351         // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b
352         value = response.getHeaders().get("x-goog-meta-checksum-sha1");
353         if (value != null) {
354             result.put("SHA-1", value);
355         }
356         // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6
357         value = response.getHeaders().get("x-goog-meta-checksum-md5");
358         if (value != null) {
359             result.put("MD5", value);
360         }
361 
362         return result.isEmpty() ? null : result;
363     }
364 
365     private static Map<String, String> extractNexus2Checksums(Response response) {
366         // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}"
367         String etag = response.getHeaders().get("ETag");
368         if (etag != null) {
369             int start = etag.indexOf("SHA1{"), end = etag.indexOf("}", start + 5);
370             if (start >= 0 && end > start) {
371                 return Collections.singletonMap("SHA-1", etag.substring(start + 5, end));
372             }
373         }
374         return null;
375     }
376 
377     @Override
378     protected void implPut(PutTask task) throws Exception {
379         Request request = client.newRequest(resolve(task)).method("PUT").timeout(requestTimeout, TimeUnit.MILLISECONDS);
380         request.headers(m -> headers.forEach(m::add));
381         if (preemptiveAuth || preemptivePutAuth) {
382             if (basicServerAuthenticationResult != null) {
383                 basicServerAuthenticationResult.apply(request);
384             }
385             if (basicProxyAuthenticationResult != null) {
386                 basicProxyAuthenticationResult.apply(request);
387             }
388         }
389         request.body(new PutTaskRequestContent(task));
390         AtomicBoolean started = new AtomicBoolean(false);
391         Response response;
392         try {
393             response = request.onRequestCommit(r -> {
394                         if (task.getDataLength() == 0) {
395                             if (started.compareAndSet(false, true)) {
396                                 try {
397                                     task.getListener().transportStarted(0, task.getDataLength());
398                                 } catch (TransferCancelledException e) {
399                                     r.abort(e);
400                                 }
401                             }
402                         }
403                     })
404                     .onRequestContent((r, b) -> {
405                         if (started.compareAndSet(false, true)) {
406                             try {
407                                 task.getListener().transportStarted(0, task.getDataLength());
408                             } catch (TransferCancelledException e) {
409                                 r.abort(e);
410                                 return;
411                             }
412                         }
413                         try {
414                             task.getListener().transportProgressed(b);
415                         } catch (TransferCancelledException e) {
416                             r.abort(e);
417                         }
418                     })
419                     .send();
420         } catch (ExecutionException e) {
421             Throwable t = e.getCause();
422             if (t instanceof IOException) {
423                 IOException ioex = (IOException) t;
424                 if (ioex.getCause() instanceof TransferCancelledException) {
425                     throw (TransferCancelledException) ioex.getCause();
426                 } else {
427                     throw ioex;
428                 }
429             } else if (t instanceof Exception) {
430                 throw (Exception) t;
431             } else {
432                 throw new RuntimeException(t);
433             }
434         }
435         if (response.getStatus() >= MULTIPLE_CHOICES) {
436             throw new HttpTransporterException(response.getStatus());
437         }
438     }
439 
440     @Override
441     protected void implClose() {
442         // noop
443     }
444 
445     /**
446      * Visible for testing.
447      */
448     static final String JETTY_INSTANCE_KEY_PREFIX = JettyTransporterFactory.class.getName() + ".jetty.";
449 
450     static final Logger LOGGER = LoggerFactory.getLogger(JettyTransporter.class);
451 
452     @SuppressWarnings("checkstyle:methodlength")
453     private HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository)
454             throws NoTransporterException {
455 
456         final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId();
457 
458         final String httpsSecurityMode = ConfigUtils.getString(
459                 session,
460                 ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT,
461                 ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(),
462                 ConfigurationProperties.HTTPS_SECURITY_MODE);
463 
464         if (!ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT.equals(httpsSecurityMode)
465                 && !ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode)) {
466             throw new IllegalArgumentException("Unsupported '" + httpsSecurityMode + "' HTTPS security mode.");
467         }
468         final boolean insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode);
469 
470         try {
471             AtomicReference<BasicAuthentication.BasicResult> serverAuth = new AtomicReference<>(null);
472             AtomicReference<BasicAuthentication.BasicResult> proxyAuth = new AtomicReference<>(null);
473             HttpClient client = (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> {
474                 SSLContext sslContext = null;
475                 BasicAuthentication basicAuthentication = null;
476                 try {
477                     try (AuthenticationContext repoAuthContext =
478                             AuthenticationContext.forRepository(session, repository)) {
479                         if (repoAuthContext != null) {
480                             sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class);
481 
482                             String username = repoAuthContext.get(AuthenticationContext.USERNAME);
483                             String password = repoAuthContext.get(AuthenticationContext.PASSWORD);
484 
485                             URI uri = URI.create(repository.getUrl());
486                             basicAuthentication =
487                                     new BasicAuthentication(uri, Authentication.ANY_REALM, username, password);
488                             if (preemptiveAuth || preemptivePutAuth) {
489                                 serverAuth.set(new BasicAuthentication.BasicResult(
490                                         uri, HttpHeader.AUTHORIZATION, username, password));
491                             }
492                         }
493                     }
494 
495                     if (sslContext == null) {
496                         if (insecure) {
497                             sslContext = SSLContext.getInstance("TLS");
498                             X509TrustManager tm = new X509TrustManager() {
499                                 @Override
500                                 public void checkClientTrusted(X509Certificate[] chain, String authType) {}
501 
502                                 @Override
503                                 public void checkServerTrusted(X509Certificate[] chain, String authType) {}
504 
505                                 @Override
506                                 public X509Certificate[] getAcceptedIssuers() {
507                                     return new X509Certificate[0];
508                                 }
509                             };
510                             sslContext.init(null, new X509TrustManager[] {tm}, null);
511                         } else {
512                             sslContext = SSLContext.getDefault();
513                         }
514                     }
515 
516                     int connectTimeout = ConfigUtils.getInteger(
517                             session,
518                             ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
519                             ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(),
520                             ConfigurationProperties.CONNECT_TIMEOUT);
521 
522                     SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
523                     sslContextFactory.setSslContext(sslContext);
524                     if (insecure) {
525                         sslContextFactory.setEndpointIdentificationAlgorithm(null);
526                         sslContextFactory.setHostnameVerifier((name, context) -> true);
527                     }
528 
529                     ClientConnector clientConnector = new ClientConnector();
530                     clientConnector.setSslContextFactory(sslContextFactory);
531 
532                     HTTP2Client http2Client = new HTTP2Client(clientConnector);
533                     ClientConnectionFactoryOverHTTP2.HTTP2 http2 =
534                             new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
535 
536                     HttpClientTransportDynamic transport;
537                     if ("https".equalsIgnoreCase(repository.getProtocol())) {
538                         transport = new HttpClientTransportDynamic(
539                                 clientConnector, http2, HttpClientConnectionFactory.HTTP11); // HTTPS, prefer H2
540                     } else {
541                         transport = new HttpClientTransportDynamic(
542                                 clientConnector,
543                                 HttpClientConnectionFactory.HTTP11,
544                                 http2); // plaintext HTTP, H2 cannot be used
545                     }
546 
547                     HttpClient httpClient = new HttpClient(transport);
548                     httpClient.setConnectTimeout(connectTimeout);
549                     httpClient.setFollowRedirects(true);
550                     httpClient.setMaxRedirects(2);
551 
552                     httpClient.setUserAgentField(null); // we manage it
553 
554                     if (basicAuthentication != null) {
555                         httpClient.getAuthenticationStore().addAuthentication(basicAuthentication);
556                     }
557 
558                     if (repository.getProxy() != null) {
559                         HttpProxy proxy = new HttpProxy(
560                                 repository.getProxy().getHost(),
561                                 repository.getProxy().getPort());
562 
563                         httpClient.getProxyConfiguration().addProxy(proxy);
564                         try (AuthenticationContext proxyAuthContext =
565                                 AuthenticationContext.forProxy(session, repository)) {
566                             if (proxyAuthContext != null) {
567                                 String username = proxyAuthContext.get(AuthenticationContext.USERNAME);
568                                 String password = proxyAuthContext.get(AuthenticationContext.PASSWORD);
569 
570                                 BasicAuthentication proxyAuthentication = new BasicAuthentication(
571                                         proxy.getURI(), Authentication.ANY_REALM, username, password);
572 
573                                 httpClient.getAuthenticationStore().addAuthentication(proxyAuthentication);
574                                 if (preemptiveAuth || preemptivePutAuth) {
575                                     proxyAuth.set(new BasicAuthentication.BasicResult(
576                                             proxy.getURI(), HttpHeader.PROXY_AUTHORIZATION, username, password));
577                                 }
578                             }
579                         }
580                     }
581                     if (!session.addOnSessionEndedHandler(() -> {
582                         try {
583                             httpClient.stop();
584                         } catch (Exception e) {
585                             throw new RuntimeException(e);
586                         }
587                     })) {
588                         LOGGER.warn(
589                                 "Using Resolver 2 feature without Resolver 2 session handling, you may leak resources.");
590                     }
591                     httpClient.start();
592                     return httpClient;
593                 } catch (Exception e) {
594                     throw new WrapperEx(e);
595                 }
596             });
597             if (serverAuth.get() != null) {
598                 session.getData().set(instanceKey + ".serverAuth", serverAuth.get());
599             }
600             if (proxyAuth.get() != null) {
601                 session.getData().set(instanceKey + ".proxyAuth", proxyAuth.get());
602             }
603             return client;
604         } catch (WrapperEx e) {
605             throw new NoTransporterException(repository, e.getCause());
606         }
607     }
608 
609     private static final class WrapperEx extends RuntimeException {
610         private WrapperEx(Throwable cause) {
611             super(cause);
612         }
613     }
614 }