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