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