View Javadoc

1   package org.apache.maven.wagon.shared.http;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.commons.httpclient.Credentials;
23  import org.apache.commons.httpclient.Header;
24  import org.apache.commons.httpclient.HostConfiguration;
25  import org.apache.commons.httpclient.HttpClient;
26  import org.apache.commons.httpclient.HttpConnectionManager;
27  import org.apache.commons.httpclient.HttpMethod;
28  import org.apache.commons.httpclient.HttpStatus;
29  import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
30  import org.apache.commons.httpclient.NTCredentials;
31  import org.apache.commons.httpclient.UsernamePasswordCredentials;
32  import org.apache.commons.httpclient.auth.AuthScope;
33  import org.apache.commons.httpclient.cookie.CookiePolicy;
34  import org.apache.commons.httpclient.methods.GetMethod;
35  import org.apache.commons.httpclient.methods.HeadMethod;
36  import org.apache.commons.httpclient.methods.PutMethod;
37  import org.apache.commons.httpclient.methods.RequestEntity;
38  import org.apache.commons.httpclient.params.HttpMethodParams;
39  import org.apache.commons.httpclient.util.DateParseException;
40  import org.apache.commons.httpclient.util.DateUtil;
41  import org.apache.maven.wagon.InputData;
42  import org.apache.maven.wagon.OutputData;
43  import org.apache.maven.wagon.PathUtils;
44  import org.apache.maven.wagon.ResourceDoesNotExistException;
45  import org.apache.maven.wagon.StreamWagon;
46  import org.apache.maven.wagon.TransferFailedException;
47  import org.apache.maven.wagon.Wagon;
48  import org.apache.maven.wagon.authorization.AuthorizationException;
49  import org.apache.maven.wagon.events.TransferEvent;
50  import org.apache.maven.wagon.proxy.ProxyInfo;
51  import org.apache.maven.wagon.repository.Repository;
52  import org.apache.maven.wagon.resource.Resource;
53  import org.codehaus.plexus.util.IOUtil;
54  import org.codehaus.plexus.util.StringUtils;
55  
56  import java.io.File;
57  import java.io.FileInputStream;
58  import java.io.FileOutputStream;
59  import java.io.IOException;
60  import java.io.InputStream;
61  import java.io.OutputStream;
62  import java.net.URLEncoder;
63  import java.text.SimpleDateFormat;
64  import java.util.Date;
65  import java.util.Iterator;
66  import java.util.Locale;
67  import java.util.Properties;
68  import java.util.TimeZone;
69  import java.util.zip.GZIPInputStream;
70  
71  /**
72   * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
73   * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
74   */
75  public abstract class AbstractHttpClientWagon
76      extends StreamWagon
77  {
78      private final class RequestEntityImplementation
79          implements RequestEntity
80      {
81          private final Resource resource;
82  
83          private final Wagon wagon;
84  
85          private final File source;
86  
87          private RequestEntityImplementation( final InputStream stream, final Resource resource, final Wagon wagon,
88                                               final File source )
89              throws TransferFailedException
90          {
91              if ( source != null )
92              {
93                  this.source = source;
94              }
95              else
96              {
97                  FileOutputStream fos = null;
98                  try
99                  {
100                     this.source = File.createTempFile( "http-wagon.", ".tmp" );
101                     this.source.deleteOnExit();
102 
103                     fos = new FileOutputStream( this.source );
104                     IOUtil.copy( stream, fos );
105                 }
106                 catch ( IOException e )
107                 {
108                     fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
109                     throw new TransferFailedException( "Failed to buffer stream contents to temp file for upload.", e );
110                 }
111                 finally
112                 {
113                     IOUtil.close( fos );
114                 }
115             }
116 
117             this.resource = resource;
118             this.wagon = wagon;
119         }
120 
121         public long getContentLength()
122         {
123             return resource.getContentLength();
124         }
125 
126         public String getContentType()
127         {
128             return null;
129         }
130 
131         public boolean isRepeatable()
132         {
133             return true;
134         }
135 
136         public void writeRequest( OutputStream output )
137             throws IOException
138         {
139             byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
140 
141             TransferEvent transferEvent =
142                 new TransferEvent( wagon, resource, TransferEvent.TRANSFER_PROGRESS, TransferEvent.REQUEST_PUT );
143             transferEvent.setTimestamp( System.currentTimeMillis() );
144 
145             FileInputStream fin = null;
146             try
147             {
148                 fin = new FileInputStream( source );
149                 int remaining = Integer.MAX_VALUE;
150                 while ( remaining > 0 )
151                 {
152                     int n = fin.read( buffer, 0, Math.min( buffer.length, remaining ) );
153 
154                     if ( n == -1 )
155                     {
156                         break;
157                     }
158 
159                     fireTransferProgress( transferEvent, buffer, n );
160 
161                     output.write( buffer, 0, n );
162 
163                     remaining -= n;
164                 }
165             }
166             finally
167             {
168                 IOUtil.close( fin );
169             }
170 
171             output.flush();
172         }
173     }
174 
175     protected static final int SC_NULL = -1;
176 
177     protected static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone( "GMT" );
178 
179     private HttpClient client;
180 
181     protected HttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
182 
183     /**
184      * @deprecated Use httpConfiguration instead.
185      */
186     private Properties httpHeaders;
187 
188     /**
189      * @since 1.0-beta-6
190      */
191     private HttpConfiguration httpConfiguration;
192 
193     private HttpMethod getMethod;
194 
195     public void openConnectionInternal()
196     {
197         repository.setUrl( getURL( repository ) );
198         client = new HttpClient( connectionManager );
199 
200         // WAGON-273: default the cookie-policy to browser compatible
201         client.getParams().setCookiePolicy( CookiePolicy.BROWSER_COMPATIBILITY );
202 
203         String username = null;
204         String password = null;
205 
206         if ( authenticationInfo != null )
207         {
208             username = authenticationInfo.getUserName();
209 
210             password = authenticationInfo.getPassword();
211 
212             client.getParams().setAuthenticationPreemptive( true );
213         }
214 
215         String host = getRepository().getHost();
216 
217         if ( StringUtils.isNotEmpty( username ) && StringUtils.isNotEmpty( password ) )
218         {
219             Credentials creds = new UsernamePasswordCredentials( username, password );
220 
221             int port = getRepository().getPort() > -1 ? getRepository().getPort() : AuthScope.ANY_PORT;
222 
223             AuthScope scope = new AuthScope( host, port );
224             client.getState().setCredentials( scope, creds );
225         }
226 
227         HostConfiguration hc = new HostConfiguration();
228 
229         ProxyInfo proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
230         if ( proxyInfo != null )
231         {
232             String proxyUsername = proxyInfo.getUserName();
233             String proxyPassword = proxyInfo.getPassword();
234             String proxyHost = proxyInfo.getHost();
235             int proxyPort = proxyInfo.getPort();
236             String proxyNtlmHost = proxyInfo.getNtlmHost();
237             String proxyNtlmDomain = proxyInfo.getNtlmDomain();
238             if ( proxyHost != null )
239             {
240                 hc.setProxy( proxyHost, proxyPort );
241 
242                 if ( proxyUsername != null && proxyPassword != null )
243                 {
244                     Credentials creds;
245                     if ( proxyNtlmHost != null || proxyNtlmDomain != null )
246                     {
247                         creds = new NTCredentials( proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain );
248                     }
249                     else
250                     {
251                         creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
252                     }
253 
254                     int port = proxyInfo.getPort() > -1 ? proxyInfo.getPort() : AuthScope.ANY_PORT;
255 
256                     AuthScope scope = new AuthScope( proxyHost, port );
257                     client.getState().setProxyCredentials( scope, creds );
258                 }
259             }
260         }
261 
262         hc.setHost( host );
263 
264         //start a session with the webserver
265         client.setHostConfiguration( hc );
266     }
267 
268     public void closeConnection()
269     {
270         if ( connectionManager instanceof MultiThreadedHttpConnectionManager )
271         {
272             ( (MultiThreadedHttpConnectionManager) connectionManager ).shutdown();
273         }
274     }
275 
276     public void put( File source, String resourceName )
277         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
278     {
279         Resource resource = new Resource( resourceName );
280 
281         firePutInitiated( resource, source );
282 
283         resource.setContentLength( source.length() );
284 
285         resource.setLastModified( source.lastModified() );
286 
287         put( null, resource, source );
288     }
289 
290     public void putFromStream( final InputStream stream, String destination, long contentLength, long lastModified )
291         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
292     {
293         Resource resource = new Resource( destination );
294 
295         firePutInitiated( resource, null );
296 
297         resource.setContentLength( contentLength );
298 
299         resource.setLastModified( lastModified );
300 
301         put( stream, resource, null );
302     }
303 
304     private void put( final InputStream stream, Resource resource, File source )
305         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
306     {
307         StringBuilder url = new StringBuilder( getRepository().getUrl() );
308         String[] parts = StringUtils.split( resource.getName(), "/" );
309         for ( String part : parts )// int i = 0; i < parts.length; i++ )
310         {
311             // TODO: Fix encoding...
312             // url += "/" + URLEncoder.encode( parts[i], System.getProperty("file.encoding") );
313             if ( !url.toString().endsWith( "/" ) )
314             {
315                 url.append( '/' );
316             }
317             url.append( URLEncoder.encode( part ) );
318         }
319 
320         //Parent directories need to be created before posting
321         try
322         {
323             mkdirs( PathUtils.dirname( resource.getName() ) );
324         }
325         catch ( IOException e )
326         {
327             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
328         }
329 
330         PutMethod putMethod = new PutMethod( url.toString() );
331 
332         firePutStarted( resource, source );
333 
334         try
335         {
336             putMethod.setRequestEntity( new RequestEntityImplementation( stream, resource, this, source ) );
337 
338             int statusCode;
339             try
340             {
341                 statusCode = execute( putMethod );
342             }
343             catch ( IOException e )
344             {
345                 fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
346 
347                 throw new TransferFailedException( e.getMessage(), e );
348             }
349 
350             fireTransferDebug( url + " - Status code: " + statusCode );
351 
352             // Check that we didn't run out of retries.
353             switch ( statusCode )
354             {
355                 // Success Codes
356                 case HttpStatus.SC_OK: // 200
357                 case HttpStatus.SC_CREATED: // 201
358                 case HttpStatus.SC_ACCEPTED: // 202
359                 case HttpStatus.SC_NO_CONTENT:  // 204
360                     break;
361 
362                 case SC_NULL:
363                 {
364                     TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
365                     fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
366                     throw e;
367                 }
368 
369                 case HttpStatus.SC_FORBIDDEN:
370                     fireSessionConnectionRefused();
371                     throw new AuthorizationException( "Access denied to: " + url );
372 
373                 case HttpStatus.SC_NOT_FOUND:
374                     throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
375 
376                     //add more entries here
377                 default:
378                 {
379                     TransferFailedException e = new TransferFailedException(
380                         "Failed to transfer file: " + url + ". Return code is: " + statusCode );
381                     fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
382                     throw e;
383                 }
384             }
385 
386             firePutCompleted( resource, source );
387         }
388         finally
389         {
390             putMethod.releaseConnection();
391         }
392     }
393 
394     protected void mkdirs( String dirname )
395         throws IOException
396     {
397         // do nothing as default.
398     }
399 
400     public boolean resourceExists( String resourceName )
401         throws TransferFailedException, AuthorizationException
402     {
403         StringBuilder url = new StringBuilder( getRepository().getUrl() );
404         if ( !url.toString().endsWith( "/" ) )
405         {
406             url.append( '/' );
407         }
408         url.append( resourceName );
409         HeadMethod headMethod = new HeadMethod( url.toString() );
410         int statusCode;
411         try
412         {
413             statusCode = execute( headMethod );
414         }
415         catch ( IOException e )
416         {
417             throw new TransferFailedException( e.getMessage(), e );
418         }
419         try
420         {
421             switch ( statusCode )
422             {
423                 case HttpStatus.SC_OK:
424                     return true;
425 
426                 case HttpStatus.SC_NOT_MODIFIED:
427                     return true;
428 
429                 case SC_NULL:
430                     throw new TransferFailedException( "Failed to transfer file: " + url );
431 
432                 case HttpStatus.SC_FORBIDDEN:
433                     throw new AuthorizationException( "Access denied to: " + url );
434 
435                 case HttpStatus.SC_UNAUTHORIZED:
436                     throw new AuthorizationException( "Not authorized." );
437 
438                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
439                     throw new AuthorizationException( "Not authorized by proxy." );
440 
441                 case HttpStatus.SC_NOT_FOUND:
442                     return false;
443 
444                 //add more entries here
445                 default:
446                     throw new TransferFailedException(
447                         "Failed to transfer file: " + url + ". Return code is: " + statusCode );
448             }
449         }
450         finally
451         {
452             headMethod.releaseConnection();
453         }
454     }
455 
456     protected int execute( HttpMethod httpMethod )
457         throws IOException
458     {
459         int statusCode;
460 
461         setParameters( httpMethod );
462         setHeaders( httpMethod );
463 
464         statusCode = client.executeMethod( httpMethod );
465         return statusCode;
466     }
467 
468     protected void setParameters( HttpMethod method )
469     {
470         HttpMethodConfiguration config =
471             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
472         if ( config != null )
473         {
474             HttpMethodParams params = config.asMethodParams( method.getParams() );
475             if ( params != null )
476             {
477                 method.setParams( params );
478             }
479         }
480 
481         if ( config == null || config.getConnectionTimeout() == HttpMethodConfiguration.DEFAULT_CONNECTION_TIMEOUT )
482         {
483             method.getParams().setSoTimeout( getTimeout() );
484         }
485     }
486 
487     protected void setHeaders( HttpMethod method )
488     {
489         HttpMethodConfiguration config =
490             httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration( method );
491         if ( config == null || config.isUseDefaultHeaders() )
492         {
493             // TODO: merge with the other headers and have some better defaults, unify with lightweight headers
494             method.addRequestHeader( "Cache-control", "no-cache" );
495             method.addRequestHeader( "Cache-store", "no-store" );
496             method.addRequestHeader( "Pragma", "no-cache" );
497             method.addRequestHeader( "Expires", "0" );
498             method.addRequestHeader( "Accept-Encoding", "gzip" );
499         }
500 
501         if ( httpHeaders != null )
502         {
503             for ( Iterator i = httpHeaders.keySet().iterator(); i.hasNext(); )
504             {
505                 String header = (String) i.next();
506                 method.addRequestHeader( header, httpHeaders.getProperty( header ) );
507             }
508         }
509 
510         Header[] headers = config == null ? null : config.asRequestHeaders();
511         if ( headers != null )
512         {
513             for ( int i = 0; i < headers.length; i++ )
514             {
515                 method.addRequestHeader( headers[i] );
516             }
517         }
518     }
519 
520     /**
521      * getUrl
522      * Implementors can override this to remove unwanted parts of the url such as role-hints
523      *
524      * @param repository
525      * @return
526      */
527     protected String getURL( Repository repository )
528     {
529         return repository.getUrl();
530     }
531 
532     protected HttpClient getClient()
533     {
534         return client;
535     }
536 
537     public void setConnectionManager( HttpConnectionManager connectionManager )
538     {
539         this.connectionManager = connectionManager;
540     }
541 
542     public Properties getHttpHeaders()
543     {
544         return httpHeaders;
545     }
546 
547     public void setHttpHeaders( Properties httpHeaders )
548     {
549         this.httpHeaders = httpHeaders;
550     }
551 
552     public HttpConfiguration getHttpConfiguration()
553     {
554         return httpConfiguration;
555     }
556 
557     public void setHttpConfiguration( HttpConfiguration httpConfiguration )
558     {
559         this.httpConfiguration = httpConfiguration;
560     }
561 
562     public void fillInputData( InputData inputData )
563         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
564     {
565         Resource resource = inputData.getResource();
566 
567         StringBuilder url = new StringBuilder( getRepository().getUrl() );
568         if ( !url.toString().endsWith( "/" ) )
569         {
570             url.append( '/' );
571         }
572         url.append( resource.getName() );
573 
574         getMethod = new GetMethod( url.toString() );
575         long timestamp = resource.getLastModified();
576         if ( timestamp > 0 )
577         {
578             SimpleDateFormat fmt = new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US );
579             fmt.setTimeZone( GMT_TIME_ZONE );
580             Header hdr = new Header( "If-Modified-Since", fmt.format( new Date( timestamp ) ) );
581             fireTransferDebug( "sending ==> " + hdr + "(" + timestamp + ")" );
582             getMethod.addRequestHeader( hdr );
583         }
584 
585         int statusCode;
586         try
587         {
588             statusCode = execute( getMethod );
589         }
590         catch ( IOException e )
591         {
592             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
593 
594             throw new TransferFailedException( e.getMessage(), e );
595         }
596 
597         fireTransferDebug( url + " - Status code: " + statusCode );
598 
599         // TODO [BP]: according to httpclient docs, really should swallow the output on error. verify if that is
600         // required
601         switch ( statusCode )
602         {
603             case HttpStatus.SC_OK:
604                 break;
605 
606             case HttpStatus.SC_NOT_MODIFIED:
607                 // return, leaving last modified set to original value so getIfNewer should return unmodified
608                 return;
609 
610             case SC_NULL:
611             {
612                 TransferFailedException e = new TransferFailedException( "Failed to transfer file: " + url );
613                 fireTransferError( resource, e, TransferEvent.REQUEST_GET );
614                 throw e;
615             }
616 
617             case HttpStatus.SC_FORBIDDEN:
618                 fireSessionConnectionRefused();
619                 throw new AuthorizationException( "Access denied to: " + url );
620 
621             case HttpStatus.SC_UNAUTHORIZED:
622                 fireSessionConnectionRefused();
623                 throw new AuthorizationException( "Not authorized." );
624 
625             case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
626                 fireSessionConnectionRefused();
627                 throw new AuthorizationException( "Not authorized by proxy." );
628 
629             case HttpStatus.SC_NOT_FOUND:
630                 throw new ResourceDoesNotExistException( "File: " + url + " does not exist" );
631 
632                 // add more entries here
633             default:
634             {
635                 cleanupGetTransfer( resource );
636                 TransferFailedException e = new TransferFailedException(
637                     "Failed to transfer file: " + url + ". Return code is: " + statusCode );
638                 fireTransferError( resource, e, TransferEvent.REQUEST_GET );
639                 throw e;
640             }
641         }
642 
643         InputStream is = null;
644 
645         Header contentLengthHeader = getMethod.getResponseHeader( "Content-Length" );
646 
647         if ( contentLengthHeader != null )
648         {
649             try
650             {
651                 long contentLength = Integer.valueOf( contentLengthHeader.getValue() ).intValue();
652 
653                 resource.setContentLength( contentLength );
654             }
655             catch ( NumberFormatException e )
656             {
657                 fireTransferDebug(
658                     "error parsing content length header '" + contentLengthHeader.getValue() + "' " + e );
659             }
660         }
661 
662         Header lastModifiedHeader = getMethod.getResponseHeader( "Last-Modified" );
663 
664         long lastModified = 0;
665 
666         if ( lastModifiedHeader != null )
667         {
668             try
669             {
670                 lastModified = DateUtil.parseDate( lastModifiedHeader.getValue() ).getTime();
671 
672                 resource.setLastModified( lastModified );
673             }
674             catch ( DateParseException e )
675             {
676                 fireTransferDebug( "Unable to parse last modified header" );
677             }
678 
679             fireTransferDebug( "last-modified = " + lastModifiedHeader.getValue() + " (" + lastModified + ")" );
680         }
681 
682         Header contentEncoding = getMethod.getResponseHeader( "Content-Encoding" );
683         boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding.getValue() );
684 
685         try
686         {
687             is = getMethod.getResponseBodyAsStream();
688             if ( isGZipped )
689             {
690                 is = new GZIPInputStream( is );
691             }
692         }
693         catch ( IOException e )
694         {
695             fireTransferError( resource, e, TransferEvent.REQUEST_GET );
696 
697             String msg =
698                 "Error occurred while retrieving from remote repository:" + getRepository() + ": " + e.getMessage();
699 
700             throw new TransferFailedException( msg, e );
701         }
702 
703         inputData.setInputStream( is );
704     }
705 
706     protected void cleanupGetTransfer( Resource resource )
707     {
708         if ( getMethod != null )
709         {
710             getMethod.releaseConnection();
711         }
712     }
713 
714     @Override
715     public void putFromStream( InputStream stream, String destination )
716         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
717     {
718         putFromStream( stream, destination, -1, -1 );
719     }
720 
721     @Override
722     protected void putFromStream( InputStream stream, Resource resource )
723         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
724     {
725         putFromStream( stream, resource.getName( ), -1, -1 );
726     }
727 
728     @Override
729     public void fillOutputData( OutputData outputData )
730         throws TransferFailedException
731     {
732         // no needed in this implementation but throw an Exception if used
733         throw new IllegalStateException( "this wagon http client must not use fillOutputData" );
734     }
735 }