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.codehaus.plexus.util.IOUtil;
75  import org.codehaus.plexus.util.StringUtils;
76  
77  import javax.net.ssl.HttpsURLConnection;
78  import javax.net.ssl.SSLContext;
79  import java.io.ByteArrayInputStream;
80  import java.io.Closeable;
81  import java.io.File;
82  import java.io.FileInputStream;
83  import java.io.IOException;
84  import java.io.InputStream;
85  import java.io.OutputStream;
86  import java.net.URLEncoder;
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 conncurrent 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 conncurrent 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     private static PoolingHttpClientConnectionManager createConnManager()
272     {
273 
274         String sslProtocolsStr = System.getProperty( "https.protocols" );
275         String cipherSuitesStr = System.getProperty( "https.cipherSuites" );
276         String[] sslProtocols = sslProtocolsStr != null ? sslProtocolsStr.split( " *, *" ) : null;
277         String[] cipherSuites = cipherSuitesStr != null ? cipherSuitesStr.split( " *, *" ) : null;
278 
279         SSLConnectionSocketFactory sslConnectionSocketFactory;
280         if ( SSL_INSECURE )
281         {
282             try
283             {
284                 SSLContext sslContext = new SSLContextBuilder().useSSL().loadTrustMaterial( null,
285                                                                                             new RelaxedTrustStrategy(
286                                                                                                 IGNORE_SSL_VALIDITY_DATES ) ).build();
287                 sslConnectionSocketFactory = new SSLConnectionSocketFactory( sslContext, sslProtocols, cipherSuites,
288                                                                              SSL_ALLOW_ALL
289                                                                                  ? SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER
290                                                                                  : SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
291             }
292             catch ( Exception ex )
293             {
294                 throw new SSLInitializationException( ex.getMessage(), ex );
295             }
296         }
297         else
298         {
299             sslConnectionSocketFactory =
300                 new SSLConnectionSocketFactory( HttpsURLConnection.getDefaultSSLSocketFactory(), sslProtocols,
301                                                 cipherSuites,
302                                                 SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
303         }
304 
305         Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register( "http",
306                                                                                                                  PlainConnectionSocketFactory.INSTANCE ).register(
307             "https", sslConnectionSocketFactory ).build();
308 
309         PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager( registry );
310         if ( PERSISTENT_POOL )
311         {
312             connManager.setDefaultMaxPerRoute( MAX_CONN_PER_ROUTE );
313             connManager.setMaxTotal( MAX_CONN_TOTAL );
314         }
315         else
316         {
317             connManager.setMaxTotal( 1 );
318         }
319         return connManager;
320     }
321 
322     private static CloseableHttpClient CLIENT = createClient();
323 
324     private static CloseableHttpClient createClient()
325     {
326         return HttpClientBuilder.create().useSystemProperties().disableConnectionState().setConnectionManager(
327             CONN_MAN ).build();
328     }
329 
330     private static String DEFAULT_USER_AGENT = getDefaultUserAgent();
331 
332     private static String getDefaultUserAgent()
333     {
334         Properties props = new Properties();
335 
336         InputStream is = AbstractHttpClientWagon.class.getResourceAsStream(
337             "/META-INF/maven/org.apache.maven.wagon/wagon-http/pom.properties" );
338         if ( is != null )
339         {
340             try
341             {
342                 props.load( is );
343             }
344             catch ( IOException ignore )
345             {
346             }
347             finally
348             {
349                 IOUtil.close( is );
350             }
351         }
352 
353         String ver = props.getProperty( "version", "unknown-version" );
354         return "Apache-Maven-Wagon/" + ver + " (Java " + System.getProperty( "java.version" ) + "; ";
355     }
356 
357 
358     private CredentialsProvider credentialsProvider;
359 
360     private AuthCache authCache;
361 
362     private HttpClientContext localContext;
363 
364     private Closeable closeable;
365 
366     /**
367      * @plexus.configuration
368      * @deprecated Use httpConfiguration instead.
369      */
370     private Properties httpHeaders;
371 
372     /**
373      * @since 1.0-beta-6
374      */
375     private HttpConfiguration httpConfiguration;
376 
377     public void openConnectionInternal()
378     {
379         repository.setUrl( getURL( repository ) );
380 
381         localContext = HttpClientContext.create();
382         credentialsProvider = new BasicCredentialsProvider();
383         authCache = new BasicAuthCache();
384         localContext.setCredentialsProvider( credentialsProvider );
385         localContext.setAuthCache( authCache );
386 
387         if ( authenticationInfo != null )
388         {
389 
390             String username = authenticationInfo.getUserName();
391             String password = authenticationInfo.getPassword();
392 
393             if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
394             {
395                 Credentials creds = new UsernamePasswordCredentials( username, password );
396 
397                 String host = getRepository().getHost();
398                 int port = getRepository().getPort() > -1 ? getRepository().getPort() : AuthScope.ANY_PORT;
399 
400                 credentialsProvider.setCredentials( new AuthScope( host, port ), creds );
401             }
402         }
403 
404         ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
405         if ( proxyInfo != null )
406         {
407             String proxyUsername = proxyInfo.getUserName();
408             String proxyPassword = proxyInfo.getPassword();
409             String proxyHost = proxyInfo.getHost();
410             String proxyNtlmHost = proxyInfo.getNtlmHost();
411             String proxyNtlmDomain = proxyInfo.getNtlmDomain();
412             if ( proxyHost != null )
413             {
414                 if ( proxyUsername != null && proxyPassword != null )
415                 {
416                     Credentials creds;
417                     if ( proxyNtlmHost != null || proxyNtlmDomain != null )
418                     {
419                         creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
420                     }
421                     else
422                     {
423                         creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
424                     }
425 
426                     int port = proxyInfo.getPort() > -1 ? proxyInfo.getPort() : AuthScope.ANY_PORT;
427 
428                     AuthScope authScope = new AuthScope( proxyHost, port );
429                     credentialsProvider.setCredentials( authScope, creds );
430                 }
431             }
432         }
433     }
434 
435     public void closeConnection()
436     {
437         if ( !PERSISTENT_POOL )
438         {
439             CONN_MAN.closeIdleConnections( 0, TimeUnit.MILLISECONDS );
440         }
441     }
442 
443     public void put( File source, String resourceName )
444         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
445     {
446         Resource resource = new Resource( resourceName );
447 
448         firePutInitiated( resource, source );
449 
450         resource.setContentLength( source.length() );
451 
452         resource.setLastModified( source.lastModified() );
453 
454         put( null, resource, source );
455     }
456 
457     public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
458         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
459     {
460         Resource resource = new Resource( destination );
461 
462         firePutInitiated( resource, null );
463 
464         resource.setContentLength( contentLength );
465 
466         resource.setLastModified( lastModified );
467 
468         put( stream, resource, null );
469     }
470 
471     private void put( final InputStream stream, Resource resource, File source )
472         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
473     {
474         put( resource, source, new RequestEntityImplementation( stream, resource, this, source ) );
475     }
476 
477     private void put( Resource resource, File source, HttpEntity httpEntity )
478         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
479     {
480 
481         StringBuilder url = new StringBuilder( getURL( getRepository() ) );
482         String[] parts = StringUtils.split( resource.getName(), "/" );
483         for ( String part : parts )
484         {
485             // TODO: Fix encoding...
486             // url += "/" + URLEncoder.encode( parts[i], System.getProperty("file.encoding") );
487             if ( !url.toString().endsWith( "/" ) )
488             {
489                 url.append( '/' );
490             }
491             url.append( URLEncoder.encode( part ) );
492         }
493         put( resource, source, httpEntity, url.toString() );
494     }
495 
496     private void put( Resource resource, File source, HttpEntity httpEntity, String url )
497         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
498     {
499 
500         //Parent directories need to be created before posting
501         try
502         {
503             mkdirs( PathUtils.dirname( resource.getName() ) );
504         }
505         catch ( HttpException he )
506         {
507             fireTransferError( resource, he, TransferEvent.REQUEST_GET );
508         }
509         catch ( IOException e )
510         {
511             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
512         }
513 
514         // preemptive for put
515         // TODO: is it a good idea, though? 'Expect-continue' handshake would serve much better
516 
517         Repository repo = getRepository();
518         HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
519         AuthScope targetScope = new AuthScope( targetHost );
520 
521         if ( credentialsProvider.getCredentials( targetScope ) != null )
522         {
523             BasicScheme targetAuth = new BasicScheme();
524             try {
525                 targetAuth.processChallenge(new BasicHeader(AUTH.WWW_AUTH, "BASIC preemptive"));
526                 authCache.put( targetHost, targetAuth  );
527             }
528             catch ( MalformedChallengeException ignore )
529             {
530             }
531         }
532 
533         HttpPut putMethod = new HttpPut( url );
534 
535         firePutStarted( resource, source );
536 
537         try
538         {
539             putMethod.setEntity( httpEntity );
540 
541             CloseableHttpResponse response = execute( putMethod );
542             try
543             {
544                 int statusCode = response.getStatusLine().getStatusCode();
545                 String reasonPhrase = ", ReasonPhrase: " + response.getStatusLine().getReasonPhrase() + ".";
546                 fireTransferDebug( url + " - Status code: " + statusCode + reasonPhrase );
547 
548                 // Check that we didn't run out of retries.
549                 switch ( statusCode )
550                 {
551                     // Success Codes
552                     case HttpStatus.SC_OK: // 200
553                     case HttpStatus.SC_CREATED: // 201
554                     case HttpStatus.SC_ACCEPTED: // 202
555                     case HttpStatus.SC_NO_CONTENT:  // 204
556                         break;
557                     // 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"
558                     case HttpStatus.SC_MOVED_PERMANENTLY: // 301
559                     case HttpStatus.SC_MOVED_TEMPORARILY: // 302
560                     case HttpStatus.SC_SEE_OTHER: // 303
561                         put( resource, source, httpEntity, calculateRelocatedUrl( response ) );
562                         return;
563                     case HttpStatus.SC_FORBIDDEN:
564                         fireSessionConnectionRefused();
565                         throw new AuthorizationException( "Access denied to: " + url + reasonPhrase );
566 
567                     case HttpStatus.SC_NOT_FOUND:
568                         throw new ResourceDoesNotExistException( "File: " + url + " does not exist" + reasonPhrase );
569 
570                         //add more entries here
571                     default:
572                     {
573                         TransferFailedException e = new TransferFailedException(
574                             "Failed to transfer file: " + url + ". Return code is: " + statusCode + reasonPhrase );
575                         fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
576                         throw e;
577                     }
578                 }
579 
580                 firePutCompleted( resource, source );
581 
582                 EntityUtils.consume( response.getEntity() );
583             }
584             finally
585             {
586                 response.close();
587             }
588         }
589         catch ( IOException e )
590         {
591             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
592 
593             throw new TransferFailedException( e.getMessage(), e );
594         }
595         catch ( HttpException e )
596         {
597             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
598 
599             throw new TransferFailedException( e.getMessage(), e );
600         }
601     }
602 
603     protected String calculateRelocatedUrl( HttpResponse response )
604     {
605         Header locationHeader = response.getFirstHeader( "Location" );
606         String locationField = locationHeader.getValue();
607         // is it a relative Location or a full ?
608         return locationField.startsWith( "http" ) ? locationField : getURL( getRepository() ) + '/' + locationField;
609     }
610 
611     protected void mkdirs( String dirname )
612         throws HttpException, IOException
613     {
614         // nothing to do
615     }
616 
617     public boolean resourceExists( String resourceName )
618         throws TransferFailedException, AuthorizationException
619     {
620         String repositoryUrl = getRepository().getUrl();
621         String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + resourceName;
622         HttpHead headMethod = new HttpHead( url );
623         try
624         {
625             CloseableHttpResponse response = execute( headMethod );
626             try
627             {
628                 int statusCode = response.getStatusLine().getStatusCode();
629                 String reasonPhrase = ", ReasonPhrase: " + response.getStatusLine().getReasonPhrase() + ".";
630                 boolean result;
631                 switch ( statusCode )
632                 {
633                     case HttpStatus.SC_OK:
634                         result = true;
635                         break;
636                     case HttpStatus.SC_NOT_MODIFIED:
637                         result = true;
638                         break;
639                     case HttpStatus.SC_FORBIDDEN:
640                         throw new AuthorizationException( "Access denied to: " + url + reasonPhrase );
641 
642                     case HttpStatus.SC_UNAUTHORIZED:
643                         throw new AuthorizationException( "Not authorized " + reasonPhrase );
644 
645                     case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
646                         throw new AuthorizationException( "Not authorized by proxy " + reasonPhrase );
647 
648                     case HttpStatus.SC_NOT_FOUND:
649                         result = false;
650                         break;
651                     //add more entries here
652                     default:
653                         throw new TransferFailedException(
654                             "Failed to transfer file: " + url + ". Return code is: " + statusCode + reasonPhrase );
655                 }
656 
657                 EntityUtils.consume( response.getEntity() );
658                 return result;
659             }
660             finally
661             {
662                 response.close();
663             }
664         }
665         catch ( IOException e )
666         {
667             throw new TransferFailedException( e.getMessage(), e );
668         }
669         catch ( HttpException e )
670         {
671             throw new TransferFailedException( e.getMessage(), e );
672         }
673     }
674 
675     protected CloseableHttpResponse execute( HttpUriRequest httpMethod )
676         throws HttpException, IOException
677     {
678         setHeaders( httpMethod );
679         String userAgent = getUserAgent( httpMethod );
680         if ( userAgent != null )
681         {
682             httpMethod.setHeader( HTTP.USER_AGENT, userAgent );
683         }
684 
685         RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
686         // WAGON-273: default the cookie-policy to browser compatible
687         requestConfigBuilder.setCookieSpec( CookieSpecs.BROWSER_COMPATIBILITY );
688 
689         Repository repo = getRepository();
690         ProxyInfo proxyInfo = getProxyInfo( repo.getProtocol(), repo.getHost() );
691         if ( proxyInfo != null )
692         {
693             HttpHost proxy = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
694             requestConfigBuilder.setProxy( proxy );
695         }
696 
697         HttpMethodConfiguration config =
698             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( httpMethod );
699 
700         if ( config != null )
701         {
702             ConfigurationUtils.copyConfig( config, requestConfigBuilder );
703         }
704         else
705         {
706             requestConfigBuilder.setSocketTimeout( getReadTimeout() );
707         }
708 
709         localContext.setRequestConfig( requestConfigBuilder.build() );
710 
711         if ( config != null && config.isUsePreemptive() )
712         {
713             HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
714             AuthScope targetScope = new AuthScope( targetHost );
715 
716             if ( credentialsProvider.getCredentials( targetScope ) != null )
717             {
718                 BasicScheme targetAuth = new BasicScheme();
719                 targetAuth.processChallenge( new BasicHeader(AUTH.WWW_AUTH, "BASIC preemptive" ) );
720                 authCache.put( targetHost, targetAuth  );
721             }
722         }
723 
724         if ( proxyInfo != null )
725         {
726             if ( proxyInfo.getHost() != null )
727             {
728                 HttpHost proxyHost = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
729                 AuthScope proxyScope = new AuthScope( proxyHost );
730 
731                 String proxyUsername = proxyInfo.getUserName();
732                 String proxyPassword = proxyInfo.getPassword();
733                 String proxyNtlmHost = proxyInfo.getNtlmHost();
734                 String proxyNtlmDomain = proxyInfo.getNtlmDomain();
735 
736                 if ( proxyUsername != null && proxyPassword != null )
737                 {
738                     Credentials creds;
739                     if ( proxyNtlmHost != null || proxyNtlmDomain != null )
740                     {
741                         creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
742                     }
743                     else
744                     {
745                         creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
746                     }
747 
748                     credentialsProvider.setCredentials( proxyScope, creds );
749                     BasicScheme proxyAuth = new BasicScheme();
750                     proxyAuth.processChallenge( new BasicHeader(AUTH.PROXY_AUTH, "BASIC preemptive" ) );
751                     authCache.put( proxyHost, proxyAuth );
752                 }
753             }
754         }
755 
756         return CLIENT.execute( httpMethod, localContext );
757     }
758 
759     protected void setHeaders( HttpUriRequest method )
760     {
761         HttpMethodConfiguration config =
762             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
763         if ( config == null || config.isUseDefaultHeaders() )
764         {
765             // TODO: merge with the other headers and have some better defaults, unify with lightweight headers
766             method.addHeader( "Cache-control", "no-cache" );
767             method.addHeader( "Cache-store", "no-store" );
768             method.addHeader( "Pragma", "no-cache" );
769             method.addHeader( "Expires", "0" );
770             method.addHeader( "Accept-Encoding", "gzip" );
771             method.addHeader( "User-Agent", DEFAULT_USER_AGENT );
772         }
773 
774         if ( httpHeaders != null )
775         {
776             for ( Map.Entry<Object, Object> entry : httpHeaders.entrySet() )
777             {
778                 method.addHeader( (String) entry.getKey(), (String) entry.getValue() );
779             }
780         }
781 
782         Header[] headers = config == null ? null : config.asRequestHeaders();
783         if ( headers != null )
784         {
785             for ( Header header : headers )
786             {
787                 method.addHeader( header );
788             }
789         }
790     }
791 
792     protected String getUserAgent( HttpUriRequest method )
793     {
794         if ( httpHeaders != null )
795         {
796             String value = (String) httpHeaders.get( "User-Agent" );
797             if ( value != null )
798             {
799                 return value;
800             }
801         }
802         HttpMethodConfiguration config =
803             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
804 
805         if ( config != null )
806         {
807             return (String) config.getHeaders().get( "User-Agent" );
808         }
809         return null;
810     }
811 
812     /**
813      * getUrl
814      * Implementors can override this to remove unwanted parts of the url such as role-hints
815      *
816      * @param repository
817      * @return
818      */
819     protected String getURL( Repository repository )
820     {
821         return repository.getUrl();
822     }
823 
824     public HttpConfiguration getHttpConfiguration()
825     {
826         return httpConfiguration;
827     }
828 
829     public void setHttpConfiguration( HttpConfiguration httpConfiguration )
830     {
831         this.httpConfiguration = httpConfiguration;
832     }
833 
834     public void fillInputData( InputData inputData )
835         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
836     {
837         Resource resource = inputData.getResource();
838 
839         String repositoryUrl = getRepository().getUrl();
840         String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + resource.getName();
841         HttpGet getMethod = new HttpGet( url );
842         long timestamp = resource.getLastModified();
843         if ( timestamp > 0 )
844         {
845             SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
846             fmt.setTimeZone( GMT_TIME_ZONE );
847             Header hdr = new BasicHeader( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
848             fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
849             getMethod.addHeader( hdr );
850         }
851 
852         try
853         {
854             CloseableHttpResponse response = execute( getMethod );
855             closeable = response;
856             int statusCode = response.getStatusLine().getStatusCode();
857 
858             String reasonPhrase = ", ReasonPhrase:" + response.getStatusLine().getReasonPhrase() + ".";
859 
860             fireTransferDebug( url + " - Status code: " + statusCode + reasonPhrase );
861 
862             switch ( statusCode )
863             {
864                 case HttpStatus.SC_OK:
865                     break;
866 
867                 case HttpStatus.SC_NOT_MODIFIED:
868                     // return, leaving last modified set to original value so getIfNewer should return unmodified
869                     return;
870                 case HttpStatus.SC_FORBIDDEN:
871                     fireSessionConnectionRefused();
872                     throw new AuthorizationException( "Access denied to: " + url + " " + reasonPhrase );
873 
874                 case HttpStatus.SC_UNAUTHORIZED:
875                     fireSessionConnectionRefused();
876                     throw new AuthorizationException( "Not authorized " + reasonPhrase );
877 
878                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
879                     fireSessionConnectionRefused();
880                     throw new AuthorizationException( "Not authorized by proxy " + reasonPhrase );
881 
882                 case HttpStatus.SC_NOT_FOUND:
883                     throw new ResourceDoesNotExistException( "File: " + url + " " + reasonPhrase );
884 
885                     // add more entries here
886                 default:
887                 {
888                     cleanupGetTransfer( resource );
889                     TransferFailedException e = new TransferFailedException(
890                         "Failed to transfer file: " + url + ". Return code is: " + statusCode + " " + reasonPhrase );
891                     fireTransferError( resource, e, TransferEvent.REQUEST_GET );
892                     throw e;
893                 }
894             }
895 
896             Header contentLengthHeader = response.getFirstHeader( "Content-Length" );
897 
898             if ( contentLengthHeader != null )
899             {
900                 try
901                 {
902                     long contentLength = Long.parseLong( contentLengthHeader.getValue() );
903 
904                     resource.setContentLength( contentLength );
905                 }
906                 catch ( NumberFormatException e )
907                 {
908                     fireTransferDebug(
909                         "error parsing content length header '" + contentLengthHeader.getValue() + "' " + e );
910                 }
911             }
912 
913             Header lastModifiedHeader = response.getFirstHeader( "Last-Modified" );
914             if ( lastModifiedHeader != null )
915             {
916                 Date lastModified = DateUtils.parseDate( lastModifiedHeader.getValue() );
917                 if ( lastModified != null )
918                 {
919                     resource.setLastModified( lastModified.getTime() );
920                     fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() +
921                                            " (" + lastModified.getTime() + ")" );
922                 }
923             }
924 
925             HttpEntity entity = response.getEntity();
926             if ( entity != null )
927             {
928                 inputData.setInputStream( entity.getContent() );
929             }
930         }
931         catch ( IOException e )
932         {
933             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
934 
935             throw new TransferFailedException( e.getMessage(), e );
936         }
937         catch ( HttpException e )
938         {
939             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
940 
941             throw new TransferFailedException( e.getMessage(), e );
942         }
943     }
944 
945     protected void cleanupGetTransfer( Resource resource )
946     {
947         if ( closeable != null )
948         {
949             try
950             {
951                 closeable.close();
952             }
953             catch ( IOException ignore )
954             {
955             }
956 
957         }
958     }
959 
960 
961     @Override
962     public void putFromStream( InputStream stream, String destination )
963         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
964     {
965         putFromStream( stream, destination, -1, -1 );
966     }
967 
968     @Override
969     protected void putFromStream( InputStream stream, Resource resource )
970         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
971     {
972         putFromStream( stream, resource.getName(), -1, -1 );
973     }
974 
975     public Properties getHttpHeaders()
976     {
977         return httpHeaders;
978     }
979 
980     public void setHttpHeaders( Properties httpHeaders )
981     {
982         this.httpHeaders = httpHeaders;
983     }
984 
985     @Override
986     public void fillOutputData( OutputData outputData )
987         throws TransferFailedException
988     {
989         // no needed in this implementation but throw an Exception if used
990         throw new IllegalStateException( "this wagon http client must not use fillOutputData" );
991     }
992 }