blob: 3bf4291204c3c1c1ad3afa3fd747d964ec849e55 [file] [log] [blame]
package org.apache.maven.wagon.shared.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.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthSchemeProvider;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.ChallengeState;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.ServiceUnavailableRetryStrategy;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.DateUtils;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLInitializationException;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.auth.BasicSchemeFactory;
import org.apache.http.impl.auth.DigestSchemeFactory;
import org.apache.http.impl.auth.NTLMSchemeFactory;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.apache.maven.wagon.InputData;
import org.apache.maven.wagon.OutputData;
import org.apache.maven.wagon.PathUtils;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.StreamWagon;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.Wagon;
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.repository.Repository;
import org.apache.maven.wagon.resource.Resource;
import org.codehaus.plexus.util.StringUtils;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
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.formatTransferDebugMessage;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;
/**
* @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
* @author <a href="mailto:james@atlassian.com">James William Dumay</a>
*/
public abstract class AbstractHttpClientWagon
extends StreamWagon
{
private final class WagonHttpEntity
extends AbstractHttpEntity
{
private final Resource resource;
private final Wagon wagon;
private InputStream stream;
private File source;
private long length = -1;
private boolean repeatable;
private WagonHttpEntity( final InputStream stream, final Resource resource, final Wagon wagon,
final File source )
throws TransferFailedException
{
if ( source != null )
{
this.source = source;
this.repeatable = true;
}
else
{
this.stream = stream;
this.repeatable = false;
}
this.resource = resource;
this.length = resource == null ? -1 : resource.getContentLength();
this.wagon = wagon;
}
public long getContentLength()
{
return length;
}
public InputStream getContent()
throws IOException, IllegalStateException
{
if ( this.source != null )
{
return new FileInputStream( this.source );
}
return stream;
}
public boolean isRepeatable()
{
return repeatable;
}
public void writeTo( final OutputStream output )
throws IOException
{
if ( output == null )
{
throw new NullPointerException( "output cannot be null" );
}
TransferEvent transferEvent =
new TransferEvent( wagon, resource, TransferEvent.TRANSFER_PROGRESS, TransferEvent.REQUEST_PUT );
transferEvent.setTimestamp( System.currentTimeMillis() );
try ( ReadableByteChannel input = ( this.source != null )
? new RandomAccessFile( this.source, "r" ).getChannel()
: Channels.newChannel( stream ) )
{
ByteBuffer buffer = ByteBuffer.allocate( getBufferCapacityForTransfer( this.length ) );
int halfBufferCapacity = buffer.capacity() / 2;
long remaining = this.length < 0L ? Long.MAX_VALUE : this.length;
while ( remaining > 0L )
{
int read = input.read( buffer );
if ( read == -1 )
{
// EOF, but some data has not been written yet.
if ( buffer.position() != 0 )
{
buffer.flip();
fireTransferProgress( transferEvent, buffer.array(), buffer.limit() );
output.write( buffer.array(), 0, buffer.limit() );
buffer.clear();
}
break;
}
// Prevent minichunking / fragmentation: when less than half the buffer is utilized,
// read some more bytes before writing and firing progress.
if ( buffer.position() < halfBufferCapacity )
{
continue;
}
buffer.flip();
fireTransferProgress( transferEvent, buffer.array(), buffer.limit() );
output.write( buffer.array(), 0, buffer.limit() );
remaining -= buffer.limit();
buffer.clear();
}
output.flush();
}
}
public boolean isStreaming()
{
return true;
}
}
private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone( "GMT" );
/**
* use http(s) connection pool mechanism.
* <b>enabled by default</b>
*/
private static boolean persistentPool =
Boolean.valueOf( System.getProperty( "maven.wagon.http.pool", "true" ) );
/**
* skip failure on certificate validity checks.
* <b>disabled by default</b>
*/
private static final boolean SSL_INSECURE =
Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.insecure", "false" ) );
/**
* if using sslInsecure, certificate date issues will be ignored
* <b>disabled by default</b>
*/
private static final boolean IGNORE_SSL_VALIDITY_DATES =
Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.ignore.validity.dates", "false" ) );
/**
* If enabled, ssl hostname verifier does not check hostname. Disable this will use a browser compat hostname
* verifier <b>disabled by default</b>
*/
private static final boolean SSL_ALLOW_ALL =
Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.allowall", "false" ) );
/**
* Maximum concurrent connections per distinct route.
* <b>20 by default</b>
*/
private static final int MAX_CONN_PER_ROUTE =
Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxPerRoute", "20" ) );
/**
* Maximum concurrent connections in total.
* <b>40 by default</b>
*/
private static final int MAX_CONN_TOTAL =
Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxTotal", "40" ) );
/**
* Time to live in seconds for an HTTP connection. After that time, the connection will be dropped.
* Intermediates tend to drop connections after some idle period. Set to -1 to maintain connections
* indefinitely. This value defaults to 300 seconds.
*
* @since 3.2
*/
private static final long CONN_TTL =
Long.getLong( "maven.wagon.httpconnectionManager.ttlSeconds", 300L );
/**
* Internal connection manager
*/
private static HttpClientConnectionManager httpClientConnectionManager = createConnManager();
/**
* See RFC6585
*/
protected static final int SC_TOO_MANY_REQUESTS = 429;
/**
* For exponential backoff.
*/
/**
* Initial seconds to back off when a HTTP 429 received.
* Subsequent 429 responses result in exponental backoff.
* <b>5 by default</b>
*
* @since 2.7
*/
private int initialBackoffSeconds =
Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.backoffSeconds", "5" ) );
/**
* The maximum amount of time we want to back off in the case of
* repeated HTTP 429 response codes.
*
* @since 2.7
*/
private static final int MAX_BACKOFF_WAIT_SECONDS =
Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxBackoffSeconds", "180" ) );
protected int backoff( int wait, String url )
throws InterruptedException, TransferFailedException
{
TimeUnit.SECONDS.sleep( wait );
int nextWait = wait * 2;
if ( nextWait >= getMaxBackoffWaitSeconds() )
{
throw new TransferFailedException(
"Waited too long to access: " + url + ". Return code is: " + SC_TOO_MANY_REQUESTS );
}
return nextWait;
}
@SuppressWarnings( "checkstyle:linelength" )
private static PoolingHttpClientConnectionManager createConnManager()
{
String sslProtocolsStr = System.getProperty( "https.protocols" );
String cipherSuitesStr = System.getProperty( "https.cipherSuites" );
String[] sslProtocols = sslProtocolsStr != null ? sslProtocolsStr.split( " *, *" ) : null;
String[] cipherSuites = cipherSuitesStr != null ? cipherSuitesStr.split( " *, *" ) : null;
SSLConnectionSocketFactory sslConnectionSocketFactory;
if ( SSL_INSECURE )
{
try
{
SSLContext sslContext = new SSLContextBuilder().useSSL().loadTrustMaterial( null,
new RelaxedTrustStrategy(
IGNORE_SSL_VALIDITY_DATES ) ).build();
sslConnectionSocketFactory = new SSLConnectionSocketFactory( sslContext, sslProtocols, cipherSuites,
SSL_ALLOW_ALL
? SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER
: SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
}
catch ( Exception ex )
{
throw new SSLInitializationException( ex.getMessage(), ex );
}
}
else
{
sslConnectionSocketFactory =
new SSLConnectionSocketFactory( HttpsURLConnection.getDefaultSSLSocketFactory(), sslProtocols,
cipherSuites,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
}
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register( "http",
PlainConnectionSocketFactory.INSTANCE ).register(
"https", sslConnectionSocketFactory ).build();
PoolingHttpClientConnectionManager connManager =
new PoolingHttpClientConnectionManager( registry, null, null, null, CONN_TTL, TimeUnit.SECONDS );
if ( persistentPool )
{
connManager.setDefaultMaxPerRoute( MAX_CONN_PER_ROUTE );
connManager.setMaxTotal( MAX_CONN_TOTAL );
}
else
{
connManager.setMaxTotal( 1 );
}
return connManager;
}
/**
* The type of the retry handler, defaults to {@code standard}.
* Values can be {@link default DefaultHttpRequestRetryHandler},
* or {@link standard StandardHttpRequestRetryHandler},
* or a fully qualified name class with a no-arg.
*
* @since 3.2
*/
private static final String RETRY_HANDLER_CLASS =
System.getProperty( "maven.wagon.http.retryHandler.class", "standard" );
/**
* Whether or not methods that have successfully sent their request will be retried,
* defaults to {@code false}.
* Note: only used for default and standard retry handlers.
*
* @since 3.2
*/
private static final boolean RETRY_HANDLER_REQUEST_SENT_ENABLED =
Boolean.getBoolean( "maven.wagon.http.retryHandler.requestSentEnabled" );
/**
* Number of retries for the retry handler, defaults to 3.
* Note: only used for default and standard retry handlers.
*
* @since 3.2
*/
private static final int RETRY_HANDLER_COUNT =
Integer.getInteger( "maven.wagon.http.retryHandler.count", 3 );
/**
* Comma-separated list of non-retryable exception classes.
* Note: only used for default retry handler.
*
* @since 3.2
*/
private static final String RETRY_HANDLER_EXCEPTIONS =
System.getProperty( "maven.wagon.http.retryHandler.nonRetryableClasses" );
private static HttpRequestRetryHandler createRetryHandler()
{
switch ( RETRY_HANDLER_CLASS )
{
case "default":
if ( StringUtils.isEmpty( RETRY_HANDLER_EXCEPTIONS ) )
{
return new DefaultHttpRequestRetryHandler(
RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED );
}
return new DefaultHttpRequestRetryHandler(
RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED, getNonRetryableExceptions() )
{
};
case "standard":
return new StandardHttpRequestRetryHandler( RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED );
default:
try
{
final ClassLoader classLoader = AbstractHttpClientWagon.class.getClassLoader();
return HttpRequestRetryHandler.class.cast( classLoader.loadClass( RETRY_HANDLER_CLASS )
.getConstructor().newInstance() );
}
catch ( final Exception e )
{
throw new IllegalArgumentException( e );
}
}
}
/**
* The type of the serviceUnavailableRetryStrategy, defaults to {@code none}.
* Values can be {@link default DefaultServiceUnavailableRetryStrategy},
* or {@link standard StandardServiceUnavailableRetryStrategy}, or
* a fully qualified name class with a no-arg or none to not use a ServiceUnavailableRetryStrategy.
*/
private static final String SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS =
System.getProperty( "maven.wagon.http.serviceUnavailableRetryStrategy.class", "none" );
/**
* Interval in milliseconds between retries when using a serviceUnavailableRetryStrategy.
* <b>1000 by default</b>
*/
private static final int SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL =
Integer.getInteger( "maven.wagon.http.serviceUnavailableRetryStrategy.retryInterval", 1000 );
/**
* Maximum number of retries when using a serviceUnavailableRetryStrategy.
* <b>5 by default</b>
*/
private static final int SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES =
Integer.getInteger( "maven.wagon.http.serviceUnavailableRetryStrategy.maxRetries", 5 );
private static ServiceUnavailableRetryStrategy createServiceUnavailableRetryStrategy()
{
switch ( SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS )
{
case "none": return null;
case "default":
return new DefaultServiceUnavailableRetryStrategy(
SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES, SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL );
case "standard":
return new StandardServiceUnavailableRetryStrategy(
SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES, SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL );
default:
try
{
final ClassLoader classLoader = AbstractHttpClientWagon.class.getClassLoader();
return ServiceUnavailableRetryStrategy.class.cast(
classLoader.loadClass( SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS )
.getConstructor().newInstance() );
}
catch ( final Exception e )
{
throw new IllegalArgumentException( e );
}
}
}
private static Registry<AuthSchemeProvider> createAuthSchemeRegistry()
{
return RegistryBuilder.<AuthSchemeProvider>create()
.register( AuthSchemes.BASIC, new BasicSchemeFactory( StandardCharsets.UTF_8 ) )
.register( AuthSchemes.DIGEST, new DigestSchemeFactory( StandardCharsets.UTF_8 ) )
.register( AuthSchemes.NTLM, new NTLMSchemeFactory() )
.build();
}
private static Collection<Class<? extends IOException>> getNonRetryableExceptions()
{
final List<Class<? extends IOException>> exceptions = new ArrayList<>();
final ClassLoader loader = AbstractHttpClientWagon.class.getClassLoader();
for ( final String ex : RETRY_HANDLER_EXCEPTIONS.split( "," ) )
{
try
{
exceptions.add( ( Class<? extends IOException> ) loader.loadClass( ex ) );
}
catch ( final ClassNotFoundException e )
{
throw new IllegalArgumentException( e );
}
}
return exceptions;
}
private static CloseableHttpClient httpClient = createClient();
private static CloseableHttpClient createClient()
{
return HttpClientBuilder.create() //
.useSystemProperties() //
.disableConnectionState() //
.setConnectionManager( httpClientConnectionManager ) //
.setRetryHandler( createRetryHandler() )
.setServiceUnavailableRetryStrategy( createServiceUnavailableRetryStrategy() )
.setDefaultAuthSchemeRegistry( createAuthSchemeRegistry() )
.build();
}
private CredentialsProvider credentialsProvider;
private AuthCache authCache;
private Closeable closeable;
/**
* @plexus.configuration
* @deprecated Use httpConfiguration instead.
*/
private Properties httpHeaders;
/**
* @since 1.0-beta-6
*/
private HttpConfiguration httpConfiguration;
/**
* Basic auth scope overrides
* @since 2.8
*/
private BasicAuthScope basicAuth;
/**
* Proxy basic auth scope overrides
* @since 2.8
*/
private BasicAuthScope proxyAuth;
public void openConnectionInternal()
{
repository.setUrl( getURL( repository ) );
credentialsProvider = new BasicCredentialsProvider();
authCache = new BasicAuthCache();
if ( authenticationInfo != null )
{
String username = authenticationInfo.getUserName();
String password = authenticationInfo.getPassword();
if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
{
Credentials creds = new UsernamePasswordCredentials( username, password );
String host = getRepository().getHost();
int port = getRepository().getPort();
credentialsProvider.setCredentials( getBasicAuthScope().getScope( host, port ), creds );
}
}
ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
if ( proxyInfo != null )
{
String proxyUsername = proxyInfo.getUserName();
String proxyPassword = proxyInfo.getPassword();
String proxyHost = proxyInfo.getHost();
String proxyNtlmHost = proxyInfo.getNtlmHost();
String proxyNtlmDomain = proxyInfo.getNtlmDomain();
if ( proxyHost != null )
{
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 );
}
int proxyPort = proxyInfo.getPort();
AuthScope authScope = getProxyBasicAuthScope().getScope( proxyHost, proxyPort );
credentialsProvider.setCredentials( authScope, creds );
}
}
}
}
public void closeConnection()
{
if ( !persistentPool )
{
httpClientConnectionManager.closeIdleConnections( 0, TimeUnit.MILLISECONDS );
}
if ( authCache != null )
{
authCache.clear();
authCache = null;
}
if ( credentialsProvider != null )
{
credentialsProvider.clear();
credentialsProvider = null;
}
}
public static CloseableHttpClient getHttpClient()
{
return httpClient;
}
public static void setPersistentPool( boolean persistentPool )
{
persistentPool = persistentPool;
}
public static void setPoolingHttpClientConnectionManager(
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager )
{
httpClientConnectionManager = poolingHttpClientConnectionManager;
httpClient = createClient();
}
public void put( File source, String resourceName )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
Resource resource = new Resource( resourceName );
firePutInitiated( resource, source );
resource.setContentLength( source.length() );
resource.setLastModified( source.lastModified() );
put( null, resource, source );
}
public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
Resource resource = new Resource( destination );
firePutInitiated( resource, null );
resource.setContentLength( contentLength );
resource.setLastModified( lastModified );
put( stream, resource, null );
}
private void put( final InputStream stream, Resource resource, File source )
throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
{
put( resource, source, new WagonHttpEntity( stream, resource, this, source ) );
}
private void put( Resource resource, File source, HttpEntity httpEntity )
throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
{
put( resource, source, httpEntity, buildUrl( resource ) );
}
/**
* 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 buildUrl( resource.getName() );
}
/**
* Builds a complete URL string from the repository URL and the relative path of the resource passed.
*
* @param resourceName the resourcerelative path
* @return the complete URL
*/
private String buildUrl( String resourceName )
{
return EncodingUtil.encodeURLToString( getRepository().getUrl(), resourceName );
}
private void put( Resource resource, File source, HttpEntity httpEntity, String url )
throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
{
put( getInitialBackoffSeconds(), resource, source, httpEntity, url );
}
private void put( int wait, Resource resource, File source, HttpEntity httpEntity, String url )
throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
{
//Parent directories need to be created before posting
try
{
mkdirs( PathUtils.dirname( resource.getName() ) );
}
catch ( HttpException he )
{
fireTransferError( resource, he, TransferEvent.REQUEST_PUT );
}
catch ( IOException e )
{
fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
}
// preemptive for put
// TODO: is it a good idea, though? 'Expect-continue' handshake would serve much better
Repository repo = getRepository();
HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
AuthScope targetScope = getBasicAuthScope().getScope( targetHost );
if ( credentialsProvider.getCredentials( targetScope ) != null )
{
BasicScheme targetAuth = new BasicScheme();
authCache.put( targetHost, targetAuth );
}
HttpPut putMethod = new HttpPut( url );
firePutStarted( resource, source );
try
{
putMethod.setEntity( httpEntity );
CloseableHttpResponse response = execute( putMethod );
try
{
fireTransferDebug( formatTransferDebugMessage( url, response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
int statusCode = response.getStatusLine().getStatusCode();
// Check that we didn't run out of retries.
switch ( statusCode )
{
// Success Codes
case HttpStatus.SC_OK: // 200
case HttpStatus.SC_CREATED: // 201
case HttpStatus.SC_ACCEPTED: // 202
case HttpStatus.SC_NO_CONTENT: // 204
break;
// handle all redirect even if http specs says " the user agent MUST NOT automatically redirect
// the request unless it can be confirmed by the user"
case HttpStatus.SC_MOVED_PERMANENTLY: // 301
case HttpStatus.SC_MOVED_TEMPORARILY: // 302
case HttpStatus.SC_SEE_OTHER: // 303
put( resource, source, httpEntity, calculateRelocatedUrl( response ) );
return;
//case HttpStatus.SC_UNAUTHORIZED:
case HttpStatus.SC_FORBIDDEN:
fireSessionConnectionRefused();
throw new AuthorizationException( formatAuthorizationMessage( url,
response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
case HttpStatus.SC_NOT_FOUND:
throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( url,
response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
case SC_TOO_MANY_REQUESTS:
put( backoff( wait, url ), resource, source, httpEntity, url );
break;
//add more entries here
default:
TransferFailedException e = new TransferFailedException( formatTransferFailedMessage( url,
response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
throw e;
}
firePutCompleted( resource, source );
EntityUtils.consume( response.getEntity() );
}
finally
{
response.close();
}
}
catch ( IOException | HttpException | InterruptedException e )
{
fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
throw new TransferFailedException( formatTransferFailedMessage( url, getProxyInfo() ), e );
}
}
protected String calculateRelocatedUrl( HttpResponse response )
{
Header locationHeader = response.getFirstHeader( "Location" );
String locationField = locationHeader.getValue();
// is it a relative Location or a full ?
return locationField.startsWith( "http" ) ? locationField : getURL( getRepository() ) + '/' + locationField;
}
protected void mkdirs( String dirname )
throws HttpException, IOException
{
// nothing to do
}
public boolean resourceExists( String resourceName )
throws TransferFailedException, AuthorizationException
{
return resourceExists( getInitialBackoffSeconds(), resourceName );
}
private boolean resourceExists( int wait, String resourceName )
throws TransferFailedException, AuthorizationException
{
String url = buildUrl( resourceName );
HttpHead headMethod = new HttpHead( url );
try
{
CloseableHttpResponse response = execute( headMethod );
try
{
int statusCode = response.getStatusLine().getStatusCode();
boolean result;
switch ( statusCode )
{
case HttpStatus.SC_OK:
result = true;
break;
case HttpStatus.SC_NOT_MODIFIED:
result = true;
break;
case HttpStatus.SC_FORBIDDEN:
case HttpStatus.SC_UNAUTHORIZED:
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
throw new AuthorizationException( formatAuthorizationMessage( url,
response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
case HttpStatus.SC_NOT_FOUND:
result = false;
break;
case SC_TOO_MANY_REQUESTS:
return resourceExists( backoff( wait, resourceName ), resourceName );
//add more entries here
default:
throw new TransferFailedException( formatTransferFailedMessage( url,
response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
}
EntityUtils.consume( response.getEntity() );
return result;
}
finally
{
response.close();
}
}
catch ( IOException | HttpException | InterruptedException e )
{
throw new TransferFailedException( formatTransferFailedMessage( url, getProxyInfo() ), e );
}
}
protected CloseableHttpResponse execute( HttpUriRequest httpMethod )
throws HttpException, IOException
{
setHeaders( httpMethod );
String userAgent = getUserAgent( httpMethod );
if ( userAgent != null )
{
httpMethod.setHeader( HTTP.USER_AGENT, userAgent );
}
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
// WAGON-273: default the cookie-policy to browser compatible
requestConfigBuilder.setCookieSpec( CookieSpecs.BROWSER_COMPATIBILITY );
Repository repo = getRepository();
ProxyInfo proxyInfo = getProxyInfo( repo.getProtocol(), repo.getHost() );
if ( proxyInfo != null )
{
HttpHost proxy = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
requestConfigBuilder.setProxy( proxy );
}
HttpMethodConfiguration config =
httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( httpMethod );
if ( config != null )
{
ConfigurationUtils.copyConfig( config, requestConfigBuilder );
}
else
{
requestConfigBuilder.setSocketTimeout( getReadTimeout() );
if ( httpMethod instanceof HttpPut )
{
requestConfigBuilder.setExpectContinueEnabled( true );
}
}
if ( httpMethod instanceof HttpPut )
{
requestConfigBuilder.setRedirectsEnabled( false );
}
HttpClientContext localContext = HttpClientContext.create();
localContext.setCredentialsProvider( credentialsProvider );
localContext.setAuthCache( authCache );
localContext.setRequestConfig( requestConfigBuilder.build() );
if ( config != null && config.isUsePreemptive() )
{
HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
AuthScope targetScope = getBasicAuthScope().getScope( targetHost );
if ( credentialsProvider.getCredentials( targetScope ) != null )
{
BasicScheme targetAuth = new BasicScheme();
authCache.put( targetHost, targetAuth );
}
}
if ( proxyInfo != null )
{
if ( proxyInfo.getHost() != null )
{
HttpHost proxyHost = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
AuthScope proxyScope = getProxyBasicAuthScope().getScope( proxyHost );
if ( credentialsProvider.getCredentials( proxyScope ) != null )
{
/* This is extremely ugly because we need to set challengeState to PROXY, but
* the constructor is deprecated. Alternatively, we could subclass BasicScheme
* to ProxyBasicScheme and set the state internally in the constructor.
*/
BasicScheme proxyAuth = new BasicScheme( ChallengeState.PROXY );
authCache.put( proxyHost, proxyAuth );
}
}
}
return httpClient.execute( httpMethod, localContext );
}
public void setHeaders( HttpUriRequest method )
{
HttpMethodConfiguration config =
httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
if ( config == null || config.isUseDefaultHeaders() )
{
// TODO: merge with the other headers and have some better defaults, unify with lightweight headers
method.addHeader( "Cache-control", "no-cache" );
method.addHeader( "Cache-store", "no-store" );
method.addHeader( "Pragma", "no-cache" );
}
if ( httpHeaders != null )
{
for ( Map.Entry<Object, Object> entry : httpHeaders.entrySet() )
{
method.setHeader( (String) entry.getKey(), (String) entry.getValue() );
}
}
Header[] headers = config == null ? null : config.asRequestHeaders();
if ( headers != null )
{
for ( Header header : headers )
{
method.setHeader( header );
}
}
Header userAgentHeader = method.getFirstHeader( HTTP.USER_AGENT );
if ( userAgentHeader == null )
{
String userAgent = getUserAgent( method );
if ( userAgent != null )
{
method.setHeader( HTTP.USER_AGENT, userAgent );
}
}
}
protected String getUserAgent( HttpUriRequest method )
{
if ( httpHeaders != null )
{
String value = (String) httpHeaders.get( HTTP.USER_AGENT );
if ( value != null )
{
return value;
}
}
HttpMethodConfiguration config =
httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
if ( config != null )
{
return (String) config.getHeaders().get( HTTP.USER_AGENT );
}
return null;
}
/**
* getUrl
* Implementors can override this to remove unwanted parts of the url such as role-hints
*
* @param repository
* @return
*/
protected String getURL( Repository repository )
{
return repository.getUrl();
}
public HttpConfiguration getHttpConfiguration()
{
return httpConfiguration;
}
public void setHttpConfiguration( HttpConfiguration httpConfiguration )
{
this.httpConfiguration = httpConfiguration;
}
/**
* Get the override values for standard HttpClient AuthScope
*
* @return the basicAuth
*/
public BasicAuthScope getBasicAuthScope()
{
if ( basicAuth == null )
{
basicAuth = new BasicAuthScope();
}
return basicAuth;
}
/**
* Set the override values for standard HttpClient AuthScope
*
* @param basicAuth the AuthScope to set
*/
public void setBasicAuthScope( BasicAuthScope basicAuth )
{
this.basicAuth = basicAuth;
}
/**
* Get the override values for proxy HttpClient AuthScope
*
* @return the proxyAuth
*/
public BasicAuthScope getProxyBasicAuthScope()
{
if ( proxyAuth == null )
{
proxyAuth = new BasicAuthScope();
}
return proxyAuth;
}
/**
* Set the override values for proxy HttpClient AuthScope
*
* @param proxyAuth the AuthScope to set
*/
public void setProxyBasicAuthScope( BasicAuthScope proxyAuth )
{
this.proxyAuth = proxyAuth;
}
public void fillInputData( InputData inputData )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
fillInputData( getInitialBackoffSeconds(), inputData );
}
private void fillInputData( int wait, InputData inputData )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
Resource resource = inputData.getResource();
String url = buildUrl( resource );
HttpGet getMethod = new HttpGet( url );
long timestamp = resource.getLastModified();
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 BasicHeader( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
getMethod.addHeader( hdr );
}
try
{
CloseableHttpResponse response = execute( getMethod );
closeable = response;
fireTransferDebug( formatTransferDebugMessage( url, response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
int statusCode = response.getStatusLine().getStatusCode();
switch ( statusCode )
{
case HttpStatus.SC_OK:
break;
case HttpStatus.SC_NOT_MODIFIED:
// return, leaving last modified set to original value so getIfNewer should return unmodified
return;
case HttpStatus.SC_FORBIDDEN:
case HttpStatus.SC_UNAUTHORIZED:
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
fireSessionConnectionRefused();
throw new AuthorizationException( formatAuthorizationMessage( url,
response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(),
getProxyInfo() ) );
case HttpStatus.SC_NOT_FOUND:
throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( url,
response.getStatusLine().getStatusCode(),
response.getStatusLine().getReasonPhrase(), getProxyInfo() ) );
case SC_TOO_MANY_REQUESTS:
fillInputData( backoff( wait, url ), inputData );
break;
// add more entries here
default:
cleanupGetTransfer( resource );
TransferFailedException e = new TransferFailedException( formatTransferFailedMessage( url,
response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(),
getProxyInfo() ) );
fireTransferError( resource, e, TransferEvent.REQUEST_GET );
throw e;
}
Header contentLengthHeader = response.getFirstHeader( "Content-Length" );
if ( contentLengthHeader != null )
{
try
{
long contentLength = Long.parseLong( contentLengthHeader.getValue() );
resource.setContentLength( contentLength );
}
catch ( NumberFormatException e )
{
fireTransferDebug(
"error parsing content length header '" + contentLengthHeader.getValue() + "' " + e );
}
}
Header lastModifiedHeader = response.getFirstHeader( "Last-Modified" );
if ( lastModifiedHeader != null )
{
Date lastModified = DateUtils.parseDate( lastModifiedHeader.getValue() );
if ( lastModified != null )
{
resource.setLastModified( lastModified.getTime() );
fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() + " ("
+ lastModified.getTime() + ")" );
}
}
HttpEntity entity = response.getEntity();
if ( entity != null )
{
inputData.setInputStream( entity.getContent() );
}
}
catch ( IOException | HttpException | InterruptedException e )
{
fireTransferError( resource, e, TransferEvent.REQUEST_GET );
throw new TransferFailedException( formatTransferFailedMessage( url, getProxyInfo() ), e );
}
}
protected void cleanupGetTransfer( Resource resource )
{
if ( closeable != null )
{
try
{
closeable.close();
}
catch ( IOException ignore )
{
// ignore
}
}
}
@Override
public void putFromStream( InputStream stream, String destination )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
putFromStream( stream, destination, -1, -1 );
}
@Override
protected void putFromStream( InputStream stream, Resource resource )
throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
{
putFromStream( stream, resource.getName(), -1, -1 );
}
public Properties getHttpHeaders()
{
return httpHeaders;
}
public void setHttpHeaders( Properties httpHeaders )
{
this.httpHeaders = httpHeaders;
}
@Override
public void fillOutputData( OutputData outputData )
throws TransferFailedException
{
// no needed in this implementation but throw an Exception if used
throw new IllegalStateException( "this wagon http client must not use fillOutputData" );
}
protected CredentialsProvider getCredentialsProvider()
{
return credentialsProvider;
}
protected AuthCache getAuthCache()
{
return authCache;
}
public int getInitialBackoffSeconds()
{
return initialBackoffSeconds;
}
public void setInitialBackoffSeconds( int initialBackoffSeconds )
{
this.initialBackoffSeconds = initialBackoffSeconds;
}
public static int getMaxBackoffWaitSeconds()
{
return MAX_BACKOFF_WAIT_SECONDS;
}
}