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