View Javadoc
1   package org.apache.maven.wagon.providers.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.io.IOUtils;
23  import org.apache.maven.wagon.ConnectionException;
24  import org.apache.maven.wagon.InputData;
25  import org.apache.maven.wagon.OutputData;
26  import org.apache.maven.wagon.ResourceDoesNotExistException;
27  import org.apache.maven.wagon.StreamWagon;
28  import org.apache.maven.wagon.TransferFailedException;
29  import org.apache.maven.wagon.authentication.AuthenticationException;
30  import org.apache.maven.wagon.authorization.AuthorizationException;
31  import org.apache.maven.wagon.events.TransferEvent;
32  import org.apache.maven.wagon.proxy.ProxyInfo;
33  import org.apache.maven.wagon.resource.Resource;
34  import org.apache.maven.wagon.shared.http.EncodingUtil;
35  import org.apache.maven.wagon.shared.http.HtmlFileListParser;
36  import org.codehaus.plexus.util.Base64;
37  
38  import java.io.FileNotFoundException;
39  import java.io.IOException;
40  import java.io.InputStream;
41  import java.io.OutputStream;
42  import java.net.HttpURLConnection;
43  import java.net.InetSocketAddress;
44  import java.net.MalformedURLException;
45  import java.net.PasswordAuthentication;
46  import java.net.Proxy;
47  import java.net.Proxy.Type;
48  import java.net.SocketAddress;
49  import java.net.URL;
50  import java.util.ArrayList;
51  import java.util.List;
52  import java.util.Properties;
53  import java.util.regex.Matcher;
54  import java.util.regex.Pattern;
55  import java.util.zip.DeflaterInputStream;
56  import java.util.zip.GZIPInputStream;
57  
58  import static java.lang.Integer.parseInt;
59  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.UNKNOWN_STATUS_CODE;
60  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage;
61  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
62  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;
63  
64  /**
65   * LightweightHttpWagon, using JDK's HttpURLConnection.
66   *
67   * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
68   * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup"
69   * @see HttpURLConnection
70   */
71  public class LightweightHttpWagon
72      extends StreamWagon
73  {
74      private boolean preemptiveAuthentication;
75  
76      private HttpURLConnection putConnection;
77  
78      private Proxy proxy = Proxy.NO_PROXY;
79  
80      private static final Pattern IOEXCEPTION_MESSAGE_PATTERN = Pattern.compile( "Server returned HTTP response code: "
81              + "(\\d\\d\\d) for URL: (.*)" );
82  
83      public static final int MAX_REDIRECTS = 10;
84  
85      /**
86       * Whether to use any proxy cache or not.
87       *
88       * @plexus.configuration default="false"
89       */
90      private boolean useCache;
91  
92      /**
93       * @plexus.configuration
94       */
95      private Properties httpHeaders;
96  
97      /**
98       * @plexus.requirement
99       */
100     private volatile LightweightHttpWagonAuthenticator authenticator;
101 
102     /**
103      * Builds a complete URL string from the repository URL and the relative path of the resource passed.
104      *
105      * @param resource the resource to extract the relative path from.
106      * @return the complete URL
107      */
108     private String buildUrl( Resource resource )
109     {
110         return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() );
111     }
112 
113     public void fillInputData( InputData inputData )
114         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
115     {
116         Resource resource = inputData.getResource();
117 
118         String visitingUrl = buildUrl( resource );
119 
120         List<String> visitedUrls = new ArrayList<>();
121 
122         for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ )
123         {
124             if ( visitedUrls.contains( visitingUrl ) )
125             {
126                 // TODO add a test for this message
127                 throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl );
128             }
129             visitedUrls.add( visitingUrl );
130 
131             URL url = null;
132             try
133             {
134                 url = new URL( visitingUrl );
135             }
136             catch ( MalformedURLException e )
137             {
138                 // TODO add test for this
139                 throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e );
140             }
141 
142             HttpURLConnection urlConnection = null;
143 
144             try
145             {
146                 urlConnection = ( HttpURLConnection ) url.openConnection( this.proxy );
147             }
148             catch ( IOException e )
149             {
150                 // TODO: add test for this
151                 String message = formatTransferFailedMessage( visitingUrl, UNKNOWN_STATUS_CODE,
152                         null, getProxyInfo() );
153                 // TODO include e.getMessage appended to main message?
154                 throw new TransferFailedException( message, e );
155             }
156 
157             try
158             {
159 
160                 urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" );
161                 if ( !useCache )
162                 {
163                     urlConnection.setRequestProperty( "Pragma", "no-cache" );
164                 }
165 
166                 addHeaders( urlConnection );
167 
168                 // TODO: handle all response codes
169                 int responseCode = urlConnection.getResponseCode();
170                 String reasonPhrase = urlConnection.getResponseMessage();
171 
172                 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN
173                         || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED )
174                 {
175                     throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ),
176                             responseCode, reasonPhrase, getProxyInfo() ) );
177                 }
178                 if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM
179                         || responseCode == HttpURLConnection.HTTP_MOVED_TEMP )
180                 {
181                     visitingUrl = urlConnection.getHeaderField( "Location" );
182                     continue;
183                 }
184 
185                 InputStream is = urlConnection.getInputStream();
186                 String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" );
187                 boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding );
188                 if ( isGZipped )
189                 {
190                     is = new GZIPInputStream( is );
191                 }
192                 boolean isDeflated = contentEncoding != null && "deflate".equalsIgnoreCase( contentEncoding );
193                 if ( isDeflated )
194                 {
195                     is = new DeflaterInputStream( is );
196                 }
197                 inputData.setInputStream( is );
198                 resource.setLastModified( urlConnection.getLastModified() );
199                 resource.setContentLength( urlConnection.getContentLength() );
200                 break;
201 
202             }
203             catch ( FileNotFoundException e )
204             {
205                 // this could be 404 Not Found or 410 Gone - we don't have access to which it was.
206                 // TODO: 2019-10-03 url used should list all visited/redirected urls, not just the original
207                 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
208                         UNKNOWN_STATUS_CODE, null, getProxyInfo() ), e );
209             }
210             catch ( IOException originalIOException )
211             {
212                 throw convertHttpUrlConnectionException( originalIOException, urlConnection, buildUrl( resource ) );
213             }
214 
215         }
216 
217     }
218 
219     private void addHeaders( HttpURLConnection urlConnection )
220     {
221         if ( httpHeaders != null )
222         {
223             for ( Object header : httpHeaders.keySet() )
224             {
225                 urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) );
226             }
227         }
228         setAuthorization( urlConnection );
229     }
230 
231     private void setAuthorization( HttpURLConnection urlConnection )
232     {
233         if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null )
234         {
235             String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword();
236             String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) );
237             urlConnection.setRequestProperty( "Authorization", "Basic " + encoded );
238         }
239     }
240 
241     public void fillOutputData( OutputData outputData )
242         throws TransferFailedException
243     {
244         Resource resource = outputData.getResource();
245         try
246         {
247             URL url = new URL( buildUrl( resource ) );
248             putConnection = (HttpURLConnection) url.openConnection( this.proxy );
249 
250             addHeaders( putConnection );
251 
252             putConnection.setRequestMethod( "PUT" );
253             putConnection.setDoOutput( true );
254             outputData.setOutputStream( putConnection.getOutputStream() );
255         }
256         catch ( IOException e )
257         {
258             throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
259         }
260     }
261 
262     protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
263         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
264     {
265         try
266         {
267             String reasonPhrase = putConnection.getResponseMessage();
268             int statusCode = putConnection.getResponseCode();
269 
270             switch ( statusCode )
271             {
272                 // Success Codes
273                 case HttpURLConnection.HTTP_OK: // 200
274                 case HttpURLConnection.HTTP_CREATED: // 201
275                 case HttpURLConnection.HTTP_ACCEPTED: // 202
276                 case HttpURLConnection.HTTP_NO_CONTENT: // 204
277                     break;
278 
279                 // TODO: handle 401 explicitly?
280                 case HttpURLConnection.HTTP_FORBIDDEN:
281                     throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode,
282                             reasonPhrase, getProxyInfo() ) );
283 
284                 case HttpURLConnection.HTTP_NOT_FOUND:
285                     throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
286                             statusCode, reasonPhrase, getProxyInfo() ) );
287 
288                 // add more entries here
289                 default:
290                     throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ),
291                             statusCode, reasonPhrase, getProxyInfo() ) ) ;
292             }
293         }
294         catch ( IOException e )
295         {
296             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
297             throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) );
298         }
299     }
300 
301     protected void openConnectionInternal()
302         throws ConnectionException, AuthenticationException
303     {
304         final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() );
305         if ( proxyInfo != null )
306         {
307             this.proxy = getProxy( proxyInfo );
308             this.proxyInfo = proxyInfo;
309         }
310         authenticator.setWagon( this );
311 
312         boolean usePreemptiveAuthentication =
313             Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean(
314                 repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication;
315 
316         setPreemptiveAuthentication( usePreemptiveAuthentication );
317     }
318 
319     @SuppressWarnings( "deprecation" )
320     public PasswordAuthentication requestProxyAuthentication()
321     {
322         if ( proxyInfo != null && proxyInfo.getUserName() != null )
323         {
324             String password = "";
325             if ( proxyInfo.getPassword() != null )
326             {
327                 password = proxyInfo.getPassword();
328             }
329             return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() );
330         }
331         return null;
332     }
333 
334     public PasswordAuthentication requestServerAuthentication()
335     {
336         if ( authenticationInfo != null && authenticationInfo.getUserName() != null )
337         {
338             String password = "";
339             if ( authenticationInfo.getPassword() != null )
340             {
341                 password = authenticationInfo.getPassword();
342             }
343             return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() );
344         }
345         return null;
346     }
347 
348     private Proxy getProxy( ProxyInfo proxyInfo )
349     {
350         return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) );
351     }
352 
353     private Type getProxyType( ProxyInfo proxyInfo )
354     {
355         if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals(
356             proxyInfo.getType() ) )
357         {
358             return Type.SOCKS;
359         }
360         else
361         {
362             return Type.HTTP;
363         }
364     }
365 
366     public SocketAddress getSocketAddress( ProxyInfo proxyInfo )
367     {
368         return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() );
369     }
370 
371     public void closeConnection()
372         throws ConnectionException
373     {
374         //FIXME WAGON-375 use persistent connection feature provided by the jdk
375         if ( putConnection != null )
376         {
377             putConnection.disconnect();
378         }
379         authenticator.resetWagon();
380     }
381 
382     public List<String> getFileList( String destinationDirectory )
383         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
384     {
385         InputData inputData = new InputData();
386 
387         if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) )
388         {
389             destinationDirectory += "/";
390         }
391 
392         String url = buildUrl( new Resource( destinationDirectory ) );
393 
394         Resource resource = new Resource( destinationDirectory );
395 
396         inputData.setResource( resource );
397 
398         fillInputData( inputData );
399 
400         InputStream is = inputData.getInputStream();
401 
402         try
403         {
404 
405             if ( is == null )
406             {
407                 throw new TransferFailedException(
408                     url + " - Could not open input stream for resource: '" + resource + "'" );
409             }
410 
411             final List<String> htmlFileList = HtmlFileListParser.parseFileList( url, is );
412             is.close();
413             is = null;
414             return htmlFileList;
415         }
416         catch ( final IOException e )
417         {
418             throw new TransferFailedException( "Failure transferring " + resource.getName(), e );
419         }
420         finally
421         {
422             IOUtils.closeQuietly( is );
423         }
424     }
425 
426     public boolean resourceExists( String resourceName )
427         throws TransferFailedException, AuthorizationException
428     {
429         HttpURLConnection headConnection;
430 
431         try
432         {
433             Resource resource = new Resource( resourceName );
434             URL url = new URL( buildUrl( resource ) );
435             headConnection = (HttpURLConnection) url.openConnection( this.proxy );
436 
437             addHeaders( headConnection );
438 
439             headConnection.setRequestMethod( "HEAD" );
440             headConnection.setDoOutput( true );
441 
442             int statusCode = headConnection.getResponseCode();
443 
444             switch ( statusCode )
445             {
446                 case HttpURLConnection.HTTP_OK:
447                     return true;
448 
449                 case HttpURLConnection.HTTP_FORBIDDEN:
450                     throw new AuthorizationException( "Access denied to: " + url );
451 
452                 case HttpURLConnection.HTTP_NOT_FOUND:
453                     return false;
454 
455                 case HttpURLConnection.HTTP_UNAUTHORIZED:
456                     throw new AuthorizationException( "Access denied to: " + url );
457 
458                 default:
459                     throw new TransferFailedException(
460                         "Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode );
461             }
462         }
463         catch ( IOException e )
464         {
465             throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
466         }
467     }
468 
469     public boolean isUseCache()
470     {
471         return useCache;
472     }
473 
474     public void setUseCache( boolean useCache )
475     {
476         this.useCache = useCache;
477     }
478 
479     public Properties getHttpHeaders()
480     {
481         return httpHeaders;
482     }
483 
484     public void setHttpHeaders( Properties httpHeaders )
485     {
486         this.httpHeaders = httpHeaders;
487     }
488 
489     void setSystemProperty( String key, String value )
490     {
491         if ( value != null )
492         {
493             System.setProperty( key, value );
494         }
495         else
496         {
497             System.getProperties().remove( key );
498         }
499     }
500 
501     public void setPreemptiveAuthentication( boolean preemptiveAuthentication )
502     {
503         this.preemptiveAuthentication = preemptiveAuthentication;
504     }
505 
506     public LightweightHttpWagonAuthenticator getAuthenticator()
507     {
508         return authenticator;
509     }
510 
511     public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator )
512     {
513         this.authenticator = authenticator;
514     }
515 
516     /**
517      * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the
518      * equivalent {@link TransferFailedException}.
519      * <p>
520      * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting
521      * accessors. The returned exception will include the passed IOException as a cause and a message that is as
522      * descriptive as possible.
523      *
524      * @param originalIOException an IOException thrown from an HttpURLConnection operation
525      * @param urlConnection       instance that triggered the IOException
526      * @param url                 originating url that triggered the IOException
527      * @return exception that is representative of the original cause
528      */
529     private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException,
530                                                                        HttpURLConnection urlConnection,
531                                                                        String url )
532     {
533         // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException
534         // In that case, one may attempt to get the status code and reason phrase
535         // from the errorstream. We do this, but by way of the following code path
536         // getResponseCode()/getResponseMessage() - calls -> getHeaderFields()
537         // getHeaderFields() - calls -> getErrorStream()
538         try
539         {
540             // call getResponseMessage first since impl calls getResponseCode as part of that anyways
541             String errorResponseMessage = urlConnection.getResponseMessage(); // may be null
542             int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned
543             String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage,
544                     getProxyInfo() );
545             return new TransferFailedException( message, originalIOException );
546 
547         }
548         catch ( IOException errorStreamException )
549         {
550             // there was a problem using the standard methods, need to fall back to other options
551         }
552 
553         // Attempt to parse the status code and URL which can be included in an IOException message
554         // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java
555         // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913
556         String ioMsg = originalIOException.getMessage();
557         if ( ioMsg != null )
558         {
559             Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg );
560             if ( matcher.matches() )
561             {
562                 String codeStr = matcher.group( 1 );
563                 String urlStr = matcher.group( 2 );
564 
565                 int code = UNKNOWN_STATUS_CODE;
566                 try
567                 {
568                     code = parseInt( codeStr );
569                 }
570                 catch ( NumberFormatException nfe )
571                 {
572                     // if here there is a regex problem
573                 }
574 
575                 String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() );
576                 return new TransferFailedException( message, originalIOException );
577             }
578         }
579 
580         String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() );
581         return new TransferFailedException( message, originalIOException );
582     }
583 
584 }