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