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