| 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 ); |
| } |
| } |
| |
| } |
| |
| } |