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