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