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                 // TODO Move 401/407 to AuthenticationException after WAGON-587
173                 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN
174                         || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED
175                         || responseCode == HttpURLConnection.HTTP_PROXY_AUTH )
176                 {
177                     throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ),
178                             responseCode, reasonPhrase, getProxyInfo() ) );
179                 }
180                 if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM
181                         || responseCode == HttpURLConnection.HTTP_MOVED_TEMP )
182                 {
183                     visitingUrl = urlConnection.getHeaderField( "Location" );
184                     continue;
185                 }
186 
187                 InputStream is = urlConnection.getInputStream();
188                 String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" );
189                 boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding );
190                 if ( isGZipped )
191                 {
192                     is = new GZIPInputStream( is );
193                 }
194                 boolean isDeflated = contentEncoding != null && "deflate".equalsIgnoreCase( contentEncoding );
195                 if ( isDeflated )
196                 {
197                     is = new DeflaterInputStream( is );
198                 }
199                 inputData.setInputStream( is );
200                 resource.setLastModified( urlConnection.getLastModified() );
201                 resource.setContentLength( urlConnection.getContentLength() );
202                 break;
203 
204             }
205             catch ( FileNotFoundException e )
206             {
207                 // this could be 404 Not Found or 410 Gone - we don't have access to which it was.
208                 // TODO: 2019-10-03 url used should list all visited/redirected urls, not just the original
209                 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
210                         UNKNOWN_STATUS_CODE, null, getProxyInfo() ), e );
211             }
212             catch ( IOException originalIOException )
213             {
214                 throw convertHttpUrlConnectionException( originalIOException, urlConnection, buildUrl( resource ) );
215             }
216 
217         }
218 
219     }
220 
221     private void addHeaders( HttpURLConnection urlConnection )
222     {
223         if ( httpHeaders != null )
224         {
225             for ( Object header : httpHeaders.keySet() )
226             {
227                 urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) );
228             }
229         }
230         setAuthorization( urlConnection );
231     }
232 
233     private void setAuthorization( HttpURLConnection urlConnection )
234     {
235         if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null )
236         {
237             String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword();
238             String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) );
239             urlConnection.setRequestProperty( "Authorization", "Basic " + encoded );
240         }
241     }
242 
243     public void fillOutputData( OutputData outputData )
244         throws TransferFailedException
245     {
246         Resource resource = outputData.getResource();
247         try
248         {
249             URL url = new URL( buildUrl( resource ) );
250             putConnection = (HttpURLConnection) url.openConnection( this.proxy );
251 
252             addHeaders( putConnection );
253 
254             putConnection.setRequestMethod( "PUT" );
255             putConnection.setDoOutput( true );
256             outputData.setOutputStream( putConnection.getOutputStream() );
257         }
258         catch ( IOException e )
259         {
260             throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
261         }
262     }
263 
264     protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
265         throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
266     {
267         try
268         {
269             String reasonPhrase = putConnection.getResponseMessage();
270             int statusCode = putConnection.getResponseCode();
271 
272             switch ( statusCode )
273             {
274                 // Success Codes
275                 case HttpURLConnection.HTTP_OK: // 200
276                 case HttpURLConnection.HTTP_CREATED: // 201
277                 case HttpURLConnection.HTTP_ACCEPTED: // 202
278                 case HttpURLConnection.HTTP_NO_CONTENT: // 204
279                     break;
280 
281                 // TODO Move 401/407 to AuthenticationException after WAGON-587
282                 case HttpURLConnection.HTTP_FORBIDDEN:
283                 case HttpURLConnection.HTTP_UNAUTHORIZED:
284                 case HttpURLConnection.HTTP_PROXY_AUTH:
285                     throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode,
286                             reasonPhrase, getProxyInfo() ) );
287 
288                 case HttpURLConnection.HTTP_NOT_FOUND:
289                 case HttpURLConnection.HTTP_GONE:
290                     throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
291                             statusCode, reasonPhrase, getProxyInfo() ) );
292 
293                 // add more entries here
294                 default:
295                     throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ),
296                             statusCode, reasonPhrase, getProxyInfo() ) ) ;
297             }
298         }
299         catch ( IOException e )
300         {
301             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
302             throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) );
303         }
304     }
305 
306     protected void openConnectionInternal()
307         throws ConnectionException, AuthenticationException
308     {
309         final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() );
310         if ( proxyInfo != null )
311         {
312             this.proxy = getProxy( proxyInfo );
313             this.proxyInfo = proxyInfo;
314         }
315         authenticator.setWagon( this );
316 
317         boolean usePreemptiveAuthentication =
318             Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean(
319                 repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication;
320 
321         setPreemptiveAuthentication( usePreemptiveAuthentication );
322     }
323 
324     @SuppressWarnings( "deprecation" )
325     public PasswordAuthentication requestProxyAuthentication()
326     {
327         if ( proxyInfo != null && proxyInfo.getUserName() != null )
328         {
329             String password = "";
330             if ( proxyInfo.getPassword() != null )
331             {
332                 password = proxyInfo.getPassword();
333             }
334             return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() );
335         }
336         return null;
337     }
338 
339     public PasswordAuthentication requestServerAuthentication()
340     {
341         if ( authenticationInfo != null && authenticationInfo.getUserName() != null )
342         {
343             String password = "";
344             if ( authenticationInfo.getPassword() != null )
345             {
346                 password = authenticationInfo.getPassword();
347             }
348             return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() );
349         }
350         return null;
351     }
352 
353     private Proxy getProxy( ProxyInfo proxyInfo )
354     {
355         return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) );
356     }
357 
358     private Type getProxyType( ProxyInfo proxyInfo )
359     {
360         if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals(
361             proxyInfo.getType() ) )
362         {
363             return Type.SOCKS;
364         }
365         else
366         {
367             return Type.HTTP;
368         }
369     }
370 
371     public SocketAddress getSocketAddress( ProxyInfo proxyInfo )
372     {
373         return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() );
374     }
375 
376     public void closeConnection()
377         throws ConnectionException
378     {
379         //FIXME WAGON-375 use persistent connection feature provided by the jdk
380         if ( putConnection != null )
381         {
382             putConnection.disconnect();
383         }
384         authenticator.resetWagon();
385     }
386 
387     public List<String> getFileList( String destinationDirectory )
388         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
389     {
390         InputData inputData = new InputData();
391 
392         if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) )
393         {
394             destinationDirectory += "/";
395         }
396 
397         String url = buildUrl( new Resource( destinationDirectory ) );
398 
399         Resource resource = new Resource( destinationDirectory );
400 
401         inputData.setResource( resource );
402 
403         fillInputData( inputData );
404 
405         InputStream is = inputData.getInputStream();
406 
407         try
408         {
409 
410             if ( is == null )
411             {
412                 throw new TransferFailedException(
413                     url + " - Could not open input stream for resource: '" + resource + "'" );
414             }
415 
416             final List<String> htmlFileList = HtmlFileListParser.parseFileList( url, is );
417             is.close();
418             is = null;
419             return htmlFileList;
420         }
421         catch ( final IOException e )
422         {
423             throw new TransferFailedException( "Failure transferring " + resource.getName(), e );
424         }
425         finally
426         {
427             IOUtils.closeQuietly( is );
428         }
429     }
430 
431     public boolean resourceExists( String resourceName )
432         throws TransferFailedException, AuthorizationException
433     {
434         HttpURLConnection headConnection;
435 
436         try
437         {
438             Resource resource = new Resource( resourceName );
439             URL url = new URL( buildUrl( resource ) );
440             headConnection = (HttpURLConnection) url.openConnection( this.proxy );
441 
442             addHeaders( headConnection );
443 
444             headConnection.setRequestMethod( "HEAD" );
445 
446             int statusCode = headConnection.getResponseCode();
447             String reasonPhrase = headConnection.getResponseMessage();
448 
449             switch ( statusCode )
450             {
451                 case HttpURLConnection.HTTP_OK:
452                     return true;
453 
454                 case HttpURLConnection.HTTP_NOT_FOUND:
455                 case HttpURLConnection.HTTP_GONE:
456                     return false;
457 
458                 // TODO Move 401/407 to AuthenticationException after WAGON-587
459                 case HttpURLConnection.HTTP_FORBIDDEN:
460                 case HttpURLConnection.HTTP_UNAUTHORIZED:
461                 case HttpURLConnection.HTTP_PROXY_AUTH:
462                     throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ),
463                             statusCode, reasonPhrase, getProxyInfo() ) );
464 
465                 default:
466                     throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ),
467                             statusCode, reasonPhrase, getProxyInfo() ) );
468             }
469         }
470         catch ( IOException e )
471         {
472             throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
473         }
474     }
475 
476     public boolean isUseCache()
477     {
478         return useCache;
479     }
480 
481     public void setUseCache( boolean useCache )
482     {
483         this.useCache = useCache;
484     }
485 
486     public Properties getHttpHeaders()
487     {
488         return httpHeaders;
489     }
490 
491     public void setHttpHeaders( Properties httpHeaders )
492     {
493         this.httpHeaders = httpHeaders;
494     }
495 
496     void setSystemProperty( String key, String value )
497     {
498         if ( value != null )
499         {
500             System.setProperty( key, value );
501         }
502         else
503         {
504             System.getProperties().remove( key );
505         }
506     }
507 
508     public void setPreemptiveAuthentication( boolean preemptiveAuthentication )
509     {
510         this.preemptiveAuthentication = preemptiveAuthentication;
511     }
512 
513     public LightweightHttpWagonAuthenticator getAuthenticator()
514     {
515         return authenticator;
516     }
517 
518     public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator )
519     {
520         this.authenticator = authenticator;
521     }
522 
523     /**
524      * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the
525      * equivalent {@link TransferFailedException}.
526      * <p>
527      * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting
528      * accessors. The returned exception will include the passed IOException as a cause and a message that is as
529      * descriptive as possible.
530      *
531      * @param originalIOException an IOException thrown from an HttpURLConnection operation
532      * @param urlConnection       instance that triggered the IOException
533      * @param url                 originating url that triggered the IOException
534      * @return exception that is representative of the original cause
535      */
536     private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException,
537                                                                        HttpURLConnection urlConnection,
538                                                                        String url )
539     {
540         // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException
541         // In that case, one may attempt to get the status code and reason phrase
542         // from the errorstream. We do this, but by way of the following code path
543         // getResponseCode()/getResponseMessage() - calls -> getHeaderFields()
544         // getHeaderFields() - calls -> getErrorStream()
545         try
546         {
547             // call getResponseMessage first since impl calls getResponseCode as part of that anyways
548             String errorResponseMessage = urlConnection.getResponseMessage(); // may be null
549             int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned
550             String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage,
551                     getProxyInfo() );
552             return new TransferFailedException( message, originalIOException );
553 
554         }
555         catch ( IOException errorStreamException )
556         {
557             // there was a problem using the standard methods, need to fall back to other options
558         }
559 
560         // Attempt to parse the status code and URL which can be included in an IOException message
561         // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java
562         // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913
563         String ioMsg = originalIOException.getMessage();
564         if ( ioMsg != null )
565         {
566             Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg );
567             if ( matcher.matches() )
568             {
569                 String codeStr = matcher.group( 1 );
570                 String urlStr = matcher.group( 2 );
571 
572                 int code = UNKNOWN_STATUS_CODE;
573                 try
574                 {
575                     code = parseInt( codeStr );
576                 }
577                 catch ( NumberFormatException nfe )
578                 {
579                     // if here there is a regex problem
580                 }
581 
582                 String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() );
583                 return new TransferFailedException( message, originalIOException );
584             }
585         }
586 
587         String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() );
588         return new TransferFailedException( message, originalIOException );
589     }
590 
591 }