blob: e119d6acf3ba032200fb1f5996847a032cabbed0 [file] [log] [blame]
package org.apache.maven.wagon.providers.http;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Stack;
import java.util.TimeZone;
import java.util.zip.GZIPInputStream;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.OptionsMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.util.DateParseException;
import org.apache.commons.httpclient.util.DateParser;
import org.apache.maven.wagon.AbstractWagon;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.events.TransferEvent;
import org.apache.maven.wagon.providers.http.dav.DavResource;
import org.apache.maven.wagon.providers.http.dav.MkColMethod;
import org.apache.maven.wagon.providers.http.dav.MultiStatus;
import org.apache.maven.wagon.providers.http.dav.PropFindMethod;
import org.apache.maven.wagon.providers.http.links.LinkParser;
import org.apache.maven.wagon.resource.Resource;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.xml.sax.SAXException;
/**
* @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
* @version $Id$
*/
public class HttpWagon
extends AbstractWagon
{
private static final int SC_NULL = -1;
private HttpClient client;
private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone( "GMT" );
private HttpConnectionManager connectionManager;
private LinkParser linkParser = new LinkParser();
protected boolean isDav;
public void openConnection()
{
client = new HttpClient( connectionManager );
String username = null;
String password = null;
if ( authenticationInfo != null )
{
username = authenticationInfo.getUserName();
password = authenticationInfo.getPassword();
}
String host = getRepository().getHost();
if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
{
Credentials creds = new UsernamePasswordCredentials( username, password );
client.getState().setCredentials( null, host, creds );
client.getState().setAuthenticationPreemptive( true );
}
HostConfiguration hc = new HostConfiguration();
if ( proxyInfo != null )
{
String proxyUsername = proxyInfo.getUserName();
String proxyPassword = proxyInfo.getPassword();
String proxyHost = proxyInfo.getHost();
int proxyPort = proxyInfo.getPort();
String proxyNtlmHost = proxyInfo.getNtlmHost();
String proxyNtlmDomain = proxyInfo.getNtlmDomain();
if ( proxyHost != null )
{
hc.setProxy( proxyHost, proxyPort );
if ( proxyUsername != null && proxyPassword != null )
{
Credentials creds;
if ( proxyNtlmHost != null || proxyNtlmDomain != null )
{
creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
}
else
{
creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
}
client.getState().setProxyCredentials( null, proxyHost, creds );
client.getState().setAuthenticationPreemptive( true );
}
}
}
hc.setHost( host );
//start a session with the webserver
client.setHostConfiguration( hc );
}
// put
public void put( File source, String resourceName )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
String url = getRepository().getUrl() + "/" + resourceName;
Resource resource = new Resource( resourceName );
// Try simple put first (works 90% of the time)
int statusCode = putSource( url, source, resource );
if ( isSuccessfulPUT( statusCode ) )
{
// expected path.
return;
}
// Problem. Check that the collections exist.
if ( ( isDav ) && ( statusCode == HttpStatus.SC_CONFLICT ) )
{
URI absoluteURI = toURI( url );
URI parentURI = absoluteURI.resolve( "./" ).normalize();
Stack/*<URI>*/missingPaths = davGetMissingPaths( parentURI );
if ( missingPaths.empty() )
{
throw new TransferFailedException( "Unable to put (Conflict, collections exist) file "
+ source.getAbsolutePath() + " to " + absoluteURI.toASCIIString() );
}
while ( !missingPaths.empty() )
{
URI missingPath = (URI) missingPaths.pop();
davCreateCollection( missingPath );
}
statusCode = putSource( url, source, resource );
if ( isSuccessfulPUT( statusCode ) )
{
// Expected Good result.
return;
}
}
throw new TransferFailedException( "Unable to upload (" + statusCode + "/"
+ HttpStatus.getStatusText( statusCode ) + ") file " + source.getAbsolutePath() + " to " + url );
}
/**
* HTTP RFC 2616 section 9.6 "PUT": "If an existing resource is modified,
* either the 200 (OK) or 204 (No Content) response codes SHOULD be sent
* to indicate successful completion of the request."
*/
private boolean isSuccessfulPUT( int status )
{
return ( ( status == HttpStatus.SC_CREATED ) || ( status == HttpStatus.SC_OK ) || ( status == HttpStatus.SC_NO_CONTENT ) );
}
private int putSource( String url, File source, Resource resource )
throws ResourceDoesNotExistException, TransferFailedException, AuthorizationException
{
firePutInitiated( resource, source );
PutMethod putMethod = new PutMethod( url );
putMethod.getParams().setSoTimeout( getTimeout() );
// TODO: worry about setting the Mime-Type on the request header.
try
{
InputStream is = new PutInputStream( source, resource, this, getTransferEventSupport() );
putMethod.setRequestBody( is );
}
catch ( FileNotFoundException e )
{
fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
throw new ResourceDoesNotExistException( "Source file does not exist: " + source, e );
}
int statusCode = execute( putMethod );
fireTransferDebug( url + " - Status code: " + statusCode );
// Check that we didn't run out of retries.
switch ( statusCode )
{
// Success Codes
/**
* HTTP RFC 2616 section 9.6 "PUT": "If an existing resource is modified,
* either the 200 (OK) or 204 (No Content) response codes SHOULD be sent
* to indicate successful completion of the request."
*/
case HttpStatus.SC_OK: // 200
case HttpStatus.SC_CREATED: // 201
case HttpStatus.SC_ACCEPTED: // 202
case HttpStatus.SC_NO_CONTENT: // 204
break;
/* 409/Conflict is usually seen when attempting to PUT on a
* WebDAV server without the parent Collections existing first.
*
* We want to exit out of this and allow the put() routine to
* manage the creation of the collections.
*/
case HttpStatus.SC_CONFLICT: // 409
break;
case SC_NULL:
throw new TransferFailedException( "Failed to transfer file: " + url );
case HttpStatus.SC_FORBIDDEN:
throw new AuthorizationException( "Access denied to: " + url );
case HttpStatus.SC_NOT_FOUND:
throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
//add more entries here
default:
throw new TransferFailedException( "Failed to transfer file: " + url + ". Return code is: "
+ statusCode );
}
putMethod.releaseConnection();
firePutCompleted( resource, source );
return statusCode;
}
public void davCreateCollection( URI uri )
throws TransferFailedException
{
MkColMethod method = new MkColMethod( uri );
try
{
int status = client.executeMethod( method );
if ( status == HttpStatus.SC_CREATED )
{
return;
}
throw new TransferFailedException( "Unable to create collection (" + status + "/"
+ HttpStatus.getStatusText( status ) + "): " + uri.toASCIIString() );
}
catch ( HttpException e )
{
throw new TransferFailedException( "Unable to create collection: " + uri.toASCIIString(), e );
}
catch ( IOException e )
{
throw new TransferFailedException( "Unable to create collection: " + uri.toASCIIString(), e );
}
finally
{
method.releaseConnection();
}
}
/**
* Collect the stack of missing paths.
*
* @param absoluteURI the abosoluteURI to start from.
* @return
* @throws TransferFailedException
*/
public Stack/*<URI>*/davGetMissingPaths( URI targetURI )
throws TransferFailedException
{
URI baseuri = toURI( getRepository().getUrl() );
Stack/*<URI>*/missingPaths = new Stack/*<URI>*/();
URI currentURI = targetURI;
if ( !currentURI.getPath().endsWith( "/" ) )
{
try
{
currentURI = new URI( targetURI.toASCIIString() + "/" );
}
catch ( URISyntaxException e )
{
fireTransferDebug( "Should never happen: " + e.getMessage() );
}
}
boolean done = false;
while ( !done )
{
if ( targetURI.equals( baseuri ) )
{
done = true;
break;
}
if ( davCollectionExists( currentURI ) )
{
done = true;
break;
}
missingPaths.push( currentURI );
currentURI = currentURI.resolve( "../" ).normalize();
}
return missingPaths;
}
public boolean davCollectionExists( URI uri )
throws TransferFailedException
{
PropFindMethod method = new PropFindMethod( uri );
try
{
method.setDepth( 1 );
int status = client.executeMethod( method );
if ( ( status == HttpStatus.SC_MULTI_STATUS ) || ( status == HttpStatus.SC_OK ) )
{
MultiStatus multistatus = method.getMultiStatus();
if ( multistatus == null )
{
return false;
}
DavResource resource = multistatus.getResource( uri.getPath() );
if ( resource == null )
{
return false;
}
return resource.isCollection();
}
if ( status == HttpStatus.SC_BAD_REQUEST )
{
throw new TransferFailedException( "Bad HTTP Request (400) during PROPFIND on \"" + uri.toASCIIString()
+ "\"" );
}
}
catch ( HttpException e )
{
fireTransferDebug( "Can't determine if collection exists: " + uri + " : " + e.getMessage() );
}
catch ( IOException e )
{
fireTransferDebug( "Can't determine if collection exists: " + uri + " : " + e.getMessage() );
}
finally
{
method.releaseConnection();
}
return false;
}
public void closeConnection()
{
}
public void get( String resourceName, File destination )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
get( resourceName, destination, 0 );
}
public boolean getIfNewer( String resourceName, File destination, long timestamp )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
return get( resourceName, destination, timestamp );
}
/**
* @param resourceName
* @param destination
* @param timestamp the timestamp to check against, only downloading if newer. If <code>0</code>, always download
* @return <code>true</code> if newer version was downloaded, <code>false</code> otherwise.
* @throws TransferFailedException
* @throws ResourceDoesNotExistException
* @throws AuthorizationException
*/
public boolean get( String resourceName, File destination, long timestamp )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
Resource resource = new Resource( resourceName );
fireGetInitiated( resource, destination );
boolean retValue = false;
String url = getRepository().getUrl() + "/" + resourceName;
GetMethod getMethod = new GetMethod( url );
getMethod.getParams().setSoTimeout( getTimeout() );
try
{
// TODO: make these configurable
getMethod.addRequestHeader( "Cache-control", "no-cache" );
getMethod.addRequestHeader( "Cache-store", "no-store" );
getMethod.addRequestHeader( "Pragma", "no-cache" );
getMethod.addRequestHeader( "Expires", "0" );
getMethod.addRequestHeader( "Accept-Encoding", "gzip" );
if ( timestamp > 0 )
{
SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
fmt.setTimeZone( GMT_TIME_ZONE );
Header hdr = new Header( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
getMethod.addRequestHeader( hdr );
}
int statusCode = execute( getMethod );
fireTransferDebug( url + " - Status code: " + statusCode );
// TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is required
switch ( statusCode )
{
case HttpStatus.SC_OK:
break;
case HttpStatus.SC_NOT_MODIFIED:
return false;
case SC_NULL:
throw new TransferFailedException( "Failed to transfer file: " + url );
case HttpStatus.SC_FORBIDDEN:
throw new AuthorizationException( "Access denied to: " + url );
case HttpStatus.SC_UNAUTHORIZED:
throw new AuthorizationException( "Not authorized." );
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
throw new AuthorizationException( "Not authorized by proxy." );
case HttpStatus.SC_NOT_FOUND:
throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
//add more entries here
default:
throw new TransferFailedException( "Failed to transfer file: " + url + ". Return code is: "
+ statusCode );
}
InputStream is = null;
Header contentLengthHeader = getMethod.getResponseHeader( "Content-Length" );
if ( contentLengthHeader != null )
{
try
{
long contentLength = Integer.valueOf( contentLengthHeader.getValue() ).intValue();
resource.setContentLength( contentLength );
}
catch ( NumberFormatException e )
{
fireTransferDebug( "error parsing content length header '" + contentLengthHeader.getValue() + "' "
+ e );
}
}
Header lastModifiedHeader = getMethod.getResponseHeader( "Last-Modified" );
long lastModified = 0;
if ( lastModifiedHeader != null )
{
try
{
lastModified = DateParser.parseDate( lastModifiedHeader.getValue() ).getTime();
}
catch ( DateParseException e )
{
fireTransferDebug( "Unable to parse last modified header" );
}
fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() + " (" + lastModified + ")" );
}
// always get if timestamp is 0 (ie, target doesn't exist), otherwise only if older than the remote file
if ( timestamp == 0 || timestamp < lastModified )
{
retValue = true;
Header contentEncoding = getMethod.getResponseHeader( "Content-Encoding" );
boolean isGZipped = contentEncoding == null ? false : "gzip".equalsIgnoreCase( contentEncoding
.getValue() );
try
{
is = getMethod.getResponseBodyAsStream();
if ( isGZipped )
{
is = new GZIPInputStream( is );
}
getTransfer( resource, destination, is );
}
catch ( IOException e )
{
fireTransferError( resource, e, TransferEvent.REQUEST_GET );
if ( destination.exists() )
{
boolean deleted = destination.delete();
if ( !deleted )
{
destination.deleteOnExit();
}
}
String msg = "Error occurred while deploying to remote repository:" + getRepository();
throw new TransferFailedException( msg, e );
}
finally
{
IOUtil.close( is );
}
if ( lastModified > 0 )
{
resource.setLastModified( lastModified );
}
}
else
{
fireTransferDebug( "Local file is newer: not downloaded" );
}
return retValue;
}
finally
{
getMethod.releaseConnection();
}
}
public List getFileList( String destinationDirectory )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
if ( !destinationDirectory.endsWith( "/" ) )
{
destinationDirectory += "/";
}
String url = getRepository().getUrl() + "/" + destinationDirectory;
if ( isDav )
{
return getDavListing( url );
}
else
{
return getHttpListing( url );
}
}
private List getDavListing( String url )
throws TransferFailedException
{
URI absoluteURI = toURI( url );
PropFindMethod method = new PropFindMethod( absoluteURI );
try
{
int status = client.executeMethod( method );
if ( ( status == HttpStatus.SC_MULTI_STATUS ) || ( status == HttpStatus.SC_OK ) )
{
MultiStatus multistatus = method.getMultiStatus();
Set/*<String>*/listing = new HashSet/*<String>*/();
Iterator itresources = multistatus.getResources().iterator();
while ( itresources.hasNext() )
{
DavResource resource = (DavResource) itresources.next();
if ( resource.isCollection() )
{
continue;
}
String href = resource.getHref();
int idx = href.lastIndexOf( '/' );
if ( idx < 0 )
{
listing.add( href );
}
else
{
listing.add( href.substring( idx + 1 ) );
}
}
return new ArrayList( listing );
}
return Collections.emptyList();
}
catch ( IOException e )
{
throw new TransferFailedException( "Could not read response body.", e );
}
finally
{
method.releaseConnection();
}
}
private List getHttpListing( String url )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
URI absoluteURI = toURI( url );
GetMethod getMethod = new GetMethod( url );
getMethod.getParams().setSoTimeout( getTimeout() );
try
{
// TODO: make these configurable
getMethod.addRequestHeader( "Cache-control", "no-cache" );
getMethod.addRequestHeader( "Cache-store", "no-store" );
getMethod.addRequestHeader( "Pragma", "no-cache" );
getMethod.addRequestHeader( "Expires", "0" );
int statusCode = execute( getMethod );
fireTransferDebug( url + " - Status code: " + statusCode );
// TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is required
switch ( statusCode )
{
case HttpStatus.SC_OK:
break;
case SC_NULL:
throw new TransferFailedException( "Failed to transfer file: " );
case HttpStatus.SC_FORBIDDEN:
throw new AuthorizationException( "Access denied to: " + url );
case HttpStatus.SC_UNAUTHORIZED:
throw new AuthorizationException( "Not authorized." );
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
throw new AuthorizationException( "Not authorized by proxy." );
case HttpStatus.SC_NOT_FOUND:
throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
//add more entries here
default:
throw new TransferFailedException( "Failed to transfer file: " + url + ". Return code is: "
+ statusCode );
}
InputStream is = null;
is = getMethod.getResponseBodyAsStream();
Set/*<String>*/links = linkParser.collectLinks( absoluteURI, is );
return new ArrayList( links );
}
catch ( IOException e )
{
throw new TransferFailedException( "Could not read response body.", e );
}
catch ( SAXException e )
{
throw new TransferFailedException( "Could not parse response body.", e );
}
finally
{
getMethod.releaseConnection();
}
}
private URI toURI( String url )
throws TransferFailedException
{
try
{
return new URI( url );
}
catch ( URISyntaxException e )
{
throw new TransferFailedException( "Invalid uri: " + url );
}
}
public boolean resourceExists( String resourceName )
throws TransferFailedException, AuthorizationException
{
String url = getRepository().getUrl() + "/" + resourceName;
HeadMethod headMethod = new HeadMethod( url );
headMethod.getParams().setSoTimeout( getTimeout() );
int statusCode = execute( headMethod );
try
{
switch ( statusCode )
{
case HttpStatus.SC_OK:
return true;
case HttpStatus.SC_NOT_MODIFIED:
return true;
case SC_NULL:
throw new TransferFailedException( "Failed to transfer file: " + url );
case HttpStatus.SC_FORBIDDEN:
throw new AuthorizationException( "Access denied to: " + url );
case HttpStatus.SC_UNAUTHORIZED:
throw new AuthorizationException( "Not authorized." );
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
throw new AuthorizationException( "Not authorized by proxy." );
case HttpStatus.SC_NOT_FOUND:
return false;
//add more entries here
default:
throw new TransferFailedException( "Failed to transfer file: " + url + ". Return code is: "
+ statusCode );
}
}
finally
{
headMethod.releaseConnection();
}
}
private int execute( HttpMethod httpMethod )
throws TransferFailedException
{
int statusCode = SC_NULL;
try
{
// execute the method.
statusCode = client.executeMethod( httpMethod );
}
catch ( IOException e )
{
throw new TransferFailedException( e.getMessage(), e );
}
return statusCode;
}
public void setConnectionManager( HttpConnectionManager connectionManager )
{
this.connectionManager = connectionManager;
}
public boolean isWebDavCapableServer()
{
String baseurl = getRepository().getUrl() + "/";
OptionsMethod method = new OptionsMethod( baseurl );
try
{
int status = client.executeMethod( method );
if ( status == HttpStatus.SC_OK )
{
// Test DAV Header Options
boolean supportsDav1 = false;
Header davOptionHeader = method.getResponseHeader( "dav" );
if ( davOptionHeader != null )
{
String davSupport[] = StringUtils.split( davOptionHeader.getValue(), "," );
for ( int i = 0; i < davSupport.length; i++ )
{
String support = davSupport[i].trim();
support = support.trim();
if ( "1".equals( support ) )
{
supportsDav1 = true;
}
}
}
if ( !supportsDav1 )
{
fireTransferDebug( "No DAV Support: " + baseurl );
return false;
}
// Not validate.
String requiredMethods[] = new String[] { "HEAD", "GET", "MKCOL", "PROPFIND", "PUT", "OPTIONS" };
boolean supportsRequired = true;
for ( int i = 0; i < requiredMethods.length; i++ )
{
String required = requiredMethods[i];
if ( !method.isAllowed( required ) )
{
fireTransferDebug( "No " + required + " Support: " + baseurl );
supportsRequired = false;
}
}
return supportsRequired;
}
}
catch ( HttpException e )
{
fireTransferDebug( "Unable to get OPTIONS from " + baseurl + " : " + e.getMessage() );
}
catch ( IOException e )
{
fireTransferDebug( "Unable to get OPTIONS from " + baseurl + " : " + e.getMessage() );
}
finally
{
method.releaseConnection();
}
return false;
}
}