blob: 15fa36a0ab5852ad5e271411c06e153fc8dba995 [file] [log] [blame]
package org.eclipse.aether.transport.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.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.params.AuthParams;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.DateUtils;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.conn.params.ConnRouteParams;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DecompressingHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.util.EntityUtils;
import org.eclipse.aether.ConfigurationProperties;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.Proxy;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
import org.eclipse.aether.spi.connector.transport.GetTask;
import org.eclipse.aether.spi.connector.transport.PeekTask;
import org.eclipse.aether.spi.connector.transport.PutTask;
import org.eclipse.aether.spi.connector.transport.TransportTask;
import org.eclipse.aether.transfer.NoTransporterException;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.util.ConfigUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A transporter for HTTP/HTTPS.
*/
final class HttpTransporter
extends AbstractTransporter
{
private static final Pattern CONTENT_RANGE_PATTERN =
Pattern.compile( "\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*" );
private static final Logger LOGGER = LoggerFactory.getLogger( HttpTransporter.class );
private final AuthenticationContext repoAuthContext;
private final AuthenticationContext proxyAuthContext;
private final URI baseUri;
private final HttpHost server;
private final HttpHost proxy;
private final HttpClient client;
private final Map<?, ?> headers;
private final LocalState state;
HttpTransporter( RemoteRepository repository, RepositorySystemSession session )
throws NoTransporterException
{
if ( !"http".equalsIgnoreCase( repository.getProtocol() )
&& !"https".equalsIgnoreCase( repository.getProtocol() ) )
{
throw new NoTransporterException( repository );
}
try
{
baseUri = new URI( repository.getUrl() ).parseServerAuthority();
if ( baseUri.isOpaque() )
{
throw new URISyntaxException( repository.getUrl(), "URL must not be opaque" );
}
server = URIUtils.extractHost( baseUri );
if ( server == null )
{
throw new URISyntaxException( repository.getUrl(), "URL lacks host name" );
}
}
catch ( URISyntaxException e )
{
throw new NoTransporterException( repository, e.getMessage(), e );
}
proxy = toHost( repository.getProxy() );
repoAuthContext = AuthenticationContext.forRepository( session, repository );
proxyAuthContext = AuthenticationContext.forProxy( session, repository );
state = new LocalState( session, repository, new SslConfig( session, repoAuthContext ) );
headers =
ConfigUtils.getMap( session, Collections.emptyMap(), ConfigurationProperties.HTTP_HEADERS + "."
+ repository.getId(), ConfigurationProperties.HTTP_HEADERS );
DefaultHttpClient client = new DefaultHttpClient( state.getConnectionManager() );
configureClient( client.getParams(), session, repository, proxy );
client.setCredentialsProvider( toCredentialsProvider( server, repoAuthContext, proxy, proxyAuthContext ) );
this.client = new DecompressingHttpClient( client );
}
private static HttpHost toHost( Proxy proxy )
{
HttpHost host = null;
if ( proxy != null )
{
host = new HttpHost( proxy.getHost(), proxy.getPort() );
}
return host;
}
private static void configureClient( HttpParams params, RepositorySystemSession session,
RemoteRepository repository, HttpHost proxy )
{
AuthParams.setCredentialCharset( params, ConfigUtils.getString( session,
ConfigurationProperties.DEFAULT_HTTP_CREDENTIAL_ENCODING,
ConfigurationProperties.HTTP_CREDENTIAL_ENCODING + "." + repository.getId(),
ConfigurationProperties.HTTP_CREDENTIAL_ENCODING ) );
ConnRouteParams.setDefaultProxy( params, proxy );
HttpConnectionParams.setConnectionTimeout( params, ConfigUtils.getInteger( session,
ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(),
ConfigurationProperties.CONNECT_TIMEOUT ) );
HttpConnectionParams.setSoTimeout( params, ConfigUtils.getInteger( session,
ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(),
ConfigurationProperties.REQUEST_TIMEOUT ) );
HttpProtocolParams.setUserAgent( params, ConfigUtils.getString( session,
ConfigurationProperties.DEFAULT_USER_AGENT,
ConfigurationProperties.USER_AGENT ) );
}
private static CredentialsProvider toCredentialsProvider( HttpHost server, AuthenticationContext serverAuthCtx,
HttpHost proxy, AuthenticationContext proxyAuthCtx )
{
CredentialsProvider provider = toCredentialsProvider( server.getHostName(), AuthScope.ANY_PORT, serverAuthCtx );
if ( proxy != null )
{
CredentialsProvider p = toCredentialsProvider( proxy.getHostName(), proxy.getPort(), proxyAuthCtx );
provider = new DemuxCredentialsProvider( provider, p, proxy );
}
return provider;
}
private static CredentialsProvider toCredentialsProvider( String host, int port, AuthenticationContext ctx )
{
DeferredCredentialsProvider provider = new DeferredCredentialsProvider();
if ( ctx != null )
{
AuthScope basicScope = new AuthScope( host, port );
provider.setCredentials( basicScope, new DeferredCredentialsProvider.BasicFactory( ctx ) );
AuthScope ntlmScope = new AuthScope( host, port, AuthScope.ANY_REALM, "ntlm" );
provider.setCredentials( ntlmScope, new DeferredCredentialsProvider.NtlmFactory( ctx ) );
}
return provider;
}
LocalState getState()
{
return state;
}
private URI resolve( TransportTask task )
{
return UriUtils.resolve( baseUri, task.getLocation() );
}
public int classify( Throwable error )
{
if ( error instanceof HttpResponseException
&& ( (HttpResponseException) error ).getStatusCode() == HttpStatus.SC_NOT_FOUND )
{
return ERROR_NOT_FOUND;
}
return ERROR_OTHER;
}
@Override
protected void implPeek( PeekTask task )
throws Exception
{
HttpHead request = commonHeaders( new HttpHead( resolve( task ) ) );
execute( request, null );
}
@Override
protected void implGet( GetTask task )
throws Exception
{
EntityGetter getter = new EntityGetter( task );
HttpGet request = commonHeaders( new HttpGet( resolve( task ) ) );
resume( request, task );
try
{
execute( request, getter );
}
catch ( HttpResponseException e )
{
if ( e.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED && request.containsHeader( HttpHeaders.RANGE ) )
{
request = commonHeaders( new HttpGet( request.getURI() ) );
execute( request, getter );
return;
}
throw e;
}
}
@Override
protected void implPut( PutTask task )
throws Exception
{
PutTaskEntity entity = new PutTaskEntity( task );
HttpPut request = commonHeaders( entity( new HttpPut( resolve( task ) ), entity ) );
try
{
execute( request, null );
}
catch ( HttpResponseException e )
{
if ( e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED && request.containsHeader( HttpHeaders.EXPECT ) )
{
state.setExpectContinue( false );
request = commonHeaders( entity( new HttpPut( request.getURI() ), entity ) );
execute( request, null );
return;
}
throw e;
}
}
private void execute( HttpUriRequest request, EntityGetter getter )
throws Exception
{
try
{
SharingHttpContext context = new SharingHttpContext( state );
prepare( request, context );
HttpResponse response = client.execute( server, request, context );
try
{
context.close();
handleStatus( response );
if ( getter != null )
{
getter.handle( response );
}
}
finally
{
EntityUtils.consumeQuietly( response.getEntity() );
}
}
catch ( IOException e )
{
if ( e.getCause() instanceof TransferCancelledException )
{
throw (Exception) e.getCause();
}
throw e;
}
}
private void prepare( HttpUriRequest request, SharingHttpContext context )
{
boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase( request.getMethod() );
if ( state.getWebDav() == null && ( put || isPayloadPresent( request ) ) )
{
try
{
HttpOptions req = commonHeaders( new HttpOptions( request.getURI() ) );
HttpResponse response = client.execute( server, req, context );
state.setWebDav( isWebDav( response ) );
EntityUtils.consumeQuietly( response.getEntity() );
}
catch ( IOException e )
{
LOGGER.debug( "Failed to prepare HTTP context", e );
}
}
if ( put && Boolean.TRUE.equals( state.getWebDav() ) )
{
mkdirs( request.getURI(), context );
}
}
private boolean isWebDav( HttpResponse response )
{
return response.containsHeader( HttpHeaders.DAV );
}
@SuppressWarnings( "checkstyle:magicnumber" )
private void mkdirs( URI uri, SharingHttpContext context )
{
List<URI> dirs = UriUtils.getDirectories( baseUri, uri );
int index = 0;
for ( ; index < dirs.size(); index++ )
{
try
{
HttpResponse response =
client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
try
{
int status = response.getStatusLine().getStatusCode();
if ( status < 300 || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
{
break;
}
else if ( status == HttpStatus.SC_CONFLICT )
{
continue;
}
handleStatus( response );
}
finally
{
EntityUtils.consumeQuietly( response.getEntity() );
}
}
catch ( IOException e )
{
LOGGER.debug( "Failed to create parent directory {}", dirs.get( index ), e );
return;
}
}
for ( index--; index >= 0; index-- )
{
try
{
HttpResponse response =
client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
try
{
handleStatus( response );
}
finally
{
EntityUtils.consumeQuietly( response.getEntity() );
}
}
catch ( IOException e )
{
LOGGER.debug( "Failed to create parent directory {}", dirs.get( index ), e );
return;
}
}
}
private <T extends HttpEntityEnclosingRequest> T entity( T request, HttpEntity entity )
{
request.setEntity( entity );
return request;
}
private boolean isPayloadPresent( HttpUriRequest request )
{
if ( request instanceof HttpEntityEnclosingRequest )
{
HttpEntity entity = ( (HttpEntityEnclosingRequest) request ).getEntity();
return entity != null && entity.getContentLength() != 0;
}
return false;
}
private <T extends HttpUriRequest> T commonHeaders( T request )
{
request.setHeader( HttpHeaders.CACHE_CONTROL, "no-cache, no-store" );
request.setHeader( HttpHeaders.PRAGMA, "no-cache" );
if ( state.isExpectContinue() && isPayloadPresent( request ) )
{
request.setHeader( HttpHeaders.EXPECT, "100-continue" );
}
for ( Map.Entry<?, ?> entry : headers.entrySet() )
{
if ( !( entry.getKey() instanceof String ) )
{
continue;
}
if ( entry.getValue() instanceof String )
{
request.setHeader( entry.getKey().toString(), entry.getValue().toString() );
}
else
{
request.removeHeaders( entry.getKey().toString() );
}
}
if ( !state.isExpectContinue() )
{
request.removeHeaders( HttpHeaders.EXPECT );
}
return request;
}
@SuppressWarnings( "checkstyle:magicnumber" )
private <T extends HttpUriRequest> T resume( T request, GetTask task )
{
long resumeOffset = task.getResumeOffset();
if ( resumeOffset > 0L && task.getDataFile() != null )
{
request.setHeader( HttpHeaders.RANGE, "bytes=" + resumeOffset + '-' );
request.setHeader( HttpHeaders.IF_UNMODIFIED_SINCE,
DateUtils.formatDate( new Date( task.getDataFile().lastModified() - 60L * 1000L ) ) );
request.setHeader( HttpHeaders.ACCEPT_ENCODING, "identity" );
}
return request;
}
@SuppressWarnings( "checkstyle:magicnumber" )
private void handleStatus( HttpResponse response )
throws HttpResponseException
{
int status = response.getStatusLine().getStatusCode();
if ( status >= 300 )
{
throw new HttpResponseException( status, response.getStatusLine().getReasonPhrase() + " (" + status + ")" );
}
}
@Override
protected void implClose()
{
AuthenticationContext.close( repoAuthContext );
AuthenticationContext.close( proxyAuthContext );
state.close();
}
private class EntityGetter
{
private final GetTask task;
EntityGetter( GetTask task )
{
this.task = task;
}
public void handle( HttpResponse response )
throws IOException, TransferCancelledException
{
HttpEntity entity = response.getEntity();
if ( entity == null )
{
entity = new ByteArrayEntity( new byte[0] );
}
long offset = 0L, length = entity.getContentLength();
String range = getHeader( response, HttpHeaders.CONTENT_RANGE );
if ( range != null )
{
Matcher m = CONTENT_RANGE_PATTERN.matcher( range );
if ( !m.matches() )
{
throw new IOException( "Invalid Content-Range header for partial download: " + range );
}
offset = Long.parseLong( m.group( 1 ) );
length = Long.parseLong( m.group( 2 ) ) + 1L;
if ( offset < 0L || offset >= length || ( offset > 0L && offset != task.getResumeOffset() ) )
{
throw new IOException( "Invalid Content-Range header for partial download from offset "
+ task.getResumeOffset() + ": " + range );
}
}
InputStream is = entity.getContent();
utilGet( task, is, true, length, offset > 0L );
extractChecksums( response );
}
private void extractChecksums( HttpResponse response )
{
// Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}"
String etag = getHeader( response, HttpHeaders.ETAG );
if ( etag != null )
{
int start = etag.indexOf( "SHA1{" ), end = etag.indexOf( "}", start + 5 );
if ( start >= 0 && end > start )
{
task.setChecksum( "SHA-1", etag.substring( start + 5, end ) );
}
}
}
private String getHeader( HttpResponse response, String name )
{
Header header = response.getFirstHeader( name );
return ( header != null ) ? header.getValue() : null;
}
}
private class PutTaskEntity
extends AbstractHttpEntity
{
private final PutTask task;
PutTaskEntity( PutTask task )
{
this.task = task;
}
public boolean isRepeatable()
{
return true;
}
public boolean isStreaming()
{
return false;
}
public long getContentLength()
{
return task.getDataLength();
}
public InputStream getContent()
throws IOException
{
return task.newInputStream();
}
public void writeTo( OutputStream os )
throws IOException
{
try
{
utilPut( task, os, false );
}
catch ( TransferCancelledException e )
{
throw (IOException) new InterruptedIOException().initCause( e );
}
}
}
}