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