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