blob: 3366e53d8e248c108dd05ec7365233320d7d61a0 [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 org.apache.commons.io.IOUtils;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.InputData;
import org.apache.maven.wagon.OutputData;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.StreamWagon;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.events.TransferEvent;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.resource.Resource;
import org.apache.maven.wagon.shared.http.EncodingUtil;
import org.apache.maven.wagon.shared.http.HtmlFileListParser;
import org.codehaus.plexus.util.Base64;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.SocketAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.DeflaterInputStream;
import java.util.zip.GZIPInputStream;
import static java.lang.Integer.parseInt;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.UNKNOWN_STATUS_CODE;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;
/**
* LightweightHttpWagon, using JDK's HttpURLConnection.
*
* @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
* @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup"
* @see HttpURLConnection
*/
public class LightweightHttpWagon
extends StreamWagon
{
private boolean preemptiveAuthentication;
private HttpURLConnection putConnection;
private Proxy proxy = Proxy.NO_PROXY;
private static final Pattern IOEXCEPTION_MESSAGE_PATTERN = Pattern.compile( "Server returned HTTP response code: "
+ "(\\d\\d\\d) for URL: (.*)" );
public static final int MAX_REDIRECTS = 10;
/**
* Whether to use any proxy cache or not.
*
* @plexus.configuration default="false"
*/
private boolean useCache;
/**
* @plexus.configuration
*/
private Properties httpHeaders;
/**
* @plexus.requirement
*/
private volatile LightweightHttpWagonAuthenticator authenticator;
/**
* Builds a complete URL string from the repository URL and the relative path of the resource passed.
*
* @param resource the resource to extract the relative path from.
* @return the complete URL
*/
private String buildUrl( Resource resource )
{
return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() );
}
public void fillInputData( InputData inputData )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
Resource resource = inputData.getResource();
String visitingUrl = buildUrl( resource );
List<String> visitedUrls = new ArrayList<>();
for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ )
{
if ( visitedUrls.contains( visitingUrl ) )
{
// TODO add a test for this message
throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl );
}
visitedUrls.add( visitingUrl );
URL url = null;
try
{
url = new URL( visitingUrl );
}
catch ( MalformedURLException e )
{
// TODO add test for this
throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e );
}
HttpURLConnection urlConnection = null;
try
{
urlConnection = ( HttpURLConnection ) url.openConnection( this.proxy );
}
catch ( IOException e )
{
// TODO: add test for this
String message = formatTransferFailedMessage( visitingUrl, UNKNOWN_STATUS_CODE,
null, getProxyInfo() );
// TODO include e.getMessage appended to main message?
throw new TransferFailedException( message, e );
}
try
{
urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" );
if ( !useCache )
{
urlConnection.setRequestProperty( "Pragma", "no-cache" );
}
addHeaders( urlConnection );
// TODO: handle all response codes
int responseCode = urlConnection.getResponseCode();
String reasonPhrase = urlConnection.getResponseMessage();
if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN
|| responseCode == HttpURLConnection.HTTP_UNAUTHORIZED
|| responseCode == HttpURLConnection.HTTP_PROXY_AUTH )
{
throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ),
responseCode, reasonPhrase, getProxyInfo() ) );
}
if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP )
{
visitingUrl = urlConnection.getHeaderField( "Location" );
continue;
}
InputStream is = urlConnection.getInputStream();
String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" );
boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding );
if ( isGZipped )
{
is = new GZIPInputStream( is );
}
boolean isDeflated = contentEncoding != null && "deflate".equalsIgnoreCase( contentEncoding );
if ( isDeflated )
{
is = new DeflaterInputStream( is );
}
inputData.setInputStream( is );
resource.setLastModified( urlConnection.getLastModified() );
resource.setContentLength( urlConnection.getContentLength() );
break;
}
catch ( FileNotFoundException e )
{
// this could be 404 Not Found or 410 Gone - we don't have access to which it was.
// TODO: 2019-10-03 url used should list all visited/redirected urls, not just the original
throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
UNKNOWN_STATUS_CODE, null, getProxyInfo() ), e );
}
catch ( IOException originalIOException )
{
throw convertHttpUrlConnectionException( originalIOException, urlConnection, buildUrl( resource ) );
}
}
}
private void addHeaders( HttpURLConnection urlConnection )
{
if ( httpHeaders != null )
{
for ( Object header : httpHeaders.keySet() )
{
urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) );
}
}
setAuthorization( urlConnection );
}
private void setAuthorization( HttpURLConnection urlConnection )
{
if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null )
{
String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword();
String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) );
urlConnection.setRequestProperty( "Authorization", "Basic " + encoded );
}
}
public void fillOutputData( OutputData outputData )
throws TransferFailedException
{
Resource resource = outputData.getResource();
try
{
URL url = new URL( buildUrl( resource ) );
putConnection = (HttpURLConnection) url.openConnection( this.proxy );
addHeaders( putConnection );
putConnection.setRequestMethod( "PUT" );
putConnection.setDoOutput( true );
if ( resource.getContentLength() != -1 )
{
putConnection.setFixedLengthStreamingMode( resource.getContentLength() );
}
else
{
putConnection.setChunkedStreamingMode( 0 );
}
outputData.setOutputStream( putConnection.getOutputStream() );
}
catch ( IOException e )
{
throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
}
}
protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
{
try
{
String reasonPhrase = putConnection.getResponseMessage();
int statusCode = putConnection.getResponseCode();
switch ( statusCode )
{
// Success Codes
case HttpURLConnection.HTTP_OK: // 200
case HttpURLConnection.HTTP_CREATED: // 201
case HttpURLConnection.HTTP_ACCEPTED: // 202
case HttpURLConnection.HTTP_NO_CONTENT: // 204
break;
case HttpURLConnection.HTTP_FORBIDDEN:
case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_PROXY_AUTH:
throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode,
reasonPhrase, getProxyInfo() ) );
case HttpURLConnection.HTTP_NOT_FOUND:
throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
statusCode, reasonPhrase, getProxyInfo() ) );
// add more entries here
default:
throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ),
statusCode, reasonPhrase, getProxyInfo() ) ) ;
}
}
catch ( IOException e )
{
fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) );
}
}
protected void openConnectionInternal()
throws ConnectionException, AuthenticationException
{
final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() );
if ( proxyInfo != null )
{
this.proxy = getProxy( proxyInfo );
this.proxyInfo = proxyInfo;
}
authenticator.setWagon( this );
boolean usePreemptiveAuthentication =
Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean(
repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication;
setPreemptiveAuthentication( usePreemptiveAuthentication );
}
@SuppressWarnings( "deprecation" )
public PasswordAuthentication requestProxyAuthentication()
{
if ( proxyInfo != null && proxyInfo.getUserName() != null )
{
String password = "";
if ( proxyInfo.getPassword() != null )
{
password = proxyInfo.getPassword();
}
return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() );
}
return null;
}
public PasswordAuthentication requestServerAuthentication()
{
if ( authenticationInfo != null && authenticationInfo.getUserName() != null )
{
String password = "";
if ( authenticationInfo.getPassword() != null )
{
password = authenticationInfo.getPassword();
}
return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() );
}
return null;
}
private Proxy getProxy( ProxyInfo proxyInfo )
{
return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) );
}
private Type getProxyType( ProxyInfo proxyInfo )
{
if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals(
proxyInfo.getType() ) )
{
return Type.SOCKS;
}
else
{
return Type.HTTP;
}
}
public SocketAddress getSocketAddress( ProxyInfo proxyInfo )
{
return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() );
}
public void closeConnection()
throws ConnectionException
{
//FIXME WAGON-375 use persistent connection feature provided by the jdk
if ( putConnection != null )
{
putConnection.disconnect();
}
authenticator.resetWagon();
}
public List<String> getFileList( String destinationDirectory )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
InputData inputData = new InputData();
if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) )
{
destinationDirectory += "/";
}
String url = buildUrl( new Resource( destinationDirectory ) );
Resource resource = new Resource( destinationDirectory );
inputData.setResource( resource );
fillInputData( inputData );
InputStream is = inputData.getInputStream();
try
{
if ( is == null )
{
throw new TransferFailedException(
url + " - Could not open input stream for resource: '" + resource + "'" );
}
final List<String> htmlFileList = HtmlFileListParser.parseFileList( url, is );
is.close();
is = null;
return htmlFileList;
}
catch ( final IOException e )
{
throw new TransferFailedException( "Failure transferring " + resource.getName(), e );
}
finally
{
IOUtils.closeQuietly( is );
}
}
public boolean resourceExists( String resourceName )
throws TransferFailedException, AuthorizationException
{
HttpURLConnection headConnection;
try
{
Resource resource = new Resource( resourceName );
URL url = new URL( buildUrl( resource ) );
headConnection = (HttpURLConnection) url.openConnection( this.proxy );
addHeaders( headConnection );
headConnection.setRequestMethod( "HEAD" );
headConnection.setDoOutput( true );
int statusCode = headConnection.getResponseCode();
switch ( statusCode )
{
case HttpURLConnection.HTTP_OK:
return true;
case HttpURLConnection.HTTP_FORBIDDEN:
throw new AuthorizationException( "Access denied to: " + url );
case HttpURLConnection.HTTP_NOT_FOUND:
return false;
case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_PROXY_AUTH:
throw new AuthorizationException( "Access denied to: " + url );
default:
throw new TransferFailedException(
"Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode );
}
}
catch ( IOException e )
{
throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
}
}
public boolean isUseCache()
{
return useCache;
}
public void setUseCache( boolean useCache )
{
this.useCache = useCache;
}
public Properties getHttpHeaders()
{
return httpHeaders;
}
public void setHttpHeaders( Properties httpHeaders )
{
this.httpHeaders = httpHeaders;
}
void setSystemProperty( String key, String value )
{
if ( value != null )
{
System.setProperty( key, value );
}
else
{
System.getProperties().remove( key );
}
}
public void setPreemptiveAuthentication( boolean preemptiveAuthentication )
{
this.preemptiveAuthentication = preemptiveAuthentication;
}
public LightweightHttpWagonAuthenticator getAuthenticator()
{
return authenticator;
}
public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator )
{
this.authenticator = authenticator;
}
/**
* Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the
* equivalent {@link TransferFailedException}.
* <p>
* Details are extracted from the error stream if possible, either directly or indirectly by way of supporting
* accessors. The returned exception will include the passed IOException as a cause and a message that is as
* descriptive as possible.
*
* @param originalIOException an IOException thrown from an HttpURLConnection operation
* @param urlConnection instance that triggered the IOException
* @param url originating url that triggered the IOException
* @return exception that is representative of the original cause
*/
private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException,
HttpURLConnection urlConnection,
String url )
{
// javadoc of HttpUrlConnection, HTTP transfer errors throw IOException
// In that case, one may attempt to get the status code and reason phrase
// from the errorstream. We do this, but by way of the following code path
// getResponseCode()/getResponseMessage() - calls -> getHeaderFields()
// getHeaderFields() - calls -> getErrorStream()
try
{
// call getResponseMessage first since impl calls getResponseCode as part of that anyways
String errorResponseMessage = urlConnection.getResponseMessage(); // may be null
int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned
String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage,
getProxyInfo() );
return new TransferFailedException( message, originalIOException );
}
catch ( IOException errorStreamException )
{
// there was a problem using the standard methods, need to fall back to other options
}
// Attempt to parse the status code and URL which can be included in an IOException message
// https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java
// .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913
String ioMsg = originalIOException.getMessage();
if ( ioMsg != null )
{
Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg );
if ( matcher.matches() )
{
String codeStr = matcher.group( 1 );
String urlStr = matcher.group( 2 );
int code = UNKNOWN_STATUS_CODE;
try
{
code = parseInt( codeStr );
}
catch ( NumberFormatException nfe )
{
// if here there is a regex problem
}
String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() );
return new TransferFailedException( message, originalIOException );
}
}
String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() );
return new TransferFailedException( message, originalIOException );
}
}