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;
056import org.apache.maven.wagon.shared.http.EncodingUtil;
057
058import java.io.ByteArrayInputStream;
059import java.io.File;
060import java.io.FileInputStream;
061import java.io.IOException;
062import java.io.InputStream;
063import java.io.OutputStream;
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        String username = null;
201        String password = null;
202        String domain = null;
203
204        if ( authenticationInfo != null )
205        {
206            username = authenticationInfo.getUserName();
207
208            if ( StringUtils.contains( username, "\\" ) )
209            {
210                String[] domainAndUsername = username.split( "\\\\" );
211                domain = domainAndUsername[0];
212                username = domainAndUsername[1];
213            }
214
215            password = authenticationInfo.getPassword();
216
217
218        }
219
220        String host = getRepository().getHost();
221
222        if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
223        {
224            Credentials creds;
225            if ( domain != null )
226            {
227                creds = new NTCredentials( username, password, host, domain );
228            }
229            else
230            {
231                creds = new UsernamePasswordCredentials( username, password );
232            }
233
234            int port = getRepository().getPort() > -1 ? getRepository().getPort() : AuthScope.ANY_PORT;
235
236            AuthScope scope = new AuthScope( host, port );
237            client.getState().setCredentials( scope, creds );
238        }
239
240        HostConfiguration hc = new HostConfiguration();
241
242        ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
243        if ( proxyInfo != null )
244        {
245            String proxyUsername = proxyInfo.getUserName();
246            String proxyPassword = proxyInfo.getPassword();
247            String proxyHost = proxyInfo.getHost();
248            int proxyPort = proxyInfo.getPort();
249            String proxyNtlmHost = proxyInfo.getNtlmHost();
250            String proxyNtlmDomain = proxyInfo.getNtlmDomain();
251            if ( proxyHost != null )
252            {
253                hc.setProxy( proxyHost, proxyPort );
254
255                if ( proxyUsername != null && proxyPassword != null )
256                {
257                    Credentials creds;
258                    if ( proxyNtlmHost != null || proxyNtlmDomain != null )
259                    {
260                        creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
261                    }
262                    else
263                    {
264                        creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
265                    }
266
267                    int port = proxyInfo.getPort() > -1 ? proxyInfo.getPort() : AuthScope.ANY_PORT;
268
269                    AuthScope scope = new AuthScope( proxyHost, port );
270                    client.getState().setProxyCredentials( scope, creds );
271                }
272            }
273        }
274
275        hc.setHost( host );
276
277        //start a session with the webserver
278        client.setHostConfiguration( hc );
279    }
280
281    public void closeConnection()
282    {
283        if ( connectionManager instanceof MultiThreadedHttpConnectionManager )
284        {
285            ( (MultiThreadedHttpConnectionManager) connectionManager ).shutdown();
286        }
287    }
288
289    public void put( File source, String resourceName )
290        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
291    {
292        Resource resource = new Resource( resourceName );
293
294        firePutInitiated( resource, source );
295
296        resource.setContentLength( source.length() );
297
298        resource.setLastModified( source.lastModified() );
299
300        put( null, resource, source );
301    }
302
303    public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
304        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
305    {
306        Resource resource = new Resource( destination );
307
308        firePutInitiated( resource, null );
309
310        resource.setContentLength( contentLength );
311
312        resource.setLastModified( lastModified );
313
314        put( stream, resource, null );
315    }
316
317    private void put( final InputStream stream, Resource resource, File source )
318        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
319    {
320        RequestEntityImplementation requestEntityImplementation =
321            new RequestEntityImplementation( stream, resource, this, source );
322
323        put( resource, source, requestEntityImplementation, buildUrl( resource ) );
324
325    }
326
327    /**
328     * Builds a complete URL string from the repository URL and the relative path of the resource passed.
329     *
330     * @param resource the resource to extract the relative path from.
331     * @return the complete URL
332     */
333    private String buildUrl( Resource resource )
334    {
335        return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() );
336    }
337
338    private void put( Resource resource, File source, RequestEntityImplementation requestEntityImplementation,
339                      String url )
340        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
341    {
342        // preemptive true for put
343        client.getParams().setAuthenticationPreemptive( true );
344
345        //Parent directories need to be created before posting
346        try
347        {
348            mkdirs( PathUtils.dirname( resource.getName() ) );
349        }
350        catch ( IOException e )
351        {
352            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
353        }
354
355        PutMethod putMethod = new PutMethod( url );
356
357        firePutStarted( resource, source );
358
359        try
360        {
361            putMethod.setRequestEntity( requestEntityImplementation );
362
363            int statusCode;
364            try
365            {
366                statusCode = execute( putMethod );
367
368            }
369            catch ( IOException e )
370            {
371                fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
372
373                throw new TransferFailedException( e.getMessage(), e );
374            }
375
376            fireTransferDebug( url + " - Status code: " + statusCode );
377
378            // Check that we didn't run out of retries.
379            switch ( statusCode )
380            {
381                // Success Codes
382                case HttpStatus.SC_OK: // 200
383                case HttpStatus.SC_CREATED: // 201
384                case HttpStatus.SC_ACCEPTED: // 202
385                case HttpStatus.SC_NO_CONTENT:  // 204
386                    break;
387
388                // 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"
389                case HttpStatus.SC_MOVED_PERMANENTLY: // 301
390                case HttpStatus.SC_MOVED_TEMPORARILY: // 302
391                case HttpStatus.SC_SEE_OTHER: // 303
392                    String relocatedUrl = calculateRelocatedUrl( putMethod );
393                    fireTransferDebug( "relocate to " + relocatedUrl );
394                    put( resource, source, requestEntityImplementation, relocatedUrl );
395                    return;
396
397                case SC_NULL:
398                {
399                    TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
400                    fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
401                    throw e;
402                }
403
404                case HttpStatus.SC_FORBIDDEN:
405                    fireSessionConnectionRefused();
406                    throw new AuthorizationException( "Access denied to: " + url );
407
408                case HttpStatus.SC_NOT_FOUND:
409                    throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
410
411                    //add more entries here
412                default:
413                {
414                    TransferFailedException e = new TransferFailedException(
415                        "Failed to transfer file: " + url + ". Return code is: " + statusCode );
416                    fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
417                    throw e;
418                }
419            }
420
421            firePutCompleted( resource, source );
422        }
423        finally
424        {
425            putMethod.releaseConnection();
426        }
427    }
428
429    protected String calculateRelocatedUrl( EntityEnclosingMethod method )
430    {
431        Header locationHeader = method.getResponseHeader( "Location" );
432        String locationField = locationHeader.getValue();
433        // is it a relative Location or a full ?
434        return locationField.startsWith( "http" ) ? locationField : getURL( getRepository() ) + '/' + locationField;
435    }
436
437    protected void mkdirs( String dirname )
438        throws IOException
439    {
440        // do nothing as default.
441    }
442
443    public boolean resourceExists( String resourceName )
444        throws TransferFailedException, AuthorizationException
445    {
446        StringBuilder url = new StringBuilder( getRepository().getUrl() );
447        if ( !url.toString().endsWith( "/" ) )
448        {
449            url.append( '/' );
450        }
451        url.append( resourceName );
452        HeadMethod headMethod = new HeadMethod( url.toString() );
453
454        int statusCode;
455        try
456        {
457            statusCode = execute( headMethod );
458        }
459        catch ( IOException e )
460        {
461            throw new TransferFailedException( e.getMessage(), e );
462        }
463        try
464        {
465            switch ( statusCode )
466            {
467                case HttpStatus.SC_OK:
468                    return true;
469
470                case HttpStatus.SC_NOT_MODIFIED:
471                    return true;
472
473                case SC_NULL:
474                    throw new TransferFailedException( "Failed to transfer file: " + url );
475
476                case HttpStatus.SC_FORBIDDEN:
477                    throw new AuthorizationException( "Access denied to: " + url );
478
479                case HttpStatus.SC_UNAUTHORIZED:
480                    throw new AuthorizationException( "Not authorized." );
481
482                case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
483                    throw new AuthorizationException( "Not authorized by proxy." );
484
485                case HttpStatus.SC_NOT_FOUND:
486                    return false;
487
488                //add more entries here
489                default:
490                    throw new TransferFailedException(
491                        "Failed to transfer file: " + url + ". Return code is: " + statusCode );
492            }
493        }
494        finally
495        {
496            headMethod.releaseConnection();
497        }
498    }
499
500    protected int execute( HttpMethod httpMethod )
501        throws IOException
502    {
503        int statusCode;
504
505        setParameters( httpMethod );
506        setHeaders( httpMethod );
507
508        statusCode = client.executeMethod( httpMethod );
509        return statusCode;
510    }
511
512    protected void setParameters( HttpMethod method )
513    {
514        HttpMethodConfiguration config =
515            httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
516        if ( config != null )
517        {
518            HttpMethodParams params = config.asMethodParams( method.getParams() );
519            if ( params != null )
520            {
521                method.setParams( params );
522            }
523        }
524
525        if ( config == null || config.getConnectionTimeout() == HttpMethodConfiguration.DEFAULT_CONNECTION_TIMEOUT )
526        {
527            method.getParams().setSoTimeout( getTimeout() );
528        }
529    }
530
531    protected void setHeaders( HttpMethod method )
532    {
533        HttpMethodConfiguration config =
534            httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
535        if ( config == null || config.isUseDefaultHeaders() )
536        {
537            // TODO: merge with the other headers and have some better defaults, unify with lightweight headers
538            method.addRequestHeader( "Cache-control", "no-cache" );
539            method.addRequestHeader( "Cache-store", "no-store" );
540            method.addRequestHeader( "Pragma", "no-cache" );
541            method.addRequestHeader( "Expires", "0" );
542            method.addRequestHeader( "Accept-Encoding", "gzip" );
543        }
544
545        if ( httpHeaders != null )
546        {
547            for ( Object header : httpHeaders.keySet() )
548            {
549                method.addRequestHeader( (String) header, httpHeaders.getProperty( (String) header ) );
550            }
551        }
552
553        Header[] headers = config == null ? null : config.asRequestHeaders();
554        if ( headers != null )
555        {
556            for ( Header header : headers )
557            {
558                method.addRequestHeader( header );
559            }
560        }
561    }
562
563    /**
564     * getUrl
565     * Implementors can override this to remove unwanted parts of the url such as role-hints
566     *
567     * @param repository
568     * @return
569     */
570    protected String getURL( Repository repository )
571    {
572        return repository.getUrl();
573    }
574
575    protected HttpClient getClient()
576    {
577        return client;
578    }
579
580    public void setConnectionManager( HttpConnectionManager connectionManager )
581    {
582        this.connectionManager = connectionManager;
583    }
584
585    public Properties getHttpHeaders()
586    {
587        return httpHeaders;
588    }
589
590    public void setHttpHeaders( Properties httpHeaders )
591    {
592        this.httpHeaders = httpHeaders;
593    }
594
595    public HttpConfiguration getHttpConfiguration()
596    {
597        return httpConfiguration;
598    }
599
600    public void setHttpConfiguration( HttpConfiguration httpConfiguration )
601    {
602        this.httpConfiguration = httpConfiguration;
603    }
604
605    public void fillInputData( InputData inputData )
606        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
607    {
608        Resource resource = inputData.getResource();
609
610        StringBuilder url = new StringBuilder( getRepository().getUrl() );
611        if ( !url.toString().endsWith( "/" ) )
612        {
613            url.append( '/' );
614        }
615        url.append( resource.getName() );
616
617        getMethod = new GetMethod( url.toString() );
618
619        long timestamp = resource.getLastModified();
620        if ( timestamp > 0 )
621        {
622            SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
623            fmt.setTimeZone( GMT_TIME_ZONE );
624            Header hdr = new Header( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
625            fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
626            getMethod.addRequestHeader( hdr );
627        }
628
629        int statusCode;
630        try
631        {
632            statusCode = execute( getMethod );
633        }
634        catch ( IOException e )
635        {
636            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
637
638            throw new TransferFailedException( e.getMessage(), e );
639        }
640
641        fireTransferDebug( url + " - Status code: " + statusCode );
642
643        // TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is
644        // required
645        switch ( statusCode )
646        {
647            case HttpStatus.SC_OK:
648                break;
649
650            case HttpStatus.SC_NOT_MODIFIED:
651                // return, leaving last modified set to original value so getIfNewer should return unmodified
652                return;
653
654            case SC_NULL:
655            {
656                TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
657                fireTransferError( resource, e, TransferEvent.REQUEST_GET );
658                throw e;
659            }
660
661            case HttpStatus.SC_FORBIDDEN:
662                fireSessionConnectionRefused();
663                throw new AuthorizationException( "Access denied to: " + url );
664
665            case HttpStatus.SC_UNAUTHORIZED:
666                fireSessionConnectionRefused();
667                throw new AuthorizationException( "Not authorized." );
668
669            case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
670                fireSessionConnectionRefused();
671                throw new AuthorizationException( "Not authorized by proxy." );
672
673            case HttpStatus.SC_NOT_FOUND:
674                throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
675
676                // add more entries here
677            default:
678            {
679                cleanupGetTransfer( resource );
680                TransferFailedException e = new TransferFailedException(
681                    "Failed to transfer file: " + url + ". Return code is: " + statusCode );
682                fireTransferError( resource, e, TransferEvent.REQUEST_GET );
683                throw e;
684            }
685        }
686
687        InputStream is = null;
688
689        Header contentLengthHeader = getMethod.getResponseHeader( "Content-Length" );
690
691        if ( contentLengthHeader != null )
692        {
693            try
694            {
695                long contentLength = Integer.valueOf( contentLengthHeader.getValue() ).intValue();
696
697                resource.setContentLength( contentLength );
698            }
699            catch ( NumberFormatException e )
700            {
701                fireTransferDebug(
702                    "error parsing content length header '" + contentLengthHeader.getValue() + "' " + e );
703            }
704        }
705
706        Header lastModifiedHeader = getMethod.getResponseHeader( "Last-Modified" );
707
708        long lastModified = 0;
709
710        if ( lastModifiedHeader != null )
711        {
712            try
713            {
714                lastModified = DateUtil.parseDate( lastModifiedHeader.getValue() ).getTime();
715
716                resource.setLastModified( lastModified );
717            }
718            catch ( DateParseException e )
719            {
720                fireTransferDebug( "Unable to parse last modified header" );
721            }
722
723            fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() + " (" + lastModified + ")" );
724        }
725
726        Header contentEncoding = getMethod.getResponseHeader( "Content-Encoding" );
727        boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding.getValue() );
728
729        try
730        {
731            is = getMethod.getResponseBodyAsStream();
732            if ( isGZipped )
733            {
734                is = new GZIPInputStream( is );
735            }
736        }
737        catch ( IOException e )
738        {
739            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
740
741            String msg =
742                "Error occurred while retrieving from remote repository:" + getRepository() + ": " + e.getMessage();
743
744            throw new TransferFailedException( msg, e );
745        }
746
747        inputData.setInputStream( is );
748    }
749
750    protected void cleanupGetTransfer( Resource resource )
751    {
752        if ( getMethod != null )
753        {
754            getMethod.releaseConnection();
755        }
756    }
757
758    @Override
759    public void putFromStream( InputStream stream, String destination )
760        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
761    {
762        putFromStream( stream, destination, -1, -1 );
763    }
764
765    @Override
766    protected void putFromStream( InputStream stream, Resource resource )
767        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
768    {
769        putFromStream( stream, resource.getName(), -1, -1 );
770    }
771
772    @Override
773    public void fillOutputData( OutputData outputData )
774        throws TransferFailedException
775    {
776        // no needed in this implementation but throw an Exception if used
777        throw new IllegalStateException( "this wagon http client must not use fillOutputData" );
778    }
779}