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