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