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