001package org.apache.maven.wagon.shared.http;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.http.Header;
023import org.apache.http.HttpEntity;
024import org.apache.http.HttpException;
025import org.apache.http.HttpHost;
026import org.apache.http.HttpResponse;
027import org.apache.http.HttpStatus;
028import org.apache.http.auth.AuthSchemeProvider;
029import org.apache.http.auth.AuthScope;
030import org.apache.http.auth.ChallengeState;
031import org.apache.http.auth.Credentials;
032import org.apache.http.auth.NTCredentials;
033import org.apache.http.auth.UsernamePasswordCredentials;
034import org.apache.http.client.AuthCache;
035import org.apache.http.client.CredentialsProvider;
036import org.apache.http.client.HttpRequestRetryHandler;
037import org.apache.http.client.ServiceUnavailableRetryStrategy;
038import org.apache.http.client.config.AuthSchemes;
039import org.apache.http.client.config.CookieSpecs;
040import org.apache.http.client.config.RequestConfig;
041import org.apache.http.client.methods.CloseableHttpResponse;
042import org.apache.http.client.methods.HttpGet;
043import org.apache.http.client.methods.HttpHead;
044import org.apache.http.client.methods.HttpPut;
045import org.apache.http.client.methods.HttpUriRequest;
046import org.apache.http.client.protocol.HttpClientContext;
047import org.apache.http.client.utils.DateUtils;
048import org.apache.http.config.Registry;
049import org.apache.http.config.RegistryBuilder;
050import org.apache.http.conn.HttpClientConnectionManager;
051import org.apache.http.conn.socket.ConnectionSocketFactory;
052import org.apache.http.conn.socket.PlainConnectionSocketFactory;
053import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
054import org.apache.http.conn.ssl.SSLContextBuilder;
055import org.apache.http.conn.ssl.SSLInitializationException;
056import org.apache.http.entity.AbstractHttpEntity;
057import org.apache.http.impl.auth.BasicScheme;
058import org.apache.http.impl.auth.BasicSchemeFactory;
059import org.apache.http.impl.auth.DigestSchemeFactory;
060import org.apache.http.impl.auth.NTLMSchemeFactory;
061import org.apache.http.impl.client.BasicAuthCache;
062import org.apache.http.impl.client.BasicCredentialsProvider;
063import org.apache.http.impl.client.CloseableHttpClient;
064import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
065import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;
066import org.apache.http.impl.client.HttpClientBuilder;
067import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
068import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
069import org.apache.http.message.BasicHeader;
070import org.apache.http.protocol.HTTP;
071import org.apache.http.util.EntityUtils;
072import org.apache.maven.wagon.InputData;
073import org.apache.maven.wagon.OutputData;
074import org.apache.maven.wagon.PathUtils;
075import org.apache.maven.wagon.ResourceDoesNotExistException;
076import org.apache.maven.wagon.StreamWagon;
077import org.apache.maven.wagon.TransferFailedException;
078import org.apache.maven.wagon.Wagon;
079import org.apache.maven.wagon.authorization.AuthorizationException;
080import org.apache.maven.wagon.events.TransferEvent;
081import org.apache.maven.wagon.proxy.ProxyInfo;
082import org.apache.maven.wagon.repository.Repository;
083import org.apache.maven.wagon.resource.Resource;
084import org.codehaus.plexus.util.StringUtils;
085
086import javax.net.ssl.HttpsURLConnection;
087import javax.net.ssl.SSLContext;
088import java.io.Closeable;
089import java.io.File;
090import java.io.FileInputStream;
091import java.io.IOException;
092import java.io.InputStream;
093import java.io.OutputStream;
094import java.io.RandomAccessFile;
095import java.nio.ByteBuffer;
096import java.nio.channels.Channels;
097import java.nio.channels.ReadableByteChannel;
098import java.nio.charset.StandardCharsets;
099import java.text.SimpleDateFormat;
100import java.util.ArrayList;
101import java.util.Collection;
102import java.util.Date;
103import java.util.List;
104import java.util.Locale;
105import java.util.Map;
106import java.util.Properties;
107import java.util.TimeZone;
108import java.util.concurrent.TimeUnit;
109
110import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage;
111import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
112import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferDebugMessage;
113import 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 */
119public 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}