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            return HtmlFileListParser.parseFileList( url, is );
368        }
369        finally
370        {
371            IOUtils.closeQuietly( is );
372        }
373    }
374
375    public boolean resourceExists( String resourceName )
376        throws TransferFailedException, AuthorizationException
377    {
378        HttpURLConnection headConnection;
379
380        try
381        {
382            Resource resource = new Resource( resourceName );
383            URL url = new URL( buildUrl( resource ) );
384            headConnection = (HttpURLConnection) url.openConnection( this.proxy );
385
386            addHeaders( headConnection );
387
388            headConnection.setRequestMethod( "HEAD" );
389            headConnection.setDoOutput( true );
390
391            int statusCode = headConnection.getResponseCode();
392
393            switch ( statusCode )
394            {
395                case HttpURLConnection.HTTP_OK:
396                    return true;
397
398                case HttpURLConnection.HTTP_FORBIDDEN:
399                    throw new AuthorizationException( "Access denied to: " + url );
400
401                case HttpURLConnection.HTTP_NOT_FOUND:
402                    return false;
403
404                case HttpURLConnection.HTTP_UNAUTHORIZED:
405                    throw new AuthorizationException( "Access denied to: " + url );
406
407                default:
408                    throw new TransferFailedException(
409                        "Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode );
410            }
411        }
412        catch ( IOException e )
413        {
414            throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
415        }
416    }
417
418    public boolean isUseCache()
419    {
420        return useCache;
421    }
422
423    public void setUseCache( boolean useCache )
424    {
425        this.useCache = useCache;
426    }
427
428    public Properties getHttpHeaders()
429    {
430        return httpHeaders;
431    }
432
433    public void setHttpHeaders( Properties httpHeaders )
434    {
435        this.httpHeaders = httpHeaders;
436    }
437
438    void setSystemProperty( String key, String value )
439    {
440        if ( value != null )
441        {
442            System.setProperty( key, value );
443        }
444        else
445        {
446            System.getProperties().remove( key );
447        }
448    }
449
450    public void setPreemptiveAuthentication( boolean preemptiveAuthentication )
451    {
452        this.preemptiveAuthentication = preemptiveAuthentication;
453    }
454
455    public LightweightHttpWagonAuthenticator getAuthenticator()
456    {
457        return authenticator;
458    }
459
460    public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator )
461    {
462        this.authenticator = authenticator;
463    }
464}