package org.apache.maven.wagon.providers.http;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.commons.io.IOUtils;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.InputData;
import org.apache.maven.wagon.OutputData;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.StreamWagon;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.events.TransferEvent;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.resource.Resource;
import org.apache.maven.wagon.shared.http.EncodingUtil;
import org.apache.maven.wagon.shared.http.HtmlFileListParser;
import org.codehaus.plexus.util.Base64;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.SocketAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.DeflaterInputStream;
import java.util.zip.GZIPInputStream;

import static java.lang.Integer.parseInt;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.UNKNOWN_STATUS_CODE;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage;

LightweightHttpWagon, using JDK's HttpURLConnection.
Author:Michal Maczka
See Also:
@plexus.componentrole="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup"
/** * LightweightHttpWagon, using JDK's HttpURLConnection. * * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a> * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="http" instantiation-strategy="per-lookup" * @see HttpURLConnection */
public class LightweightHttpWagon extends StreamWagon { private boolean preemptiveAuthentication; private HttpURLConnection putConnection; private Proxy proxy = Proxy.NO_PROXY; private static final Pattern IOEXCEPTION_MESSAGE_PATTERN = Pattern.compile( "Server returned HTTP response code: " + "(\\d\\d\\d) for URL: (.*)" ); public static final int MAX_REDIRECTS = 10;
Whether to use any proxy cache or not.
@plexus.configurationdefault="false"
/** * Whether to use any proxy cache or not. * * @plexus.configuration default="false" */
private boolean useCache;
@plexus.configuration
/** * @plexus.configuration */
private Properties httpHeaders;
@plexus.requirement
/** * @plexus.requirement */
private volatile LightweightHttpWagonAuthenticator authenticator;
Builds a complete URL string from the repository URL and the relative path of the resource passed.
Params:
  • resource – the resource to extract the relative path from.
Returns:the complete URL
/** * Builds a complete URL string from the repository URL and the relative path of the resource passed. * * @param resource the resource to extract the relative path from. * @return the complete URL */
private String buildUrl( Resource resource ) { return EncodingUtil.encodeURLToString( getRepository().getUrl(), resource.getName() ); } public void fillInputData( InputData inputData ) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { Resource resource = inputData.getResource(); String visitingUrl = buildUrl( resource ); List<String> visitedUrls = new ArrayList<>(); for ( int redirectCount = 0; redirectCount < MAX_REDIRECTS; redirectCount++ ) { if ( visitedUrls.contains( visitingUrl ) ) { // TODO add a test for this message throw new TransferFailedException( "Cyclic http redirect detected. Aborting! " + visitingUrl ); } visitedUrls.add( visitingUrl ); URL url = null; try { url = new URL( visitingUrl ); } catch ( MalformedURLException e ) { // TODO add test for this throw new ResourceDoesNotExistException( "Invalid repository URL: " + e.getMessage(), e ); } HttpURLConnection urlConnection = null; try { urlConnection = ( HttpURLConnection ) url.openConnection( this.proxy ); } catch ( IOException e ) { // TODO: add test for this String message = formatTransferFailedMessage( visitingUrl, UNKNOWN_STATUS_CODE, null, getProxyInfo() ); // TODO include e.getMessage appended to main message? throw new TransferFailedException( message, e ); } try { urlConnection.setRequestProperty( "Accept-Encoding", "gzip,deflate" ); if ( !useCache ) { urlConnection.setRequestProperty( "Pragma", "no-cache" ); } addHeaders( urlConnection ); // TODO: handle all response codes int responseCode = urlConnection.getResponseCode(); String reasonPhrase = urlConnection.getResponseMessage(); if ( responseCode == HttpURLConnection.HTTP_FORBIDDEN || responseCode == HttpURLConnection.HTTP_UNAUTHORIZED ) { throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), responseCode, reasonPhrase, getProxyInfo() ) ); } if ( responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP ) { visitingUrl = urlConnection.getHeaderField( "Location" ); continue; } InputStream is = urlConnection.getInputStream(); String contentEncoding = urlConnection.getHeaderField( "Content-Encoding" ); boolean isGZipped = contentEncoding != null && "gzip".equalsIgnoreCase( contentEncoding ); if ( isGZipped ) { is = new GZIPInputStream( is ); } boolean isDeflated = contentEncoding != null && "deflate".equalsIgnoreCase( contentEncoding ); if ( isDeflated ) { is = new DeflaterInputStream( is ); } inputData.setInputStream( is ); resource.setLastModified( urlConnection.getLastModified() ); resource.setContentLength( urlConnection.getContentLength() ); break; } catch ( FileNotFoundException e ) { // this could be 404 Not Found or 410 Gone - we don't have access to which it was. // TODO: 2019-10-03 url used should list all visited/redirected urls, not just the original throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ), UNKNOWN_STATUS_CODE, null, getProxyInfo() ), e ); } catch ( IOException originalIOException ) { throw convertHttpUrlConnectionException( originalIOException, urlConnection, buildUrl( resource ) ); } } } private void addHeaders( HttpURLConnection urlConnection ) { if ( httpHeaders != null ) { for ( Object header : httpHeaders.keySet() ) { urlConnection.setRequestProperty( (String) header, httpHeaders.getProperty( (String) header ) ); } } setAuthorization( urlConnection ); } private void setAuthorization( HttpURLConnection urlConnection ) { if ( preemptiveAuthentication && authenticationInfo != null && authenticationInfo.getUserName() != null ) { String credentials = authenticationInfo.getUserName() + ":" + authenticationInfo.getPassword(); String encoded = new String( Base64.encodeBase64( credentials.getBytes() ) ); urlConnection.setRequestProperty( "Authorization", "Basic " + encoded ); } } public void fillOutputData( OutputData outputData ) throws TransferFailedException { Resource resource = outputData.getResource(); try { URL url = new URL( buildUrl( resource ) ); putConnection = (HttpURLConnection) url.openConnection( this.proxy ); addHeaders( putConnection ); putConnection.setRequestMethod( "PUT" ); putConnection.setDoOutput( true ); outputData.setOutputStream( putConnection.getOutputStream() ); } catch ( IOException e ) { throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); } } protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { try { String reasonPhrase = putConnection.getResponseMessage(); int statusCode = putConnection.getResponseCode(); switch ( statusCode ) { // Success Codes case HttpURLConnection.HTTP_OK: // 200 case HttpURLConnection.HTTP_CREATED: // 201 case HttpURLConnection.HTTP_ACCEPTED: // 202 case HttpURLConnection.HTTP_NO_CONTENT: // 204 break; // TODO: handle 401 explicitly? case HttpURLConnection.HTTP_FORBIDDEN: throw new AuthorizationException( formatAuthorizationMessage( buildUrl( resource ), statusCode, reasonPhrase, getProxyInfo() ) ); case HttpURLConnection.HTTP_NOT_FOUND: throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( buildUrl( resource ), statusCode, reasonPhrase, getProxyInfo() ) ); // add more entries here default: throw new TransferFailedException( formatTransferFailedMessage( buildUrl( resource ), statusCode, reasonPhrase, getProxyInfo() ) ) ; } } catch ( IOException e ) { fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); throw convertHttpUrlConnectionException( e, putConnection, buildUrl( resource ) ); } } protected void openConnectionInternal() throws ConnectionException, AuthenticationException { final ProxyInfo proxyInfo = getProxyInfo( "http", getRepository().getHost() ); if ( proxyInfo != null ) { this.proxy = getProxy( proxyInfo ); this.proxyInfo = proxyInfo; } authenticator.setWagon( this ); boolean usePreemptiveAuthentication = Boolean.getBoolean( "maven.wagon.http.preemptiveAuthentication" ) || Boolean.parseBoolean( repository.getParameter( "preemptiveAuthentication" ) ) || this.preemptiveAuthentication; setPreemptiveAuthentication( usePreemptiveAuthentication ); } @SuppressWarnings( "deprecation" ) public PasswordAuthentication requestProxyAuthentication() { if ( proxyInfo != null && proxyInfo.getUserName() != null ) { String password = ""; if ( proxyInfo.getPassword() != null ) { password = proxyInfo.getPassword(); } return new PasswordAuthentication( proxyInfo.getUserName(), password.toCharArray() ); } return null; } public PasswordAuthentication requestServerAuthentication() { if ( authenticationInfo != null && authenticationInfo.getUserName() != null ) { String password = ""; if ( authenticationInfo.getPassword() != null ) { password = authenticationInfo.getPassword(); } return new PasswordAuthentication( authenticationInfo.getUserName(), password.toCharArray() ); } return null; } private Proxy getProxy( ProxyInfo proxyInfo ) { return new Proxy( getProxyType( proxyInfo ), getSocketAddress( proxyInfo ) ); } private Type getProxyType( ProxyInfo proxyInfo ) { if ( ProxyInfo.PROXY_SOCKS4.equals( proxyInfo.getType() ) || ProxyInfo.PROXY_SOCKS5.equals( proxyInfo.getType() ) ) { return Type.SOCKS; } else { return Type.HTTP; } } public SocketAddress getSocketAddress( ProxyInfo proxyInfo ) { return InetSocketAddress.createUnresolved( proxyInfo.getHost(), proxyInfo.getPort() ); } public void closeConnection() throws ConnectionException { //FIXME WAGON-375 use persistent connection feature provided by the jdk if ( putConnection != null ) { putConnection.disconnect(); } authenticator.resetWagon(); } public List<String> getFileList( String destinationDirectory ) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { InputData inputData = new InputData(); if ( destinationDirectory.length() > 0 && !destinationDirectory.endsWith( "/" ) ) { destinationDirectory += "/"; } String url = buildUrl( new Resource( destinationDirectory ) ); Resource resource = new Resource( destinationDirectory ); inputData.setResource( resource ); fillInputData( inputData ); InputStream is = inputData.getInputStream(); try { if ( is == null ) { throw new TransferFailedException( url + " - Could not open input stream for resource: '" + resource + "'" ); } final List<String> htmlFileList = HtmlFileListParser.parseFileList( url, is ); is.close(); is = null; return htmlFileList; } catch ( final IOException e ) { throw new TransferFailedException( "Failure transferring " + resource.getName(), e ); } finally { IOUtils.closeQuietly( is ); } } public boolean resourceExists( String resourceName ) throws TransferFailedException, AuthorizationException { HttpURLConnection headConnection; try { Resource resource = new Resource( resourceName ); URL url = new URL( buildUrl( resource ) ); headConnection = (HttpURLConnection) url.openConnection( this.proxy ); addHeaders( headConnection ); headConnection.setRequestMethod( "HEAD" ); headConnection.setDoOutput( true ); int statusCode = headConnection.getResponseCode(); switch ( statusCode ) { case HttpURLConnection.HTTP_OK: return true; case HttpURLConnection.HTTP_FORBIDDEN: throw new AuthorizationException( "Access denied to: " + url ); case HttpURLConnection.HTTP_NOT_FOUND: return false; case HttpURLConnection.HTTP_UNAUTHORIZED: throw new AuthorizationException( "Access denied to: " + url ); default: throw new TransferFailedException( "Failed to look for file: " + buildUrl( resource ) + ". Return code is: " + statusCode ); } } catch ( IOException e ) { throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); } } public boolean isUseCache() { return useCache; } public void setUseCache( boolean useCache ) { this.useCache = useCache; } public Properties getHttpHeaders() { return httpHeaders; } public void setHttpHeaders( Properties httpHeaders ) { this.httpHeaders = httpHeaders; } void setSystemProperty( String key, String value ) { if ( value != null ) { System.setProperty( key, value ); } else { System.getProperties().remove( key ); } } public void setPreemptiveAuthentication( boolean preemptiveAuthentication ) { this.preemptiveAuthentication = preemptiveAuthentication; } public LightweightHttpWagonAuthenticator getAuthenticator() { return authenticator; } public void setAuthenticator( LightweightHttpWagonAuthenticator authenticator ) { this.authenticator = authenticator; }
Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the equivalent TransferFailedException.

Details are extracted from the error stream if possible, either directly or indirectly by way of supporting accessors. The returned exception will include the passed IOException as a cause and a message that is as descriptive as possible.

Params:
  • originalIOException – an IOException thrown from an HttpURLConnection operation
  • urlConnection – instance that triggered the IOException
  • url – originating url that triggered the IOException
Returns:exception that is representative of the original cause
/** * Convert the IOException that is thrown for most transfer errors that HttpURLConnection encounters to the * equivalent {@link TransferFailedException}. * <p> * Details are extracted from the error stream if possible, either directly or indirectly by way of supporting * accessors. The returned exception will include the passed IOException as a cause and a message that is as * descriptive as possible. * * @param originalIOException an IOException thrown from an HttpURLConnection operation * @param urlConnection instance that triggered the IOException * @param url originating url that triggered the IOException * @return exception that is representative of the original cause */
private TransferFailedException convertHttpUrlConnectionException( IOException originalIOException, HttpURLConnection urlConnection, String url ) { // javadoc of HttpUrlConnection, HTTP transfer errors throw IOException // In that case, one may attempt to get the status code and reason phrase // from the errorstream. We do this, but by way of the following code path // getResponseCode()/getResponseMessage() - calls -> getHeaderFields() // getHeaderFields() - calls -> getErrorStream() try { // call getResponseMessage first since impl calls getResponseCode as part of that anyways String errorResponseMessage = urlConnection.getResponseMessage(); // may be null int errorResponseCode = urlConnection.getResponseCode(); // may be -1 if the code cannot be discerned String message = formatTransferFailedMessage( url, errorResponseCode, errorResponseMessage, getProxyInfo() ); return new TransferFailedException( message, originalIOException ); } catch ( IOException errorStreamException ) { // there was a problem using the standard methods, need to fall back to other options } // Attempt to parse the status code and URL which can be included in an IOException message // https://github.com/AdoptOpenJDK/openjdk-jdk11/blame/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java // .base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L1911L1913 String ioMsg = originalIOException.getMessage(); if ( ioMsg != null ) { Matcher matcher = IOEXCEPTION_MESSAGE_PATTERN.matcher( ioMsg ); if ( matcher.matches() ) { String codeStr = matcher.group( 1 ); String urlStr = matcher.group( 2 ); int code = UNKNOWN_STATUS_CODE; try { code = parseInt( codeStr ); } catch ( NumberFormatException nfe ) { // if here there is a regex problem } String message = formatTransferFailedMessage( urlStr, code, null, getProxyInfo() ); return new TransferFailedException( message, originalIOException ); } } String message = formatTransferFailedMessage( url, UNKNOWN_STATUS_CODE, null, getProxyInfo() ); return new TransferFailedException( message, originalIOException ); } }