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