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.maven.wagon.ConnectionException;
23  import org.apache.maven.wagon.InputData;
24  import org.apache.maven.wagon.OutputData;
25  import org.apache.maven.wagon.ResourceDoesNotExistException;
26  import org.apache.maven.wagon.StreamWagon;
27  import org.apache.maven.wagon.TransferFailedException;
28  import org.apache.maven.wagon.authentication.AuthenticationException;
29  import org.apache.maven.wagon.authorization.AuthorizationException;
30  import org.apache.maven.wagon.events.TransferEvent;
31  import org.apache.maven.wagon.proxy.ProxyInfo;
32  import org.apache.maven.wagon.resource.Resource;
33  import org.apache.maven.wagon.shared.http.EncodingUtil;
34  import org.codehaus.plexus.util.Base64;
35  
36  import java.io.FileNotFoundException;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.net.HttpURLConnection;
41  import java.net.InetSocketAddress;
42  import java.net.MalformedURLException;
43  import java.net.PasswordAuthentication;
44  import java.net.Proxy;
45  import java.net.Proxy.Type;
46  import java.net.SocketAddress;
47  import java.net.URL;
48  import java.util.ArrayList;
49  import java.util.List;
50  import java.util.Properties;
51  import java.util.regex.Matcher;
52  import java.util.regex.Pattern;
53  import java.util.zip.DeflaterInputStream;
54  import java.util.zip.GZIPInputStream;
55  
56  import static java.lang.Integer.parseInt;
57  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.UNKNOWN_STATUS_CODE;
58  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage;
59  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
60  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;
61  
62  /**
63   * LightweightHttpWagon, using JDK's HttpURLConnection.
64   *
65   * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
66   * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup"
67   * @see HttpURLConnection
68   */
69  public class LightweightHttpWagon
70      extends StreamWagon
71  {
72      private boolean preemptiveAuthentication;
73  
74      private HttpURLConnection putConnection;
75  
76      private Proxy proxy = Proxy.NO_PROXY;
77  
78      private static final Pattern IOEXCEPTION_MESSAGE_PATTERN = Pattern.compile( "Server returned HTTP response code: "
79              + "(\\d\\d\\d) for URL: (.*)" );
80  
81      public static final int MAX_REDIRECTS = 10;
82  
83      /**
84       * Whether to use any proxy cache or not.
85       *
86       * @plexus.configuration default="false"
87       */
88      private boolean useCache;
89  
90      /**
91       * @plexus.configuration
92       */
93      private Properties httpHeaders;
94  
95      /**
96       * @plexus.requirement
97       */
98      private volatile LightweightHttpWagonAuthenticator authenticator;
99  
100     /**
101      * Builds a complete URL string from the repository URL and the relative path of the resource passed.
102      *
103      * @param resource the resource to extract the relative path from.
104      * @return the complete URL
105      */
106     private String buildUrl( Resource resource )
107     {
108         return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() );
109     }
110 
111     public void fillInputData( InputData inputData )
112         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
113     {
114         Resource resource = inputData.getResource();
115 
116         String visitingUrl = buildUrl( resource );
117 
118         List<String> visitedUrls = new ArrayList<>();
119 
120         for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ )
121         {
122             if ( visitedUrls.contains( visitingUrl ) )
123             {
124                 // TODO add a test for this message
125                 throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl );
126             }
127             visitedUrls.add( visitingUrl );
128 
129             URL url = null;
130             try
131             {
132                 url = new URL( visitingUrl );
133             }
134             catch ( MalformedURLException e )
135             {
136                 // TODO add test for this
137                 throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e );
138             }
139 
140             HttpURLConnection urlConnection = null;
141 
142             try
143             {
144                 urlConnection = ( HttpURLConnection ) url.openConnection( this.proxy );
145             }
146             catch ( IOException e )
147             {
148                 // TODO: add test for this
149                 String message = formatTransferFailedMessage( visitingUrl, UNKNOWN_STATUS_CODE,
150                         null, getProxyInfo() );
151                 // TODO include e.getMessage appended to main message?
152                 throw new TransferFailedException( message, e );
153             }
154 
155             try
156             {
157 
158                 urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" );
159                 if ( !useCache )
160                 {
161                     urlConnection.setRequestProperty( "Pragma", "no-cache" );
162                 }
163 
164                 addHeaders( urlConnection );
165 
166                 // TODO: handle all response codes
167                 int responseCode = urlConnection.getResponseCode();
168                 String reasonPhrase = urlConnection.getResponseMessage();
169 
170                 // TODO Move 401/407 to AuthenticationException after WAGON-587
171                 if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN
172                         || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED
173                         || responseCode == HttpURLConnection.HTTP_PROXY_AUTH )
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 Move 401/407 to AuthenticationException after WAGON-587
280                 case HttpURLConnection.HTTP_FORBIDDEN:
281                 case HttpURLConnection.HTTP_UNAUTHORIZED:
282                 case HttpURLConnection.HTTP_PROXY_AUTH:
283                     throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode,
284                             reasonPhrase, getProxyInfo() ) );
285 
286                 case HttpURLConnection.HTTP_NOT_FOUND:
287                 case HttpURLConnection.HTTP_GONE:
288                     throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ),
289                             statusCode, reasonPhrase, getProxyInfo() ) );
290 
291                 // add more entries here
292                 default:
293                     throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ),
294                             statusCode, reasonPhrase, getProxyInfo() ) ) ;
295             }
296         }
297         catch ( IOException e )
298         {
299             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
300             throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) );
301         }
302     }
303 
304     protected void openConnectionInternal()
305         throws ConnectionException, AuthenticationException
306     {
307         final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() );
308         if ( proxyInfo != null )
309         {
310             this.proxy = getProxy( proxyInfo );
311             this.proxyInfo = proxyInfo;
312         }
313         authenticator.setWagon( this );
314 
315         boolean usePreemptiveAuthentication =
316             Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean(
317                 repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication;
318 
319         setPreemptiveAuthentication( usePreemptiveAuthentication );
320     }
321 
322     @SuppressWarnings( "deprecation" )
323     public PasswordAuthentication requestProxyAuthentication()
324     {
325         if ( proxyInfo != null && proxyInfo.getUserName() != null )
326         {
327             String password = "";
328             if ( proxyInfo.getPassword() != null )
329             {
330                 password = proxyInfo.getPassword();
331             }
332             return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() );
333         }
334         return null;
335     }
336 
337     public PasswordAuthentication requestServerAuthentication()
338     {
339         if ( authenticationInfo != null && authenticationInfo.getUserName() != null )
340         {
341             String password = "";
342             if ( authenticationInfo.getPassword() != null )
343             {
344                 password = authenticationInfo.getPassword();
345             }
346             return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() );
347         }
348         return null;
349     }
350 
351     private Proxy getProxy( ProxyInfo proxyInfo )
352     {
353         return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) );
354     }
355 
356     private Type getProxyType( ProxyInfo proxyInfo )
357     {
358         if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals(
359             proxyInfo.getType() ) )
360         {
361             return Type.SOCKS;
362         }
363         else
364         {
365             return Type.HTTP;
366         }
367     }
368 
369     public SocketAddress getSocketAddress( ProxyInfo proxyInfo )
370     {
371         return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() );
372     }
373 
374     public void closeConnection()
375         throws ConnectionException
376     {
377         //FIXME WAGON-375 use persistent connection feature provided by the jdk
378         if ( putConnection != null )
379         {
380             putConnection.disconnect();
381         }
382         authenticator.resetWagon();
383     }
384 
385     public boolean resourceExists( String resourceName )
386         throws TransferFailedException, AuthorizationException
387     {
388         HttpURLConnection headConnection;
389 
390         try
391         {
392             Resource resource = new Resource( resourceName );
393             URL url = new URL( buildUrl( resource ) );
394             headConnection = (HttpURLConnection) url.openConnection( this.proxy );
395 
396             addHeaders( headConnection );
397 
398             headConnection.setRequestMethod( "HEAD" );
399 
400             int statusCode = headConnection.getResponseCode();
401             String reasonPhrase = headConnection.getResponseMessage();
402 
403             switch ( statusCode )
404             {
405                 case HttpURLConnection.HTTP_OK:
406                     return true;
407 
408                 case HttpURLConnection.HTTP_NOT_FOUND:
409                 case HttpURLConnection.HTTP_GONE:
410                     return false;
411 
412                 // TODO Move 401/407 to AuthenticationException after WAGON-587
413                 case HttpURLConnection.HTTP_FORBIDDEN:
414                 case HttpURLConnection.HTTP_UNAUTHORIZED:
415                 case HttpURLConnection.HTTP_PROXY_AUTH:
416                     throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ),
417                             statusCode, reasonPhrase, getProxyInfo() ) );
418 
419                 default:
420                     throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ),
421                             statusCode, reasonPhrase, getProxyInfo() ) );
422             }
423         }
424         catch ( IOException e )
425         {
426             throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
427         }
428     }
429 
430     public boolean isUseCache()
431     {
432         return useCache;
433     }
434 
435     public void setUseCache( boolean useCache )
436     {
437         this.useCache = useCache;
438     }
439 
440     public Properties getHttpHeaders()
441     {
442         return httpHeaders;
443     }
444 
445     public void setHttpHeaders( Properties httpHeaders )
446     {
447         this.httpHeaders = httpHeaders;
448     }
449 
450     void setSystemProperty( String key, String value )
451     {
452         if ( value != null )
453         {
454             System.setProperty( key, value );
455         }
456         else
457         {
458             System.getProperties().remove( key );
459         }
460     }
461 
462     public void setPreemptiveAuthentication( boolean preemptiveAuthentication )
463     {
464         this.preemptiveAuthentication = preemptiveAuthentication;
465     }
466 
467     public LightweightHttpWagonAuthenticator getAuthenticator()
468     {
469         return authenticator;
470     }
471 
472     public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator )
473     {
474         this.authenticator = authenticator;
475     }
476 
477     /**
478      * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the
479      * equivalent {@link TransferFailedException}.
480      * <p>
481      * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting
482      * accessors. The returned exception will include the passed IOException as a cause and a message that is as
483      * descriptive as possible.
484      *
485      * @param originalIOException an IOException thrown from an HttpURLConnection operation
486      * @param urlConnection       instance that triggered the IOException
487      * @param url                 originating url that triggered the IOException
488      * @return exception that is representative of the original cause
489      */
490     private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException,
491                                                                        HttpURLConnection urlConnection,
492                                                                        String url )
493     {
494         // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException
495         // In that case, one may attempt to get the status code and reason phrase
496         // from the errorstream. We do this, but by way of the following code path
497         // getResponseCode()/getResponseMessage() - calls -> getHeaderFields()
498         // getHeaderFields() - calls -> getErrorStream()
499         try
500         {
501             // call getResponseMessage first since impl calls getResponseCode as part of that anyways
502             String errorResponseMessage = urlConnection.getResponseMessage(); // may be null
503             int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned
504             String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage,
505                     getProxyInfo() );
506             return new TransferFailedException( message, originalIOException );
507 
508         }
509         catch ( IOException errorStreamException )
510         {
511             // there was a problem using the standard methods, need to fall back to other options
512         }
513 
514         // Attempt to parse the status code and URL which can be included in an IOException message
515         // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java
516         // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913
517         String ioMsg = originalIOException.getMessage();
518         if ( ioMsg != null )
519         {
520             Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg );
521             if ( matcher.matches() )
522             {
523                 String codeStr = matcher.group( 1 );
524                 String urlStr = matcher.group( 2 );
525 
526                 int code = UNKNOWN_STATUS_CODE;
527                 try
528                 {
529                     code = parseInt( codeStr );
530                 }
531                 catch ( NumberFormatException nfe )
532                 {
533                     // if here there is a regex problem
534                 }
535 
536                 String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() );
537                 return new TransferFailedException( message, originalIOException );
538             }
539         }
540 
541         String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() );
542         return new TransferFailedException( message, originalIOException );
543     }
544 
545 }