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