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