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 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN 173 || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED ) 174 { 175 throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), 176 responseCode, reasonPhrase, getProxyInfo() ) ); 177 } 178 if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM 179 || responseCode == HttpURLConnection.HTTP_MOVED_TEMP ) 180 { 181 visitingUrl = urlConnection.getHeaderField( "Location" ); 182 continue; 183 } 184 185 InputStream is = urlConnection.getInputStream(); 186 String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" ); 187 boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding ); 188 if ( isGZipped ) 189 { 190 is = new GZIPInputStream( is ); 191 } 192 boolean isDeflated = contentEncoding != null && "deflate".equalsIgnoreCase( contentEncoding ); 193 if ( isDeflated ) 194 { 195 is = new DeflaterInputStream( is ); 196 } 197 inputData.setInputStream( is ); 198 resource.setLastModified( urlConnection.getLastModified() ); 199 resource.setContentLength( urlConnection.getContentLength() ); 200 break; 201 202 } 203 catch ( FileNotFoundException e ) 204 { 205 // this could be 404 Not Found or 410 Gone - we don't have access to which it was. 206 // TODO: 2019-10-03 url used should list all visited/redirected urls, not just the original 207 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ), 208 UNKNOWN_STATUS_CODE, null, getProxyInfo() ), e ); 209 } 210 catch ( IOException originalIOException ) 211 { 212 throw convertHttpUrlConnectionException( originalIOException, urlConnection, buildUrl( resource ) ); 213 } 214 215 } 216 217 } 218 219 private void addHeaders( HttpURLConnection urlConnection ) 220 { 221 if ( httpHeaders != null ) 222 { 223 for ( Object header : httpHeaders.keySet() ) 224 { 225 urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) ); 226 } 227 } 228 setAuthorization( urlConnection ); 229 } 230 231 private void setAuthorization( HttpURLConnection urlConnection ) 232 { 233 if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null ) 234 { 235 String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword(); 236 String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) ); 237 urlConnection.setRequestProperty( "Authorization", "Basic " + encoded ); 238 } 239 } 240 241 public void fillOutputData( OutputData outputData ) 242 throws TransferFailedException 243 { 244 Resource resource = outputData.getResource(); 245 try 246 { 247 URL url = new URL( buildUrl( resource ) ); 248 putConnection = (HttpURLConnection) url.openConnection( this.proxy ); 249 250 addHeaders( putConnection ); 251 252 putConnection.setRequestMethod( "PUT" ); 253 putConnection.setDoOutput( true ); 254 outputData.setOutputStream( putConnection.getOutputStream() ); 255 } 256 catch ( IOException e ) 257 { 258 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 259 } 260 } 261 262 protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) 263 throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException 264 { 265 try 266 { 267 String reasonPhrase = putConnection.getResponseMessage(); 268 int statusCode = putConnection.getResponseCode(); 269 270 switch ( statusCode ) 271 { 272 // Success Codes 273 case HttpURLConnection.HTTP_OK: // 200 274 case HttpURLConnection.HTTP_CREATED: // 201 275 case HttpURLConnection.HTTP_ACCEPTED: // 202 276 case HttpURLConnection.HTTP_NO_CONTENT: // 204 277 break; 278 279 // TODO: handle 401 explicitly? 280 case HttpURLConnection.HTTP_FORBIDDEN: 281 throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode, 282 reasonPhrase, getProxyInfo() ) ); 283 284 case HttpURLConnection.HTTP_NOT_FOUND: 285 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ), 286 statusCode, reasonPhrase, getProxyInfo() ) ); 287 288 // add more entries here 289 default: 290 throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ), 291 statusCode, reasonPhrase, getProxyInfo() ) ) ; 292 } 293 } 294 catch ( IOException e ) 295 { 296 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 297 throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) ); 298 } 299 } 300 301 protected void openConnectionInternal() 302 throws ConnectionException, AuthenticationException 303 { 304 final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() ); 305 if ( proxyInfo != null ) 306 { 307 this.proxy = getProxy( proxyInfo ); 308 this.proxyInfo = proxyInfo; 309 } 310 authenticator.setWagon( this ); 311 312 boolean usePreemptiveAuthentication = 313 Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean( 314 repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication; 315 316 setPreemptiveAuthentication( usePreemptiveAuthentication ); 317 } 318 319 @SuppressWarnings( "deprecation" ) 320 public PasswordAuthentication requestProxyAuthentication() 321 { 322 if ( proxyInfo != null && proxyInfo.getUserName() != null ) 323 { 324 String password = ""; 325 if ( proxyInfo.getPassword() != null ) 326 { 327 password = proxyInfo.getPassword(); 328 } 329 return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() ); 330 } 331 return null; 332 } 333 334 public PasswordAuthentication requestServerAuthentication() 335 { 336 if ( authenticationInfo != null && authenticationInfo.getUserName() != null ) 337 { 338 String password = ""; 339 if ( authenticationInfo.getPassword() != null ) 340 { 341 password = authenticationInfo.getPassword(); 342 } 343 return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() ); 344 } 345 return null; 346 } 347 348 private Proxy getProxy( ProxyInfo proxyInfo ) 349 { 350 return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) ); 351 } 352 353 private Type getProxyType( ProxyInfo proxyInfo ) 354 { 355 if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals( 356 proxyInfo.getType() ) ) 357 { 358 return Type.SOCKS; 359 } 360 else 361 { 362 return Type.HTTP; 363 } 364 } 365 366 public SocketAddress getSocketAddress( ProxyInfo proxyInfo ) 367 { 368 return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() ); 369 } 370 371 public void closeConnection() 372 throws ConnectionException 373 { 374 //FIXME WAGON-375 use persistent connection feature provided by the jdk 375 if ( putConnection != null ) 376 { 377 putConnection.disconnect(); 378 } 379 authenticator.resetWagon(); 380 } 381 382 public List<String> getFileList( String destinationDirectory ) 383 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 384 { 385 InputData inputData = new InputData(); 386 387 if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) ) 388 { 389 destinationDirectory += "/"; 390 } 391 392 String url = buildUrl( new Resource( destinationDirectory ) ); 393 394 Resource resource = new Resource( destinationDirectory ); 395 396 inputData.setResource( resource ); 397 398 fillInputData( inputData ); 399 400 InputStream is = inputData.getInputStream(); 401 402 try 403 { 404 405 if ( is == null ) 406 { 407 throw new TransferFailedException( 408 url + " - Could not open input stream for resource: '" + resource + "'" ); 409 } 410 411 final List<String> htmlFileList = HtmlFileListParser.parseFileList( url, is ); 412 is.close(); 413 is = null; 414 return htmlFileList; 415 } 416 catch ( final IOException e ) 417 { 418 throw new TransferFailedException( "Failure transferring " + resource.getName(), e ); 419 } 420 finally 421 { 422 IOUtils.closeQuietly( is ); 423 } 424 } 425 426 public boolean resourceExists( String resourceName ) 427 throws TransferFailedException, AuthorizationException 428 { 429 HttpURLConnection headConnection; 430 431 try 432 { 433 Resource resource = new Resource( resourceName ); 434 URL url = new URL( buildUrl( resource ) ); 435 headConnection = (HttpURLConnection) url.openConnection( this.proxy ); 436 437 addHeaders( headConnection ); 438 439 headConnection.setRequestMethod( "HEAD" ); 440 headConnection.setDoOutput( true ); 441 442 int statusCode = headConnection.getResponseCode(); 443 444 switch ( statusCode ) 445 { 446 case HttpURLConnection.HTTP_OK: 447 return true; 448 449 case HttpURLConnection.HTTP_FORBIDDEN: 450 throw new AuthorizationException( "Access denied to: " + url ); 451 452 case HttpURLConnection.HTTP_NOT_FOUND: 453 return false; 454 455 case HttpURLConnection.HTTP_UNAUTHORIZED: 456 throw new AuthorizationException( "Access denied to: " + url ); 457 458 default: 459 throw new TransferFailedException( 460 "Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode ); 461 } 462 } 463 catch ( IOException e ) 464 { 465 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 466 } 467 } 468 469 public boolean isUseCache() 470 { 471 return useCache; 472 } 473 474 public void setUseCache( boolean useCache ) 475 { 476 this.useCache = useCache; 477 } 478 479 public Properties getHttpHeaders() 480 { 481 return httpHeaders; 482 } 483 484 public void setHttpHeaders( Properties httpHeaders ) 485 { 486 this.httpHeaders = httpHeaders; 487 } 488 489 void setSystemProperty( String key, String value ) 490 { 491 if ( value != null ) 492 { 493 System.setProperty( key, value ); 494 } 495 else 496 { 497 System.getProperties().remove( key ); 498 } 499 } 500 501 public void setPreemptiveAuthentication( boolean preemptiveAuthentication ) 502 { 503 this.preemptiveAuthentication = preemptiveAuthentication; 504 } 505 506 public LightweightHttpWagonAuthenticator getAuthenticator() 507 { 508 return authenticator; 509 } 510 511 public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator ) 512 { 513 this.authenticator = authenticator; 514 } 515 516 /** 517 * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the 518 * equivalent {@link TransferFailedException}. 519 * <p> 520 * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting 521 * accessors. The returned exception will include the passed IOException as a cause and a message that is as 522 * descriptive as possible. 523 * 524 * @param originalIOException an IOException thrown from an HttpURLConnection operation 525 * @param urlConnection instance that triggered the IOException 526 * @param url originating url that triggered the IOException 527 * @return exception that is representative of the original cause 528 */ 529 private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException, 530 HttpURLConnection urlConnection, 531 String url ) 532 { 533 // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException 534 // In that case, one may attempt to get the status code and reason phrase 535 // from the errorstream. We do this, but by way of the following code path 536 // getResponseCode()/getResponseMessage() - calls -> getHeaderFields() 537 // getHeaderFields() - calls -> getErrorStream() 538 try 539 { 540 // call getResponseMessage first since impl calls getResponseCode as part of that anyways 541 String errorResponseMessage = urlConnection.getResponseMessage(); // may be null 542 int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned 543 String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage, 544 getProxyInfo() ); 545 return new TransferFailedException( message, originalIOException ); 546 547 } 548 catch ( IOException errorStreamException ) 549 { 550 // there was a problem using the standard methods, need to fall back to other options 551 } 552 553 // Attempt to parse the status code and URL which can be included in an IOException message 554 // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java 555 // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913 556 String ioMsg = originalIOException.getMessage(); 557 if ( ioMsg != null ) 558 { 559 Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg ); 560 if ( matcher.matches() ) 561 { 562 String codeStr = matcher.group( 1 ); 563 String urlStr = matcher.group( 2 ); 564 565 int code = UNKNOWN_STATUS_CODE; 566 try 567 { 568 code = parseInt( codeStr ); 569 } 570 catch ( NumberFormatException nfe ) 571 { 572 // if here there is a regex problem 573 } 574 575 String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() ); 576 return new TransferFailedException( message, originalIOException ); 577 } 578 } 579 580 String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() ); 581 return new TransferFailedException( message, originalIOException ); 582 } 583 584}