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