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