001package org.apache.maven.wagon.providers.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.commons.io.IOUtils; 023import org.apache.maven.wagon.ConnectionException; 024import org.apache.maven.wagon.InputData; 025import org.apache.maven.wagon.OutputData; 026import org.apache.maven.wagon.ResourceDoesNotExistException; 027import org.apache.maven.wagon.StreamWagon; 028import org.apache.maven.wagon.TransferFailedException; 029import org.apache.maven.wagon.authentication.AuthenticationException; 030import org.apache.maven.wagon.authorization.AuthorizationException; 031import org.apache.maven.wagon.events.TransferEvent; 032import org.apache.maven.wagon.proxy.ProxyInfo; 033import org.apache.maven.wagon.resource.Resource; 034import org.apache.maven.wagon.shared.http.EncodingUtil; 035import org.apache.maven.wagon.shared.http.HtmlFileListParser; 036import org.codehaus.plexus.util.Base64; 037 038import java.io.FileNotFoundException; 039import java.io.IOException; 040import java.io.InputStream; 041import java.io.OutputStream; 042import java.net.HttpURLConnection; 043import java.net.InetSocketAddress; 044import java.net.MalformedURLException; 045import java.net.PasswordAuthentication; 046import java.net.Proxy; 047import java.net.Proxy.Type; 048import java.net.SocketAddress; 049import java.net.URL; 050import java.util.ArrayList; 051import java.util.List; 052import java.util.Properties; 053import java.util.regex.Matcher; 054import java.util.regex.Pattern; 055import java.util.zip.DeflaterInputStream; 056import java.util.zip.GZIPInputStream; 057 058import static java.lang.Integer.parseInt; 059import static org.apache.maven.wagon.shared.http.HttpMessageUtils.UNKNOWN_STATUS_CODE; 060import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage; 061import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage; 062import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage; 063 064/** 065 * LightweightHttpWagon, using JDK's HttpURLConnection. 066 * 067 * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a> 068 * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup" 069 * @see HttpURLConnection 070 */ 071public class LightweightHttpWagon 072 extends StreamWagon 073{ 074 private boolean preemptiveAuthentication; 075 076 private HttpURLConnection putConnection; 077 078 private Proxy proxy = Proxy.NO_PROXY; 079 080 private static final Pattern IOEXCEPTION_MESSAGE_PATTERN = Pattern.compile( "Server returned HTTP response code: " 081 + "(\\d\\d\\d) for URL: (.*)" ); 082 083 public static final int MAX_REDIRECTS = 10; 084 085 /** 086 * Whether to use any proxy cache or not. 087 * 088 * @plexus.configuration default="false" 089 */ 090 private boolean useCache; 091 092 /** 093 * @plexus.configuration 094 */ 095 private Properties httpHeaders; 096 097 /** 098 * @plexus.requirement 099 */ 100 private volatile LightweightHttpWagonAuthenticator authenticator; 101 102 /** 103 * Builds a complete URL string from the repository URL and the relative path of the resource passed. 104 * 105 * @param resource the resource to extract the relative path from. 106 * @return the complete URL 107 */ 108 private String buildUrl( Resource resource ) 109 { 110 return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() ); 111 } 112 113 public void fillInputData( InputData inputData ) 114 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 115 { 116 Resource resource = inputData.getResource(); 117 118 String visitingUrl = buildUrl( resource ); 119 120 List<String> visitedUrls = new ArrayList<>(); 121 122 for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ ) 123 { 124 if ( visitedUrls.contains( visitingUrl ) ) 125 { 126 // TODO add a test for this message 127 throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl ); 128 } 129 visitedUrls.add( visitingUrl ); 130 131 URL url = null; 132 try 133 { 134 url = new URL( visitingUrl ); 135 } 136 catch ( MalformedURLException e ) 137 { 138 // TODO add test for this 139 throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e ); 140 } 141 142 HttpURLConnection urlConnection = null; 143 144 try 145 { 146 urlConnection = ( HttpURLConnection ) url.openConnection( this.proxy ); 147 } 148 catch ( IOException e ) 149 { 150 // TODO: add test for this 151 String message = formatTransferFailedMessage( visitingUrl, UNKNOWN_STATUS_CODE, 152 null, getProxyInfo() ); 153 // TODO include e.getMessage appended to main message? 154 throw new TransferFailedException( message, e ); 155 } 156 157 try 158 { 159 160 urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" ); 161 if ( !useCache ) 162 { 163 urlConnection.setRequestProperty( "Pragma", "no-cache" ); 164 } 165 166 addHeaders( urlConnection ); 167 168 // TODO: handle all response codes 169 int responseCode = urlConnection.getResponseCode(); 170 String reasonPhrase = urlConnection.getResponseMessage(); 171 172 // TODO Move 401/407 to AuthenticationException after WAGON-587 173 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN 174 || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED 175 || responseCode == HttpURLConnection.HTTP_PROXY_AUTH ) 176 { 177 throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), 178 responseCode, reasonPhrase, getProxyInfo() ) ); 179 } 180 if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM 181 || responseCode == HttpURLConnection.HTTP_MOVED_TEMP ) 182 { 183 visitingUrl = urlConnection.getHeaderField( "Location" ); 184 continue; 185 } 186 187 InputStream is = urlConnection.getInputStream(); 188 String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" ); 189 boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding ); 190 if ( isGZipped ) 191 { 192 is = new GZIPInputStream( is ); 193 } 194 boolean isDeflated = contentEncoding != null && "deflate".equalsIgnoreCase( contentEncoding ); 195 if ( isDeflated ) 196 { 197 is = new DeflaterInputStream( is ); 198 } 199 inputData.setInputStream( is ); 200 resource.setLastModified( urlConnection.getLastModified() ); 201 resource.setContentLength( urlConnection.getContentLength() ); 202 break; 203 204 } 205 catch ( FileNotFoundException e ) 206 { 207 // this could be 404 Not Found or 410 Gone - we don't have access to which it was. 208 // TODO: 2019-10-03 url used should list all visited/redirected urls, not just the original 209 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ), 210 UNKNOWN_STATUS_CODE, null, getProxyInfo() ), e ); 211 } 212 catch ( IOException originalIOException ) 213 { 214 throw convertHttpUrlConnectionException( originalIOException, urlConnection, buildUrl( resource ) ); 215 } 216 217 } 218 219 } 220 221 private void addHeaders( HttpURLConnection urlConnection ) 222 { 223 if ( httpHeaders != null ) 224 { 225 for ( Object header : httpHeaders.keySet() ) 226 { 227 urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) ); 228 } 229 } 230 setAuthorization( urlConnection ); 231 } 232 233 private void setAuthorization( HttpURLConnection urlConnection ) 234 { 235 if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null ) 236 { 237 String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword(); 238 String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) ); 239 urlConnection.setRequestProperty( "Authorization", "Basic " + encoded ); 240 } 241 } 242 243 public void fillOutputData( OutputData outputData ) 244 throws TransferFailedException 245 { 246 Resource resource = outputData.getResource(); 247 try 248 { 249 URL url = new URL( buildUrl( resource ) ); 250 putConnection = (HttpURLConnection) url.openConnection( this.proxy ); 251 252 addHeaders( putConnection ); 253 254 putConnection.setRequestMethod( "PUT" ); 255 putConnection.setDoOutput( true ); 256 outputData.setOutputStream( putConnection.getOutputStream() ); 257 } 258 catch ( IOException e ) 259 { 260 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 261 } 262 } 263 264 protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) 265 throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException 266 { 267 try 268 { 269 String reasonPhrase = putConnection.getResponseMessage(); 270 int statusCode = putConnection.getResponseCode(); 271 272 switch ( statusCode ) 273 { 274 // Success Codes 275 case HttpURLConnection.HTTP_OK: // 200 276 case HttpURLConnection.HTTP_CREATED: // 201 277 case HttpURLConnection.HTTP_ACCEPTED: // 202 278 case HttpURLConnection.HTTP_NO_CONTENT: // 204 279 break; 280 281 // TODO Move 401/407 to AuthenticationException after WAGON-587 282 case HttpURLConnection.HTTP_FORBIDDEN: 283 case HttpURLConnection.HTTP_UNAUTHORIZED: 284 case HttpURLConnection.HTTP_PROXY_AUTH: 285 throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode, 286 reasonPhrase, getProxyInfo() ) ); 287 288 case HttpURLConnection.HTTP_NOT_FOUND: 289 case HttpURLConnection.HTTP_GONE: 290 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ), 291 statusCode, reasonPhrase, getProxyInfo() ) ); 292 293 // add more entries here 294 default: 295 throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ), 296 statusCode, reasonPhrase, getProxyInfo() ) ) ; 297 } 298 } 299 catch ( IOException e ) 300 { 301 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 302 throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) ); 303 } 304 } 305 306 protected void openConnectionInternal() 307 throws ConnectionException, AuthenticationException 308 { 309 final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() ); 310 if ( proxyInfo != null ) 311 { 312 this.proxy = getProxy( proxyInfo ); 313 this.proxyInfo = proxyInfo; 314 } 315 authenticator.setWagon( this ); 316 317 boolean usePreemptiveAuthentication = 318 Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean( 319 repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication; 320 321 setPreemptiveAuthentication( usePreemptiveAuthentication ); 322 } 323 324 @SuppressWarnings( "deprecation" ) 325 public PasswordAuthentication requestProxyAuthentication() 326 { 327 if ( proxyInfo != null && proxyInfo.getUserName() != null ) 328 { 329 String password = ""; 330 if ( proxyInfo.getPassword() != null ) 331 { 332 password = proxyInfo.getPassword(); 333 } 334 return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() ); 335 } 336 return null; 337 } 338 339 public PasswordAuthentication requestServerAuthentication() 340 { 341 if ( authenticationInfo != null && authenticationInfo.getUserName() != null ) 342 { 343 String password = ""; 344 if ( authenticationInfo.getPassword() != null ) 345 { 346 password = authenticationInfo.getPassword(); 347 } 348 return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() ); 349 } 350 return null; 351 } 352 353 private Proxy getProxy( ProxyInfo proxyInfo ) 354 { 355 return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) ); 356 } 357 358 private Type getProxyType( ProxyInfo proxyInfo ) 359 { 360 if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals( 361 proxyInfo.getType() ) ) 362 { 363 return Type.SOCKS; 364 } 365 else 366 { 367 return Type.HTTP; 368 } 369 } 370 371 public SocketAddress getSocketAddress( ProxyInfo proxyInfo ) 372 { 373 return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() ); 374 } 375 376 public void closeConnection() 377 throws ConnectionException 378 { 379 //FIXME WAGON-375 use persistent connection feature provided by the jdk 380 if ( putConnection != null ) 381 { 382 putConnection.disconnect(); 383 } 384 authenticator.resetWagon(); 385 } 386 387 public List<String> getFileList( String destinationDirectory ) 388 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 389 { 390 InputData inputData = new InputData(); 391 392 if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) ) 393 { 394 destinationDirectory += "/"; 395 } 396 397 String url = buildUrl( new Resource( destinationDirectory ) ); 398 399 Resource resource = new Resource( destinationDirectory ); 400 401 inputData.setResource( resource ); 402 403 fillInputData( inputData ); 404 405 InputStream is = inputData.getInputStream(); 406 407 try 408 { 409 410 if ( is == null ) 411 { 412 throw new TransferFailedException( 413 url + " - Could not open input stream for resource: '" + resource + "'" ); 414 } 415 416 final List<String> htmlFileList = HtmlFileListParser.parseFileList( url, is ); 417 is.close(); 418 is = null; 419 return htmlFileList; 420 } 421 catch ( final IOException e ) 422 { 423 throw new TransferFailedException( "Failure transferring " + resource.getName(), e ); 424 } 425 finally 426 { 427 IOUtils.closeQuietly( is ); 428 } 429 } 430 431 public boolean resourceExists( String resourceName ) 432 throws TransferFailedException, AuthorizationException 433 { 434 HttpURLConnection headConnection; 435 436 try 437 { 438 Resource resource = new Resource( resourceName ); 439 URL url = new URL( buildUrl( resource ) ); 440 headConnection = (HttpURLConnection) url.openConnection( this.proxy ); 441 442 addHeaders( headConnection ); 443 444 headConnection.setRequestMethod( "HEAD" ); 445 446 int statusCode = headConnection.getResponseCode(); 447 String reasonPhrase = headConnection.getResponseMessage(); 448 449 switch ( statusCode ) 450 { 451 case HttpURLConnection.HTTP_OK: 452 return true; 453 454 case HttpURLConnection.HTTP_NOT_FOUND: 455 case HttpURLConnection.HTTP_GONE: 456 return false; 457 458 // TODO Move 401/407 to AuthenticationException after WAGON-587 459 case HttpURLConnection.HTTP_FORBIDDEN: 460 case HttpURLConnection.HTTP_UNAUTHORIZED: 461 case HttpURLConnection.HTTP_PROXY_AUTH: 462 throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), 463 statusCode, reasonPhrase, getProxyInfo() ) ); 464 465 default: 466 throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ), 467 statusCode, reasonPhrase, getProxyInfo() ) ); 468 } 469 } 470 catch ( IOException e ) 471 { 472 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 473 } 474 } 475 476 public boolean isUseCache() 477 { 478 return useCache; 479 } 480 481 public void setUseCache( boolean useCache ) 482 { 483 this.useCache = useCache; 484 } 485 486 public Properties getHttpHeaders() 487 { 488 return httpHeaders; 489 } 490 491 public void setHttpHeaders( Properties httpHeaders ) 492 { 493 this.httpHeaders = httpHeaders; 494 } 495 496 void setSystemProperty( String key, String value ) 497 { 498 if ( value != null ) 499 { 500 System.setProperty( key, value ); 501 } 502 else 503 { 504 System.getProperties().remove( key ); 505 } 506 } 507 508 public void setPreemptiveAuthentication( boolean preemptiveAuthentication ) 509 { 510 this.preemptiveAuthentication = preemptiveAuthentication; 511 } 512 513 public LightweightHttpWagonAuthenticator getAuthenticator() 514 { 515 return authenticator; 516 } 517 518 public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator ) 519 { 520 this.authenticator = authenticator; 521 } 522 523 /** 524 * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the 525 * equivalent {@link TransferFailedException}. 526 * <p> 527 * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting 528 * accessors. The returned exception will include the passed IOException as a cause and a message that is as 529 * descriptive as possible. 530 * 531 * @param originalIOException an IOException thrown from an HttpURLConnection operation 532 * @param urlConnection instance that triggered the IOException 533 * @param url originating url that triggered the IOException 534 * @return exception that is representative of the original cause 535 */ 536 private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException, 537 HttpURLConnection urlConnection, 538 String url ) 539 { 540 // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException 541 // In that case, one may attempt to get the status code and reason phrase 542 // from the errorstream. We do this, but by way of the following code path 543 // getResponseCode()/getResponseMessage() - calls -> getHeaderFields() 544 // getHeaderFields() - calls -> getErrorStream() 545 try 546 { 547 // call getResponseMessage first since impl calls getResponseCode as part of that anyways 548 String errorResponseMessage = urlConnection.getResponseMessage(); // may be null 549 int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned 550 String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage, 551 getProxyInfo() ); 552 return new TransferFailedException( message, originalIOException ); 553 554 } 555 catch ( IOException errorStreamException ) 556 { 557 // there was a problem using the standard methods, need to fall back to other options 558 } 559 560 // Attempt to parse the status code and URL which can be included in an IOException message 561 // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java 562 // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913 563 String ioMsg = originalIOException.getMessage(); 564 if ( ioMsg != null ) 565 { 566 Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg ); 567 if ( matcher.matches() ) 568 { 569 String codeStr = matcher.group( 1 ); 570 String urlStr = matcher.group( 2 ); 571 572 int code = UNKNOWN_STATUS_CODE; 573 try 574 { 575 code = parseInt( codeStr ); 576 } 577 catch ( NumberFormatException nfe ) 578 { 579 // if here there is a regex problem 580 } 581 582 String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() ); 583 return new TransferFailedException( message, originalIOException ); 584 } 585 } 586 587 String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() ); 588 return new TransferFailedException( message, originalIOException ); 589 } 590 591}