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.zip.GZIPInputStream; 054 055/** 056 * LightweightHttpWagon, using JDK's HttpURLConnection. 057 * 058 * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a> 059 * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup" 060 * @see HttpURLConnection 061 */ 062public class LightweightHttpWagon 063 extends StreamWagon 064{ 065 private boolean preemptiveAuthentication; 066 067 private HttpURLConnection putConnection; 068 069 private Proxy proxy = Proxy.NO_PROXY; 070 071 public static final int MAX_REDIRECTS = 10; 072 073 /** 074 * Whether to use any proxy cache or not. 075 * 076 * @plexus.configuration default="false" 077 */ 078 private boolean useCache; 079 080 /** 081 * @plexus.configuration 082 */ 083 private Properties httpHeaders; 084 085 /** 086 * @plexus.requirement 087 */ 088 private volatile LightweightHttpWagonAuthenticator authenticator; 089 090 /** 091 * Builds a complete URL string from the repository URL and the relative path of the resource passed. 092 * 093 * @param resource the resource to extract the relative path from. 094 * @return the complete URL 095 */ 096 private String buildUrl( Resource resource ) 097 { 098 return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() ); 099 } 100 101 public void fillInputData( InputData inputData ) 102 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 103 { 104 Resource resource = inputData.getResource(); 105 106 String visitingUrl = buildUrl( resource ); 107 try 108 { 109 List<String> visitedUrls = new ArrayList<String>(); 110 111 for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ ) 112 { 113 if ( visitedUrls.contains( visitingUrl ) ) 114 { 115 throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl ); 116 } 117 visitedUrls.add( visitingUrl ); 118 119 URL url = new URL( visitingUrl ); 120 HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection( this.proxy ); 121 122 urlConnection.setRequestProperty( "Accept-Encoding", "gzip" ); 123 if ( !useCache ) 124 { 125 urlConnection.setRequestProperty( "Pragma", "no-cache" ); 126 } 127 128 addHeaders( urlConnection ); 129 130 // TODO: handle all response codes 131 int responseCode = urlConnection.getResponseCode(); 132 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN 133 || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED ) 134 { 135 throw new AuthorizationException( "Access denied to: " + buildUrl( resource ) ); 136 } 137 if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM 138 || responseCode == HttpURLConnection.HTTP_MOVED_TEMP ) 139 { 140 visitingUrl = urlConnection.getHeaderField( "Location" ); 141 continue; 142 } 143 144 InputStream is = urlConnection.getInputStream(); 145 String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" ); 146 boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding ); 147 if ( isGZipped ) 148 { 149 is = new GZIPInputStream( is ); 150 } 151 inputData.setInputStream( is ); 152 resource.setLastModified( urlConnection.getLastModified() ); 153 resource.setContentLength( urlConnection.getContentLength() ); 154 break; 155 } 156 } 157 catch ( MalformedURLException e ) 158 { 159 throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e ); 160 } 161 catch ( FileNotFoundException e ) 162 { 163 throw new ResourceDoesNotExistException( "Unable to locate resource in repository", e ); 164 } 165 catch ( IOException e ) 166 { 167 StringBuilder message = new StringBuilder( "Error transferring file: " ); 168 message.append( e.getMessage() ); 169 message.append( " from " + visitingUrl ); 170 if ( getProxyInfo() != null && getProxyInfo().getHost() != null ) 171 { 172 message.append( " with proxyInfo " ).append( getProxyInfo().toString() ); 173 } 174 throw new TransferFailedException( message.toString(), e ); 175 } 176 } 177 178 private void addHeaders( HttpURLConnection urlConnection ) 179 { 180 if ( httpHeaders != null ) 181 { 182 for ( Object header : httpHeaders.keySet() ) 183 { 184 urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) ); 185 } 186 } 187 setAuthorization( urlConnection ); 188 } 189 190 private void setAuthorization( HttpURLConnection urlConnection ) 191 { 192 if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null ) 193 { 194 String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword(); 195 String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) ); 196 urlConnection.setRequestProperty( "Authorization", "Basic " + encoded ); 197 } 198 } 199 200 public void fillOutputData( OutputData outputData ) 201 throws TransferFailedException 202 { 203 Resource resource = outputData.getResource(); 204 try 205 { 206 URL url = new URL( buildUrl( resource ) ); 207 putConnection = (HttpURLConnection) url.openConnection( this.proxy ); 208 209 addHeaders( putConnection ); 210 211 putConnection.setRequestMethod( "PUT" ); 212 putConnection.setDoOutput( true ); 213 outputData.setOutputStream( putConnection.getOutputStream() ); 214 } 215 catch ( IOException e ) 216 { 217 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 218 } 219 } 220 221 protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) 222 throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException 223 { 224 try 225 { 226 int statusCode = putConnection.getResponseCode(); 227 228 switch ( statusCode ) 229 { 230 // Success Codes 231 case HttpURLConnection.HTTP_OK: // 200 232 case HttpURLConnection.HTTP_CREATED: // 201 233 case HttpURLConnection.HTTP_ACCEPTED: // 202 234 case HttpURLConnection.HTTP_NO_CONTENT: // 204 235 break; 236 237 case HttpURLConnection.HTTP_FORBIDDEN: 238 throw new AuthorizationException( "Access denied to: " + buildUrl( resource ) ); 239 240 case HttpURLConnection.HTTP_NOT_FOUND: 241 throw new ResourceDoesNotExistException( "File: " + buildUrl( resource ) + " does not exist" ); 242 243 // add more entries here 244 default: 245 throw new TransferFailedException( 246 "Failed to transfer file: " + buildUrl( resource ) + ". Return code is: " + statusCode ); 247 } 248 } 249 catch ( IOException e ) 250 { 251 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 252 253 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 254 } 255 } 256 257 protected void openConnectionInternal() 258 throws ConnectionException, AuthenticationException 259 { 260 final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() ); 261 if ( proxyInfo != null ) 262 { 263 this.proxy = getProxy( proxyInfo ); 264 } 265 authenticator.setWagon( this ); 266 267 boolean usePreemptiveAuthentication = 268 Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean( 269 repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication; 270 271 setPreemptiveAuthentication( usePreemptiveAuthentication ); 272 } 273 274 @SuppressWarnings( "deprecation" ) 275 public PasswordAuthentication requestProxyAuthentication() 276 { 277 if ( proxyInfo != null && proxyInfo.getUserName() != null ) 278 { 279 String password = ""; 280 if ( proxyInfo.getPassword() != null ) 281 { 282 password = proxyInfo.getPassword(); 283 } 284 return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() ); 285 } 286 return null; 287 } 288 289 public PasswordAuthentication requestServerAuthentication() 290 { 291 if ( authenticationInfo != null && authenticationInfo.getUserName() != null ) 292 { 293 String password = ""; 294 if ( authenticationInfo.getPassword() != null ) 295 { 296 password = authenticationInfo.getPassword(); 297 } 298 return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() ); 299 } 300 return null; 301 } 302 303 private Proxy getProxy( ProxyInfo proxyInfo ) 304 { 305 return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) ); 306 } 307 308 private Type getProxyType( ProxyInfo proxyInfo ) 309 { 310 if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals( 311 proxyInfo.getType() ) ) 312 { 313 return Type.SOCKS; 314 } 315 else 316 { 317 return Type.HTTP; 318 } 319 } 320 321 public SocketAddress getSocketAddress( ProxyInfo proxyInfo ) 322 { 323 return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() ); 324 } 325 326 public void closeConnection() 327 throws ConnectionException 328 { 329 //FIXME WAGON-375 use persistent connection feature provided by the jdk 330 if ( putConnection != null ) 331 { 332 putConnection.disconnect(); 333 } 334 authenticator.resetWagon(); 335 } 336 337 public List<String> getFileList( String destinationDirectory ) 338 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 339 { 340 InputData inputData = new InputData(); 341 342 if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) ) 343 { 344 destinationDirectory += "/"; 345 } 346 347 String url = buildUrl( new Resource( destinationDirectory ) ); 348 349 Resource resource = new Resource( destinationDirectory ); 350 351 inputData.setResource( resource ); 352 353 fillInputData( inputData ); 354 355 InputStream is = inputData.getInputStream(); 356 357 try 358 { 359 360 if ( is == null ) 361 { 362 throw new TransferFailedException( 363 url + " - Could not open input stream for resource: '" + resource + "'" ); 364 } 365 366 return HtmlFileListParser.parseFileList( url, is ); 367 } 368 finally 369 { 370 IOUtils.closeQuietly( is ); 371 } 372 } 373 374 public boolean resourceExists( String resourceName ) 375 throws TransferFailedException, AuthorizationException 376 { 377 HttpURLConnection headConnection; 378 379 try 380 { 381 Resource resource = new Resource( resourceName ); 382 URL url = new URL( buildUrl( resource ) ); 383 headConnection = (HttpURLConnection) url.openConnection( this.proxy ); 384 385 addHeaders( headConnection ); 386 387 headConnection.setRequestMethod( "HEAD" ); 388 headConnection.setDoOutput( true ); 389 390 int statusCode = headConnection.getResponseCode(); 391 392 switch ( statusCode ) 393 { 394 case HttpURLConnection.HTTP_OK: 395 return true; 396 397 case HttpURLConnection.HTTP_FORBIDDEN: 398 throw new AuthorizationException( "Access denied to: " + url ); 399 400 case HttpURLConnection.HTTP_NOT_FOUND: 401 return false; 402 403 case HttpURLConnection.HTTP_UNAUTHORIZED: 404 throw new AuthorizationException( "Access denied to: " + url ); 405 406 default: 407 throw new TransferFailedException( 408 "Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode ); 409 } 410 } 411 catch ( IOException e ) 412 { 413 throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); 414 } 415 } 416 417 public boolean isUseCache() 418 { 419 return useCache; 420 } 421 422 public void setUseCache( boolean useCache ) 423 { 424 this.useCache = useCache; 425 } 426 427 public Properties getHttpHeaders() 428 { 429 return httpHeaders; 430 } 431 432 public void setHttpHeaders( Properties httpHeaders ) 433 { 434 this.httpHeaders = httpHeaders; 435 } 436 437 void setSystemProperty( String key, String value ) 438 { 439 if ( value != null ) 440 { 441 System.setProperty( key, value ); 442 } 443 else 444 { 445 System.getProperties().remove( key ); 446 } 447 } 448 449 public void setPreemptiveAuthentication( boolean preemptiveAuthentication ) 450 { 451 this.preemptiveAuthentication = preemptiveAuthentication; 452 } 453 454 public LightweightHttpWagonAuthenticator getAuthenticator() 455 { 456 return authenticator; 457 } 458 459 public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator ) 460 { 461 this.authenticator = authenticator; 462 } 463}