/** * Copyright (c) 2008-2011 Sonatype, Inc. * All rights reserved. Includes the third-party code listed at http://www.sonatype.com/products/nexus/attributions. * * This program is free software: you can redistribute it and/or modify it only under the terms of the GNU Affero General * Public License Version 3 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License Version 3 * for more details. * * You should have received a copy of the GNU Affero General Public License Version 3 along with this program. If not, see * http://www.gnu.org/licenses. * * Sonatype Nexus (TM) Open Source Version is available from Sonatype, Inc. Sonatype and Sonatype Nexus are trademarks of * Sonatype, Inc. Apache Maven is a trademark of the Apache Foundation. M2Eclipse is a trademark of the Eclipse Foundation. * All other trademarks are the property of their respective owners. */ package org.sonatype.nexus.proxy.storage.remote.commonshttpclient; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.apache.commons.httpclient.CustomMultiThreadedHttpConnectionManager; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; import org.apache.commons.httpclient.NTCredentials; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthPolicy; import org.apache.commons.httpclient.auth.AuthScope; import org.codehaus.plexus.logging.Logger; import org.sonatype.nexus.proxy.repository.ClientSSLRemoteAuthenticationSettings; import org.sonatype.nexus.proxy.repository.NtlmRemoteAuthenticationSettings; import org.sonatype.nexus.proxy.repository.RemoteAuthenticationSettings; import org.sonatype.nexus.proxy.repository.RemoteProxySettings; import org.sonatype.nexus.proxy.repository.UsernamePasswordRemoteAuthenticationSettings; import org.sonatype.nexus.proxy.storage.remote.RemoteStorageContext; import org.sonatype.nexus.util.SystemPropertiesHelper; public class HttpClientProxyUtil { public static final String CONNECTION_POOL_SIZE_KEY = "httpClient.connectionPoolSize"; public static final String NTLM_IS_IN_USE_KEY = "httpClient.ntlmIsInUse"; private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger( HttpClientProxyUtil.class ); public static void applyProxyToHttpClient( HttpClient httpClient, RemoteStorageContext ctx, Logger logger ) { httpClient.setHttpConnectionManager( new CustomMultiThreadedHttpConnectionManager() ); // getting the timeout from RemoteStorageContext. The value we get depends on per-repo and global settings. // The value will "cascade" from repo level to global level, see imple of it. int timeout = ctx.getRemoteConnectionSettings().getConnectionTimeout(); // getting the connection pool size, using a little trick to allow us "backdoor" to tune it using system // properties, but defaulting it to the same we had before (httpClient defaults) int connectionPoolSize = SystemPropertiesHelper.getInteger( CONNECTION_POOL_SIZE_KEY, MultiThreadedHttpConnectionManager.DEFAULT_MAX_TOTAL_CONNECTIONS ); httpClient.getHttpConnectionManager().getParams().setConnectionTimeout( timeout ); httpClient.getHttpConnectionManager().getParams().setSoTimeout( timeout ); httpClient.getHttpConnectionManager().getParams().setTcpNoDelay( true ); httpClient.getHttpConnectionManager().getParams().setMaxTotalConnections( connectionPoolSize ); // NOTE: connPool is _per_ repo, hence all of those will connect to same host (unless mirrors are used) // so, we are violating intentionally the RFC and we let the whole pool size to chase same host httpClient.getHttpConnectionManager().getParams().setMaxConnectionsPerHost( HostConfiguration.ANY_HOST_CONFIGURATION, connectionPoolSize ); // Setting auth if needed HostConfiguration httpConfiguration = httpClient.getHostConfiguration(); // BASIC and DIGEST auth only RemoteAuthenticationSettings ras = ctx.getRemoteAuthenticationSettings(); boolean isSimpleAuthUsed = false; boolean isNtlmUsed = false; if ( ras != null ) { List<String> authPrefs = new ArrayList<String>( 2 ); authPrefs.add( AuthPolicy.DIGEST ); authPrefs.add( AuthPolicy.BASIC ); if ( ras instanceof ClientSSLRemoteAuthenticationSettings ) { // ClientSSLRemoteAuthenticationSettings cras = (ClientSSLRemoteAuthenticationSettings) ras; // TODO - implement this } else if ( ras instanceof NtlmRemoteAuthenticationSettings ) { NtlmRemoteAuthenticationSettings nras = (NtlmRemoteAuthenticationSettings) ras; // Using NTLM auth, adding it as first in policies authPrefs.add( 0, AuthPolicy.NTLM ); log( Level.INFO, "... authentication setup for NTLM domain \"" + nras.getNtlmDomain() + "\"", logger ); httpConfiguration.setHost( nras.getNtlmHost() ); httpClient.getState().setCredentials( AuthScope.ANY, new NTCredentials( nras.getUsername(), nras.getPassword(), nras.getNtlmHost(), nras.getNtlmDomain() ) ); isNtlmUsed = true; } else if ( ras instanceof UsernamePasswordRemoteAuthenticationSettings ) { UsernamePasswordRemoteAuthenticationSettings uras = (UsernamePasswordRemoteAuthenticationSettings) ras; // Using Username/Pwd auth, will not add NTLM log( Level.INFO, "... authentication setup for remote storage with username \"" + uras.getUsername() + "\"", logger ); httpClient.getState().setCredentials( AuthScope.ANY, new UsernamePasswordCredentials( uras.getUsername(), uras.getPassword() ) ); isSimpleAuthUsed = true; } httpClient.getParams().setParameter( AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs ); } RemoteProxySettings rps = ctx.getRemoteProxySettings(); boolean isProxyUsed = false; if ( rps.isEnabled() ) { isProxyUsed = true; log( Level.INFO, "... proxy setup with host \"" + rps.getHostname() + "\"", logger ); httpConfiguration.setProxy( rps.getHostname(), rps.getPort() ); // check if we have non-proxy hosts if ( rps.getNonProxyHosts() != null && !rps.getNonProxyHosts().isEmpty() ) { Set<Pattern> nonProxyHostPatterns = new HashSet<Pattern>( rps.getNonProxyHosts().size() ); for ( String nonProxyHostRegex : rps.getNonProxyHosts() ) { try { nonProxyHostPatterns.add( Pattern.compile( nonProxyHostRegex, Pattern.CASE_INSENSITIVE ) ); } catch ( PatternSyntaxException e ) { LOG.warn( "Invalid non proxy host regex: " + nonProxyHostRegex, e ); } } httpConfiguration.getParams().setParameter( CustomMultiThreadedHttpConnectionManager.NON_PROXY_HOSTS_PATTERNS_KEY, nonProxyHostPatterns ); } if ( rps.getProxyAuthentication() != null ) { ras = rps.getProxyAuthentication(); List<String> authPrefs = new ArrayList<String>( 2 ); authPrefs.add( AuthPolicy.DIGEST ); authPrefs.add( AuthPolicy.BASIC ); if ( ras instanceof ClientSSLRemoteAuthenticationSettings ) { // ClientSSLRemoteAuthenticationSettings cras = (ClientSSLRemoteAuthenticationSettings) ras; // TODO - implement this } else if ( ras instanceof NtlmRemoteAuthenticationSettings ) { NtlmRemoteAuthenticationSettings nras = (NtlmRemoteAuthenticationSettings) ras; // Using NTLM auth, adding it as first in policies authPrefs.add( 0, AuthPolicy.NTLM ); if ( ctx.getRemoteAuthenticationSettings() != null && ( ctx.getRemoteAuthenticationSettings() instanceof NtlmRemoteAuthenticationSettings ) ) { log( Level.WARNING, "... Apache Commons HttpClient 3.x is unable to use NTLM auth scheme\n" + " for BOTH server side and proxy side authentication!\n" + " You MUST reconfigure server side auth and use BASIC/DIGEST scheme\n" + " if you have to use NTLM proxy, otherwise it will not work!\n" + " *** SERVER SIDE AUTH OVERRIDDEN", logger ); } log( Level.WARNING, "... proxy authentication setup for NTLM domain \"" + nras.getNtlmDomain() + "\"", logger ); httpConfiguration.setHost( nras.getNtlmHost() ); httpClient.getState().setProxyCredentials( AuthScope.ANY, new NTCredentials( nras.getUsername(), nras.getPassword(), nras.getNtlmHost(), nras.getNtlmDomain() ) ); isNtlmUsed = true; } else if ( ras instanceof UsernamePasswordRemoteAuthenticationSettings ) { UsernamePasswordRemoteAuthenticationSettings uras = (UsernamePasswordRemoteAuthenticationSettings) ras; // Using Username/Pwd auth, will not add NTLM log( Level.INFO, "... proxy authentication setup for remote storage with username \"" + uras.getUsername() + "\"", logger ); httpClient.getState().setProxyCredentials( AuthScope.ANY, new UsernamePasswordCredentials( uras.getUsername(), uras.getPassword() ) ); } httpClient.getParams().setParameter( AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs ); } } // set preemptive only for simplest scenario: // no proxy and BASIC auth is used if ( isSimpleAuthUsed && !isProxyUsed ) { log( Level.INFO, "... simple scenario: simple authentication used with no proxy in between target and us, will use preemptive authentication", logger ); // we have authentication, let's do it preemptive httpClient.getParams().setAuthenticationPreemptive( true ); } // mark the fact that NTLM is in use if ( isNtlmUsed ) { ctx.putContextObject( NTLM_IS_IN_USE_KEY, Boolean.TRUE ); } else { ctx.putContextObject( NTLM_IS_IN_USE_KEY, Boolean.FALSE ); } } /** * Coding around plexus logger as this class is NOT a component and should not be using this type of logging. * * @param level * @param message * @param logger */ private static void log( Level level, String message, Logger logger ) { if ( logger != null ) { if ( level.equals( Level.SEVERE ) ) { logger.error( message ); } else if ( level.equals( Level.WARNING ) ) { logger.warn( message ); } else if ( level.equals( Level.INFO ) ) { logger.info( message ); } else { logger.debug( message ); } } else { if ( level.equals( Level.SEVERE ) ) { LOG.error( message ); } else if ( level.equals( Level.WARNING ) ) { LOG.warn( message ); } else if ( level.equals( Level.INFO ) ) { LOG.info( message ); } else { LOG.debug( message ); } } } }