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