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
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.NoSuchAlgorithmException;
31 import java.util.ArrayList;
32 import java.util.Collection;
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.ConfigurationProperties.HttpVersion;
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.HttpTransportPropertiesBuilder;
54 import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
55 import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
56 import org.eclipse.aether.spi.io.PathProcessor;
57 import org.eclipse.aether.transfer.HttpTransportProperty;
58 import org.eclipse.aether.transfer.NoTransporterException;
59 import org.eclipse.aether.transfer.TransferCancelledException;
60 import org.eclipse.aether.transfer.TransferEvent;
61 import org.eclipse.aether.util.ConfigUtils;
62 import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils;
63 import org.eclipse.jetty.client.Authentication;
64 import org.eclipse.jetty.client.BasicAuthentication;
65 import org.eclipse.jetty.client.HttpClient;
66 import org.eclipse.jetty.client.HttpProxy;
67 import org.eclipse.jetty.client.InputStreamResponseListener;
68 import org.eclipse.jetty.client.Request;
69 import org.eclipse.jetty.client.Response;
70 import org.eclipse.jetty.client.transport.HttpClientConnectionFactory;
71 import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
72 import org.eclipse.jetty.http.HttpField;
73 import org.eclipse.jetty.http.HttpHeader;
74 import org.eclipse.jetty.http2.client.HTTP2Client;
75 import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2;
76 import org.eclipse.jetty.http3.client.HTTP3Client;
77 import org.eclipse.jetty.http3.client.HTTP3ClientQuicConfiguration;
78 import org.eclipse.jetty.http3.client.transport.ClientConnectionFactoryOverHTTP3;
79 import org.eclipse.jetty.io.ClientConnectionFactory;
80 import org.eclipse.jetty.io.ClientConnector;
81 import org.eclipse.jetty.io.EndPoint.SslSessionData;
82 import org.eclipse.jetty.quic.quiche.client.QuicheClientQuicConfiguration;
83 import org.eclipse.jetty.quic.quiche.client.QuicheTransport;
84 import org.eclipse.jetty.util.ssl.SslContextFactory;
85
86 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.ACCEPT_ENCODING;
87 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_LENGTH;
88 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE;
89 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE_PATTERN;
90 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.IF_UNMODIFIED_SINCE;
91 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.LAST_MODIFIED;
92 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.MULTIPLE_CHOICES;
93 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.PRECONDITION_FAILED;
94 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.RANGE;
95 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.USER_AGENT;
96
97
98
99
100
101
102 final class JettyTransporter extends AbstractTransporter implements HttpTransporter {
103 private static final long MODIFICATION_THRESHOLD = 60L * 1000L;
104
105 private final RepositorySystemSession session;
106
107 private final RemoteRepository repository;
108
109 private final ChecksumExtractor checksumExtractor;
110
111 private final PathProcessor pathProcessor;
112
113 private final URI baseUri;
114
115 private final HttpClient client;
116
117 private final int connectTimeout;
118
119 private final int requestTimeout;
120
121 private final Map<String, String> headers;
122
123 private final boolean preemptiveAuth;
124
125 private final boolean preemptivePutAuth;
126
127 private final boolean sendRfc9457Accept;
128
129 private final boolean insecure;
130
131 private final AtomicReference<BasicAuthentication.BasicResult> basicServerAuthenticationResult;
132
133 private final AtomicReference<BasicAuthentication.BasicResult> basicProxyAuthenticationResult;
134
135 JettyTransporter(
136 RepositorySystemSession session,
137 RemoteRepository repository,
138 ChecksumExtractor checksumExtractor,
139 PathProcessor pathProcessor)
140 throws NoTransporterException {
141 this.session = session;
142 this.repository = repository;
143 this.checksumExtractor = checksumExtractor;
144 this.pathProcessor = pathProcessor;
145 try {
146 this.baseUri = HttpTransporterUtils.getBaseUri(repository);
147 } catch (URISyntaxException e) {
148 throw new NoTransporterException(repository, e.getMessage(), e);
149 }
150
151 HashMap<String, String> headers = new HashMap<>();
152 String userAgent = HttpTransporterUtils.getUserAgent(session, repository);
153 if (userAgent != null) {
154 headers.put(USER_AGENT, userAgent);
155 }
156 Map<String, String> configuredHeaders = HttpTransporterUtils.getHttpHeaders(session, repository);
157 if (configuredHeaders != null) {
158 headers.putAll(configuredHeaders);
159 }
160
161 this.headers = headers;
162
163 this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository);
164 this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository);
165 this.preemptiveAuth = HttpTransporterUtils.isHttpPreemptiveAuth(session, repository);
166 this.preemptivePutAuth = HttpTransporterUtils.isHttpPreemptivePutAuth(session, repository);
167 this.sendRfc9457Accept = HttpTransporterUtils.isHttpSendRfc9457Accept(session, repository);
168 final String httpsSecurityMode = HttpTransporterUtils.getHttpsSecurityMode(session, repository);
169 this.insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode);
170
171 this.basicServerAuthenticationResult = new AtomicReference<>(null);
172 this.basicProxyAuthenticationResult = new AtomicReference<>(null);
173 this.client = createClient();
174 }
175
176 private void mayApplyPreemptiveAuth(Request request) {
177 if (basicServerAuthenticationResult.get() != null) {
178 basicServerAuthenticationResult.get().apply(request);
179 }
180 if (basicProxyAuthenticationResult.get() != null) {
181 basicProxyAuthenticationResult.get().apply(request);
182 }
183 }
184
185 private URI resolve(TransportTask task) {
186 return baseUri.resolve(task.getLocation());
187 }
188
189 @Override
190 protected void implPeek(PeekTask task) throws Exception {
191 Request request = client.newRequest(resolve(task)).method("HEAD");
192 request.headers(m -> headers.forEach(m::add));
193 if (preemptiveAuth) {
194 mayApplyPreemptiveAuth(request);
195 }
196
197 Map<String, HttpField> rawResponseHeaders = new HashMap<>();
198 request.onResponseHeader((r, field) -> {
199 rawResponseHeaders.put(field.getLowerCaseName(), field);
200 return true;
201 });
202 Response response = request.send();
203 Map<TransferEvent.TransportPropertyKey, Object> transportProperties =
204 createTransportProperties(request, rawResponseHeaders);
205 task.getListener().transportPropertiesAvailable(transportProperties);
206 if (response.getStatus() >= MULTIPLE_CHOICES) {
207 throw new HttpTransporterException(response.getStatus());
208 }
209 }
210
211 @Override
212 protected void implGet(GetTask task) throws Exception {
213 boolean resume = task.getResumeOffset() > 0L && task.getDataPath() != null;
214 Response response;
215 InputStreamResponseListener listener;
216
217 while (true) {
218 Request request = client.newRequest(resolve(task)).method("GET");
219 request.headers(m -> headers.forEach(m::add));
220 if (preemptiveAuth) {
221 mayApplyPreemptiveAuth(request);
222 }
223 if (sendRfc9457Accept) {
224 JettyRFC9457Reporter.INSTANCE.prepareRequest(request);
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
239 Map<String, HttpField> rawResponseHeaders = new HashMap<>();
240 request.onResponseHeader((r, field) -> {
241 rawResponseHeaders.put(field.getLowerCaseName(), field);
242 return true;
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 Map<TransferEvent.TransportPropertyKey, Object> transportProperties =
257 createTransportProperties(request, rawResponseHeaders);
258 task.getListener().transportPropertiesAvailable(transportProperties);
259 if (response.getStatus() >= MULTIPLE_CHOICES) {
260 if (resume && response.getStatus() == PRECONDITION_FAILED) {
261 resume = false;
262 continue;
263 }
264 JettyRFC9457Reporter.INSTANCE.generateException(listener, (statusCode, reasonPhrase) -> {
265 throw new HttpTransporterException(statusCode);
266 });
267 }
268 break;
269 }
270
271 long offset = 0L, length = response.getHeaders().getLongField(CONTENT_LENGTH);
272 if (resume) {
273 String range = response.getHeaders().get(CONTENT_RANGE);
274 if (range != null) {
275 Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
276 if (!m.matches()) {
277 throw new IOException("Invalid Content-Range header for partial download: " + range);
278 }
279 offset = Long.parseLong(m.group(1));
280 length = Long.parseLong(m.group(2)) + 1L;
281 if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
282 throw new IOException("Invalid Content-Range header for partial download from offset "
283 + task.getResumeOffset() + ": " + range);
284 }
285 }
286 }
287
288 final boolean downloadResumed = offset > 0L;
289 final Path dataFile = task.getDataPath();
290 if (dataFile == null) {
291 try (InputStream is = listener.getInputStream()) {
292 utilGet(task, is, true, length, downloadResumed);
293 }
294 } else {
295 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
296 task.setDataPath(tempFile.getPath(), downloadResumed);
297 if (downloadResumed && Files.isRegularFile(dataFile)) {
298 try (InputStream inputStream = Files.newInputStream(dataFile)) {
299 Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
300 }
301 }
302 try (InputStream is = listener.getInputStream()) {
303 utilGet(task, is, true, length, downloadResumed);
304 }
305 tempFile.move();
306 } finally {
307 task.setDataPath(dataFile);
308 }
309 }
310 if (task.getDataPath() != null && response.getHeaders().getDateField(LAST_MODIFIED) != -1) {
311 long lastModified =
312 response.getHeaders().getDateField(LAST_MODIFIED);
313 if (lastModified != -1) {
314 pathProcessor.setLastModified(task.getDataPath(), lastModified);
315 }
316 }
317 Map<String, String> checksums = checksumExtractor.extractChecksums(headerGetter(response));
318 if (checksums != null && !checksums.isEmpty()) {
319 checksums.forEach(task::setChecksum);
320 }
321 }
322
323 private Map<TransferEvent.TransportPropertyKey, Object> createTransportProperties(
324 Request request, Map<String, HttpField> rawResponseHeaders) {
325 HttpTransportPropertiesBuilder builder =
326 new HttpTransportPropertiesBuilder(toHttpVersion(request.getVersion()));
327 SslSessionData sslSessionData = request.getConnection().getSslSessionData();
328 if (sslSessionData != null && sslSessionData.sslSession() != null) {
329 builder.withSslProtocol(sslSessionData.sslSession().getProtocol());
330 builder.withSslCipherSuite(sslSessionData.sslSession().getCipherSuite());
331 }
332 if (rawResponseHeaders.containsKey("content-encoding")) {
333 builder.withContentCoding(rawResponseHeaders.get("content-encoding").getValue());
334 }
335 return builder.build();
336 }
337
338 static HttpTransportProperty.HttpVersion toHttpVersion(org.eclipse.jetty.http.HttpVersion version) {
339 switch (version) {
340 case HTTP_1_0:
341 return HttpTransportProperty.HttpVersion.HTTP_1_0;
342 case HTTP_1_1:
343 return HttpTransportProperty.HttpVersion.HTTP_1_1;
344 case HTTP_2:
345 return HttpTransportProperty.HttpVersion.HTTP_2;
346 case HTTP_3:
347 return HttpTransportProperty.HttpVersion.HTTP_3;
348 default:
349 throw new IllegalArgumentException("Unknown version " + version.toString());
350 }
351 }
352
353 private static Function<String, String> headerGetter(Response response) {
354 return s -> response.getHeaders().get(s);
355 }
356
357 @Override
358 protected void implPut(PutTask task) throws Exception {
359 Request request = client.newRequest(resolve(task)).method("PUT");
360 request.headers(m -> headers.forEach(m::add));
361 if (sendRfc9457Accept) {
362 JettyRFC9457Reporter.INSTANCE.prepareRequest(request);
363 }
364 if (preemptiveAuth || preemptivePutAuth) {
365 mayApplyPreemptiveAuth(request);
366 }
367 request.body(PutTaskRequestContent.from(task));
368
369 Map<String, HttpField> rawResponseHeaders = new HashMap<>();
370 request.onResponseHeader((r, field) -> {
371 rawResponseHeaders.put(field.getLowerCaseName(), field);
372 return true;
373 });
374 AtomicBoolean started = new AtomicBoolean(false);
375 Response response;
376 try (InputStreamResponseListener listener = new InputStreamResponseListener()) {
377 request.onRequestCommit(r -> {
378 if (task.getDataLength() == 0) {
379 if (started.compareAndSet(false, true)) {
380 try {
381 task.getListener().transportStarted(0, task.getDataLength());
382 } catch (TransferCancelledException e) {
383 r.abort(e);
384 }
385 }
386 }
387 })
388 .onRequestContent((r, b) -> {
389 if (started.compareAndSet(false, true)) {
390 try {
391 task.getListener().transportStarted(0, task.getDataLength());
392 } catch (TransferCancelledException e) {
393 r.abort(e);
394 return;
395 }
396 }
397 try {
398 task.getListener().transportProgressed(b);
399 } catch (TransferCancelledException e) {
400 r.abort(e);
401 }
402 })
403 .send(listener);
404 response = listener.get(requestTimeout, TimeUnit.MILLISECONDS);
405 task.getListener().transportPropertiesAvailable(createTransportProperties(request, rawResponseHeaders));
406 if (response.getStatus() >= MULTIPLE_CHOICES) {
407 JettyRFC9457Reporter.INSTANCE.generateException(listener, (statusCode, reasonPhrase) -> {
408 throw new HttpTransporterException(statusCode);
409 });
410 }
411 } catch (ExecutionException e) {
412 Throwable t = e.getCause();
413 if (t instanceof IOException ioex) {
414 if (ioex.getCause() instanceof TransferCancelledException) {
415 throw (TransferCancelledException) ioex.getCause();
416 } else {
417 throw ioex;
418 }
419 } else if (t instanceof Exception) {
420 throw (Exception) t;
421 } else {
422 throw new RuntimeException(t);
423 }
424 }
425 }
426
427 @Override
428 protected void implClose() {
429 try {
430 this.client.stop();
431 } catch (Exception e) {
432 throw new RuntimeException(e);
433 }
434 }
435
436 @SuppressWarnings("checkstyle:methodlength")
437 private HttpClient createClient() throws RuntimeException {
438 BasicAuthentication.BasicResult serverAuth = null;
439 BasicAuthentication.BasicResult proxyAuth = null;
440 SSLContext sslContext = null;
441 BasicAuthentication basicAuthentication = null;
442 try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) {
443 if (repoAuthContext != null) {
444 sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class);
445
446 String username = repoAuthContext.get(AuthenticationContext.USERNAME);
447 String password = repoAuthContext.get(AuthenticationContext.PASSWORD);
448
449 URI uri = URI.create(repository.getUrl());
450 basicAuthentication = new BasicAuthentication(uri, Authentication.ANY_REALM, username, password);
451 if (preemptiveAuth || preemptivePutAuth) {
452 serverAuth = new BasicAuthentication.BasicResult(uri, HttpHeader.AUTHORIZATION, username, password);
453 }
454 }
455 }
456
457 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
458 if (insecure) {
459
460 sslContextFactory.setEndpointIdentificationAlgorithm(null);
461 sslContextFactory.setHostnameVerifier((name, context) -> true);
462 sslContextFactory.setTrustAll(true);
463 } else {
464 try {
465 if (sslContext == null) {
466
467
468 sslContext = SSLContext.getDefault();
469 }
470 } catch (NoSuchAlgorithmException e) {
471 throw new IllegalStateException("SSL Context setup failure", e);
472 }
473 }
474
475 if (sslContext != null) {
476
477 sslContextFactory.setSslContext(sslContext);
478 }
479
480 ClientConnector clientConnector = new ClientConnector();
481 clientConnector.setSslContextFactory(sslContextFactory);
482
483 Collection<ClientConnectionFactory.Info> connectors = new ArrayList<>();
484 if ("https".equalsIgnoreCase(repository.getProtocol())) {
485 HttpVersion httpVersion = HttpTransporterUtils.getHttpVersion(session, repository);
486 switch (httpVersion) {
487 case MAXIMUM:
488 case HTTP_3:
489 QuicheClientQuicConfiguration clientQuicConfig =
490 HTTP3ClientQuicConfiguration.configure(new QuicheClientQuicConfiguration());
491 HTTP3Client http3Client = new HTTP3Client(clientQuicConfig, clientConnector);
492 QuicheTransport transport = new QuicheTransport(clientQuicConfig);
493 ClientConnectionFactoryOverHTTP3.HTTP3 http3 =
494 new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client, transport);
495 connectors.add(http3);
496 break;
497 case HTTP_2:
498 case DEFAULT:
499 HTTP2Client http2Client = new HTTP2Client(clientConnector);
500 ClientConnectionFactoryOverHTTP2.HTTP2 http2 =
501 new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
502 connectors.add(http2);
503 break;
504 default:
505 break;
506 }
507 }
508 connectors.add(HttpClientConnectionFactory.HTTP11);
509
510 HttpClientTransportDynamic dynamicTransport = new HttpClientTransportDynamic(
511 clientConnector, connectors.toArray(new ClientConnectionFactory.Info[0]));
512 HttpClient httpClient = new HttpClient(dynamicTransport);
513 httpClient.setConnectTimeout(connectTimeout);
514 httpClient.setIdleTimeout(requestTimeout);
515 httpClient.setFollowRedirects(ConfigUtils.getBoolean(
516 session,
517 JettyTransporterConfigurationKeys.DEFAULT_FOLLOW_REDIRECTS,
518 JettyTransporterConfigurationKeys.CONFIG_PROP_FOLLOW_REDIRECTS));
519 httpClient.setMaxRedirects(ConfigUtils.getInteger(
520 session,
521 JettyTransporterConfigurationKeys.DEFAULT_MAX_REDIRECTS,
522 JettyTransporterConfigurationKeys.CONFIG_PROP_MAX_REDIRECTS));
523
524 httpClient.setUserAgentField(null);
525
526 if (basicAuthentication != null) {
527 httpClient.getAuthenticationStore().addAuthentication(basicAuthentication);
528 }
529
530 if (repository.getProxy() != null) {
531 HttpProxy proxy = new HttpProxy(
532 repository.getProxy().getHost(), repository.getProxy().getPort());
533
534 httpClient.getProxyConfiguration().addProxy(proxy);
535 try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) {
536 if (proxyAuthContext != null) {
537 String username = proxyAuthContext.get(AuthenticationContext.USERNAME);
538 String password = proxyAuthContext.get(AuthenticationContext.PASSWORD);
539
540 BasicAuthentication proxyAuthentication =
541 new BasicAuthentication(proxy.getURI(), Authentication.ANY_REALM, username, password);
542
543 httpClient.getAuthenticationStore().addAuthentication(proxyAuthentication);
544 if (preemptiveAuth || preemptivePutAuth) {
545 proxyAuth = new BasicAuthentication.BasicResult(
546 proxy.getURI(), HttpHeader.PROXY_AUTHORIZATION, username, password);
547 }
548 }
549 }
550 }
551 if (serverAuth != null) {
552 this.basicServerAuthenticationResult.set(serverAuth);
553 }
554 if (proxyAuth != null) {
555 this.basicProxyAuthenticationResult.set(proxyAuth);
556 }
557
558 try {
559 httpClient.start();
560 return httpClient;
561 } catch (Exception e) {
562 if (e instanceof RuntimeException) {
563 throw (RuntimeException) e;
564 } else {
565 throw new IllegalStateException("Jetty client start failure", e);
566 }
567 }
568 }
569 }