View Javadoc
1   package org.apache.maven.wagon.providers.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 org.apache.http.Header;
23  import org.apache.http.HttpEntity;
24  import org.apache.http.HttpException;
25  import org.apache.http.HttpHost;
26  import org.apache.http.HttpResponse;
27  import org.apache.http.HttpStatus;
28  import org.apache.http.auth.AuthScope;
29  import org.apache.http.auth.ChallengeState;
30  import org.apache.http.auth.Credentials;
31  import org.apache.http.auth.NTCredentials;
32  import org.apache.http.auth.UsernamePasswordCredentials;
33  import org.apache.http.client.AuthCache;
34  import org.apache.http.client.CredentialsProvider;
35  import org.apache.http.client.config.CookieSpecs;
36  import org.apache.http.client.config.RequestConfig;
37  import org.apache.http.client.methods.CloseableHttpResponse;
38  import org.apache.http.client.methods.HttpGet;
39  import org.apache.http.client.methods.HttpHead;
40  import org.apache.http.client.methods.HttpPut;
41  import org.apache.http.client.methods.HttpUriRequest;
42  import org.apache.http.client.protocol.HttpClientContext;
43  import org.apache.http.client.utils.DateUtils;
44  import org.apache.http.config.Registry;
45  import org.apache.http.config.RegistryBuilder;
46  import org.apache.http.conn.HttpClientConnectionManager;
47  import org.apache.http.conn.socket.ConnectionSocketFactory;
48  import org.apache.http.conn.socket.PlainConnectionSocketFactory;
49  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
50  import org.apache.http.conn.ssl.SSLContextBuilder;
51  import org.apache.http.conn.ssl.SSLInitializationException;
52  import org.apache.http.entity.AbstractHttpEntity;
53  import org.apache.http.impl.auth.BasicScheme;
54  import org.apache.http.impl.client.BasicAuthCache;
55  import org.apache.http.impl.client.BasicCredentialsProvider;
56  import org.apache.http.impl.client.CloseableHttpClient;
57  import org.apache.http.impl.client.HttpClientBuilder;
58  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
59  import org.apache.http.message.BasicHeader;
60  import org.apache.http.protocol.HTTP;
61  import org.apache.http.util.EntityUtils;
62  import org.apache.maven.wagon.InputData;
63  import org.apache.maven.wagon.OutputData;
64  import org.apache.maven.wagon.PathUtils;
65  import org.apache.maven.wagon.ResourceDoesNotExistException;
66  import org.apache.maven.wagon.StreamWagon;
67  import org.apache.maven.wagon.TransferFailedException;
68  import org.apache.maven.wagon.Wagon;
69  import org.apache.maven.wagon.authorization.AuthorizationException;
70  import org.apache.maven.wagon.events.TransferEvent;
71  import org.apache.maven.wagon.proxy.ProxyInfo;
72  import org.apache.maven.wagon.repository.Repository;
73  import org.apache.maven.wagon.resource.Resource;
74  import org.apache.maven.wagon.shared.http.EncodingUtil;
75  import org.codehaus.plexus.util.IOUtil;
76  import org.codehaus.plexus.util.StringUtils;
77  
78  import javax.net.ssl.HttpsURLConnection;
79  import javax.net.ssl.SSLContext;
80  import java.io.ByteArrayInputStream;
81  import java.io.Closeable;
82  import java.io.File;
83  import java.io.FileInputStream;
84  import java.io.IOException;
85  import java.io.InputStream;
86  import java.io.OutputStream;
87  import java.nio.ByteBuffer;
88  import java.text.SimpleDateFormat;
89  import java.util.Date;
90  import java.util.Locale;
91  import java.util.Map;
92  import java.util.Properties;
93  import java.util.TimeZone;
94  import java.util.concurrent.TimeUnit;
95  
96  /**
97   * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
98   * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
99   */
100 public abstract class AbstractHttpClientWagon
101     extends StreamWagon
102 {
103     private final class RequestEntityImplementation
104         extends AbstractHttpEntity
105     {
106 
107         private static final int BUFFER_SIZE = 2048;
108 
109         private final Resource resource;
110 
111         private final Wagon wagon;
112 
113         private ByteBuffer byteBuffer;
114 
115         private File source;
116 
117         private long length = -1;
118 
119         private RequestEntityImplementation( final InputStream stream, final Resource resource, final Wagon wagon,
120                                              final File source )
121             throws TransferFailedException
122         {
123             if ( source != null )
124             {
125                 this.source = source;
126             }
127             else
128             {
129                 try
130                 {
131                     byte[] bytes = IOUtil.toByteArray( stream );
132                     byteBuffer = ByteBuffer.allocate( bytes.length );
133                     byteBuffer.put( bytes );
134                     stream.close();
135                 }
136                 catch ( IOException e )
137                 {
138                     throw new TransferFailedException( e.getMessage(), e );
139                 }
140                 finally
141                 {
142                     IOUtil.close( stream );
143                 }
144             }
145             this.resource = resource;
146             this.length = resource == null ? -1 : resource.getContentLength();
147 
148             this.wagon = wagon;
149         }
150 
151         public long getContentLength()
152         {
153             return length;
154         }
155 
156         public InputStream getContent()
157             throws IOException, IllegalStateException
158         {
159             if ( this.source != null )
160             {
161                 return new FileInputStream( this.source );
162             }
163             return new ByteArrayInputStream( this.byteBuffer.array() );
164         }
165 
166         public boolean isRepeatable()
167         {
168             return true;
169         }
170 
171         public void writeTo( final OutputStream outputStream )
172             throws IOException
173         {
174             if ( outputStream == null )
175             {
176                 throw new NullPointerException( "outputStream cannot be null" );
177             }
178             TransferEvent transferEvent =
179                 new TransferEvent( wagon, resource, TransferEvent.TRANSFER_PROGRESS, TransferEvent.REQUEST_PUT );
180             transferEvent.setTimestamp( System.currentTimeMillis() );
181             InputStream instream = ( this.source != null )
182                 ? new FileInputStream( this.source )
183                 : new ByteArrayInputStream( this.byteBuffer.array() );
184             try
185             {
186                 byte[] buffer = new byte[BUFFER_SIZE];
187                 int l;
188                 if ( this.length < 0 )
189                 {
190                     // until EOF
191                     while ( ( l = instream.read( buffer ) ) != -1 )
192                     {
193                         fireTransferProgress( transferEvent, buffer, -1 );
194                         outputStream.write( buffer, 0, l );
195                     }
196                 }
197                 else
198                 {
199                     // no need to consume more than length
200                     long remaining = this.length;
201                     while ( remaining > 0 )
202                     {
203                         l = instream.read( buffer, 0, (int) Math.min( BUFFER_SIZE, remaining ) );
204                         if ( l == -1 )
205                         {
206                             break;
207                         }
208                         fireTransferProgress( transferEvent, buffer, (int) Math.min( BUFFER_SIZE, remaining ) );
209                         outputStream.write( buffer, 0, l );
210                         remaining -= l;
211                     }
212                 }
213             }
214             finally
215             {
216                 instream.close();
217             }
218         }
219 
220         public boolean isStreaming()
221         {
222             return true;
223         }
224     }
225 
226     private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone( "GMT" );
227 
228     /**
229      * use http(s) connection pool mechanism.
230      * <b>enabled by default</b>
231      */
232     private static boolean persistentPool =
233         Boolean.valueOf( System.getProperty( "maven.wagon.http.pool", "true" ) );
234 
235     /**
236      * skip failure on certificate validity checks.
237      * <b>disabled by default</b>
238      */
239     private static final boolean SSL_INSECURE =
240         Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.insecure", "false" ) );
241 
242     /**
243      * if using sslInsecure, certificate date issues will be ignored
244      * <b>disabled by default</b>
245      */
246     private static final boolean IGNORE_SSL_VALIDITY_DATES =
247         Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.ignore.validity.dates", "false" ) );
248 
249     /**
250      * If enabled, ssl hostname verifier does not check hostname. Disable this will use a browser compat hostname
251      * verifier <b>disabled by default</b>
252      */
253     private static final boolean SSL_ALLOW_ALL =
254         Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.allowall", "false" ) );
255 
256 
257     /**
258      * Maximum concurrent connections per distinct route.
259      * <b>20 by default</b>
260      */
261     private static final int MAX_CONN_PER_ROUTE =
262         Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxPerRoute", "20" ) );
263 
264     /**
265      * Maximum concurrent connections in total.
266      * <b>40 by default</b>
267      */
268     private static final int MAX_CONN_TOTAL =
269         Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxTotal", "40" ) );
270 
271     /**
272      * Internal connection manager
273      */
274     private static HttpClientConnectionManager httpClientConnectionManager = createConnManager();
275 
276 
277     /**
278      * See RFC6585
279      */
280     protected static final int SC_TOO_MANY_REQUESTS = 429;
281 
282     /**
283      * For exponential backoff.
284      */
285 
286     /**
287      * Initial seconds to back off when a HTTP 429 received.
288      * Subsequent 429 responses result in exponental backoff.
289      * <b>5 by default</b>
290      *
291      * @since 2.7
292      */
293     private int initialBackoffSeconds =
294         Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.backoffSeconds", "5" ) );
295 
296     /**
297      * The maximum amount of time we want to back off in the case of
298      * repeated HTTP 429 response codes.
299      *
300      * @since 2.7
301      */
302     private static final int MAX_BACKOFF_WAIT_SECONDS =
303         Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxBackoffSeconds", "180" ) );
304 
305 
306     protected int backoff( int wait, String url )
307         throws InterruptedException, TransferFailedException
308     {
309         TimeUnit.SECONDS.sleep( wait );
310         int nextWait = wait * 2;
311         if ( nextWait >= getMaxBackoffWaitSeconds() )
312         {
313             throw new TransferFailedException(
314                 "Waited too long to access: " + url + ". Return code is: " + SC_TOO_MANY_REQUESTS );
315         }
316         return nextWait;
317     }
318 
319     @SuppressWarnings( "checkstyle:linelength" )
320     private static PoolingHttpClientConnectionManager createConnManager()
321     {
322 
323         String sslProtocolsStr = System.getProperty( "https.protocols" );
324         String cipherSuitesStr = System.getProperty( "https.cipherSuites" );
325         String[] sslProtocols = sslProtocolsStr != null ? sslProtocolsStr.split( " *, *" ) : null;
326         String[] cipherSuites = cipherSuitesStr != null ? cipherSuitesStr.split( " *, *" ) : null;
327 
328         SSLConnectionSocketFactory sslConnectionSocketFactory;
329         if ( SSL_INSECURE )
330         {
331             try
332             {
333                 SSLContext sslContext = new SSLContextBuilder().useSSL().loadTrustMaterial( null,
334                                                                                             new RelaxedTrustStrategy(
335                                                                                                 IGNORE_SSL_VALIDITY_DATES ) ).build();
336                 sslConnectionSocketFactory = new SSLConnectionSocketFactory( sslContext, sslProtocols, cipherSuites,
337                                                                              SSL_ALLOW_ALL
338                                                                                  ? SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER
339                                                                                  : SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
340             }
341             catch ( Exception ex )
342             {
343                 throw new SSLInitializationException( ex.getMessage(), ex );
344             }
345         }
346         else
347         {
348             sslConnectionSocketFactory =
349                 new SSLConnectionSocketFactory( HttpsURLConnection.getDefaultSSLSocketFactory(), sslProtocols,
350                                                 cipherSuites,
351                                                 SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
352         }
353 
354         Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register( "http",
355                                                                                                                  PlainConnectionSocketFactory.INSTANCE ).register(
356             "https", sslConnectionSocketFactory ).build();
357 
358         PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager( registry );
359         if ( persistentPool )
360         {
361             connManager.setDefaultMaxPerRoute( MAX_CONN_PER_ROUTE );
362             connManager.setMaxTotal( MAX_CONN_TOTAL );
363         }
364         else
365         {
366             connManager.setMaxTotal( 1 );
367         }
368         return connManager;
369     }
370 
371     private static CloseableHttpClient httpClient = createClient();
372 
373     private static CloseableHttpClient createClient()
374     {
375         return HttpClientBuilder.create() //
376             .useSystemProperties() //
377             .disableConnectionState() //
378             .setConnectionManager( httpClientConnectionManager ) //
379             .build();
380     }
381 
382     private CredentialsProvider credentialsProvider;
383 
384     private AuthCache authCache;
385 
386     private Closeable closeable;
387 
388     /**
389      * @plexus.configuration
390      * @deprecated Use httpConfiguration instead.
391      */
392     private Properties httpHeaders;
393 
394     /**
395      * @since 1.0-beta-6
396      */
397     private HttpConfiguration httpConfiguration;
398 
399     /**
400      * Basic auth scope overrides
401      * @since 2.8
402      */
403     private BasicAuthScope basicAuth;
404 
405     /**
406      * Proxy basic auth scope overrides
407      * @since 2.8
408      */
409     private BasicAuthScope proxyAuth;
410 
411     public void openConnectionInternal()
412     {
413         repository.setUrl( getURL( repository ) );
414 
415         credentialsProvider = new BasicCredentialsProvider();
416         authCache = new BasicAuthCache();
417 
418         if ( authenticationInfo != null )
419         {
420 
421             String username = authenticationInfo.getUserName();
422             String password = authenticationInfo.getPassword();
423 
424             if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
425             {
426                 Credentials creds = new UsernamePasswordCredentials( username, password );
427 
428                 String host = getRepository().getHost();
429                 int port = getRepository().getPort();
430 
431                 credentialsProvider.setCredentials( getBasicAuthScope().getScope( host, port ), creds );
432             }
433         }
434 
435         ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
436         if ( proxyInfo != null )
437         {
438             String proxyUsername = proxyInfo.getUserName();
439             String proxyPassword = proxyInfo.getPassword();
440             String proxyHost = proxyInfo.getHost();
441             String proxyNtlmHost = proxyInfo.getNtlmHost();
442             String proxyNtlmDomain = proxyInfo.getNtlmDomain();
443             if ( proxyHost != null )
444             {
445                 if ( proxyUsername != null && proxyPassword != null )
446                 {
447                     Credentials creds;
448                     if ( proxyNtlmHost != null || proxyNtlmDomain != null )
449                     {
450                         creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
451                     }
452                     else
453                     {
454                         creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
455                     }
456 
457                     int proxyPort = proxyInfo.getPort();
458 
459                     AuthScope authScope = getProxyBasicAuthScope().getScope( proxyHost, proxyPort );
460                     credentialsProvider.setCredentials( authScope, creds );
461                 }
462             }
463         }
464     }
465 
466     public void closeConnection()
467     {
468         if ( !persistentPool )
469         {
470             httpClientConnectionManager.closeIdleConnections( 0, TimeUnit.MILLISECONDS );
471         }
472 
473         if ( authCache != null )
474         {
475             authCache.clear();
476             authCache = null;
477         }
478 
479         if ( credentialsProvider != null )
480         {
481             credentialsProvider.clear();
482             credentialsProvider = null;
483         }
484     }
485 
486     public static void setPersistentPool( boolean persistentPool )
487     {
488         persistentPool = persistentPool;
489     }
490 
491     public static void setPoolingHttpClientConnectionManager(
492         PoolingHttpClientConnectionManager poolingHttpClientConnectionManager )
493     {
494         httpClientConnectionManager = poolingHttpClientConnectionManager;
495         httpClient = createClient();
496     }
497 
498     public void put( File source, String resourceName )
499         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
500     {
501         Resource resource = new Resource( resourceName );
502 
503         firePutInitiated( resource, source );
504 
505         resource.setContentLength( source.length() );
506 
507         resource.setLastModified( source.lastModified() );
508 
509         put( null, resource, source );
510     }
511 
512     public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
513         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
514     {
515         Resource resource = new Resource( destination );
516 
517         firePutInitiated( resource, null );
518 
519         resource.setContentLength( contentLength );
520 
521         resource.setLastModified( lastModified );
522 
523         put( stream, resource, null );
524     }
525 
526     private void put( final InputStream stream, Resource resource, File source )
527         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
528     {
529         put( resource, source, new RequestEntityImplementation( stream, resource, this, source ) );
530     }
531 
532     private void put( Resource resource, File source, HttpEntity httpEntity )
533         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
534     {
535         put( resource, source, httpEntity, buildUrl( resource ) );
536     }
537 
538     /**
539      * Builds a complete URL string from the repository URL and the relative path of the resource passed.
540      *
541      * @param resource the resource to extract the relative path from.
542      * @return the complete URL
543      */
544     private String buildUrl( Resource resource )
545     {
546         return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() );
547     }
548 
549 
550     private void put( Resource resource, File source, HttpEntity httpEntity, String url )
551         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
552     {
553         put( getInitialBackoffSeconds(), resource, source, httpEntity, url );
554     }
555 
556 
557     private void put( int wait, Resource resource, File source, HttpEntity httpEntity, String url )
558         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
559     {
560 
561         //Parent directories need to be created before posting
562         try
563         {
564             mkdirs( PathUtils.dirname( resource.getName() ) );
565         }
566         catch ( HttpException he )
567         {
568             fireTransferError( resource, he, TransferEvent.REQUEST_PUT );
569         }
570         catch ( IOException e )
571         {
572             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
573         }
574 
575         // preemptive for put
576         // TODO: is it a good idea, though? 'Expect-continue' handshake would serve much better
577 
578         Repository repo = getRepository();
579         HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
580         AuthScope targetScope = getBasicAuthScope().getScope( targetHost );
581 
582         if ( credentialsProvider.getCredentials( targetScope ) != null )
583         {
584             BasicScheme targetAuth = new BasicScheme();
585             authCache.put( targetHost, targetAuth );
586         }
587 
588         HttpPut putMethod = new HttpPut( url );
589 
590         firePutStarted( resource, source );
591 
592         try
593         {
594             putMethod.setEntity( httpEntity );
595 
596             CloseableHttpResponse response = execute( putMethod );
597             try
598             {
599                 int statusCode = response.getStatusLine().getStatusCode();
600                 String reasonPhrase = ", ReasonPhrase: " + response.getStatusLine().getReasonPhrase() + ".";
601                 fireTransferDebug( url + " - Status code: " + statusCode + reasonPhrase );
602 
603                 // Check that we didn't run out of retries.
604                 switch ( statusCode )
605                 {
606                     // Success Codes
607                     case HttpStatus.SC_OK: // 200
608                     case HttpStatus.SC_CREATED: // 201
609                     case HttpStatus.SC_ACCEPTED: // 202
610                     case HttpStatus.SC_NO_CONTENT:  // 204
611                         break;
612                     // handle all redirect even if http specs says " the user agent MUST NOT automatically redirect
613                     // the request unless it can be confirmed by the user"
614                     case HttpStatus.SC_MOVED_PERMANENTLY: // 301
615                     case HttpStatus.SC_MOVED_TEMPORARILY: // 302
616                     case HttpStatus.SC_SEE_OTHER: // 303
617                         put( resource, source, httpEntity, calculateRelocatedUrl( response ) );
618                         return;
619                     case HttpStatus.SC_FORBIDDEN:
620                         fireSessionConnectionRefused();
621                         throw new AuthorizationException( "Access denied to: " + url + reasonPhrase );
622 
623                     case HttpStatus.SC_NOT_FOUND:
624                         throw new ResourceDoesNotExistException( "File: " + url + " does not exist" + reasonPhrase );
625 
626                     case SC_TOO_MANY_REQUESTS:
627                         put( backoff( wait, url ), resource, source, httpEntity, url );
628                         break;
629                     //add more entries here
630                     default:
631                         TransferFailedException e = new TransferFailedException(
632                             "Failed to transfer file: " + url + ". Return code is: " + statusCode + reasonPhrase );
633                         fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
634                         throw e;
635                 }
636 
637                 firePutCompleted( resource, source );
638 
639                 EntityUtils.consume( response.getEntity() );
640             }
641             finally
642             {
643                 response.close();
644             }
645         }
646         catch ( IOException e )
647         {
648             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
649 
650             throw new TransferFailedException( e.getMessage(), e );
651         }
652         catch ( HttpException e )
653         {
654             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
655 
656             throw new TransferFailedException( e.getMessage(), e );
657         }
658         catch ( InterruptedException e )
659         {
660             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
661 
662             throw new TransferFailedException( e.getMessage(), e );
663         }
664 
665     }
666 
667     protected String calculateRelocatedUrl( HttpResponse response )
668     {
669         Header locationHeader = response.getFirstHeader( "Location" );
670         String locationField = locationHeader.getValue();
671         // is it a relative Location or a full ?
672         return locationField.startsWith( "http" ) ? locationField : getURL( getRepository() ) + '/' + locationField;
673     }
674 
675     protected void mkdirs( String dirname )
676         throws HttpException, IOException
677     {
678         // nothing to do
679     }
680 
681     public boolean resourceExists( String resourceName )
682         throws TransferFailedException, AuthorizationException
683     {
684         return resourceExists( getInitialBackoffSeconds(), resourceName );
685     }
686 
687 
688     private boolean resourceExists( int wait, String resourceName )
689         throws TransferFailedException, AuthorizationException
690     {
691         String repositoryUrl = getRepository().getUrl();
692         String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + resourceName;
693         HttpHead headMethod = new HttpHead( url );
694         try
695         {
696             CloseableHttpResponse response = execute( headMethod );
697             try
698             {
699                 int statusCode = response.getStatusLine().getStatusCode();
700                 String reasonPhrase = ", ReasonPhrase: " + response.getStatusLine().getReasonPhrase() + ".";
701                 boolean result;
702                 switch ( statusCode )
703                 {
704                     case HttpStatus.SC_OK:
705                         result = true;
706                         break;
707                     case HttpStatus.SC_NOT_MODIFIED:
708                         result = true;
709                         break;
710                     case HttpStatus.SC_FORBIDDEN:
711                         throw new AuthorizationException( "Access denied to: " + url + reasonPhrase );
712 
713                     case HttpStatus.SC_UNAUTHORIZED:
714                         throw new AuthorizationException( "Not authorized " + reasonPhrase );
715 
716                     case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
717                         throw new AuthorizationException( "Not authorized by proxy " + reasonPhrase );
718 
719                     case HttpStatus.SC_NOT_FOUND:
720                         result = false;
721                         break;
722 
723                     case SC_TOO_MANY_REQUESTS:
724                         return resourceExists( backoff( wait, resourceName ), resourceName );
725 
726                     //add more entries here
727                     default:
728                         throw new TransferFailedException(
729                             "Failed to transfer file: " + url + ". Return code is: " + statusCode + reasonPhrase );
730                 }
731 
732                 EntityUtils.consume( response.getEntity() );
733                 return result;
734             }
735             finally
736             {
737                 response.close();
738             }
739         }
740         catch ( IOException e )
741         {
742             throw new TransferFailedException( e.getMessage(), e );
743         }
744         catch ( HttpException e )
745         {
746             throw new TransferFailedException( e.getMessage(), e );
747         }
748         catch ( InterruptedException e )
749         {
750             throw new TransferFailedException( e.getMessage(), e );
751         }
752 
753     }
754 
755     protected CloseableHttpResponse execute( HttpUriRequest httpMethod )
756         throws HttpException, IOException
757     {
758         setHeaders( httpMethod );
759         String userAgent = getUserAgent( httpMethod );
760         if ( userAgent != null )
761         {
762             httpMethod.setHeader( HTTP.USER_AGENT, userAgent );
763         }
764 
765         RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
766         // WAGON-273: default the cookie-policy to browser compatible
767         requestConfigBuilder.setCookieSpec( CookieSpecs.BROWSER_COMPATIBILITY );
768 
769         Repository repo = getRepository();
770         ProxyInfo proxyInfo = getProxyInfo( repo.getProtocol(), repo.getHost() );
771         if ( proxyInfo != null )
772         {
773             HttpHost proxy = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
774             requestConfigBuilder.setProxy( proxy );
775         }
776 
777         HttpMethodConfiguration config =
778             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( httpMethod );
779 
780         if ( config != null )
781         {
782             ConfigurationUtils.copyConfig( config, requestConfigBuilder );
783         }
784         else
785         {
786             requestConfigBuilder.setSocketTimeout( getReadTimeout() );
787             if ( httpMethod instanceof HttpPut )
788             {
789                 requestConfigBuilder.setExpectContinueEnabled( true );
790             }
791         }
792 
793         if ( httpMethod instanceof HttpPut )
794         {
795             requestConfigBuilder.setRedirectsEnabled( false );
796         }
797 
798         HttpClientContext localContext = HttpClientContext.create();
799         localContext.setCredentialsProvider( credentialsProvider );
800         localContext.setAuthCache( authCache );
801         localContext.setRequestConfig( requestConfigBuilder.build() );
802 
803         if ( config != null && config.isUsePreemptive() )
804         {
805             HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
806             AuthScope targetScope = getBasicAuthScope().getScope( targetHost );
807 
808             if ( credentialsProvider.getCredentials( targetScope ) != null )
809             {
810                 BasicScheme targetAuth = new BasicScheme();
811                 authCache.put( targetHost, targetAuth );
812             }
813         }
814 
815         if ( proxyInfo != null )
816         {
817             if ( proxyInfo.getHost() != null )
818             {
819                 HttpHost proxyHost = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
820                 AuthScope proxyScope = getProxyBasicAuthScope().getScope( proxyHost );
821 
822                 if ( credentialsProvider.getCredentials( proxyScope ) != null )
823                 {
824                     /* This is extremely ugly because we need to set challengeState to PROXY, but
825                      * the constructor is deprecated. Alternatively, we could subclass BasicScheme
826                      * to ProxyBasicScheme and set the state internally in the constructor.
827                      */
828                     BasicScheme proxyAuth = new BasicScheme( ChallengeState.PROXY );
829                     authCache.put( proxyHost, proxyAuth );
830                 }
831             }
832         }
833 
834         return httpClient.execute( httpMethod, localContext );
835     }
836 
837     protected void setHeaders( HttpUriRequest method )
838     {
839         HttpMethodConfiguration config =
840             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
841         if ( config == null || config.isUseDefaultHeaders() )
842         {
843             // TODO: merge with the other headers and have some better defaults, unify with lightweight headers
844             method.addHeader( "Cache-control", "no-cache" );
845             method.addHeader( "Cache-store", "no-store" );
846             method.addHeader( "Pragma", "no-cache" );
847             method.addHeader( "Expires", "0" );
848             method.addHeader( "Accept-Encoding", "gzip" );
849         }
850 
851         if ( httpHeaders != null )
852         {
853             for ( Map.Entry<Object, Object> entry : httpHeaders.entrySet() )
854             {
855                 method.setHeader( (String) entry.getKey(), (String) entry.getValue() );
856             }
857         }
858 
859         Header[] headers = config == null ? null : config.asRequestHeaders();
860         if ( headers != null )
861         {
862             for ( Header header : headers )
863             {
864                 method.setHeader( header );
865             }
866         }
867     }
868 
869     protected String getUserAgent( HttpUriRequest method )
870     {
871         if ( httpHeaders != null )
872         {
873             String value = (String) httpHeaders.get( "User-Agent" );
874             if ( value != null )
875             {
876                 return value;
877             }
878         }
879         HttpMethodConfiguration config =
880             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
881 
882         if ( config != null )
883         {
884             return (String) config.getHeaders().get( "User-Agent" );
885         }
886         return null;
887     }
888 
889     /**
890      * getUrl
891      * Implementors can override this to remove unwanted parts of the url such as role-hints
892      *
893      * @param repository
894      * @return
895      */
896     protected String getURL( Repository repository )
897     {
898         return repository.getUrl();
899     }
900 
901     public HttpConfiguration getHttpConfiguration()
902     {
903         return httpConfiguration;
904     }
905 
906     public void setHttpConfiguration( HttpConfiguration httpConfiguration )
907     {
908         this.httpConfiguration = httpConfiguration;
909     }
910 
911     /**
912      * Get the override values for standard HttpClient AuthScope
913      *
914      * @return the basicAuth
915      */
916     public BasicAuthScope getBasicAuthScope()
917     {
918         if ( basicAuth == null )
919         {
920             basicAuth = new BasicAuthScope();
921         }
922         return basicAuth;
923     }
924 
925     /**
926      * Set the override values for standard HttpClient AuthScope
927      *
928      * @param basicAuth the AuthScope to set
929      */
930     public void setBasicAuthScope( BasicAuthScope basicAuth )
931     {
932         this.basicAuth = basicAuth;
933     }
934 
935     /**
936      * Get the override values for proxy HttpClient AuthScope
937      *
938      * @return the proxyAuth
939      */
940     public BasicAuthScope getProxyBasicAuthScope()
941     {
942         if ( proxyAuth == null )
943         {
944             proxyAuth = new BasicAuthScope();
945         }
946         return proxyAuth;
947     }
948 
949     /**
950      * Set the override values for proxy HttpClient AuthScope
951      *
952      * @param proxyAuth the AuthScope to set
953      */
954     public void setProxyBasicAuthScope( BasicAuthScope proxyAuth )
955     {
956         this.proxyAuth = proxyAuth;
957     }
958 
959     public void fillInputData( InputData inputData )
960         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
961     {
962         fillInputData( getInitialBackoffSeconds(), inputData );
963     }
964 
965     private void fillInputData( int wait, InputData inputData )
966         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
967     {
968         Resource resource = inputData.getResource();
969 
970         String repositoryUrl = getRepository().getUrl();
971         String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + resource.getName();
972         HttpGet getMethod = new HttpGet( url );
973         long timestamp = resource.getLastModified();
974         if ( timestamp > 0 )
975         {
976             SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
977             fmt.setTimeZone( GMT_TIME_ZONE );
978             Header hdr = new BasicHeader( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
979             fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
980             getMethod.addHeader( hdr );
981         }
982 
983         try
984         {
985             CloseableHttpResponse response = execute( getMethod );
986             closeable = response;
987             int statusCode = response.getStatusLine().getStatusCode();
988 
989             String reasonPhrase = ", ReasonPhrase:" + response.getStatusLine().getReasonPhrase() + ".";
990 
991             fireTransferDebug( url + " - Status code: " + statusCode + reasonPhrase );
992 
993             switch ( statusCode )
994             {
995                 case HttpStatus.SC_OK:
996                     break;
997 
998                 case HttpStatus.SC_NOT_MODIFIED:
999                     // return, leaving last modified set to original value so getIfNewer should return unmodified
1000                     return;
1001                 case HttpStatus.SC_FORBIDDEN:
1002                     fireSessionConnectionRefused();
1003                     throw new AuthorizationException( "Access denied to: " + url + " " + reasonPhrase );
1004 
1005                 case HttpStatus.SC_UNAUTHORIZED:
1006                     fireSessionConnectionRefused();
1007                     throw new AuthorizationException( "Not authorized " + reasonPhrase );
1008 
1009                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
1010                     fireSessionConnectionRefused();
1011                     throw new AuthorizationException( "Not authorized by proxy " + reasonPhrase );
1012 
1013                 case HttpStatus.SC_NOT_FOUND:
1014                     throw new ResourceDoesNotExistException( "File: " + url + " " + reasonPhrase );
1015 
1016                 case SC_TOO_MANY_REQUESTS:
1017                     fillInputData( backoff( wait, url ), inputData );
1018                     break;
1019 
1020                 // add more entries here
1021                 default:
1022                     cleanupGetTransfer( resource );
1023                     TransferFailedException e = new TransferFailedException(
1024                         "Failed to transfer file: " + url + ". Return code is: " + statusCode + " " + reasonPhrase );
1025                     fireTransferError( resource, e, TransferEvent.REQUEST_GET );
1026                     throw e;
1027             }
1028 
1029             Header contentLengthHeader = response.getFirstHeader( "Content-Length" );
1030 
1031             if ( contentLengthHeader != null )
1032             {
1033                 try
1034                 {
1035                     long contentLength = Long.parseLong( contentLengthHeader.getValue() );
1036 
1037                     resource.setContentLength( contentLength );
1038                 }
1039                 catch ( NumberFormatException e )
1040                 {
1041                     fireTransferDebug(
1042                         "error parsing content length header '" + contentLengthHeader.getValue() + "' " + e );
1043                 }
1044             }
1045 
1046             Header lastModifiedHeader = response.getFirstHeader( "Last-Modified" );
1047             if ( lastModifiedHeader != null )
1048             {
1049                 Date lastModified = DateUtils.parseDate( lastModifiedHeader.getValue() );
1050                 if ( lastModified != null )
1051                 {
1052                     resource.setLastModified( lastModified.getTime() );
1053                     fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() + " ("
1054                         + lastModified.getTime() + ")" );
1055                 }
1056             }
1057 
1058             HttpEntity entity = response.getEntity();
1059             if ( entity != null )
1060             {
1061                 inputData.setInputStream( entity.getContent() );
1062             }
1063         }
1064         catch ( IOException e )
1065         {
1066             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
1067 
1068             throw new TransferFailedException( e.getMessage(), e );
1069         }
1070         catch ( HttpException e )
1071         {
1072             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
1073 
1074             throw new TransferFailedException( e.getMessage(), e );
1075         }
1076         catch ( InterruptedException e )
1077         {
1078             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
1079 
1080             throw new TransferFailedException( e.getMessage(), e );
1081         }
1082 
1083     }
1084 
1085     protected void cleanupGetTransfer( Resource resource )
1086     {
1087         if ( closeable != null )
1088         {
1089             try
1090             {
1091                 closeable.close();
1092             }
1093             catch ( IOException ignore )
1094             {
1095                 // ignore
1096             }
1097 
1098         }
1099     }
1100 
1101 
1102     @Override
1103     public void putFromStream( InputStream stream, String destination )
1104         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
1105     {
1106         putFromStream( stream, destination, -1, -1 );
1107     }
1108 
1109     @Override
1110     protected void putFromStream( InputStream stream, Resource resource )
1111         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
1112     {
1113         putFromStream( stream, resource.getName(), -1, -1 );
1114     }
1115 
1116     public Properties getHttpHeaders()
1117     {
1118         return httpHeaders;
1119     }
1120 
1121     public void setHttpHeaders( Properties httpHeaders )
1122     {
1123         this.httpHeaders = httpHeaders;
1124     }
1125 
1126     @Override
1127     public void fillOutputData( OutputData outputData )
1128         throws TransferFailedException
1129     {
1130         // no needed in this implementation but throw an Exception if used
1131         throw new IllegalStateException( "this wagon http client must not use fillOutputData" );
1132     }
1133 
1134     public int getInitialBackoffSeconds()
1135     {
1136         return initialBackoffSeconds;
1137     }
1138 
1139     public void setInitialBackoffSeconds( int initialBackoffSeconds )
1140     {
1141         this.initialBackoffSeconds = initialBackoffSeconds;
1142     }
1143 
1144     public static int getMaxBackoffWaitSeconds()
1145     {
1146         return MAX_BACKOFF_WAIT_SECONDS;
1147     }
1148 }