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        }
265        authenticator.setWagon( this );
266
267        boolean usePreemptiveAuthentication =
268            Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean(
269                repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication;
270
271        setPreemptiveAuthentication( usePreemptiveAuthentication );
272    }
273
274    @SuppressWarnings( "deprecation" )
275    public PasswordAuthentication requestProxyAuthentication()
276    {
277        if ( proxyInfo != null && proxyInfo.getUserName() != null )
278        {
279            String password = "";
280            if ( proxyInfo.getPassword() != null )
281            {
282                password = proxyInfo.getPassword();
283            }
284            return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() );
285        }
286        return null;
287    }
288
289    public PasswordAuthentication requestServerAuthentication()
290    {
291        if ( authenticationInfo != null && authenticationInfo.getUserName() != null )
292        {
293            String password = "";
294            if ( authenticationInfo.getPassword() != null )
295            {
296                password = authenticationInfo.getPassword();
297            }
298            return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() );
299        }
300        return null;
301    }
302
303    private Proxy getProxy( ProxyInfo proxyInfo )
304    {
305        return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) );
306    }
307
308    private Type getProxyType( ProxyInfo proxyInfo )
309    {
310        if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals(
311            proxyInfo.getType() ) )
312        {
313            return Type.SOCKS;
314        }
315        else
316        {
317            return Type.HTTP;
318        }
319    }
320
321    public SocketAddress getSocketAddress( ProxyInfo proxyInfo )
322    {
323        return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() );
324    }
325
326    public void closeConnection()
327        throws ConnectionException
328    {
329        //FIXME WAGON-375 use persistent connection feature provided by the jdk
330        if ( putConnection != null )
331        {
332            putConnection.disconnect();
333        }
334        authenticator.resetWagon();
335    }
336
337    public List<String> getFileList( String destinationDirectory )
338        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
339    {
340        InputData inputData = new InputData();
341
342        if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) )
343        {
344            destinationDirectory += "/";
345        }
346
347        String url = buildUrl( new Resource( destinationDirectory ) );
348
349        Resource resource = new Resource( destinationDirectory );
350
351        inputData.setResource( resource );
352
353        fillInputData( inputData );
354
355        InputStream is = inputData.getInputStream();
356
357        try
358        {
359
360            if ( is == null )
361            {
362                throw new TransferFailedException(
363                    url + " - Could not open input stream for resource: '" + resource + "'" );
364            }
365
366            return HtmlFileListParser.parseFileList( url, is );
367        }
368        finally
369        {
370            IOUtils.closeQuietly( is );
371        }
372    }
373
374    public boolean resourceExists( String resourceName )
375        throws TransferFailedException, AuthorizationException
376    {
377        HttpURLConnection headConnection;
378
379        try
380        {
381            Resource resource = new Resource( resourceName );
382            URL url = new URL( buildUrl( resource ) );
383            headConnection = (HttpURLConnection) url.openConnection( this.proxy );
384
385            addHeaders( headConnection );
386
387            headConnection.setRequestMethod( "HEAD" );
388            headConnection.setDoOutput( true );
389
390            int statusCode = headConnection.getResponseCode();
391
392            switch ( statusCode )
393            {
394                case HttpURLConnection.HTTP_OK:
395                    return true;
396
397                case HttpURLConnection.HTTP_FORBIDDEN:
398                    throw new AuthorizationException( "Access denied to: " + url );
399
400                case HttpURLConnection.HTTP_NOT_FOUND:
401                    return false;
402
403                case HttpURLConnection.HTTP_UNAUTHORIZED:
404                    throw new AuthorizationException( "Access denied to: " + url );
405
406                default:
407                    throw new TransferFailedException(
408                        "Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode );
409            }
410        }
411        catch ( IOException e )
412        {
413            throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e );
414        }
415    }
416
417    public boolean isUseCache()
418    {
419        return useCache;
420    }
421
422    public void setUseCache( boolean useCache )
423    {
424        this.useCache = useCache;
425    }
426
427    public Properties getHttpHeaders()
428    {
429        return httpHeaders;
430    }
431
432    public void setHttpHeaders( Properties httpHeaders )
433    {
434        this.httpHeaders = httpHeaders;
435    }
436
437    void setSystemProperty( String key, String value )
438    {
439        if ( value != null )
440        {
441            System.setProperty( key, value );
442        }
443        else
444        {
445            System.getProperties().remove( key );
446        }
447    }
448
449    public void setPreemptiveAuthentication( boolean preemptiveAuthentication )
450    {
451        this.preemptiveAuthentication = preemptiveAuthentication;
452    }
453
454    public LightweightHttpWagonAuthenticator getAuthenticator()
455    {
456        return authenticator;
457    }
458
459    public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator )
460    {
461        this.authenticator = authenticator;
462    }
463}