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