001package org.apache.maven.wagon.providers.http;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.http.Header;
023import org.apache.http.HttpEntity;
024import org.apache.http.HttpException;
025import org.apache.http.HttpHost;
026import org.apache.http.HttpResponse;
027import org.apache.http.HttpStatus;
028import org.apache.http.auth.AUTH;
029import org.apache.http.auth.AuthScope;
030import org.apache.http.auth.Credentials;
031import org.apache.http.auth.MalformedChallengeException;
032import org.apache.http.auth.NTCredentials;
033import org.apache.http.auth.UsernamePasswordCredentials;
034import org.apache.http.client.AuthCache;
035import org.apache.http.client.CredentialsProvider;
036import org.apache.http.client.config.CookieSpecs;
037import org.apache.http.client.config.RequestConfig;
038import org.apache.http.client.methods.CloseableHttpResponse;
039import org.apache.http.client.methods.HttpGet;
040import org.apache.http.client.methods.HttpHead;
041import org.apache.http.client.methods.HttpPut;
042import org.apache.http.client.methods.HttpUriRequest;
043import org.apache.http.client.protocol.HttpClientContext;
044import org.apache.http.client.utils.DateUtils;
045import org.apache.http.config.Registry;
046import org.apache.http.config.RegistryBuilder;
047import org.apache.http.conn.socket.ConnectionSocketFactory;
048import org.apache.http.conn.socket.PlainConnectionSocketFactory;
049import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
050import org.apache.http.conn.ssl.SSLContextBuilder;
051import org.apache.http.conn.ssl.SSLInitializationException;
052import org.apache.http.entity.AbstractHttpEntity;
053import org.apache.http.impl.auth.BasicScheme;
054import org.apache.http.impl.client.BasicAuthCache;
055import org.apache.http.impl.client.BasicCredentialsProvider;
056import org.apache.http.impl.client.CloseableHttpClient;
057import org.apache.http.impl.client.HttpClientBuilder;
058import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
059import org.apache.http.message.BasicHeader;
060import org.apache.http.protocol.HTTP;
061import org.apache.http.util.EntityUtils;
062import org.apache.maven.wagon.InputData;
063import org.apache.maven.wagon.OutputData;
064import org.apache.maven.wagon.PathUtils;
065import org.apache.maven.wagon.ResourceDoesNotExistException;
066import org.apache.maven.wagon.StreamWagon;
067import org.apache.maven.wagon.TransferFailedException;
068import org.apache.maven.wagon.Wagon;
069import org.apache.maven.wagon.authorization.AuthorizationException;
070import org.apache.maven.wagon.events.TransferEvent;
071import org.apache.maven.wagon.proxy.ProxyInfo;
072import org.apache.maven.wagon.repository.Repository;
073import org.apache.maven.wagon.resource.Resource;
074import org.codehaus.plexus.util.IOUtil;
075import org.codehaus.plexus.util.StringUtils;
076
077import javax.net.ssl.HttpsURLConnection;
078import javax.net.ssl.SSLContext;
079import java.io.ByteArrayInputStream;
080import java.io.Closeable;
081import java.io.File;
082import java.io.FileInputStream;
083import java.io.IOException;
084import java.io.InputStream;
085import java.io.OutputStream;
086import java.net.URLEncoder;
087import java.nio.ByteBuffer;
088import java.text.SimpleDateFormat;
089import java.util.Date;
090import java.util.Locale;
091import java.util.Map;
092import java.util.Properties;
093import java.util.TimeZone;
094import java.util.concurrent.TimeUnit;
095
096/**
097 * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
098 * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
099 */
100public abstract class AbstractHttpClientWagon
101    extends StreamWagon
102{
103    private final class RequestEntityImplementation
104        extends AbstractHttpEntity
105    {
106
107        private final static int BUFFER_SIZE = 2048;
108
109        private final Resource resource;
110
111        private final Wagon wagon;
112
113        private ByteBuffer byteBuffer;
114
115        private File source;
116
117        private long length = -1;
118
119        private RequestEntityImplementation( final InputStream stream, final Resource resource, final Wagon wagon,
120                                             final File source )
121            throws TransferFailedException
122        {
123            if ( source != null )
124            {
125                this.source = source;
126            }
127            else
128            {
129                try
130                {
131                    byte[] bytes = IOUtil.toByteArray( stream );
132                    byteBuffer = ByteBuffer.allocate( bytes.length );
133                    byteBuffer.put( bytes );
134                }
135                catch ( IOException e )
136                {
137                    throw new TransferFailedException( e.getMessage(), e );
138                }
139            }
140            this.resource = resource;
141            this.length = resource == null ? -1 : resource.getContentLength();
142
143            this.wagon = wagon;
144        }
145
146        public long getContentLength()
147        {
148            return length;
149        }
150
151        public InputStream getContent()
152            throws IOException, IllegalStateException
153        {
154            if ( this.source != null )
155            {
156                return new FileInputStream( this.source );
157            }
158            return new ByteArrayInputStream( this.byteBuffer.array() );
159        }
160
161        public boolean isRepeatable()
162        {
163            return true;
164        }
165
166        public void writeTo( final OutputStream outstream )
167            throws IOException
168        {
169            if ( outstream == null )
170            {
171                throw new IllegalArgumentException( "Output stream may not be null" );
172            }
173            TransferEvent transferEvent =
174                new TransferEvent( wagon, resource, TransferEvent.TRANSFER_PROGRESS, TransferEvent.REQUEST_PUT );
175            transferEvent.setTimestamp( System.currentTimeMillis() );
176            InputStream instream = ( this.source != null )
177                ? new FileInputStream( this.source )
178                : new ByteArrayInputStream( this.byteBuffer.array() );
179            try
180            {
181                byte[] buffer = new byte[BUFFER_SIZE];
182                int l;
183                if ( this.length < 0 )
184                {
185                    // until EOF
186                    while ( ( l = instream.read( buffer ) ) != -1 )
187                    {
188                        fireTransferProgress( transferEvent, buffer, -1 );
189                        outstream.write( buffer, 0, l );
190                    }
191                }
192                else
193                {
194                    // no need to consume more than length
195                    long remaining = this.length;
196                    while ( remaining > 0 )
197                    {
198                        l = instream.read( buffer, 0, (int) Math.min( BUFFER_SIZE, remaining ) );
199                        if ( l == -1 )
200                        {
201                            break;
202                        }
203                        fireTransferProgress( transferEvent, buffer, (int) Math.min( BUFFER_SIZE, remaining ) );
204                        outstream.write( buffer, 0, l );
205                        remaining -= l;
206                    }
207                }
208            }
209            finally
210            {
211                instream.close();
212            }
213        }
214
215        public boolean isStreaming()
216        {
217            return true;
218        }
219    }
220
221    private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone( "GMT" );
222
223    /**
224     * use http(s) connection pool mechanism.
225     * <b>enabled by default</b>
226     */
227    private final static boolean PERSISTENT_POOL =
228        Boolean.valueOf( System.getProperty( "maven.wagon.http.pool", "true" ) );
229
230    /**
231     * skip failure on certificate validity checks.
232     * <b>disabled by default</b>
233     */
234    private final static boolean SSL_INSECURE =
235        Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.insecure", "false" ) );
236
237    /**
238     * if using sslInsecure, certificate date issues will be ignored
239     * <b>disabled by default</b>
240     */
241    private final static boolean IGNORE_SSL_VALIDITY_DATES =
242        Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.ignore.validity.dates", "false" ) );
243
244    /**
245     * If enabled, ssl hostname verifier does not check hostname. Disable this will use a browser compat hostname verifier
246     * <b>disabled by default</b>
247     */
248    private final static boolean SSL_ALLOW_ALL =
249        Boolean.valueOf( System.getProperty( "maven.wagon.http.ssl.allowall", "false" ) );
250
251
252    /**
253     * Maximum conncurrent connections per distinct route.
254     * <b>20 by default</b>
255     */
256    private final static int MAX_CONN_PER_ROUTE =
257        Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxPerRoute", "20" ) );
258
259    /**
260     * Maximum conncurrent connections in total.
261     * <b>40 by default</b>
262     */
263    private final static int MAX_CONN_TOTAL =
264        Integer.parseInt( System.getProperty( "maven.wagon.httpconnectionManager.maxTotal", "40" ) );
265
266    /**
267     * Internal connection manager
268     */
269    private static final PoolingHttpClientConnectionManager CONN_MAN = createConnManager();
270
271    private static PoolingHttpClientConnectionManager createConnManager()
272    {
273
274        String sslProtocolsStr = System.getProperty( "https.protocols" );
275        String cipherSuitesStr = System.getProperty( "https.cipherSuites" );
276        String[] sslProtocols = sslProtocolsStr != null ? sslProtocolsStr.split( " *, *" ) : null;
277        String[] cipherSuites = cipherSuitesStr != null ? cipherSuitesStr.split( " *, *" ) : null;
278
279        SSLConnectionSocketFactory sslConnectionSocketFactory;
280        if ( SSL_INSECURE )
281        {
282            try
283            {
284                SSLContext sslContext = new SSLContextBuilder().useSSL().loadTrustMaterial( null,
285                                                                                            new RelaxedTrustStrategy(
286                                                                                                IGNORE_SSL_VALIDITY_DATES ) ).build();
287                sslConnectionSocketFactory = new SSLConnectionSocketFactory( sslContext, sslProtocols, cipherSuites,
288                                                                             SSL_ALLOW_ALL
289                                                                                 ? SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER
290                                                                                 : SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
291            }
292            catch ( Exception ex )
293            {
294                throw new SSLInitializationException( ex.getMessage(), ex );
295            }
296        }
297        else
298        {
299            sslConnectionSocketFactory =
300                new SSLConnectionSocketFactory( HttpsURLConnection.getDefaultSSLSocketFactory(), sslProtocols,
301                                                cipherSuites,
302                                                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER );
303        }
304
305        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register( "http",
306                                                                                                                 PlainConnectionSocketFactory.INSTANCE ).register(
307            "https", sslConnectionSocketFactory ).build();
308
309        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager( registry );
310        if ( PERSISTENT_POOL )
311        {
312            connManager.setDefaultMaxPerRoute( MAX_CONN_PER_ROUTE );
313            connManager.setMaxTotal( MAX_CONN_TOTAL );
314        }
315        else
316        {
317            connManager.setMaxTotal( 1 );
318        }
319        return connManager;
320    }
321
322    private static CloseableHttpClient CLIENT = createClient();
323
324    private static CloseableHttpClient createClient()
325    {
326        return HttpClientBuilder.create().useSystemProperties().disableConnectionState().setConnectionManager(
327            CONN_MAN ).build();
328    }
329
330    private static String DEFAULT_USER_AGENT = getDefaultUserAgent();
331
332    private static String getDefaultUserAgent()
333    {
334        Properties props = new Properties();
335
336        InputStream is = AbstractHttpClientWagon.class.getResourceAsStream(
337            "/META-INF/maven/org.apache.maven.wagon/wagon-http/pom.properties" );
338        if ( is != null )
339        {
340            try
341            {
342                props.load( is );
343            }
344            catch ( IOException ignore )
345            {
346            }
347            finally
348            {
349                IOUtil.close( is );
350            }
351        }
352
353        String ver = props.getProperty( "version", "unknown-version" );
354        return "Apache-Maven-Wagon/" + ver + " (Java " + System.getProperty( "java.version" ) + "; ";
355    }
356
357
358    private CredentialsProvider credentialsProvider;
359
360    private AuthCache authCache;
361
362    private HttpClientContext localContext;
363
364    private Closeable closeable;
365
366    /**
367     * @plexus.configuration
368     * @deprecated Use httpConfiguration instead.
369     */
370    private Properties httpHeaders;
371
372    /**
373     * @since 1.0-beta-6
374     */
375    private HttpConfiguration httpConfiguration;
376
377    public void openConnectionInternal()
378    {
379        repository.setUrl( getURL( repository ) );
380
381        localContext = HttpClientContext.create();
382        credentialsProvider = new BasicCredentialsProvider();
383        authCache = new BasicAuthCache();
384        localContext.setCredentialsProvider( credentialsProvider );
385        localContext.setAuthCache( authCache );
386
387        if ( authenticationInfo != null )
388        {
389
390            String username = authenticationInfo.getUserName();
391            String password = authenticationInfo.getPassword();
392
393            if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
394            {
395                Credentials creds = new UsernamePasswordCredentials( username, password );
396
397                String host = getRepository().getHost();
398                int port = getRepository().getPort() > -1 ? getRepository().getPort() : AuthScope.ANY_PORT;
399
400                credentialsProvider.setCredentials( new AuthScope( host, port ), creds );
401            }
402        }
403
404        ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
405        if ( proxyInfo != null )
406        {
407            String proxyUsername = proxyInfo.getUserName();
408            String proxyPassword = proxyInfo.getPassword();
409            String proxyHost = proxyInfo.getHost();
410            String proxyNtlmHost = proxyInfo.getNtlmHost();
411            String proxyNtlmDomain = proxyInfo.getNtlmDomain();
412            if ( proxyHost != null )
413            {
414                if ( proxyUsername != null && proxyPassword != null )
415                {
416                    Credentials creds;
417                    if ( proxyNtlmHost != null || proxyNtlmDomain != null )
418                    {
419                        creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
420                    }
421                    else
422                    {
423                        creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
424                    }
425
426                    int port = proxyInfo.getPort() > -1 ? proxyInfo.getPort() : AuthScope.ANY_PORT;
427
428                    AuthScope authScope = new AuthScope( proxyHost, port );
429                    credentialsProvider.setCredentials( authScope, creds );
430                }
431            }
432        }
433    }
434
435    public void closeConnection()
436    {
437        if ( !PERSISTENT_POOL )
438        {
439            CONN_MAN.closeIdleConnections( 0, TimeUnit.MILLISECONDS );
440        }
441    }
442
443    public void put( File source, String resourceName )
444        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
445    {
446        Resource resource = new Resource( resourceName );
447
448        firePutInitiated( resource, source );
449
450        resource.setContentLength( source.length() );
451
452        resource.setLastModified( source.lastModified() );
453
454        put( null, resource, source );
455    }
456
457    public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
458        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
459    {
460        Resource resource = new Resource( destination );
461
462        firePutInitiated( resource, null );
463
464        resource.setContentLength( contentLength );
465
466        resource.setLastModified( lastModified );
467
468        put( stream, resource, null );
469    }
470
471    private void put( final InputStream stream, Resource resource, File source )
472        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
473    {
474        put( resource, source, new RequestEntityImplementation( stream, resource, this, source ) );
475    }
476
477    private void put( Resource resource, File source, HttpEntity httpEntity )
478        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
479    {
480
481        StringBuilder url = new StringBuilder( getURL( getRepository() ) );
482        String[] parts = StringUtils.split( resource.getName(), "/" );
483        for ( String part : parts )
484        {
485            // TODO: Fix encoding...
486            // url += "/" + URLEncoder.encode( parts[i], System.getProperty("file.encoding") );
487            if ( !url.toString().endsWith( "/" ) )
488            {
489                url.append( '/' );
490            }
491            url.append( URLEncoder.encode( part ) );
492        }
493        put( resource, source, httpEntity, url.toString() );
494    }
495
496    private void put( Resource resource, File source, HttpEntity httpEntity, String url )
497        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
498    {
499
500        //Parent directories need to be created before posting
501        try
502        {
503            mkdirs( PathUtils.dirname( resource.getName() ) );
504        }
505        catch ( HttpException he )
506        {
507            fireTransferError( resource, he, TransferEvent.REQUEST_GET );
508        }
509        catch ( IOException e )
510        {
511            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
512        }
513
514        // preemptive for put
515        // TODO: is it a good idea, though? 'Expect-continue' handshake would serve much better
516
517        Repository repo = getRepository();
518        HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
519        AuthScope targetScope = new AuthScope( targetHost );
520
521        if ( credentialsProvider.getCredentials( targetScope ) != null )
522        {
523            BasicScheme targetAuth = new BasicScheme();
524            try {
525                targetAuth.processChallenge(new BasicHeader(AUTH.WWW_AUTH, "BASIC preemptive"));
526                authCache.put( targetHost, targetAuth  );
527            }
528            catch ( MalformedChallengeException ignore )
529            {
530            }
531        }
532
533        HttpPut putMethod = new HttpPut( url );
534
535        firePutStarted( resource, source );
536
537        try
538        {
539            putMethod.setEntity( httpEntity );
540
541            CloseableHttpResponse response = execute( putMethod );
542            try
543            {
544                int statusCode = response.getStatusLine().getStatusCode();
545                String reasonPhrase = ", ReasonPhrase: " + response.getStatusLine().getReasonPhrase() + ".";
546                fireTransferDebug( url + " - Status code: " + statusCode + reasonPhrase );
547
548                // Check that we didn't run out of retries.
549                switch ( statusCode )
550                {
551                    // Success Codes
552                    case HttpStatus.SC_OK: // 200
553                    case HttpStatus.SC_CREATED: // 201
554                    case HttpStatus.SC_ACCEPTED: // 202
555                    case HttpStatus.SC_NO_CONTENT:  // 204
556                        break;
557                    // 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"
558                    case HttpStatus.SC_MOVED_PERMANENTLY: // 301
559                    case HttpStatus.SC_MOVED_TEMPORARILY: // 302
560                    case HttpStatus.SC_SEE_OTHER: // 303
561                        put( resource, source, httpEntity, calculateRelocatedUrl( response ) );
562                        return;
563                    case HttpStatus.SC_FORBIDDEN:
564                        fireSessionConnectionRefused();
565                        throw new AuthorizationException( "Access denied to: " + url + reasonPhrase );
566
567                    case HttpStatus.SC_NOT_FOUND:
568                        throw new ResourceDoesNotExistException( "File: " + url + " does not exist" + reasonPhrase );
569
570                        //add more entries here
571                    default:
572                    {
573                        TransferFailedException e = new TransferFailedException(
574                            "Failed to transfer file: " + url + ". Return code is: " + statusCode + reasonPhrase );
575                        fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
576                        throw e;
577                    }
578                }
579
580                firePutCompleted( resource, source );
581
582                EntityUtils.consume( response.getEntity() );
583            }
584            finally
585            {
586                response.close();
587            }
588        }
589        catch ( IOException e )
590        {
591            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
592
593            throw new TransferFailedException( e.getMessage(), e );
594        }
595        catch ( HttpException e )
596        {
597            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
598
599            throw new TransferFailedException( e.getMessage(), e );
600        }
601    }
602
603    protected String calculateRelocatedUrl( HttpResponse response )
604    {
605        Header locationHeader = response.getFirstHeader( "Location" );
606        String locationField = locationHeader.getValue();
607        // is it a relative Location or a full ?
608        return locationField.startsWith( "http" ) ? locationField : getURL( getRepository() ) + '/' + locationField;
609    }
610
611    protected void mkdirs( String dirname )
612        throws HttpException, IOException
613    {
614        // nothing to do
615    }
616
617    public boolean resourceExists( String resourceName )
618        throws TransferFailedException, AuthorizationException
619    {
620        String repositoryUrl = getRepository().getUrl();
621        String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + resourceName;
622        HttpHead headMethod = new HttpHead( url );
623        try
624        {
625            CloseableHttpResponse response = execute( headMethod );
626            try
627            {
628                int statusCode = response.getStatusLine().getStatusCode();
629                String reasonPhrase = ", ReasonPhrase: " + response.getStatusLine().getReasonPhrase() + ".";
630                boolean result;
631                switch ( statusCode )
632                {
633                    case HttpStatus.SC_OK:
634                        result = true;
635                        break;
636                    case HttpStatus.SC_NOT_MODIFIED:
637                        result = true;
638                        break;
639                    case HttpStatus.SC_FORBIDDEN:
640                        throw new AuthorizationException( "Access denied to: " + url + reasonPhrase );
641
642                    case HttpStatus.SC_UNAUTHORIZED:
643                        throw new AuthorizationException( "Not authorized " + reasonPhrase );
644
645                    case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
646                        throw new AuthorizationException( "Not authorized by proxy " + reasonPhrase );
647
648                    case HttpStatus.SC_NOT_FOUND:
649                        result = false;
650                        break;
651                    //add more entries here
652                    default:
653                        throw new TransferFailedException(
654                            "Failed to transfer file: " + url + ". Return code is: " + statusCode + reasonPhrase );
655                }
656
657                EntityUtils.consume( response.getEntity() );
658                return result;
659            }
660            finally
661            {
662                response.close();
663            }
664        }
665        catch ( IOException e )
666        {
667            throw new TransferFailedException( e.getMessage(), e );
668        }
669        catch ( HttpException e )
670        {
671            throw new TransferFailedException( e.getMessage(), e );
672        }
673    }
674
675    protected CloseableHttpResponse execute( HttpUriRequest httpMethod )
676        throws HttpException, IOException
677    {
678        setHeaders( httpMethod );
679        String userAgent = getUserAgent( httpMethod );
680        if ( userAgent != null )
681        {
682            httpMethod.setHeader( HTTP.USER_AGENT, userAgent );
683        }
684
685        RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
686        // WAGON-273: default the cookie-policy to browser compatible
687        requestConfigBuilder.setCookieSpec( CookieSpecs.BROWSER_COMPATIBILITY );
688
689        Repository repo = getRepository();
690        ProxyInfo proxyInfo = getProxyInfo( repo.getProtocol(), repo.getHost() );
691        if ( proxyInfo != null )
692        {
693            HttpHost proxy = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
694            requestConfigBuilder.setProxy( proxy );
695        }
696
697        HttpMethodConfiguration config =
698            httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( httpMethod );
699
700        if ( config != null )
701        {
702            ConfigurationUtils.copyConfig( config, requestConfigBuilder );
703        }
704        else
705        {
706            requestConfigBuilder.setSocketTimeout( getReadTimeout() );
707        }
708
709        localContext.setRequestConfig( requestConfigBuilder.build() );
710
711        if ( config != null && config.isUsePreemptive() )
712        {
713            HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
714            AuthScope targetScope = new AuthScope( targetHost );
715
716            if ( credentialsProvider.getCredentials( targetScope ) != null )
717            {
718                BasicScheme targetAuth = new BasicScheme();
719                targetAuth.processChallenge( new BasicHeader(AUTH.WWW_AUTH, "BASIC preemptive" ) );
720                authCache.put( targetHost, targetAuth  );
721            }
722        }
723
724        if ( proxyInfo != null )
725        {
726            if ( proxyInfo.getHost() != null )
727            {
728                HttpHost proxyHost = new HttpHost( proxyInfo.getHost(), proxyInfo.getPort() );
729                AuthScope proxyScope = new AuthScope( proxyHost );
730
731                String proxyUsername = proxyInfo.getUserName();
732                String proxyPassword = proxyInfo.getPassword();
733                String proxyNtlmHost = proxyInfo.getNtlmHost();
734                String proxyNtlmDomain = proxyInfo.getNtlmDomain();
735
736                if ( proxyUsername != null && proxyPassword != null )
737                {
738                    Credentials creds;
739                    if ( proxyNtlmHost != null || proxyNtlmDomain != null )
740                    {
741                        creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
742                    }
743                    else
744                    {
745                        creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
746                    }
747
748                    credentialsProvider.setCredentials( proxyScope, creds );
749                    BasicScheme proxyAuth = new BasicScheme();
750                    proxyAuth.processChallenge( new BasicHeader(AUTH.PROXY_AUTH, "BASIC preemptive" ) );
751                    authCache.put( proxyHost, proxyAuth );
752                }
753            }
754        }
755
756        return CLIENT.execute( httpMethod, localContext );
757    }
758
759    protected void setHeaders( HttpUriRequest method )
760    {
761        HttpMethodConfiguration config =
762            httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
763        if ( config == null || config.isUseDefaultHeaders() )
764        {
765            // TODO: merge with the other headers and have some better defaults, unify with lightweight headers
766            method.addHeader( "Cache-control", "no-cache" );
767            method.addHeader( "Cache-store", "no-store" );
768            method.addHeader( "Pragma", "no-cache" );
769            method.addHeader( "Expires", "0" );
770            method.addHeader( "Accept-Encoding", "gzip" );
771            method.addHeader( "User-Agent", DEFAULT_USER_AGENT );
772        }
773
774        if ( httpHeaders != null )
775        {
776            for ( Map.Entry<Object, Object> entry : httpHeaders.entrySet() )
777            {
778                method.addHeader( (String) entry.getKey(), (String) entry.getValue() );
779            }
780        }
781
782        Header[] headers = config == null ? null : config.asRequestHeaders();
783        if ( headers != null )
784        {
785            for ( Header header : headers )
786            {
787                method.addHeader( header );
788            }
789        }
790    }
791
792    protected String getUserAgent( HttpUriRequest method )
793    {
794        if ( httpHeaders != null )
795        {
796            String value = (String) httpHeaders.get( "User-Agent" );
797            if ( value != null )
798            {
799                return value;
800            }
801        }
802        HttpMethodConfiguration config =
803            httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
804
805        if ( config != null )
806        {
807            return (String) config.getHeaders().get( "User-Agent" );
808        }
809        return null;
810    }
811
812    /**
813     * getUrl
814     * Implementors can override this to remove unwanted parts of the url such as role-hints
815     *
816     * @param repository
817     * @return
818     */
819    protected String getURL( Repository repository )
820    {
821        return repository.getUrl();
822    }
823
824    public HttpConfiguration getHttpConfiguration()
825    {
826        return httpConfiguration;
827    }
828
829    public void setHttpConfiguration( HttpConfiguration httpConfiguration )
830    {
831        this.httpConfiguration = httpConfiguration;
832    }
833
834    public void fillInputData( InputData inputData )
835        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
836    {
837        Resource resource = inputData.getResource();
838
839        String repositoryUrl = getRepository().getUrl();
840        String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + resource.getName();
841        HttpGet getMethod = new HttpGet( url );
842        long timestamp = resource.getLastModified();
843        if ( timestamp > 0 )
844        {
845            SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
846            fmt.setTimeZone( GMT_TIME_ZONE );
847            Header hdr = new BasicHeader( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
848            fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
849            getMethod.addHeader( hdr );
850        }
851
852        try
853        {
854            CloseableHttpResponse response = execute( getMethod );
855            closeable = response;
856            int statusCode = response.getStatusLine().getStatusCode();
857
858            String reasonPhrase = ", ReasonPhrase:" + response.getStatusLine().getReasonPhrase() + ".";
859
860            fireTransferDebug( url + " - Status code: " + statusCode + reasonPhrase );
861
862            switch ( statusCode )
863            {
864                case HttpStatus.SC_OK:
865                    break;
866
867                case HttpStatus.SC_NOT_MODIFIED:
868                    // return, leaving last modified set to original value so getIfNewer should return unmodified
869                    return;
870                case HttpStatus.SC_FORBIDDEN:
871                    fireSessionConnectionRefused();
872                    throw new AuthorizationException( "Access denied to: " + url + " " + reasonPhrase );
873
874                case HttpStatus.SC_UNAUTHORIZED:
875                    fireSessionConnectionRefused();
876                    throw new AuthorizationException( "Not authorized " + reasonPhrase );
877
878                case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
879                    fireSessionConnectionRefused();
880                    throw new AuthorizationException( "Not authorized by proxy " + reasonPhrase );
881
882                case HttpStatus.SC_NOT_FOUND:
883                    throw new ResourceDoesNotExistException( "File: " + url + " " + reasonPhrase );
884
885                    // add more entries here
886                default:
887                {
888                    cleanupGetTransfer( resource );
889                    TransferFailedException e = new TransferFailedException(
890                        "Failed to transfer file: " + url + ". Return code is: " + statusCode + " " + reasonPhrase );
891                    fireTransferError( resource, e, TransferEvent.REQUEST_GET );
892                    throw e;
893                }
894            }
895
896            Header contentLengthHeader = response.getFirstHeader( "Content-Length" );
897
898            if ( contentLengthHeader != null )
899            {
900                try
901                {
902                    long contentLength = Long.parseLong( contentLengthHeader.getValue() );
903
904                    resource.setContentLength( contentLength );
905                }
906                catch ( NumberFormatException e )
907                {
908                    fireTransferDebug(
909                        "error parsing content length header '" + contentLengthHeader.getValue() + "' " + e );
910                }
911            }
912
913            Header lastModifiedHeader = response.getFirstHeader( "Last-Modified" );
914            if ( lastModifiedHeader != null )
915            {
916                Date lastModified = DateUtils.parseDate( lastModifiedHeader.getValue() );
917                if ( lastModified != null )
918                {
919                    resource.setLastModified( lastModified.getTime() );
920                    fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() +
921                                           " (" + lastModified.getTime() + ")" );
922                }
923            }
924
925            HttpEntity entity = response.getEntity();
926            if ( entity != null )
927            {
928                inputData.setInputStream( entity.getContent() );
929            }
930        }
931        catch ( IOException e )
932        {
933            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
934
935            throw new TransferFailedException( e.getMessage(), e );
936        }
937        catch ( HttpException e )
938        {
939            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
940
941            throw new TransferFailedException( e.getMessage(), e );
942        }
943    }
944
945    protected void cleanupGetTransfer( Resource resource )
946    {
947        if ( closeable != null )
948        {
949            try
950            {
951                closeable.close();
952            }
953            catch ( IOException ignore )
954            {
955            }
956
957        }
958    }
959
960
961    @Override
962    public void putFromStream( InputStream stream, String destination )
963        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
964    {
965        putFromStream( stream, destination, -1, -1 );
966    }
967
968    @Override
969    protected void putFromStream( InputStream stream, Resource resource )
970        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
971    {
972        putFromStream( stream, resource.getName(), -1, -1 );
973    }
974
975    public Properties getHttpHeaders()
976    {
977        return httpHeaders;
978    }
979
980    public void setHttpHeaders( Properties httpHeaders )
981    {
982        this.httpHeaders = httpHeaders;
983    }
984
985    @Override
986    public void fillOutputData( OutputData outputData )
987        throws TransferFailedException
988    {
989        // no needed in this implementation but throw an Exception if used
990        throw new IllegalStateException( "this wagon http client must not use fillOutputData" );
991    }
992}