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