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