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