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