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