001package org.apache.maven.wagon.providers.webdav;
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.commons.httpclient.Credentials;
023import org.apache.commons.httpclient.Header;
024import org.apache.commons.httpclient.HostConfiguration;
025import org.apache.commons.httpclient.HttpClient;
026import org.apache.commons.httpclient.HttpConnectionManager;
027import org.apache.commons.httpclient.HttpMethod;
028import org.apache.commons.httpclient.HttpStatus;
029import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
030import org.apache.commons.httpclient.NTCredentials;
031import org.apache.commons.httpclient.UsernamePasswordCredentials;
032import org.apache.commons.httpclient.auth.AuthScope;
033import org.apache.commons.httpclient.cookie.CookiePolicy;
034import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
035import org.apache.commons.httpclient.methods.GetMethod;
036import org.apache.commons.httpclient.methods.HeadMethod;
037import org.apache.commons.httpclient.methods.PutMethod;
038import org.apache.commons.httpclient.methods.RequestEntity;
039import org.apache.commons.httpclient.params.HttpMethodParams;
040import org.apache.commons.httpclient.util.DateParseException;
041import org.apache.commons.httpclient.util.DateUtil;
042import org.apache.commons.io.IOUtils;
043import org.apache.commons.lang.StringUtils;
044import org.apache.maven.wagon.InputData;
045import org.apache.maven.wagon.OutputData;
046import org.apache.maven.wagon.PathUtils;
047import org.apache.maven.wagon.ResourceDoesNotExistException;
048import org.apache.maven.wagon.StreamWagon;
049import org.apache.maven.wagon.TransferFailedException;
050import org.apache.maven.wagon.Wagon;
051import org.apache.maven.wagon.authorization.AuthorizationException;
052import org.apache.maven.wagon.events.TransferEvent;
053import org.apache.maven.wagon.proxy.ProxyInfo;
054import org.apache.maven.wagon.repository.Repository;
055import org.apache.maven.wagon.resource.Resource;
056
057import java.io.ByteArrayInputStream;
058import java.io.File;
059import java.io.FileInputStream;
060import java.io.IOException;
061import java.io.InputStream;
062import java.io.OutputStream;
063import java.net.URLEncoder;
064import java.nio.ByteBuffer;
065import java.text.SimpleDateFormat;
066import java.util.Date;
067import java.util.Locale;
068import java.util.Properties;
069import java.util.TimeZone;
070import java.util.zip.GZIPInputStream;
071
072/**
073 * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
074 * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
075 */
076public abstract class AbstractHttpClientWagon
077    extends StreamWagon
078{
079    private final class RequestEntityImplementation
080        implements RequestEntity
081    {
082        private final Resource resource;
083
084        private final Wagon wagon;
085
086        private File source;
087
088        private ByteBuffer byteBuffer;
089
090        private RequestEntityImplementation( final InputStream stream, final Resource resource, final Wagon wagon,
091                                             final File source )
092            throws TransferFailedException
093        {
094            if ( source != null )
095            {
096                this.source = source;
097            }
098            else
099            {
100                try
101                {
102                    byte[] bytes = IOUtils.toByteArray( stream );
103                    this.byteBuffer = ByteBuffer.allocate( bytes.length );
104                    this.byteBuffer.put( bytes );
105                }
106                catch ( IOException e )
107                {
108                    throw new TransferFailedException( e.getMessage(), e );
109                }
110            }
111
112            this.resource = resource;
113            this.wagon = wagon;
114        }
115
116        public long getContentLength()
117        {
118            return resource.getContentLength();
119        }
120
121        public String getContentType()
122        {
123            return null;
124        }
125
126        public boolean isRepeatable()
127        {
128            return true;
129        }
130
131        public void writeRequest( OutputStream output )
132            throws IOException
133        {
134            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
135
136            TransferEvent transferEvent =
137                new TransferEvent( wagon, resource, TransferEvent.TRANSFER_PROGRESS, TransferEvent.REQUEST_PUT );
138            transferEvent.setTimestamp( System.currentTimeMillis() );
139
140            InputStream fin = null;
141            try
142            {
143                fin = this.source != null
144                    ? new FileInputStream( source )
145                    : new ByteArrayInputStream( this.byteBuffer.array() );
146                int remaining = Integer.MAX_VALUE;
147                while ( remaining > 0 )
148                {
149                    int n = fin.read( buffer, 0, Math.min( buffer.length, remaining ) );
150
151                    if ( n == -1 )
152                    {
153                        break;
154                    }
155
156                    fireTransferProgress( transferEvent, buffer, n );
157
158                    output.write( buffer, 0, n );
159
160                    remaining -= n;
161                }
162            }
163            finally
164            {
165                IOUtils.closeQuietly( fin );
166            }
167
168            output.flush();
169        }
170    }
171
172    protected static final int SC_NULL = -1;
173
174    protected static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone( "GMT" );
175
176    private HttpClient client;
177
178    protected HttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
179
180    /**
181     * @deprecated Use httpConfiguration instead.
182     */
183    private Properties httpHeaders;
184
185    /**
186     * @since 1.0-beta-6
187     */
188    private HttpConfiguration httpConfiguration;
189
190    private HttpMethod getMethod;
191
192    public void openConnectionInternal()
193    {
194        repository.setUrl( getURL( repository ) );
195        client = new HttpClient( connectionManager );
196
197        // WAGON-273: default the cookie-policy to browser compatible
198        client.getParams().setCookiePolicy( CookiePolicy.BROWSER_COMPATIBILITY );
199
200
201
202        String username = null;
203        String password = null;
204        String domain = null;
205
206        if ( authenticationInfo != null )
207        {
208            username = authenticationInfo.getUserName();
209
210            if ( StringUtils.contains( username, "\\" ) )
211            {
212                String[] domainAndUsername = username.split( "\\\\" );
213                domain = domainAndUsername[0];
214                username = domainAndUsername[1];
215            }
216
217            password = authenticationInfo.getPassword();
218
219
220        }
221
222        String host = getRepository().getHost();
223
224        if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
225        {
226            Credentials creds;
227            if ( domain != null )
228            {
229                creds = new NTCredentials( username, password, host, domain );
230            }
231            else
232            {
233                creds = new UsernamePasswordCredentials( username, password );
234            }
235
236            int port = getRepository().getPort() > -1 ? getRepository().getPort() : AuthScope.ANY_PORT;
237
238            AuthScope scope = new AuthScope( host, port );
239            client.getState().setCredentials( scope, creds );
240        }
241
242        HostConfiguration hc = new HostConfiguration();
243
244        ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
245        if ( proxyInfo != null )
246        {
247            String proxyUsername = proxyInfo.getUserName();
248            String proxyPassword = proxyInfo.getPassword();
249            String proxyHost = proxyInfo.getHost();
250            int proxyPort = proxyInfo.getPort();
251            String proxyNtlmHost = proxyInfo.getNtlmHost();
252            String proxyNtlmDomain = proxyInfo.getNtlmDomain();
253            if ( proxyHost != null )
254            {
255                hc.setProxy( proxyHost, proxyPort );
256
257                if ( proxyUsername != null && proxyPassword != null )
258                {
259                    Credentials creds;
260                    if ( proxyNtlmHost != null || proxyNtlmDomain != null )
261                    {
262                        creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
263                    }
264                    else
265                    {
266                        creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
267                    }
268
269                    int port = proxyInfo.getPort() > -1 ? proxyInfo.getPort() : AuthScope.ANY_PORT;
270
271                    AuthScope scope = new AuthScope( proxyHost, port );
272                    client.getState().setProxyCredentials( scope, creds );
273                }
274            }
275        }
276
277        hc.setHost( host );
278
279        //start a session with the webserver
280        client.setHostConfiguration( hc );
281    }
282
283    public void closeConnection()
284    {
285        if ( connectionManager instanceof MultiThreadedHttpConnectionManager )
286        {
287            ( (MultiThreadedHttpConnectionManager) connectionManager ).shutdown();
288        }
289    }
290
291    public void put( File source, String resourceName )
292        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
293    {
294        Resource resource = new Resource( resourceName );
295
296        firePutInitiated( resource, source );
297
298        resource.setContentLength( source.length() );
299
300        resource.setLastModified( source.lastModified() );
301
302        put( null, resource, source );
303    }
304
305    public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
306        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
307    {
308        Resource resource = new Resource( destination );
309
310        firePutInitiated( resource, null );
311
312        resource.setContentLength( contentLength );
313
314        resource.setLastModified( lastModified );
315
316        put( stream, resource, null );
317    }
318
319    private void put( final InputStream stream, Resource resource, File source )
320        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
321    {
322        StringBuilder url = new StringBuilder( getRepository().getUrl() );
323        String[] parts = StringUtils.split( resource.getName(), "/" );
324        for ( String part : parts )
325        {
326            // TODO: Fix encoding...
327            if ( !url.toString().endsWith( "/" ) )
328            {
329                url.append( '/' );
330            }
331            url.append( URLEncoder.encode( part ) );
332        }
333        RequestEntityImplementation requestEntityImplementation =
334            new RequestEntityImplementation( stream, resource, this, source );
335        put( resource, source, requestEntityImplementation, url.toString() );
336
337    }
338
339    private void put( Resource resource, File source, RequestEntityImplementation requestEntityImplementation,
340                      String url )
341        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
342    {
343
344        // preemptive true for put
345        client.getParams().setAuthenticationPreemptive( true );
346
347        //Parent directories need to be created before posting
348        try
349        {
350            mkdirs( PathUtils.dirname( resource.getName() ) );
351        }
352        catch ( IOException e )
353        {
354            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
355        }
356
357        PutMethod putMethod = new PutMethod( url );
358
359        firePutStarted( resource, source );
360
361        try
362        {
363            putMethod.setRequestEntity( requestEntityImplementation );
364
365            int statusCode;
366            try
367            {
368                statusCode = execute( putMethod );
369
370            }
371            catch ( IOException e )
372            {
373                fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
374
375                throw new TransferFailedException( e.getMessage(), e );
376            }
377
378            fireTransferDebug( url + " - Status code: " + statusCode );
379
380            // Check that we didn't run out of retries.
381            switch ( statusCode )
382            {
383                // Success Codes
384                case HttpStatus.SC_OK: // 200
385                case HttpStatus.SC_CREATED: // 201
386                case HttpStatus.SC_ACCEPTED: // 202
387                case HttpStatus.SC_NO_CONTENT:  // 204
388                    break;
389
390                // 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"
391                case HttpStatus.SC_MOVED_PERMANENTLY: // 301
392                case HttpStatus.SC_MOVED_TEMPORARILY: // 302
393                case HttpStatus.SC_SEE_OTHER: // 303
394                    String relocatedUrl = calculateRelocatedUrl( putMethod );
395                    fireTransferDebug( "relocate to " + relocatedUrl );
396                    put( resource, source, requestEntityImplementation, relocatedUrl );
397                    return;
398
399                case SC_NULL:
400                {
401                    TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
402                    fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
403                    throw e;
404                }
405
406                case HttpStatus.SC_FORBIDDEN:
407                    fireSessionConnectionRefused();
408                    throw new AuthorizationException( "Access denied to: " + url );
409
410                case HttpStatus.SC_NOT_FOUND:
411                    throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
412
413                    //add more entries here
414                default:
415                {
416                    TransferFailedException e = new TransferFailedException(
417                        "Failed to transfer file: " + url + ". Return code is: " + statusCode );
418                    fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
419                    throw e;
420                }
421            }
422
423            firePutCompleted( resource, source );
424        }
425        finally
426        {
427            putMethod.releaseConnection();
428        }
429    }
430
431    protected String calculateRelocatedUrl( EntityEnclosingMethod method )
432    {
433        Header locationHeader = method.getResponseHeader( "Location" );
434        String locationField = locationHeader.getValue();
435        // is it a relative Location or a full ?
436        return locationField.startsWith( "http" ) ? locationField : getURL( getRepository() ) + '/' + locationField;
437    }
438
439    protected void mkdirs( String dirname )
440        throws IOException
441    {
442        // do nothing as default.
443    }
444
445    public boolean resourceExists( String resourceName )
446        throws TransferFailedException, AuthorizationException
447    {
448        StringBuilder url = new StringBuilder( getRepository().getUrl() );
449        if ( !url.toString().endsWith( "/" ) )
450        {
451            url.append( '/' );
452        }
453        url.append( resourceName );
454        HeadMethod headMethod = new HeadMethod( url.toString() );
455
456        int statusCode;
457        try
458        {
459            statusCode = execute( headMethod );
460        }
461        catch ( IOException e )
462        {
463            throw new TransferFailedException( e.getMessage(), e );
464        }
465        try
466        {
467            switch ( statusCode )
468            {
469                case HttpStatus.SC_OK:
470                    return true;
471
472                case HttpStatus.SC_NOT_MODIFIED:
473                    return true;
474
475                case SC_NULL:
476                    throw new TransferFailedException( "Failed to transfer file: " + url );
477
478                case HttpStatus.SC_FORBIDDEN:
479                    throw new AuthorizationException( "Access denied to: " + url );
480
481                case HttpStatus.SC_UNAUTHORIZED:
482                    throw new AuthorizationException( "Not authorized." );
483
484                case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
485                    throw new AuthorizationException( "Not authorized by proxy." );
486
487                case HttpStatus.SC_NOT_FOUND:
488                    return false;
489
490                //add more entries here
491                default:
492                    throw new TransferFailedException(
493                        "Failed to transfer file: " + url + ". Return code is: " + statusCode );
494            }
495        }
496        finally
497        {
498            headMethod.releaseConnection();
499        }
500    }
501
502    protected int execute( HttpMethod httpMethod )
503        throws IOException
504    {
505        int statusCode;
506
507        setParameters( httpMethod );
508        setHeaders( httpMethod );
509
510        statusCode = client.executeMethod( httpMethod );
511        return statusCode;
512    }
513
514    protected void setParameters( HttpMethod method )
515    {
516        HttpMethodConfiguration config =
517            httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
518        if ( config != null )
519        {
520            HttpMethodParams params = config.asMethodParams( method.getParams() );
521            if ( params != null )
522            {
523                method.setParams( params );
524            }
525        }
526
527        if ( config == null || config.getConnectionTimeout() == HttpMethodConfiguration.DEFAULT_CONNECTION_TIMEOUT )
528        {
529            method.getParams().setSoTimeout( getTimeout() );
530        }
531    }
532
533    protected void setHeaders( HttpMethod method )
534    {
535        HttpMethodConfiguration config =
536            httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
537        if ( config == null || config.isUseDefaultHeaders() )
538        {
539            // TODO: merge with the other headers and have some better defaults, unify with lightweight headers
540            method.addRequestHeader( "Cache-control", "no-cache" );
541            method.addRequestHeader( "Cache-store", "no-store" );
542            method.addRequestHeader( "Pragma", "no-cache" );
543            method.addRequestHeader( "Expires", "0" );
544            method.addRequestHeader( "Accept-Encoding", "gzip" );
545        }
546
547        if ( httpHeaders != null )
548        {
549            for ( Object header : httpHeaders.keySet() )
550            {
551                method.addRequestHeader( (String) header, httpHeaders.getProperty( (String) header ) );
552            }
553        }
554
555        Header[] headers = config == null ? null : config.asRequestHeaders();
556        if ( headers != null )
557        {
558            for ( Header header : headers )
559            {
560                method.addRequestHeader( header );
561            }
562        }
563    }
564
565    /**
566     * getUrl
567     * Implementors can override this to remove unwanted parts of the url such as role-hints
568     *
569     * @param repository
570     * @return
571     */
572    protected String getURL( Repository repository )
573    {
574        return repository.getUrl();
575    }
576
577    protected HttpClient getClient()
578    {
579        return client;
580    }
581
582    public void setConnectionManager( HttpConnectionManager connectionManager )
583    {
584        this.connectionManager = connectionManager;
585    }
586
587    public Properties getHttpHeaders()
588    {
589        return httpHeaders;
590    }
591
592    public void setHttpHeaders( Properties httpHeaders )
593    {
594        this.httpHeaders = httpHeaders;
595    }
596
597    public HttpConfiguration getHttpConfiguration()
598    {
599        return httpConfiguration;
600    }
601
602    public void setHttpConfiguration( HttpConfiguration httpConfiguration )
603    {
604        this.httpConfiguration = httpConfiguration;
605    }
606
607    public void fillInputData( InputData inputData )
608        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
609    {
610        Resource resource = inputData.getResource();
611
612        StringBuilder url = new StringBuilder( getRepository().getUrl() );
613        if ( !url.toString().endsWith( "/" ) )
614        {
615            url.append( '/' );
616        }
617        url.append( resource.getName() );
618
619        getMethod = new GetMethod( url.toString() );
620
621        long timestamp = resource.getLastModified();
622        if ( timestamp > 0 )
623        {
624            SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
625            fmt.setTimeZone( GMT_TIME_ZONE );
626            Header hdr = new Header( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
627            fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
628            getMethod.addRequestHeader( hdr );
629        }
630
631        int statusCode;
632        try
633        {
634            statusCode = execute( getMethod );
635        }
636        catch ( IOException e )
637        {
638            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
639
640            throw new TransferFailedException( e.getMessage(), e );
641        }
642
643        fireTransferDebug( url + " - Status code: " + statusCode );
644
645        // TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is
646        // required
647        switch ( statusCode )
648        {
649            case HttpStatus.SC_OK:
650                break;
651
652            case HttpStatus.SC_NOT_MODIFIED:
653                // return, leaving last modified set to original value so getIfNewer should return unmodified
654                return;
655
656            case SC_NULL:
657            {
658                TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
659                fireTransferError( resource, e, TransferEvent.REQUEST_GET );
660                throw e;
661            }
662
663            case HttpStatus.SC_FORBIDDEN:
664                fireSessionConnectionRefused();
665                throw new AuthorizationException( "Access denied to: " + url );
666
667            case HttpStatus.SC_UNAUTHORIZED:
668                fireSessionConnectionRefused();
669                throw new AuthorizationException( "Not authorized." );
670
671            case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
672                fireSessionConnectionRefused();
673                throw new AuthorizationException( "Not authorized by proxy." );
674
675            case HttpStatus.SC_NOT_FOUND:
676                throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
677
678                // add more entries here
679            default:
680            {
681                cleanupGetTransfer( resource );
682                TransferFailedException e = new TransferFailedException(
683                    "Failed to transfer file: " + url + ". Return code is: " + statusCode );
684                fireTransferError( resource, e, TransferEvent.REQUEST_GET );
685                throw e;
686            }
687        }
688
689        InputStream is = null;
690
691        Header contentLengthHeader = getMethod.getResponseHeader( "Content-Length" );
692
693        if ( contentLengthHeader != null )
694        {
695            try
696            {
697                long contentLength = Integer.valueOf( contentLengthHeader.getValue() ).intValue();
698
699                resource.setContentLength( contentLength );
700            }
701            catch ( NumberFormatException e )
702            {
703                fireTransferDebug(
704                    "error parsing content length header '" + contentLengthHeader.getValue() + "' " + e );
705            }
706        }
707
708        Header lastModifiedHeader = getMethod.getResponseHeader( "Last-Modified" );
709
710        long lastModified = 0;
711
712        if ( lastModifiedHeader != null )
713        {
714            try
715            {
716                lastModified = DateUtil.parseDate( lastModifiedHeader.getValue() ).getTime();
717
718                resource.setLastModified( lastModified );
719            }
720            catch ( DateParseException e )
721            {
722                fireTransferDebug( "Unable to parse last modified header" );
723            }
724
725            fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() + " (" + lastModified + ")" );
726        }
727
728        Header contentEncoding = getMethod.getResponseHeader( "Content-Encoding" );
729        boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding.getValue() );
730
731        try
732        {
733            is = getMethod.getResponseBodyAsStream();
734            if ( isGZipped )
735            {
736                is = new GZIPInputStream( is );
737            }
738        }
739        catch ( IOException e )
740        {
741            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
742
743            String msg =
744                "Error occurred while retrieving from remote repository:" + getRepository() + ": " + e.getMessage();
745
746            throw new TransferFailedException( msg, e );
747        }
748
749        inputData.setInputStream( is );
750    }
751
752    protected void cleanupGetTransfer( Resource resource )
753    {
754        if ( getMethod != null )
755        {
756            getMethod.releaseConnection();
757        }
758    }
759
760    @Override
761    public void putFromStream( InputStream stream, String destination )
762        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
763    {
764        putFromStream( stream, destination, -1, -1 );
765    }
766
767    @Override
768    protected void putFromStream( InputStream stream, Resource resource )
769        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
770    {
771        putFromStream( stream, resource.getName(), -1, -1 );
772    }
773
774    @Override
775    public void fillOutputData( OutputData outputData )
776        throws TransferFailedException
777    {
778        // no needed in this implementation but throw an Exception if used
779        throw new IllegalStateException( "this wagon http client must not use fillOutputData" );
780    }
781}