001package org.apache.maven.wagon.providers.http;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.commons.io.IOUtils;
023import org.apache.maven.wagon.ConnectionException;
024import org.apache.maven.wagon.InputData;
025import org.apache.maven.wagon.OutputData;
026import org.apache.maven.wagon.ResourceDoesNotExistException;
027import org.apache.maven.wagon.StreamWagon;
028import org.apache.maven.wagon.TransferFailedException;
029import org.apache.maven.wagon.authentication.AuthenticationException;
030import org.apache.maven.wagon.authorization.AuthorizationException;
031import org.apache.maven.wagon.events.TransferEvent;
032import org.apache.maven.wagon.proxy.ProxyInfo;
033import org.apache.maven.wagon.resource.Resource;
034import org.apache.maven.wagon.shared.http.EncodingUtil;
035import org.apache.maven.wagon.shared.http.HtmlFileListParser;
036import org.codehaus.plexus.util.Base64;
037
038import java.io.FileNotFoundException;
039import java.io.IOException;
040import java.io.InputStream;
041import java.io.OutputStream;
042import java.net.HttpURLConnection;
043import java.net.InetSocketAddress;
044import java.net.MalformedURLException;
045import java.net.PasswordAuthentication;
046import java.net.Proxy;
047import java.net.Proxy.Type;
048import java.net.SocketAddress;
049import java.net.URL;
050import java.util.ArrayList;
051import java.util.List;
052import java.util.Properties;
053import java.util.zip.DeflaterInputStream;
054import java.util.zip.GZIPInputStream;
055
056/**
057 * LightweightHttpWagon, using JDK's HttpURLConnection.
058 *
059 * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a>
060 * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup"
061 * @see HttpURLConnection
062 */
063public class LightweightHttpWagon
064    extends StreamWagon
065{
066    private boolean preemptiveAuthentication;
067
068    private HttpURLConnection putConnection;
069
070    private Proxy proxy = Proxy.NO_PROXY;
071
072    public static final int MAX_REDIRECTS = 10;
073
074    /**
075     * Whether to use any proxy cache or not.
076     *
077     * @plexus.configuration default="false"
078     */
079    private boolean useCache;
080
081    /**
082     * @plexus.configuration
083     */
084    private Properties httpHeaders;
085
086    /**
087     * @plexus.requirement
088     */
089    private volatile LightweightHttpWagonAuthenticator authenticator;
090
091    /**
092     * Builds a complete URL string from the repository URL and the relative path of the resource passed.
093     *
094     * @param resource the resource to extract the relative path from.
095     * @return the complete URL
096     */
097    private String buildUrl( Resource resource )
098    {
099        return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() );
100    }
101
102    public void fillInputData( InputData inputData )
103        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
104    {
105        Resource resource = inputData.getResource();
106
107        String visitingUrl = buildUrl( resource );
108        try
109        {
110            List<String> visitedUrls = new ArrayList<String>();
111
112            for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ )
113            {
114                if ( visitedUrls.contains( visitingUrl ) )
115                {
116                    throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl );
117                }
118                visitedUrls.add( visitingUrl );
119
120                URL url = new URL( visitingUrl );
121                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection( this.proxy );
122
123                urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" );
124                if ( !useCache )
125                {
126                    urlConnection.setRequestProperty( "Pragma", "no-cache" );
127                }
128
129                addHeaders( urlConnection );
130
131                // TODO: handle all response codes
132                int responseCode = urlConnection.getResponseCode();
133                if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN
134                    || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED )
135                {
136                    throw new AuthorizationException( "Access denied to: " + buildUrl( resource ) );
137                }
138                if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM
139                    || responseCode == HttpURLConnection.HTTP_MOVED_TEMP )
140                {
141                    visitingUrl = urlConnection.getHeaderField( "Location" );
142                    continue;
143                }
144
145                InputStream is = urlConnection.getInputStream();
146                String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" );
147                boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding );
148                if ( isGZipped )
149                {
150                    is = new GZIPInputStream( is );
151                }
152                boolean isDeflated = contentEncoding != null && "deflate".equalsIgnoreCase( contentEncoding );
153                if ( isDeflated )
154                {
155                    is = new DeflaterInputStream( is );
156                }
157                inputData.setInputStream( is );
158                resource.setLastModified( urlConnection.getLastModified() );
159                resource.setContentLength( urlConnection.getContentLength() );
160                break;
161            }
162        }
163        catch ( MalformedURLException e )
164        {
165            throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e );
166        }
167        catch ( FileNotFoundException e )
168        {
169            throw new ResourceDoesNotExistException( "Unable to locate resource in repository", e );
170        }
171        catch ( IOException e )
172        {
173            StringBuilder message = new StringBuilder( "Error transferring file: " );
174            message.append( e.getMessage() );
175            message.append( " from " + visitingUrl );
176            if ( getProxyInfo() != null && getProxyInfo().getHost() != null )
177            {
178                message.append( " with proxyInfo " ).append( getProxyInfo().toString() );
179            }
180            throw new TransferFailedException( message.toString(), e );
181        }
182    }
183
184    private void addHeaders( HttpURLConnection urlConnection )
185    {
186        if ( httpHeaders != null )
187        {
188            for ( Object header : httpHeaders.keySet() )
189            {
190                urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) );
191            }
192        }
193        setAuthorization( urlConnection );
194    }
195
196    private void setAuthorization( HttpURLConnection urlConnection )
197    {
198        if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null )
199        {
200            String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword();
201            String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) );
202            urlConnection.setRequestProperty( "Authorization", "Basic " + encoded );
203        }
204    }
205
206    public void fillOutputData( OutputData outputData )
207        throws TransferFailedException
208    {
209        Resource resource = outputData.getResource();
210        try
211        {
212            URL url = new URL( buildUrl( resource ) );
213            putConnection = (HttpURLConnection) url.openConnection( this.proxy );
214
215            addHeaders( putConnection );
216
217            putConnection.setRequestMethod( "PUT" );
218            putConnection.setDoOutput( true );
219            outputData.setOutputStream( putConnection.getOutputStream() );
220        }
221        catch ( IOException e )
222        {
223            throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
224        }
225    }
226
227    protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
228        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
229    {
230        try
231        {
232            int statusCode = putConnection.getResponseCode();
233
234            switch ( statusCode )
235            {
236                // Success Codes
237                case HttpURLConnection.HTTP_OK: // 200
238                case HttpURLConnection.HTTP_CREATED: // 201
239                case HttpURLConnection.HTTP_ACCEPTED: // 202
240                case HttpURLConnection.HTTP_NO_CONTENT: // 204
241                    break;
242
243                case HttpURLConnection.HTTP_FORBIDDEN:
244                    throw new AuthorizationException( "Access denied to: " + buildUrl( resource ) );
245
246                case HttpURLConnection.HTTP_NOT_FOUND:
247                    throw new ResourceDoesNotExistException( "File: " + buildUrl( resource ) + " does not exist" );
248
249                    // add more entries here
250                default:
251                    throw new TransferFailedException(
252                        "Failed to transfer file: " + buildUrl( resource ) + ". Return code is: " + statusCode );
253            }
254        }
255        catch ( IOException e )
256        {
257            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
258
259            throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
260        }
261    }
262
263    protected void openConnectionInternal()
264        throws ConnectionException, AuthenticationException
265    {
266        final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() );
267        if ( proxyInfo != null )
268        {
269            this.proxy = getProxy( proxyInfo );
270            this.proxyInfo = proxyInfo;
271        }
272        authenticator.setWagon( this );
273
274        boolean usePreemptiveAuthentication =
275            Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean(
276                repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication;
277
278        setPreemptiveAuthentication( usePreemptiveAuthentication );
279    }
280
281    @SuppressWarnings( "deprecation" )
282    public PasswordAuthentication requestProxyAuthentication()
283    {
284        if ( proxyInfo != null && proxyInfo.getUserName() != null )
285        {
286            String password = "";
287            if ( proxyInfo.getPassword() != null )
288            {
289                password = proxyInfo.getPassword();
290            }
291            return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() );
292        }
293        return null;
294    }
295
296    public PasswordAuthentication requestServerAuthentication()
297    {
298        if ( authenticationInfo != null && authenticationInfo.getUserName() != null )
299        {
300            String password = "";
301            if ( authenticationInfo.getPassword() != null )
302            {
303                password = authenticationInfo.getPassword();
304            }
305            return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() );
306        }
307        return null;
308    }
309
310    private Proxy getProxy( ProxyInfo proxyInfo )
311    {
312        return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) );
313    }
314
315    private Type getProxyType( ProxyInfo proxyInfo )
316    {
317        if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals(
318            proxyInfo.getType() ) )
319        {
320            return Type.SOCKS;
321        }
322        else
323        {
324            return Type.HTTP;
325        }
326    }
327
328    public SocketAddress getSocketAddress( ProxyInfo proxyInfo )
329    {
330        return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() );
331    }
332
333    public void closeConnection()
334        throws ConnectionException
335    {
336        //FIXME WAGON-375 use persistent connection feature provided by the jdk
337        if ( putConnection != null )
338        {
339            putConnection.disconnect();
340        }
341        authenticator.resetWagon();
342    }
343
344    public List<String> getFileList( String destinationDirectory )
345        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
346    {
347        InputData inputData = new InputData();
348
349        if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) )
350        {
351            destinationDirectory += "/";
352        }
353
354        String url = buildUrl( new Resource( destinationDirectory ) );
355
356        Resource resource = new Resource( destinationDirectory );
357
358        inputData.setResource( resource );
359
360        fillInputData( inputData );
361
362        InputStream is = inputData.getInputStream();
363
364        try
365        {
366
367            if ( is == null )
368            {
369                throw new TransferFailedException(
370                    url + " - Could not open input stream for resource: '" + resource + "'" );
371            }
372
373            final List<String> htmlFileList = HtmlFileListParser.parseFileList( url, is );
374            is.close();
375            is = null;
376            return htmlFileList;
377        }
378        catch ( final IOException e )
379        {
380            throw new TransferFailedException( "Failure transferring " + resource.getName(), e );
381        }
382        finally
383        {
384            IOUtils.closeQuietly( is );
385        }
386    }
387
388    public boolean resourceExists( String resourceName )
389        throws TransferFailedException, AuthorizationException
390    {
391        HttpURLConnection headConnection;
392
393        try
394        {
395            Resource resource = new Resource( resourceName );
396            URL url = new URL( buildUrl( resource ) );
397            headConnection = (HttpURLConnection) url.openConnection( this.proxy );
398
399            addHeaders( headConnection );
400
401            headConnection.setRequestMethod( "HEAD" );
402            headConnection.setDoOutput( true );
403
404            int statusCode = headConnection.getResponseCode();
405
406            switch ( statusCode )
407            {
408                case HttpURLConnection.HTTP_OK:
409                    return true;
410
411                case HttpURLConnection.HTTP_FORBIDDEN:
412                    throw new AuthorizationException( "Access denied to: " + url );
413
414                case HttpURLConnection.HTTP_NOT_FOUND:
415                    return false;
416
417                case HttpURLConnection.HTTP_UNAUTHORIZED:
418                    throw new AuthorizationException( "Access denied to: " + url );
419
420                default:
421                    throw new TransferFailedException(
422                        "Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode );
423            }
424        }
425        catch ( IOException e )
426        {
427            throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
428        }
429    }
430
431    public boolean isUseCache()
432    {
433        return useCache;
434    }
435
436    public void setUseCache( boolean useCache )
437    {
438        this.useCache = useCache;
439    }
440
441    public Properties getHttpHeaders()
442    {
443        return httpHeaders;
444    }
445
446    public void setHttpHeaders( Properties httpHeaders )
447    {
448        this.httpHeaders = httpHeaders;
449    }
450
451    void setSystemProperty( String key, String value )
452    {
453        if ( value != null )
454        {
455            System.setProperty( key, value );
456        }
457        else
458        {
459            System.getProperties().remove( key );
460        }
461    }
462
463    public void setPreemptiveAuthentication( boolean preemptiveAuthentication )
464    {
465        this.preemptiveAuthentication = preemptiveAuthentication;
466    }
467
468    public LightweightHttpWagonAuthenticator getAuthenticator()
469    {
470        return authenticator;
471    }
472
473    public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator )
474    {
475        this.authenticator = authenticator;
476    }
477}