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