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