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.URIUtils;
53  import org.apache.http.conn.params.ConnRouteParams;
54  import org.apache.http.entity.AbstractHttpEntity;
55  import org.apache.http.entity.ByteArrayEntity;
56  import org.apache.http.impl.client.DecompressingHttpClient;
57  import org.apache.http.impl.client.DefaultHttpClient;
58  import org.apache.http.impl.cookie.DateUtils;
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.spi.log.Logger;
74  import org.eclipse.aether.transfer.NoTransporterException;
75  import org.eclipse.aether.transfer.TransferCancelledException;
76  import org.eclipse.aether.util.ConfigUtils;
77  
78  /**
79   * A transporter for HTTP/HTTPS.
80   */
81  final class HttpTransporter
82      extends AbstractTransporter
83  {
84  
85      private static final Pattern CONTENT_RANGE_PATTERN =
86          Pattern.compile( "\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*" );
87  
88      private final Logger logger;
89  
90      private final AuthenticationContext repoAuthContext;
91  
92      private final AuthenticationContext proxyAuthContext;
93  
94      private final URI baseUri;
95  
96      private final HttpHost server;
97  
98      private final HttpHost proxy;
99  
100     private final HttpClient client;
101 
102     private final Map<?, ?> headers;
103 
104     private final LocalState state;
105 
106     public HttpTransporter( RemoteRepository repository, RepositorySystemSession session, Logger logger )
107         throws NoTransporterException
108     {
109         if ( !"http".equalsIgnoreCase( repository.getProtocol() )
110             && !"https".equalsIgnoreCase( repository.getProtocol() ) )
111         {
112             throw new NoTransporterException( repository );
113         }
114         this.logger = logger;
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,
166                                          ConfigUtils.getString( session,
167                                                                 ConfigurationProperties.DEFAULT_HTTP_CREDENTIAL_ENCODING,
168                                                                 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING + "."
169                                                                     + repository.getId(),
170                                                                 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING ) );
171         ConnRouteParams.setDefaultProxy( params, proxy );
172         HttpConnectionParams.setConnectionTimeout( params,
173                                                    ConfigUtils.getInteger( session,
174                                                                            ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
175                                                                            ConfigurationProperties.CONNECT_TIMEOUT
176                                                                                + "." + repository.getId(),
177                                                                            ConfigurationProperties.CONNECT_TIMEOUT ) );
178         HttpConnectionParams.setSoTimeout( params,
179                                            ConfigUtils.getInteger( session,
180                                                                    ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
181                                                                    ConfigurationProperties.REQUEST_TIMEOUT + "."
182                                                                        + repository.getId(),
183                                                                    ConfigurationProperties.REQUEST_TIMEOUT ) );
184         HttpProtocolParams.setUserAgent( params, ConfigUtils.getString( session,
185                                                                         ConfigurationProperties.DEFAULT_USER_AGENT,
186                                                                         ConfigurationProperties.USER_AGENT ) );
187     }
188 
189     private static CredentialsProvider toCredentialsProvider( HttpHost server, AuthenticationContext serverAuthCtx,
190                                                               HttpHost proxy, AuthenticationContext proxyAuthCtx )
191     {
192         CredentialsProvider provider = toCredentialsProvider( server.getHostName(), AuthScope.ANY_PORT, serverAuthCtx );
193         if ( proxy != null )
194         {
195             CredentialsProvider p = toCredentialsProvider( proxy.getHostName(), proxy.getPort(), proxyAuthCtx );
196             provider = new DemuxCredentialsProvider( provider, p, proxy );
197         }
198         return provider;
199     }
200 
201     private static CredentialsProvider toCredentialsProvider( String host, int port, AuthenticationContext ctx )
202     {
203         DeferredCredentialsProvider provider = new DeferredCredentialsProvider();
204         if ( ctx != null )
205         {
206             AuthScope basicScope = new AuthScope( host, port );
207             provider.setCredentials( basicScope, new DeferredCredentialsProvider.BasicFactory( ctx ) );
208 
209             AuthScope ntlmScope = new AuthScope( host, port, AuthScope.ANY_REALM, "ntlm" );
210             provider.setCredentials( ntlmScope, new DeferredCredentialsProvider.NtlmFactory( ctx ) );
211         }
212         return provider;
213     }
214 
215     LocalState getState()
216     {
217         return state;
218     }
219 
220     private URI resolve( TransportTask task )
221     {
222         return UriUtils.resolve( baseUri, task.getLocation() );
223     }
224 
225     public int classify( Throwable error )
226     {
227         if ( error instanceof HttpResponseException
228             && ( (HttpResponseException) error ).getStatusCode() == HttpStatus.SC_NOT_FOUND )
229         {
230             return ERROR_NOT_FOUND;
231         }
232         return ERROR_OTHER;
233     }
234 
235     @Override
236     protected void implPeek( PeekTask task )
237         throws Exception
238     {
239         HttpHead request = commonHeaders( new HttpHead( resolve( task ) ) );
240         execute( request, null );
241     }
242 
243     @Override
244     protected void implGet( GetTask task )
245         throws Exception
246     {
247         EntityGetter getter = new EntityGetter( task );
248         HttpGet request = commonHeaders( new HttpGet( resolve( task ) ) );
249         resume( request, task );
250         try
251         {
252             execute( request, getter );
253         }
254         catch ( HttpResponseException e )
255         {
256             if ( e.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED && request.containsHeader( HttpHeaders.RANGE ) )
257             {
258                 request = commonHeaders( new HttpGet( request.getURI() ) );
259                 execute( request, getter );
260                 return;
261             }
262             throw e;
263         }
264     }
265 
266     @Override
267     protected void implPut( PutTask task )
268         throws Exception
269     {
270         PutTaskEntity entity = new PutTaskEntity( task );
271         HttpPut request = commonHeaders( entity( new HttpPut( resolve( task ) ), entity ) );
272         try
273         {
274             execute( request, null );
275         }
276         catch ( HttpResponseException e )
277         {
278             if ( e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED && request.containsHeader( HttpHeaders.EXPECT ) )
279             {
280                 state.setExpectContinue( false );
281                 request = commonHeaders( entity( new HttpPut( request.getURI() ), entity ) );
282                 execute( request, null );
283                 return;
284             }
285             throw e;
286         }
287     }
288 
289     private void execute( HttpUriRequest request, EntityGetter getter )
290         throws Exception
291     {
292         try
293         {
294             SharingHttpContext context = new SharingHttpContext( state );
295             prepare( request, context );
296             HttpResponse response = client.execute( server, request, context );
297             try
298             {
299                 context.close();
300                 handleStatus( response );
301                 if ( getter != null )
302                 {
303                     getter.handle( response );
304                 }
305             }
306             finally
307             {
308                 EntityUtils.consumeQuietly( response.getEntity() );
309             }
310         }
311         catch ( IOException e )
312         {
313             if ( e.getCause() instanceof TransferCancelledException )
314             {
315                 throw (Exception) e.getCause();
316             }
317             throw e;
318         }
319     }
320 
321     private void prepare( HttpUriRequest request, SharingHttpContext context )
322     {
323         boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase( request.getMethod() );
324         if ( state.getWebDav() == null && ( put || isPayloadPresent( request ) ) )
325         {
326             try
327             {
328                 HttpOptions req = commonHeaders( new HttpOptions( request.getURI() ) );
329                 HttpResponse response = client.execute( server, req, context );
330                 state.setWebDav( isWebDav( response ) );
331                 EntityUtils.consumeQuietly( response.getEntity() );
332             }
333             catch ( IOException e )
334             {
335                 logger.debug( "Failed to prepare HTTP context", e );
336             }
337         }
338         if ( put && Boolean.TRUE.equals( state.getWebDav() ) )
339         {
340             mkdirs( request.getURI(), context );
341         }
342     }
343 
344     private boolean isWebDav( HttpResponse response )
345     {
346         return response.containsHeader( HttpHeaders.DAV );
347     }
348 
349     private void mkdirs( URI uri, SharingHttpContext context )
350     {
351         List<URI> dirs = UriUtils.getDirectories( baseUri, uri );
352         int index = 0;
353         for ( ; index < dirs.size(); index++ )
354         {
355             try
356             {
357                 HttpResponse response =
358                     client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
359                 try
360                 {
361                     int status = response.getStatusLine().getStatusCode();
362                     if ( status < 300 || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
363                     {
364                         break;
365                     }
366                     else if ( status == HttpStatus.SC_CONFLICT )
367                     {
368                         continue;
369                     }
370                     handleStatus( response );
371                 }
372                 finally
373                 {
374                     EntityUtils.consumeQuietly( response.getEntity() );
375                 }
376             }
377             catch ( IOException e )
378             {
379                 logger.debug( "Failed to create parent directory " + dirs.get( index ), e );
380                 return;
381             }
382         }
383         for ( index--; index >= 0; index-- )
384         {
385             try
386             {
387                 HttpResponse response =
388                     client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
389                 try
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     }
405 
406     private <T extends HttpEntityEnclosingRequest> T entity( T request, HttpEntity entity )
407     {
408         request.setEntity( entity );
409         return request;
410     }
411 
412     private boolean isPayloadPresent( HttpUriRequest request )
413     {
414         if ( request instanceof HttpEntityEnclosingRequest )
415         {
416             HttpEntity entity = ( (HttpEntityEnclosingRequest) request ).getEntity();
417             return entity != null && entity.getContentLength() != 0;
418         }
419         return false;
420     }
421 
422     private <T extends HttpUriRequest> T commonHeaders( T request )
423     {
424         request.setHeader( HttpHeaders.CACHE_CONTROL, "no-cache, no-store" );
425         request.setHeader( HttpHeaders.PRAGMA, "no-cache" );
426 
427         if ( state.isExpectContinue() && isPayloadPresent( request ) )
428         {
429             request.setHeader( HttpHeaders.EXPECT, "100-continue" );
430         }
431 
432         for ( Map.Entry<?, ?> entry : headers.entrySet() )
433         {
434             if ( !( entry.getKey() instanceof String ) )
435             {
436                 continue;
437             }
438             if ( entry.getValue() instanceof String )
439             {
440                 request.setHeader( entry.getKey().toString(), entry.getValue().toString() );
441             }
442             else
443             {
444                 request.removeHeaders( entry.getKey().toString() );
445             }
446         }
447 
448         if ( !state.isExpectContinue() )
449         {
450             request.removeHeaders( HttpHeaders.EXPECT );
451         }
452 
453         return request;
454     }
455 
456     private <T extends HttpUriRequest> T resume( T request, GetTask task )
457     {
458         long resumeOffset = task.getResumeOffset();
459         if ( resumeOffset > 0L && task.getDataFile() != null )
460         {
461             request.setHeader( HttpHeaders.RANGE, "bytes=" + Long.toString( resumeOffset ) + '-' );
462             request.setHeader( HttpHeaders.IF_UNMODIFIED_SINCE,
463                                DateUtils.formatDate( new Date( task.getDataFile().lastModified() - 60L * 1000L ) ) );
464             request.setHeader( HttpHeaders.ACCEPT_ENCODING, "identity" );
465         }
466         return request;
467     }
468 
469     private void handleStatus( HttpResponse response )
470         throws HttpResponseException
471     {
472         int status = response.getStatusLine().getStatusCode();
473         if ( status >= 300 )
474         {
475             throw new HttpResponseException( status, response.getStatusLine().getReasonPhrase() + " (" + status + ")" );
476         }
477     }
478 
479     @Override
480     protected void implClose()
481     {
482         AuthenticationContext.close( repoAuthContext );
483         AuthenticationContext.close( proxyAuthContext );
484         state.close();
485     }
486 
487     private class EntityGetter
488     {
489 
490         private final GetTask task;
491 
492         public EntityGetter( GetTask task )
493         {
494             this.task = task;
495         }
496 
497         public void handle( HttpResponse response )
498             throws IOException, TransferCancelledException
499         {
500             HttpEntity entity = response.getEntity();
501             if ( entity == null )
502             {
503                 entity = new ByteArrayEntity( new byte[0] );
504             }
505 
506             long offset = 0L, length = entity.getContentLength();
507             String range = getHeader( response, HttpHeaders.CONTENT_RANGE );
508             if ( range != null )
509             {
510                 Matcher m = CONTENT_RANGE_PATTERN.matcher( range );
511                 if ( !m.matches() )
512                 {
513                     throw new IOException( "Invalid Content-Range header for partial download: " + range );
514                 }
515                 offset = Long.parseLong( m.group( 1 ) );
516                 length = Long.parseLong( m.group( 2 ) ) + 1L;
517                 if ( offset < 0L || offset >= length || ( offset > 0L && offset != task.getResumeOffset() ) )
518                 {
519                     throw new IOException( "Invalid Content-Range header for partial download from offset "
520                         + task.getResumeOffset() + ": " + range );
521                 }
522             }
523 
524             InputStream is = entity.getContent();
525             utilGet( task, is, true, length, offset > 0L );
526             extractChecksums( response );
527         }
528 
529         private void extractChecksums( HttpResponse response )
530         {
531             // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}"
532             String etag = getHeader( response, HttpHeaders.ETAG );
533             if ( etag != null )
534             {
535                 int start = etag.indexOf( "SHA1{" ), end = etag.indexOf( "}", start + 5 );
536                 if ( start >= 0 && end > start )
537                 {
538                     task.setChecksum( "SHA-1", etag.substring( start + 5, end ) );
539                 }
540             }
541         }
542 
543         private String getHeader( HttpResponse response, String name )
544         {
545             Header header = response.getFirstHeader( name );
546             return ( header != null ) ? header.getValue() : null;
547         }
548 
549     }
550 
551     private class PutTaskEntity
552         extends AbstractHttpEntity
553     {
554 
555         private final PutTask task;
556 
557         public PutTaskEntity( PutTask task )
558         {
559             this.task = task;
560         }
561 
562         public boolean isRepeatable()
563         {
564             return true;
565         }
566 
567         public boolean isStreaming()
568         {
569             return false;
570         }
571 
572         public long getContentLength()
573         {
574             return task.getDataLength();
575         }
576 
577         public InputStream getContent()
578             throws IOException
579         {
580             return task.newInputStream();
581         }
582 
583         public void writeTo( OutputStream os )
584             throws IOException
585         {
586             try
587             {
588                 utilPut( task, os, false );
589             }
590             catch ( TransferCancelledException e )
591             {
592                 throw (IOException) new InterruptedIOException().initCause( e );
593             }
594         }
595 
596     }
597 
598 }