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.maven.wagon.ConnectionException; 023import org.apache.maven.wagon.InputData; 024import org.apache.maven.wagon.OutputData; 025import org.apache.maven.wagon.ResourceDoesNotExistException; 026import org.apache.maven.wagon.StreamWagon; 027import org.apache.maven.wagon.TransferFailedException; 028import org.apache.maven.wagon.authentication.AuthenticationException; 029import org.apache.maven.wagon.authorization.AuthorizationException; 030import org.apache.maven.wagon.events.TransferEvent; 031import org.apache.maven.wagon.proxy.ProxyInfo; 032import org.apache.maven.wagon.resource.Resource; 033import org.apache.maven.wagon.shared.http.EncodingUtil; 034import org.codehaus.plexus.util.Base64; 035 036import java.io.FileNotFoundException; 037import java.io.IOException; 038import java.io.InputStream; 039import java.io.OutputStream; 040import java.net.HttpURLConnection; 041import java.net.InetSocketAddress; 042import java.net.MalformedURLException; 043import java.net.PasswordAuthentication; 044import java.net.Proxy; 045import java.net.Proxy.Type; 046import java.net.SocketAddress; 047import java.net.URL; 048import java.util.ArrayList; 049import java.util.List; 050import java.util.Properties; 051import java.util.regex.Matcher; 052import java.util.regex.Pattern; 053import java.util.zip.DeflaterInputStream; 054import java.util.zip.GZIPInputStream; 055 056import static java.lang.Integer.parseInt; 057import static org.apache.maven.wagon.shared.http.HttpMessageUtils.UNKNOWN_STATUS_CODE; 058import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage; 059import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage; 060import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage; 061 062/** 063 * LightweightHttpWagon, using JDK's HttpURLConnection. 064 * 065 * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a> 066 * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup" 067 * @see HttpURLConnection 068 */ 069public class LightweightHttpWagon 070 extends StreamWagon 071{ 072 private boolean preemptiveAuthentication; 073 074 private HttpURLConnection putConnection; 075 076 private Proxy proxy = Proxy.NO_PROXY; 077 078 private static final Pattern IOEXCEPTION_MESSAGE_PATTERN = Pattern.compile( "Server returned HTTP response code: " 079 + "(\\d\\d\\d) for URL: (.*)" ); 080 081 public static final int MAX_REDIRECTS = 10; 082 083 /** 084 * Whether to use any proxy cache or not. 085 * 086 * @plexus.configuration default="false" 087 */ 088 private boolean useCache; 089 090 /** 091 * @plexus.configuration 092 */ 093 private Properties httpHeaders; 094 095 /** 096 * @plexus.requirement 097 */ 098 private volatile LightweightHttpWagonAuthenticator authenticator; 099 100 /** 101 * Builds a complete URL string from the repository URL and the relative path of the resource passed. 102 * 103 * @param resource the resource to extract the relative path from. 104 * @return the complete URL 105 */ 106 private String buildUrl( Resource resource ) 107 { 108 return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() ); 109 } 110 111 public void fillInputData( InputData inputData ) 112 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 113 { 114 Resource resource = inputData.getResource(); 115 116 String visitingUrl = buildUrl( resource ); 117 118 List<String> visitedUrls = new ArrayList<>(); 119 120 for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ ) 121 { 122 if ( visitedUrls.contains( visitingUrl ) ) 123 { 124 // TODO add a test for this message 125 throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl ); 126 } 127 visitedUrls.add( visitingUrl ); 128 129 URL url = null; 130 try 131 { 132 url = new URL( visitingUrl ); 133 } 134 catch ( MalformedURLException e ) 135 { 136 // TODO add test for this 137 throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e ); 138 } 139 140 HttpURLConnection urlConnection = null; 141 142 try 143 { 144 urlConnection = ( HttpURLConnection ) url.openConnection( this.proxy ); 145 } 146 catch ( IOException e ) 147 { 148 // TODO: add test for this 149 String message = formatTransferFailedMessage( visitingUrl, UNKNOWN_STATUS_CODE, 150 null, getProxyInfo() ); 151 // TODO include e.getMessage appended to main message? 152 throw new TransferFailedException( message, e ); 153 } 154 155 try 156 { 157 158 urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" ); 159 if ( !useCache ) 160 { 161 urlConnection.setRequestProperty( "Pragma", "no-cache" ); 162 } 163 164 addHeaders( urlConnection ); 165 166 // TODO: handle all response codes 167 int responseCode = urlConnection.getResponseCode(); 168 String reasonPhrase = urlConnection.getResponseMessage(); 169 170 // TODO Move 401/407 to AuthenticationException after WAGON-587 171 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN 172 || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED 173 || responseCode == HttpURLConnection.HTTP_PROXY_AUTH ) 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 Move 401/407 to AuthenticationException after WAGON-587 280 case HttpURLConnection.HTTP_FORBIDDEN: 281 case HttpURLConnection.HTTP_UNAUTHORIZED: 282 case HttpURLConnection.HTTP_PROXY_AUTH: 283 throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode, 284 reasonPhrase, getProxyInfo() ) ); 285 286 case HttpURLConnection.HTTP_NOT_FOUND: 287 case HttpURLConnection.HTTP_GONE: 288 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ), 289 statusCode, reasonPhrase, getProxyInfo() ) ); 290 291 // add more entries here 292 default: 293 throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ), 294 statusCode, reasonPhrase, getProxyInfo() ) ) ; 295 } 296 } 297 catch ( IOException e ) 298 { 299 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 300 throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) ); 301 } 302 } 303 304 protected void openConnectionInternal() 305 throws ConnectionException, AuthenticationException 306 { 307 final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() ); 308 if ( proxyInfo != null ) 309 { 310 this.proxy = getProxy( proxyInfo ); 311 this.proxyInfo = proxyInfo; 312 } 313 authenticator.setWagon( this ); 314 315 boolean usePreemptiveAuthentication = 316 Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean( 317 repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication; 318 319 setPreemptiveAuthentication( usePreemptiveAuthentication ); 320 } 321 322 @SuppressWarnings( "deprecation" ) 323 public PasswordAuthentication requestProxyAuthentication() 324 { 325 if ( proxyInfo != null && proxyInfo.getUserName() != null ) 326 { 327 String password = ""; 328 if ( proxyInfo.getPassword() != null ) 329 { 330 password = proxyInfo.getPassword(); 331 } 332 return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() ); 333 } 334 return null; 335 } 336 337 public PasswordAuthentication requestServerAuthentication() 338 { 339 if ( authenticationInfo != null && authenticationInfo.getUserName() != null ) 340 { 341 String password = ""; 342 if ( authenticationInfo.getPassword() != null ) 343 { 344 password = authenticationInfo.getPassword(); 345 } 346 return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() ); 347 } 348 return null; 349 } 350 351 private Proxy getProxy( ProxyInfo proxyInfo ) 352 { 353 return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) ); 354 } 355 356 private Type getProxyType( ProxyInfo proxyInfo ) 357 { 358 if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals( 359 proxyInfo.getType() ) ) 360 { 361 return Type.SOCKS; 362 } 363 else 364 { 365 return Type.HTTP; 366 } 367 } 368 369 public SocketAddress getSocketAddress( ProxyInfo proxyInfo ) 370 { 371 return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() ); 372 } 373 374 public void closeConnection() 375 throws ConnectionException 376 { 377 //FIXME WAGON-375 use persistent connection feature provided by the jdk 378 if ( putConnection != null ) 379 { 380 putConnection.disconnect(); 381 } 382 authenticator.resetWagon(); 383 } 384 385 public boolean resourceExists( String resourceName ) 386 throws TransferFailedException, AuthorizationException 387 { 388 HttpURLConnection headConnection; 389 390 try 391 { 392 Resource resource = new Resource( resourceName ); 393 URL url = new URL( buildUrl( resource ) ); 394 headConnection = (HttpURLConnection) url.openConnection( this.proxy ); 395 396 addHeaders( headConnection ); 397 398 headConnection.setRequestMethod( "HEAD" ); 399 400 int statusCode = headConnection.getResponseCode(); 401 String reasonPhrase = headConnection.getResponseMessage(); 402 403 switch ( statusCode ) 404 { 405 case HttpURLConnection.HTTP_OK: 406 return true; 407 408 case HttpURLConnection.HTTP_NOT_FOUND: 409 case HttpURLConnection.HTTP_GONE: 410 return false; 411 412 // TODO Move 401/407 to AuthenticationException after WAGON-587 413 case HttpURLConnection.HTTP_FORBIDDEN: 414 case HttpURLConnection.HTTP_UNAUTHORIZED: 415 case HttpURLConnection.HTTP_PROXY_AUTH: 416 throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), 417 statusCode, reasonPhrase, getProxyInfo() ) ); 418 419 default: 420 throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ), 421 statusCode, reasonPhrase, getProxyInfo() ) ); 422 } 423 } 424 catch ( IOException e ) 425 { 426 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 427 } 428 } 429 430 public boolean isUseCache() 431 { 432 return useCache; 433 } 434 435 public void setUseCache( boolean useCache ) 436 { 437 this.useCache = useCache; 438 } 439 440 public Properties getHttpHeaders() 441 { 442 return httpHeaders; 443 } 444 445 public void setHttpHeaders( Properties httpHeaders ) 446 { 447 this.httpHeaders = httpHeaders; 448 } 449 450 void setSystemProperty( String key, String value ) 451 { 452 if ( value != null ) 453 { 454 System.setProperty( key, value ); 455 } 456 else 457 { 458 System.getProperties().remove( key ); 459 } 460 } 461 462 public void setPreemptiveAuthentication( boolean preemptiveAuthentication ) 463 { 464 this.preemptiveAuthentication = preemptiveAuthentication; 465 } 466 467 public LightweightHttpWagonAuthenticator getAuthenticator() 468 { 469 return authenticator; 470 } 471 472 public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator ) 473 { 474 this.authenticator = authenticator; 475 } 476 477 /** 478 * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the 479 * equivalent {@link TransferFailedException}. 480 * <p> 481 * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting 482 * accessors. The returned exception will include the passed IOException as a cause and a message that is as 483 * descriptive as possible. 484 * 485 * @param originalIOException an IOException thrown from an HttpURLConnection operation 486 * @param urlConnection instance that triggered the IOException 487 * @param url originating url that triggered the IOException 488 * @return exception that is representative of the original cause 489 */ 490 private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException, 491 HttpURLConnection urlConnection, 492 String url ) 493 { 494 // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException 495 // In that case, one may attempt to get the status code and reason phrase 496 // from the errorstream. We do this, but by way of the following code path 497 // getResponseCode()/getResponseMessage() - calls -> getHeaderFields() 498 // getHeaderFields() - calls -> getErrorStream() 499 try 500 { 501 // call getResponseMessage first since impl calls getResponseCode as part of that anyways 502 String errorResponseMessage = urlConnection.getResponseMessage(); // may be null 503 int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned 504 String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage, 505 getProxyInfo() ); 506 return new TransferFailedException( message, originalIOException ); 507 508 } 509 catch ( IOException errorStreamException ) 510 { 511 // there was a problem using the standard methods, need to fall back to other options 512 } 513 514 // Attempt to parse the status code and URL which can be included in an IOException message 515 // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java 516 // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913 517 String ioMsg = originalIOException.getMessage(); 518 if ( ioMsg != null ) 519 { 520 Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg ); 521 if ( matcher.matches() ) 522 { 523 String codeStr = matcher.group( 1 ); 524 String urlStr = matcher.group( 2 ); 525 526 int code = UNKNOWN_STATUS_CODE; 527 try 528 { 529 code = parseInt( codeStr ); 530 } 531 catch ( NumberFormatException nfe ) 532 { 533 // if here there is a regex problem 534 } 535 536 String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() ); 537 return new TransferFailedException( message, originalIOException ); 538 } 539 } 540 541 String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() ); 542 return new TransferFailedException( message, originalIOException ); 543 } 544 545}