1 package org.eclipse.aether.transport.http;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import org.apache.http.Header;
23 import org.apache.http.HttpEntity;
24 import org.apache.http.HttpEntityEnclosingRequest;
25 import org.apache.http.HttpHeaders;
26 import org.apache.http.HttpHost;
27 import org.apache.http.HttpResponse;
28 import org.apache.http.HttpStatus;
29 import org.apache.http.auth.AuthSchemeProvider;
30 import org.apache.http.auth.AuthScope;
31 import org.apache.http.client.CredentialsProvider;
32 import org.apache.http.client.HttpResponseException;
33 import org.apache.http.client.config.AuthSchemes;
34 import org.apache.http.client.config.RequestConfig;
35 import org.apache.http.client.methods.HttpGet;
36 import org.apache.http.client.methods.HttpHead;
37 import org.apache.http.client.methods.HttpOptions;
38 import org.apache.http.client.methods.HttpPut;
39 import org.apache.http.client.methods.HttpUriRequest;
40 import org.apache.http.client.utils.DateUtils;
41 import org.apache.http.client.utils.URIUtils;
42 import org.apache.http.config.Registry;
43 import org.apache.http.config.RegistryBuilder;
44 import org.apache.http.config.SocketConfig;
45 import org.apache.http.entity.AbstractHttpEntity;
46 import org.apache.http.entity.ByteArrayEntity;
47 import org.apache.http.impl.auth.BasicSchemeFactory;
48 import org.apache.http.impl.auth.DigestSchemeFactory;
49 import org.apache.http.impl.auth.KerberosSchemeFactory;
50 import org.apache.http.impl.auth.NTLMSchemeFactory;
51 import org.apache.http.impl.auth.SPNegoSchemeFactory;
52 import org.apache.http.impl.client.CloseableHttpClient;
53 import org.apache.http.impl.client.HttpClientBuilder;
54 import org.apache.http.util.EntityUtils;
55 import org.eclipse.aether.ConfigurationProperties;
56 import org.eclipse.aether.RepositorySystemSession;
57 import org.eclipse.aether.repository.AuthenticationContext;
58 import org.eclipse.aether.repository.Proxy;
59 import org.eclipse.aether.repository.RemoteRepository;
60 import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
61 import org.eclipse.aether.spi.connector.transport.GetTask;
62 import org.eclipse.aether.spi.connector.transport.PeekTask;
63 import org.eclipse.aether.spi.connector.transport.PutTask;
64 import org.eclipse.aether.spi.connector.transport.TransportTask;
65 import org.eclipse.aether.transfer.NoTransporterException;
66 import org.eclipse.aether.transfer.TransferCancelledException;
67 import org.eclipse.aether.util.ConfigUtils;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 import java.io.IOException;
72 import java.io.InputStream;
73 import java.io.InterruptedIOException;
74 import java.io.OutputStream;
75 import java.io.UncheckedIOException;
76 import java.net.URI;
77 import java.net.URISyntaxException;
78 import java.nio.charset.Charset;
79 import java.util.Collections;
80 import java.util.Date;
81 import java.util.List;
82 import java.util.Map;
83 import java.util.regex.Matcher;
84 import java.util.regex.Pattern;
85
86
87
88
89 final class HttpTransporter
90 extends AbstractTransporter
91 {
92
93 private static final Pattern CONTENT_RANGE_PATTERN =
94 Pattern.compile( "\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*" );
95
96 private static final Logger LOGGER = LoggerFactory.getLogger( HttpTransporter.class );
97
98 private final AuthenticationContext repoAuthContext;
99
100 private final AuthenticationContext proxyAuthContext;
101
102 private final URI baseUri;
103
104 private final HttpHost server;
105
106 private final HttpHost proxy;
107
108 private final CloseableHttpClient client;
109
110 private final Map<?, ?> headers;
111
112 private final LocalState state;
113
114 HttpTransporter( RemoteRepository repository, RepositorySystemSession session )
115 throws NoTransporterException
116 {
117 if ( !"http".equalsIgnoreCase( repository.getProtocol() )
118 && !"https".equalsIgnoreCase( repository.getProtocol() ) )
119 {
120 throw new NoTransporterException( repository );
121 }
122 try
123 {
124 this.baseUri = new URI( repository.getUrl() ).parseServerAuthority();
125 if ( baseUri.isOpaque() )
126 {
127 throw new URISyntaxException( repository.getUrl(), "URL must not be opaque" );
128 }
129 this.server = URIUtils.extractHost( baseUri );
130 if ( server == null )
131 {
132 throw new URISyntaxException( repository.getUrl(), "URL lacks host name" );
133 }
134 }
135 catch ( URISyntaxException e )
136 {
137 throw new NoTransporterException( repository, e.getMessage(), e );
138 }
139 this.proxy = toHost( repository.getProxy() );
140
141 this.repoAuthContext = AuthenticationContext.forRepository( session, repository );
142 this.proxyAuthContext = AuthenticationContext.forProxy( session, repository );
143
144 this.state = new LocalState( session, repository, new SslConfig( session, repoAuthContext ) );
145
146 this.headers = ConfigUtils.getMap( session, Collections.emptyMap(),
147 ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
148 ConfigurationProperties.HTTP_HEADERS );
149
150 String credentialEncoding = ConfigUtils.getString( session,
151 ConfigurationProperties.DEFAULT_HTTP_CREDENTIAL_ENCODING,
152 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING + "." + repository.getId(),
153 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING );
154 int connectTimeout = ConfigUtils.getInteger( session,
155 ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
156 ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(),
157 ConfigurationProperties.CONNECT_TIMEOUT );
158 int requestTimeout = ConfigUtils.getInteger( session,
159 ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
160 ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(),
161 ConfigurationProperties.REQUEST_TIMEOUT );
162 String userAgent = ConfigUtils.getString( session,
163 ConfigurationProperties.DEFAULT_USER_AGENT,
164 ConfigurationProperties.USER_AGENT );
165
166 Charset credentialsCharset = Charset.forName( credentialEncoding );
167
168 Registry<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider>create()
169 .register( AuthSchemes.BASIC, new BasicSchemeFactory( credentialsCharset ) )
170 .register( AuthSchemes.DIGEST, new DigestSchemeFactory( credentialsCharset ) )
171 .register( AuthSchemes.NTLM, new NTLMSchemeFactory() )
172 .register( AuthSchemes.SPNEGO, new SPNegoSchemeFactory() )
173 .register( AuthSchemes.KERBEROS, new KerberosSchemeFactory() )
174 .build();
175
176 SocketConfig socketConfig = SocketConfig.custom()
177 .setSoTimeout( requestTimeout ).build();
178
179 RequestConfig requestConfig = RequestConfig.custom()
180 .setConnectTimeout( connectTimeout )
181 .setConnectionRequestTimeout( connectTimeout )
182 .setSocketTimeout( requestTimeout ).build();
183
184 this.client = HttpClientBuilder.create()
185 .setUserAgent( userAgent )
186 .setDefaultSocketConfig( socketConfig )
187 .setDefaultRequestConfig( requestConfig )
188 .setDefaultAuthSchemeRegistry( authSchemeRegistry )
189 .setConnectionManager( state.getConnectionManager() )
190 .setConnectionManagerShared( true )
191 .setDefaultCredentialsProvider(
192 toCredentialsProvider( server, repoAuthContext, proxy, proxyAuthContext )
193 )
194 .setProxy( proxy )
195 .build();
196 }
197
198 private static HttpHost toHost( Proxy proxy )
199 {
200 HttpHost host = null;
201 if ( proxy != null )
202 {
203 host = new HttpHost( proxy.getHost(), proxy.getPort() );
204 }
205 return host;
206 }
207
208 private static CredentialsProvider toCredentialsProvider( HttpHost server, AuthenticationContext serverAuthCtx,
209 HttpHost proxy, AuthenticationContext proxyAuthCtx )
210 {
211 CredentialsProvider provider = toCredentialsProvider( server.getHostName(), AuthScope.ANY_PORT, serverAuthCtx );
212 if ( proxy != null )
213 {
214 CredentialsProvider p = toCredentialsProvider( proxy.getHostName(), proxy.getPort(), proxyAuthCtx );
215 provider = new DemuxCredentialsProvider( provider, p, proxy );
216 }
217 return provider;
218 }
219
220 private static CredentialsProvider toCredentialsProvider( String host, int port, AuthenticationContext ctx )
221 {
222 DeferredCredentialsProvider provider = new DeferredCredentialsProvider();
223 if ( ctx != null )
224 {
225 AuthScope basicScope = new AuthScope( host, port );
226 provider.setCredentials( basicScope, new DeferredCredentialsProvider.BasicFactory( ctx ) );
227
228 AuthScope ntlmScope = new AuthScope( host, port, AuthScope.ANY_REALM, "ntlm" );
229 provider.setCredentials( ntlmScope, new DeferredCredentialsProvider.NtlmFactory( ctx ) );
230 }
231 return provider;
232 }
233
234 LocalState getState()
235 {
236 return state;
237 }
238
239 private URI resolve( TransportTask task )
240 {
241 return UriUtils.resolve( baseUri, task.getLocation() );
242 }
243
244 @Override
245 public int classify( Throwable error )
246 {
247 if ( error instanceof HttpResponseException
248 && ( (HttpResponseException) error ).getStatusCode() == HttpStatus.SC_NOT_FOUND )
249 {
250 return ERROR_NOT_FOUND;
251 }
252 return ERROR_OTHER;
253 }
254
255 @Override
256 protected void implPeek( PeekTask task )
257 throws Exception
258 {
259 HttpHead request = commonHeaders( new HttpHead( resolve( task ) ) );
260 execute( request, null );
261 }
262
263 @Override
264 protected void implGet( GetTask task )
265 throws Exception
266 {
267 EntityGetter getter = new EntityGetter( task );
268 HttpGet request = commonHeaders( new HttpGet( resolve( task ) ) );
269 resume( request, task );
270 try
271 {
272 execute( request, getter );
273 }
274 catch ( HttpResponseException e )
275 {
276 if ( e.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED && request.containsHeader( HttpHeaders.RANGE ) )
277 {
278 request = commonHeaders( new HttpGet( request.getURI() ) );
279 execute( request, getter );
280 return;
281 }
282 throw e;
283 }
284 }
285
286 @Override
287 protected void implPut( PutTask task )
288 throws Exception
289 {
290 PutTaskEntity entity = new PutTaskEntity( task );
291 HttpPut request = commonHeaders( entity( new HttpPut( resolve( task ) ), entity ) );
292 try
293 {
294 execute( request, null );
295 }
296 catch ( HttpResponseException e )
297 {
298 if ( e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED && request.containsHeader( HttpHeaders.EXPECT ) )
299 {
300 state.setExpectContinue( false );
301 request = commonHeaders( entity( new HttpPut( request.getURI() ), entity ) );
302 execute( request, null );
303 return;
304 }
305 throw e;
306 }
307 }
308
309 private void execute( HttpUriRequest request, EntityGetter getter )
310 throws Exception
311 {
312 try
313 {
314 SharingHttpContext context = new SharingHttpContext( state );
315 prepare( request, context );
316 HttpResponse response = client.execute( server, request, context );
317 try
318 {
319 context.close();
320 handleStatus( response );
321 if ( getter != null )
322 {
323 getter.handle( response );
324 }
325 }
326 finally
327 {
328 EntityUtils.consumeQuietly( response.getEntity() );
329 }
330 }
331 catch ( IOException e )
332 {
333 if ( e.getCause() instanceof TransferCancelledException )
334 {
335 throw (Exception) e.getCause();
336 }
337 throw e;
338 }
339 }
340
341 private void prepare( HttpUriRequest request, SharingHttpContext context )
342 {
343 boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase( request.getMethod() );
344 if ( state.getWebDav() == null && ( put || isPayloadPresent( request ) ) )
345 {
346 try
347 {
348 HttpOptions req = commonHeaders( new HttpOptions( request.getURI() ) );
349 HttpResponse response = client.execute( server, req, context );
350 state.setWebDav( isWebDav( response ) );
351 EntityUtils.consumeQuietly( response.getEntity() );
352 }
353 catch ( IOException e )
354 {
355 LOGGER.debug( "Failed to prepare HTTP context", e );
356 }
357 }
358 if ( put && Boolean.TRUE.equals( state.getWebDav() ) )
359 {
360 mkdirs( request.getURI(), context );
361 }
362 }
363
364 private boolean isWebDav( HttpResponse response )
365 {
366 return response.containsHeader( HttpHeaders.DAV );
367 }
368
369 @SuppressWarnings( "checkstyle:magicnumber" )
370 private void mkdirs( URI uri, SharingHttpContext context )
371 {
372 List<URI> dirs = UriUtils.getDirectories( baseUri, uri );
373 int index = 0;
374 for ( ; index < dirs.size(); index++ )
375 {
376 try
377 {
378 HttpResponse response =
379 client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
380 try
381 {
382 int status = response.getStatusLine().getStatusCode();
383 if ( status < 300 || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
384 {
385 break;
386 }
387 else if ( status == HttpStatus.SC_CONFLICT )
388 {
389 continue;
390 }
391 handleStatus( response );
392 }
393 finally
394 {
395 EntityUtils.consumeQuietly( response.getEntity() );
396 }
397 }
398 catch ( IOException e )
399 {
400 LOGGER.debug( "Failed to create parent directory {}", dirs.get( index ), e );
401 return;
402 }
403 }
404 for ( index--; index >= 0; index-- )
405 {
406 try
407 {
408 HttpResponse response =
409 client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
410 try
411 {
412 handleStatus( response );
413 }
414 finally
415 {
416 EntityUtils.consumeQuietly( response.getEntity() );
417 }
418 }
419 catch ( IOException e )
420 {
421 LOGGER.debug( "Failed to create parent directory {}", dirs.get( index ), e );
422 return;
423 }
424 }
425 }
426
427 private <T extends HttpEntityEnclosingRequest> T entity( T request, HttpEntity entity )
428 {
429 request.setEntity( entity );
430 return request;
431 }
432
433 private boolean isPayloadPresent( HttpUriRequest request )
434 {
435 if ( request instanceof HttpEntityEnclosingRequest )
436 {
437 HttpEntity entity = ( (HttpEntityEnclosingRequest) request ).getEntity();
438 return entity != null && entity.getContentLength() != 0;
439 }
440 return false;
441 }
442
443 private <T extends HttpUriRequest> T commonHeaders( T request )
444 {
445 request.setHeader( HttpHeaders.CACHE_CONTROL, "no-cache, no-store" );
446 request.setHeader( HttpHeaders.PRAGMA, "no-cache" );
447
448 if ( state.isExpectContinue() && isPayloadPresent( request ) )
449 {
450 request.setHeader( HttpHeaders.EXPECT, "100-continue" );
451 }
452
453 for ( Map.Entry<?, ?> entry : headers.entrySet() )
454 {
455 if ( !( entry.getKey() instanceof String ) )
456 {
457 continue;
458 }
459 if ( entry.getValue() instanceof String )
460 {
461 request.setHeader( entry.getKey().toString(), entry.getValue().toString() );
462 }
463 else
464 {
465 request.removeHeaders( entry.getKey().toString() );
466 }
467 }
468
469 if ( !state.isExpectContinue() )
470 {
471 request.removeHeaders( HttpHeaders.EXPECT );
472 }
473
474 return request;
475 }
476
477 @SuppressWarnings( "checkstyle:magicnumber" )
478 private <T extends HttpUriRequest> T resume( T request, GetTask task )
479 {
480 long resumeOffset = task.getResumeOffset();
481 if ( resumeOffset > 0L && task.getDataFile() != null )
482 {
483 request.setHeader( HttpHeaders.RANGE, "bytes=" + resumeOffset + '-' );
484 request.setHeader( HttpHeaders.IF_UNMODIFIED_SINCE,
485 DateUtils.formatDate( new Date( task.getDataFile().lastModified() - 60L * 1000L ) ) );
486 request.setHeader( HttpHeaders.ACCEPT_ENCODING, "identity" );
487 }
488 return request;
489 }
490
491 @SuppressWarnings( "checkstyle:magicnumber" )
492 private void handleStatus( HttpResponse response )
493 throws HttpResponseException
494 {
495 int status = response.getStatusLine().getStatusCode();
496 if ( status >= 300 )
497 {
498 throw new HttpResponseException( status, response.getStatusLine().getReasonPhrase() + " (" + status + ")" );
499 }
500 }
501
502 @Override
503 protected void implClose()
504 {
505 try
506 {
507 client.close();
508 }
509 catch ( IOException e )
510 {
511 throw new UncheckedIOException( e );
512 }
513 AuthenticationContext.close( repoAuthContext );
514 AuthenticationContext.close( proxyAuthContext );
515 state.close();
516 }
517
518 private class EntityGetter
519 {
520
521 private final GetTask task;
522
523 EntityGetter( GetTask task )
524 {
525 this.task = task;
526 }
527
528 public void handle( HttpResponse response )
529 throws IOException, TransferCancelledException
530 {
531 HttpEntity entity = response.getEntity();
532 if ( entity == null )
533 {
534 entity = new ByteArrayEntity( new byte[0] );
535 }
536
537 long offset = 0L, length = entity.getContentLength();
538 String range = getHeader( response, HttpHeaders.CONTENT_RANGE );
539 if ( range != null )
540 {
541 Matcher m = CONTENT_RANGE_PATTERN.matcher( range );
542 if ( !m.matches() )
543 {
544 throw new IOException( "Invalid Content-Range header for partial download: " + range );
545 }
546 offset = Long.parseLong( m.group( 1 ) );
547 length = Long.parseLong( m.group( 2 ) ) + 1L;
548 if ( offset < 0L || offset >= length || ( offset > 0L && offset != task.getResumeOffset() ) )
549 {
550 throw new IOException( "Invalid Content-Range header for partial download from offset "
551 + task.getResumeOffset() + ": " + range );
552 }
553 }
554
555 InputStream is = entity.getContent();
556 utilGet( task, is, true, length, offset > 0L );
557 extractChecksums( response );
558 }
559
560 private void extractChecksums( HttpResponse response )
561 {
562
563 String etag = getHeader( response, HttpHeaders.ETAG );
564 if ( etag != null )
565 {
566 int start = etag.indexOf( "SHA1{" ), end = etag.indexOf( "}", start + 5 );
567 if ( start >= 0 && end > start )
568 {
569 task.setChecksum( "SHA-1", etag.substring( start + 5, end ) );
570 }
571 }
572 }
573
574 private String getHeader( HttpResponse response, String name )
575 {
576 Header header = response.getFirstHeader( name );
577 return ( header != null ) ? header.getValue() : null;
578 }
579
580 }
581
582 private class PutTaskEntity
583 extends AbstractHttpEntity
584 {
585
586 private final PutTask task;
587
588 PutTaskEntity( PutTask task )
589 {
590 this.task = task;
591 }
592
593 @Override
594 public boolean isRepeatable()
595 {
596 return true;
597 }
598
599 @Override
600 public boolean isStreaming()
601 {
602 return false;
603 }
604
605 @Override
606 public long getContentLength()
607 {
608 return task.getDataLength();
609 }
610
611 @Override
612 public InputStream getContent()
613 throws IOException
614 {
615 return task.newInputStream();
616 }
617
618 @Override
619 public void writeTo( OutputStream os )
620 throws IOException
621 {
622 try
623 {
624 utilPut( task, os, false );
625 }
626 catch ( TransferCancelledException e )
627 {
628 throw (IOException) new InterruptedIOException().initCause( e );
629 }
630 }
631
632 }
633
634 }