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