Ver Fonte

refactor pwmHttpClient and add implementation for Java11 HttpClient

Jason Rivard há 4 anos atrás
pai
commit
2ed7fa56a5

+ 2 - 0
server/src/main/java/password/pwm/AppProperty.java

@@ -131,6 +131,7 @@ public enum AppProperty
     HTTP_CLIENT_CONNECT_TIMEOUT_MS                  ( "http.client.connectTimeoutMs" ),
     HTTP_CLIENT_REQUEST_TIMEOUT_MS                  ( "http.client.requestTimeoutMs" ),
     HTTP_CLIENT_RESPONSE_MAX_SIZE                   ( "http.client.response.maxSize" ),
+    HTTP_CLIENT_IMPLEMENTATION                      ( "http.client.implementation" ),
     HTTP_CLIENT_ENABLE_HOSTNAME_VERIFICATION        ( "http.client.enableHostnameVerification" ),
     HTTP_CLIENT_PROMISCUOUS_WORDLIST_ENABLE         ( "http.client.promiscuous.wordlist.enable" ),
     HTTP_ENABLE_GZIP                                ( "http.gzip.enable" ),
@@ -381,6 +382,7 @@ public enum AppProperty
     WORDLIST_IMPORT_LINE_COMMENTS                   ( "wordlist.import.lineComments" ),
     WORDLIST_INSPECTOR_FREQUENCY_SECONDS            ( "wordlist.inspector.frequencySeconds" ),
     WORDLIST_TEST_MODE                              ( "wordlist.testMode" ),
+    WORDLIST_BUCKET_CHECK_TIME_WARNING_MS           ( "wordlist.bucket.checkTimeWarningMs" ),
     WS_REST_CLIENT_PWRULE_HALTONERROR               ( "ws.restClient.pwRule.haltOnError" ),
     WS_REST_SERVER_SIGNING_FORM_TIMEOUT_SECONDS     ( "ws.restServer.signing.form.timeoutSeconds" ),
     WS_REST_SERVER_STATISTICS_DEFAULT_HISTORY       ( "ws.restServer.statistics.defaultHistoryDays" ),

+ 601 - 0
server/src/main/java/password/pwm/svc/httpclient/ApachePwmHttpClient.java

@@ -0,0 +1,601 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed 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.
+ */
+
+package password.pwm.svc.httpclient;
+
+import org.apache.commons.io.input.CountingInputStream;
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.conn.routing.HttpRoutePlanner;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.DefaultHostnameVerifier;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.ProxyAuthenticationStrategy;
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpContentType;
+import password.pwm.http.HttpEntityDataType;
+import password.pwm.http.HttpHeader;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.PwmURL;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.AtomicLoopIntIncrementer;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogLevel;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+public class ApachePwmHttpClient implements AutoCloseable, PwmHttpClientProvider
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ApachePwmHttpClient.class );
+
+    private static final AtomicLoopIntIncrementer CLIENT_COUNTER = new AtomicLoopIntIncrementer();
+
+    private final int clientID = CLIENT_COUNTER.next();
+
+    private PwmApplication pwmApplication;
+    private PwmHttpClientConfiguration pwmHttpClientConfiguration;
+    private HttpClientService httpClientService;
+
+    private TrustManager[] trustManagers;
+    private CloseableHttpClient httpClient;
+
+    private volatile boolean open = true;
+
+    public ApachePwmHttpClient()
+    {
+    }
+
+    public void init(
+            final PwmApplication pwmApplication,
+            final HttpClientService httpClientService,
+            final PwmHttpClientConfiguration pwmHttpClientConfiguration
+    )
+            throws PwmUnrecoverableException
+    {
+        this.pwmApplication = Objects.requireNonNull( pwmApplication );
+        this.httpClientService = Objects.requireNonNull( httpClientService );
+        this.pwmHttpClientConfiguration = pwmHttpClientConfiguration;
+
+        this.trustManagers = makeTrustManager( pwmApplication.getConfig(), pwmHttpClientConfiguration );
+        this.httpClient = makeHttpClient( pwmApplication, pwmHttpClientConfiguration, this.trustManagers );
+    }
+
+    static HostnameVerifier hostnameVerifier( final HttpTrustManagerHelper httpTrustManagerHelper )
+    {
+        return httpTrustManagerHelper.hostnameVerificationEnabled()
+                ? new DefaultHostnameVerifier()
+                : NoopHostnameVerifier.INSTANCE;
+    }
+
+    @Override
+    public void close()
+            throws Exception
+    {
+        LOGGER.trace( () -> "closed client #" + clientID );
+        httpClient.close();
+        open = false;
+    }
+
+    public boolean isOpen()
+    {
+        return open;
+    }
+
+    private static TrustManager[] makeTrustManager(
+            final Configuration appConfig,
+            final PwmHttpClientConfiguration pwmHttpClientConfiguration
+    )
+            throws PwmUnrecoverableException
+    {
+        final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( appConfig, pwmHttpClientConfiguration );
+        return httpTrustManagerHelper.makeTrustManager();
+    }
+
+    private static CloseableHttpClient makeHttpClient(
+            final PwmApplication pwmApplication,
+            final PwmHttpClientConfiguration pwmHttpClientConfiguration,
+            final TrustManager[] trustManagers
+    )
+            throws PwmUnrecoverableException
+    {
+        final Configuration appConfig = pwmApplication.getConfig();
+        final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
+        clientBuilder.setUserAgent( PwmConstants.PWM_APP_NAME );
+        final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( appConfig, pwmHttpClientConfiguration );
+
+        try
+        {
+            final SSLContext sslContext = SSLContext.getInstance( "TLS" );
+            sslContext.init(
+                    null,
+                    trustManagers,
+                    new SecureRandom() );
+            final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( sslContext, hostnameVerifier( httpTrustManagerHelper ) );
+            final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
+                    .register( "https", sslConnectionFactory )
+                    .register( "http", PlainConnectionSocketFactory.INSTANCE )
+                    .build();
+            final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager( registry );
+            clientBuilder.setSSLHostnameVerifier( hostnameVerifier( httpTrustManagerHelper ) );
+            clientBuilder.setSSLContext( sslContext );
+            clientBuilder.setSSLSocketFactory( sslConnectionFactory );
+            clientBuilder.setConnectionManager( ccm );
+            clientBuilder.setConnectionManagerShared( true );
+        }
+        catch ( final Exception e )
+        {
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "unexpected error creating promiscuous https client: " + e.getMessage() ) );
+        }
+
+        final String proxyUrl = appConfig.readSettingAsString( PwmSetting.HTTP_PROXY_URL );
+        if ( proxyUrl != null && proxyUrl.length() > 0 )
+        {
+            final URI proxyURI = URI.create( proxyUrl );
+
+            final String host = proxyURI.getHost();
+            final int port = proxyURI.getPort();
+            final HttpHost proxyHost = new HttpHost( host, port );
+
+            final String userInfo = proxyURI.getUserInfo();
+            if ( userInfo != null && userInfo.length() > 0 )
+            {
+                final String[] parts = userInfo.split( ":" );
+
+                final String username = parts[ 0 ];
+                final String password = ( parts.length > 1 ) ? parts[ 1 ] : "";
+
+                final CredentialsProvider credsProvider = new BasicCredentialsProvider();
+                credsProvider.setCredentials( new AuthScope( host, port ), new UsernamePasswordCredentials( username, password ) );
+                clientBuilder.setDefaultCredentialsProvider( credsProvider );
+                clientBuilder.setProxyAuthenticationStrategy( new ProxyAuthenticationStrategy() );
+            }
+
+            clientBuilder.setRoutePlanner( new ProxyRoutePlanner( proxyHost, appConfig ) );
+        }
+
+        clientBuilder.setDefaultRequestConfig( RequestConfig.copy( RequestConfig.DEFAULT )
+                .setSocketTimeout( Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_CLIENT_SOCKET_TIMEOUT_MS ) ) )
+                .setConnectTimeout( Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_CLIENT_CONNECT_TIMEOUT_MS ) ) )
+                .setConnectionRequestTimeout( Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_CLIENT_REQUEST_TIMEOUT_MS ) ) )
+                .build() );
+
+        return clientBuilder.build();
+    }
+
+    String entityToDebugString(
+            final String topLine,
+            final PwmHttpClientMessage pwmHttpClientMessage
+    )
+    {
+        final HttpEntityDataType dataType = pwmHttpClientMessage.getDataType();
+        final ImmutableByteArray binaryBody = pwmHttpClientMessage.getBinaryBody();
+        final String body = pwmHttpClientMessage.getBody();
+        final Map<String, String> headers = pwmHttpClientMessage.getHeaders();
+
+        final boolean isBinary = dataType == HttpEntityDataType.ByteArray;
+        final boolean emptyBody = isBinary
+                ? binaryBody == null || binaryBody.isEmpty()
+                : StringUtil.isEmpty( body );
+
+
+        final StringBuilder msg = new StringBuilder();
+        msg.append( topLine );
+        msg.append( " id=" ).append( pwmHttpClientMessage.getRequestID() ).append( ") " );
+
+        if ( emptyBody )
+        {
+            msg.append( " (no body)" );
+        }
+
+        if ( headers != null )
+        {
+            for ( final Map.Entry<String, String> headerEntry : headers.entrySet() )
+            {
+                msg.append( "\n" );
+                final HttpHeader httpHeader = HttpHeader.forHttpHeader( headerEntry.getKey() );
+                if ( httpHeader != null )
+                {
+                    final boolean sensitive = httpHeader.isSensitive();
+                    msg.append( "  header: " ).append( httpHeader.getHttpName() ).append( "=" );
+
+                    if ( sensitive )
+                    {
+                        msg.append( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
+                    }
+                    else
+                    {
+                        msg.append( headerEntry.getValue() );
+                    }
+                }
+                else
+                {
+                    // We encountered a header name that doesn't have a corresponding enum in HttpHeader,
+                    // so we can't check the sensitive flag.
+                    msg.append( "  header: " ).append( headerEntry.getKey() ).append( "=" ).append( headerEntry.getValue() );
+                }
+            }
+        }
+
+        if ( !emptyBody )
+        {
+            msg.append( "\n  body: " );
+
+            final boolean alwaysOutput = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_CLIENT_ALWAYS_LOG_ENTITIES ) );
+
+
+            if ( isBinary )
+            {
+                if ( binaryBody != null && !binaryBody.isEmpty() )
+                {
+                    msg.append( "[binary, " ).append( binaryBody.size() ).append( " bytes]" );
+                }
+                else
+                {
+                    msg.append( "[no data]" );
+                }
+            }
+            else
+            {
+                if ( StringUtil.isEmpty( body ) )
+                {
+                    msg.append( "[no data]" );
+                }
+                else
+                {
+                    msg.append( "[" ).append( body.length() ).append( " chars] " );
+
+                    if ( alwaysOutput || !pwmHttpClientConfiguration.isMaskBodyDebugOutput() )
+                    {
+                        msg.append( body );
+                    }
+                    else
+                    {
+                        msg.append( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
+                    }
+                }
+            }
+        }
+
+        return msg.toString();
+    }
+
+    @Override
+    public PwmHttpClientResponse makeRequest(
+            final PwmHttpClientRequest clientRequest,
+            final SessionLabel sessionLabel
+    )
+            throws PwmUnrecoverableException
+    {
+        try
+        {
+            return makeRequestImpl( clientRequest, sessionLabel );
+        }
+        catch ( final IOException e )
+        {
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_UNREACHABLE, "error while making http request: " + e.getMessage() ), e );
+        }
+    }
+
+    private PwmHttpClientResponse makeRequestImpl(
+            final PwmHttpClientRequest clientRequest,
+            final SessionLabel sessionLabel
+    )
+            throws IOException, PwmUnrecoverableException
+    {
+        final Instant startTime = Instant.now();
+        if ( LOGGER.isEnabled( PwmLogLevel.TRACE ) )
+        {
+            final String sslDebugText;
+            if ( clientRequest.isHttps() )
+            {
+                final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( pwmApplication.getConfig(), pwmHttpClientConfiguration );
+                sslDebugText = "using " + httpTrustManagerHelper.debugText();
+            }
+            else
+            {
+                sslDebugText = "";
+            }
+
+            LOGGER.trace( sessionLabel, () -> "client #" + clientID + " preparing to send "
+                    + clientRequest.toDebugString( this, sslDebugText ) );
+        }
+
+        final HttpResponse httpResponse = executeRequest( clientRequest );
+
+        final PwmHttpClientResponse.PwmHttpClientResponseBuilder httpClientResponseBuilder = PwmHttpClientResponse.builder();
+        httpClientResponseBuilder.requestID( clientRequest.getRequestID() );
+
+        final Optional<HttpContentType> optionalHttpContentType = contentTypeForEntity( httpResponse.getEntity() );
+
+        if ( httpResponse.getEntity() != null )
+        {
+            if ( optionalHttpContentType.isPresent() && optionalHttpContentType.get().getDataType() == HttpEntityDataType.ByteArray )
+            {
+                httpClientResponseBuilder.binaryBody( readBinaryEntityBody( httpResponse.getEntity() ) );
+                httpClientResponseBuilder.dataType( HttpEntityDataType.ByteArray );
+            }
+            else
+            {
+                httpClientResponseBuilder.body( EntityUtils.toString( httpResponse.getEntity() ) );
+                httpClientResponseBuilder.dataType( HttpEntityDataType.String );
+            }
+        }
+
+        final Map<String, String> responseHeaders = new LinkedHashMap<>();
+        if ( httpResponse.getAllHeaders() != null )
+        {
+            Arrays.stream( httpResponse.getAllHeaders() ).forEach( header -> responseHeaders.put( header.getName(), header.getValue() ) );
+        }
+
+        final PwmHttpClientResponse httpClientResponse = httpClientResponseBuilder
+                .statusCode( httpResponse.getStatusLine().getStatusCode() )
+                .contentType( optionalHttpContentType.orElse( HttpContentType.plain ) )
+                .statusPhrase( httpResponse.getStatusLine().getReasonPhrase() )
+                .headers( Collections.unmodifiableMap( responseHeaders ) )
+                .build();
+
+        final TimeDuration duration = TimeDuration.fromCurrent( startTime );
+        httpClientService.getStats().increment( HttpClientService.StatsKey.responseBytes, httpClientResponse.size() );
+        LOGGER.trace( sessionLabel, () -> "client #" + clientID + " received response (id=" + clientRequest.getRequestID() + ") in "
+                + duration.asCompactString() + ": "
+                + httpClientResponse.toDebugString( this ) );
+        return httpClientResponse;
+    }
+
+    private HttpResponse executeRequest( final PwmHttpClientRequest clientRequest )
+            throws IOException, PwmUnrecoverableException
+    {
+        final String requestBody = clientRequest.getBody();
+
+        final HttpRequestBase httpRequest;
+        switch ( clientRequest.getMethod() )
+        {
+            case POST:
+            {
+                try
+                {
+                    httpRequest = new HttpPost( new URI( clientRequest.getUrl() ).toString() );
+                    if ( requestBody != null && !requestBody.isEmpty() )
+                    {
+                        ( ( HttpPost ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
+                    }
+                }
+                catch ( final URISyntaxException e )
+                {
+                    throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "malformed url: " + clientRequest.getUrl() + ", error: " + e.getMessage() );
+                }
+            }
+            break;
+
+            case PUT:
+                httpRequest = new HttpPut( clientRequest.getUrl() );
+                if ( clientRequest.getBody() != null && !clientRequest.getBody().isEmpty() )
+                {
+                    ( ( HttpPut ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
+                }
+                break;
+
+            case PATCH:
+                httpRequest = new HttpPatch( clientRequest.getUrl() );
+                if ( clientRequest.getBody() != null && !clientRequest.getBody().isEmpty() )
+                {
+                    ( ( HttpPatch ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
+                }
+                break;
+
+            case GET:
+                httpRequest = new HttpGet( clientRequest.getUrl() );
+                break;
+
+            case DELETE:
+                httpRequest = new HttpDelete( clientRequest.getUrl() );
+                break;
+
+            default:
+                throw new IllegalStateException( "http method not yet implemented" );
+        }
+
+        if ( clientRequest.getHeaders() != null )
+        {
+            for ( final String key : clientRequest.getHeaders().keySet() )
+            {
+                final String value = clientRequest.getHeaders().get( key );
+                httpRequest.addHeader( key, value );
+            }
+        }
+
+        httpClientService.getStats().increment( HttpClientService.StatsKey.requests );
+        httpClientService.getStats().increment( HttpClientService.StatsKey.requestBytes, clientRequest.size() );
+        return httpClient.execute( httpRequest );
+    }
+
+    @Override
+    public InputStream streamForUrl( final String inputUrl )
+            throws IOException, PwmUnrecoverableException
+    {
+        final URL url = new URL( inputUrl );
+        if ( "file".equals( url.getProtocol() ) )
+        {
+            return url.openStream();
+        }
+
+        if ( "http".equals( url.getProtocol() ) || "https".equals( url.getProtocol() ) )
+        {
+
+            final PwmHttpClientRequest pwmHttpClientRequest = PwmHttpClientRequest.builder()
+                    .method( HttpMethod.GET )
+                    .url( inputUrl )
+                    .build();
+
+            final HttpResponse httpResponse = executeRequest( pwmHttpClientRequest );
+            if ( httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK )
+            {
+                final String errorMsg = "error retrieving stream for url '" + inputUrl + "', remote response: " + httpResponse.getStatusLine().toString();
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_REMOTE_ERROR_VALUE, errorMsg );
+                LOGGER.error( errorInformation );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+            return httpResponse.getEntity().getContent();
+        }
+
+        throw new IllegalArgumentException( "unknown protocol type: " + url.getProtocol() );
+    }
+
+    private static class ProxyRoutePlanner implements HttpRoutePlanner
+    {
+        private final HttpHost proxyServer;
+        private final Configuration appConfig;
+
+        ProxyRoutePlanner( final HttpHost proxyServer, final Configuration appConfig )
+        {
+            this.proxyServer = proxyServer;
+            this.appConfig = appConfig;
+        }
+
+        @Override
+        public HttpRoute determineRoute(
+                final HttpHost target,
+                final HttpRequest request,
+                final HttpContext context
+        )
+        {
+            final String targetUri = target.toURI();
+
+            final List<String> proxyExceptionUrls = appConfig.readSettingAsStringArray( PwmSetting.HTTP_PROXY_EXCEPTIONS );
+
+            if ( PwmURL.testIfUrlMatchesAllowedPattern( targetUri, proxyExceptionUrls, null ) )
+            {
+                return new HttpRoute( target );
+            }
+
+            final boolean secure = "https".equalsIgnoreCase( target.getSchemeName() );
+            return new HttpRoute( target, null, proxyServer, secure );
+        }
+    }
+
+    private static Optional<HttpContentType> contentTypeForEntity( final HttpEntity httpEntity )
+    {
+        if ( httpEntity != null )
+        {
+            final Header header = httpEntity.getContentType();
+            if ( header != null )
+            {
+                final HeaderElement[] headerElements = header.getElements();
+                if ( headerElements != null )
+                {
+                    for ( final HeaderElement headerElement : headerElements )
+                    {
+                        if ( headerElement != null )
+                        {
+                            final String name = headerElement.getName();
+                            if ( name != null )
+                            {
+                                final Optional<HttpContentType> httpContentType = HttpContentType.fromContentTypeHeader( name, null );
+                                if ( httpContentType.isPresent() )
+                                {
+                                    return httpContentType;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return Optional.empty();
+    }
+
+    private ImmutableByteArray readBinaryEntityBody( final HttpEntity httpEntity )
+            throws IOException
+    {
+        final long maxSize = JavaHelper.silentParseLong( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_CLIENT_RESPONSE_MAX_SIZE ), 100_000_000L );
+        try ( CountingInputStream contentStream = new CountingInputStream( httpEntity.getContent() ) )
+        {
+            final ByteArrayOutputStream baos = new ByteArrayOutputStream(  );
+            JavaHelper.copyWhilePredicate( contentStream, baos, aLong -> contentStream.getByteCount() <= maxSize );
+            return ImmutableByteArray.of( baos.toByteArray() );
+        }
+    }
+
+    @Override
+    public List<X509Certificate> readServerCertificates()
+            throws PwmUnrecoverableException
+    {
+        return PwmHttpClient.readServerCertificates( trustManagers );
+    }
+}
+

+ 47 - 18
server/src/main/java/password/pwm/svc/httpclient/HttpClientService.java

@@ -20,14 +20,19 @@
 
 package password.pwm.svc.httpclient;
 
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
+import password.pwm.svc.AbstractPwmService;
 import password.pwm.svc.PwmService;
 import password.pwm.util.java.StatisticCounterBundle;
 import password.pwm.util.logging.PwmLogger;
 
+import java.lang.reflect.InvocationTargetException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -37,32 +42,29 @@ import java.util.Objects;
 import java.util.WeakHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 
-public class HttpClientService implements PwmService
+public class HttpClientService extends AbstractPwmService implements PwmService
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( HttpClientService.class );
 
+    private Class<PwmHttpClientProvider> httpClientClass;
     private PwmApplication pwmApplication;
 
-    private final Map<PwmHttpClientConfiguration, ThreadLocal<PwmHttpClient>> clients = new ConcurrentHashMap<>(  );
-    private final Map<PwmHttpClient, Object> issuedClients = Collections.synchronizedMap( new WeakHashMap<>(  ) );
+    private final Map<PwmHttpClientConfiguration, ThreadLocal<PwmHttpClientProvider>> clients = new ConcurrentHashMap<>(  );
+    private final Map<PwmHttpClientProvider, Object> issuedClients = Collections.synchronizedMap( new WeakHashMap<>(  ) );
 
     private final StatisticCounterBundle<StatsKey> stats = new StatisticCounterBundle<>( StatsKey.class );
 
     enum StatsKey
     {
+        requests,
+        requestBytes,
+        responseBytes,
         createdClients,
         reusedClients,
     }
 
     public HttpClientService()
-            throws PwmUnrecoverableException
-    {
-    }
-
-    @Override
-    public STATUS status()
     {
-        return STATUS.OPEN;
     }
 
     @Override
@@ -70,6 +72,20 @@ public class HttpClientService implements PwmService
             throws PwmException
     {
         this.pwmApplication = pwmApplication;
+        final String implClassName = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_CLIENT_IMPLEMENTATION );
+        try
+        {
+            this.httpClientClass = ( Class<PwmHttpClientProvider> ) this.getClass().getClassLoader().loadClass( implClassName );
+            LOGGER.trace( () -> "loaded http client implementation: " + implClassName );
+        }
+        catch ( final Exception e )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation(
+                    PwmError.ERROR_INTERNAL, "unable to load pwmHttpClass implementation: " + e.getMessage() );
+            setStartupError( errorInformation );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+        setStatus( STATUS.OPEN );
     }
 
     @Override
@@ -99,26 +115,39 @@ public class HttpClientService implements PwmService
     {
         Objects.requireNonNull( pwmHttpClientConfiguration );
 
-        final ThreadLocal<PwmHttpClient> threadLocal = clients.computeIfAbsent(
+        final ThreadLocal<PwmHttpClientProvider> threadLocal = clients.computeIfAbsent(
                 pwmHttpClientConfiguration,
                 clientConfig -> new ThreadLocal<>() );
 
         final PwmHttpClient existingClient = threadLocal.get();
-        if ( existingClient != null && !existingClient.isClosed() )
+        if ( existingClient != null && existingClient.isOpen() )
         {
             stats.increment( StatsKey.reusedClients );
             return existingClient;
         }
 
-        final PwmHttpClient newClient = new PwmHttpClient( pwmApplication, pwmHttpClientConfiguration );
-        issuedClients.put( newClient, null );
-        threadLocal.set( newClient );
-        stats.increment( StatsKey.createdClients );
-        return newClient;
+        try
+        {
+            final PwmHttpClientProvider newClient = httpClientClass.getDeclaredConstructor().newInstance();
+            newClient.init( pwmApplication, this, pwmHttpClientConfiguration );
+            issuedClients.put( newClient, null );
+            threadLocal.set( newClient );
+            stats.increment( StatsKey.createdClients );
+            return newClient;
+        }
+        catch ( final InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to initialize pwmHttpClass implementation: " + e.getMessage() );
+        }
+    }
+
+    protected StatisticCounterBundle<StatsKey> getStats()
+    {
+        return stats;
     }
 
     @Override
-    public List<HealthRecord> healthCheck()
+    public List<HealthRecord> serviceHealthCheck()
     {
         return Collections.emptyList();
     }

+ 15 - 16
server/src/main/java/password/pwm/svc/httpclient/HttpTrustManagerHelper.java

@@ -20,36 +20,35 @@
 
 package password.pwm.svc.httpclient;
 
-import org.apache.http.conn.ssl.DefaultHostnameVerifier;
-import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.secure.PwmTrustManager;
 import password.pwm.util.secure.CertificateReadingTrustManager;
 import password.pwm.util.secure.PromiscuousTrustManager;
 import password.pwm.util.secure.PwmHashAlgorithm;
+import password.pwm.util.secure.PwmTrustManager;
 import password.pwm.util.secure.X509Utils;
 
-import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.TrustManager;
 import java.security.cert.X509Certificate;
 import java.util.Iterator;
 
+@Value
 class HttpTrustManagerHelper
 {
-    private final Configuration configuration;
+    private final Configuration appConfig;
     private final PwmHttpClientConfiguration pwmHttpClientConfiguration;
     private final PwmHttpClientConfiguration.TrustManagerType trustManagerType;
 
     HttpTrustManagerHelper(
-            final Configuration configuration,
+            final Configuration appConfig,
             final PwmHttpClientConfiguration pwmHttpClientConfiguration
     )
     {
-        this.configuration = configuration;
+        this.appConfig = appConfig;
         this.pwmHttpClientConfiguration = pwmHttpClientConfiguration;
         this.trustManagerType = pwmHttpClientConfiguration.getTrustManagerType();
     }
@@ -59,21 +58,21 @@ class HttpTrustManagerHelper
         return trustManagerType;
     }
 
-
-    HostnameVerifier hostnameVerifier()
+    boolean hostnameVerificationEnabled()
     {
         final PwmHttpClientConfiguration.TrustManagerType trustManagerType = getTrustManagerType();
         if ( trustManagerType == PwmHttpClientConfiguration.TrustManagerType.promiscuous )
         {
-            return NoopHostnameVerifier.INSTANCE;
+            return false;
         }
 
-        if ( !Boolean.parseBoolean( configuration.readAppProperty( AppProperty.HTTP_CLIENT_ENABLE_HOSTNAME_VERIFICATION ) ) )
+        final Configuration appConfig = getAppConfig();
+        if ( !Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.HTTP_CLIENT_ENABLE_HOSTNAME_VERIFICATION ) ) )
         {
-            return NoopHostnameVerifier.INSTANCE;
+            return false;
         }
 
-        return new DefaultHostnameVerifier();
+        return true;
     }
 
     TrustManager[] makeTrustManager(
@@ -93,20 +92,20 @@ class HttpTrustManagerHelper
             case promiscuousCertReader:
                 return new TrustManager[]
                         {
-                                CertificateReadingTrustManager.newCertReaderTrustManager( configuration ),
+                                CertificateReadingTrustManager.newCertReaderTrustManager( appConfig ),
                         };
 
             case configuredCertificates:
             {
                 return new TrustManager[]
                         {
-                                PwmTrustManager.createPwmTrustManager( configuration, pwmHttpClientConfiguration.getCertificates() ),
+                                PwmTrustManager.createPwmTrustManager( appConfig, pwmHttpClientConfiguration.getCertificates() ),
                         };
             }
 
             case defaultJava:
             {
-                return X509Utils.getDefaultJavaTrustManager( configuration );
+                return X509Utils.getDefaultJavaTrustManager( appConfig );
             }
 
             default:

+ 294 - 0
server/src/main/java/password/pwm/svc/httpclient/JavaPwmHttpClient.java

@@ -0,0 +1,294 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed 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.
+ */
+
+package password.pwm.svc.httpclient;
+
+import org.apache.http.HttpStatus;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpContentType;
+import password.pwm.http.HttpEntityDataType;
+import password.pwm.http.HttpHeader;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManager;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.URL;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+public class JavaPwmHttpClient implements PwmHttpClientProvider
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( JavaPwmHttpClient.class );
+
+    private PwmApplication pwmApplication;
+    private HttpClientService httpClientService;
+    private HttpClient httpClient;
+    private TrustManager[] trustManagers;
+
+    public JavaPwmHttpClient()
+    {
+    }
+
+    public void init(
+            final PwmApplication pwmApplication,
+            final HttpClientService httpClientService,
+            final PwmHttpClientConfiguration pwmHttpClientConfiguration
+    )
+            throws PwmUnrecoverableException
+    {
+        this.pwmApplication = Objects.requireNonNull( pwmApplication );
+        this.httpClientService = Objects.requireNonNull( httpClientService );
+        final Configuration appConfig = pwmApplication.getConfig();
+        final HttpTrustManagerHelper trustManagerHelper = new HttpTrustManagerHelper( pwmApplication.getConfig(), pwmHttpClientConfiguration );
+        this.trustManagers = trustManagerHelper.makeTrustManager();
+
+        try
+        {
+            final SSLContext sslContext = SSLContext.getInstance( "TLS" );
+            sslContext.init( null, this.trustManagers, pwmApplication.getSecureService().pwmRandom() );
+
+            final SSLParameters sslParameters = new SSLParameters();
+
+            if ( !trustManagerHelper.hostnameVerificationEnabled() )
+            {
+                sslParameters.setEndpointIdentificationAlgorithm( null );
+            }
+
+            final int connectTimeoutMs = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_CLIENT_CONNECT_TIMEOUT_MS ) );
+            final HttpClient.Builder builder = HttpClient.newBuilder()
+                    .followRedirects( HttpClient.Redirect.NORMAL )
+                    .connectTimeout( Duration.ofMillis( connectTimeoutMs ) )
+                    .sslContext( sslContext )
+                    .sslParameters( sslParameters );
+            applyProxyConfig( pwmApplication, builder );
+            this.httpClient = builder.build();
+        }
+        catch ( final NoSuchAlgorithmException | KeyManagementException e )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, "error creating Java HTTP Client: " + e.getMessage() );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+    }
+
+    @Override
+    public void close() throws Exception
+    {
+
+    }
+
+    @Override
+    public boolean isOpen()
+    {
+        return true;
+    }
+
+    @Override
+    public PwmHttpClientResponse makeRequest( final PwmHttpClientRequest clientRequest, final SessionLabel sessionLabel )
+            throws PwmUnrecoverableException
+    {
+        try
+        {
+            final HttpRequest httpRequest = makeJavaHttpRequest( clientRequest );
+            final HttpResponse<byte[]> response = httpClient.send( httpRequest, HttpResponse.BodyHandlers.ofByteArray() );
+            final Optional<HttpContentType> httpContentType = contentTypeForResponse( response.headers() );
+
+            final PwmHttpClientResponse.PwmHttpClientResponseBuilder builder = PwmHttpClientResponse.builder()
+                    .statusCode( response.statusCode() )
+                    .requestID( clientRequest.getRequestID() )
+                    .headers( convertResponseHeaders( response.headers() ) );
+
+            if ( response.body() != null )
+            {
+                if ( httpContentType.isPresent() && httpContentType.get().getDataType() == HttpEntityDataType.ByteArray )
+                {
+                    builder.dataType( HttpEntityDataType.ByteArray );
+                    builder.binaryBody( ImmutableByteArray.of( response.body() ) );
+                }
+                else
+                {
+                    builder.dataType( HttpEntityDataType.String );
+                    builder.body( new String( response.body(), PwmConstants.DEFAULT_CHARSET ) );
+                }
+            }
+            httpClientService.getStats().increment( HttpClientService.StatsKey.requestBytes, clientRequest.size() );
+            final PwmHttpClientResponse pwmHttpClientResponse = builder.build();
+            httpClientService.getStats().increment( HttpClientService.StatsKey.responseBytes, pwmHttpClientResponse.size() );
+            return pwmHttpClientResponse;
+
+        }
+        catch ( final IOException | InterruptedException exception )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, exception.getMessage() );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+    }
+
+    private static Optional<HttpContentType> contentTypeForResponse( final HttpHeaders httpHeaders )
+    {
+        if ( httpHeaders != null )
+        {
+            return httpHeaders.map().entrySet().stream()
+                    .filter( entry ->  StringUtil.nullSafeEqualsIgnoreCase( entry.getKey(), HttpHeader.ContentType.getHttpName() ) )
+                    .flatMap( entry -> entry.getValue().stream() )
+                    .flatMap( headerValue -> StringUtil.splitAndTrim( headerValue, ";" ).stream() )
+                    .flatMap( segmentValue -> HttpContentType.fromContentTypeHeader( segmentValue, null  ).stream() )
+                    .findFirst();
+        }
+
+
+
+        return Optional.empty();
+    }
+
+    final HttpRequest makeJavaHttpRequest( final PwmHttpClientRequest clientRequest )
+    {
+        final HttpRequest.Builder httpRequestBuilder = HttpRequest.newBuilder()
+                .uri( URI.create( clientRequest.getUrl() ) );
+
+
+        final HttpRequest.BodyPublisher bodyPublisher;
+        if ( clientRequest.getDataType() == HttpEntityDataType.ByteArray  && clientRequest.getBinaryBody() != null )
+        {
+            bodyPublisher = HttpRequest.BodyPublishers.ofInputStream( () -> clientRequest.getBinaryBody().newByteArrayInputStream() );
+        }
+        else if ( clientRequest.getDataType() == HttpEntityDataType.String  && clientRequest.getBody() != null )
+        {
+            bodyPublisher = HttpRequest.BodyPublishers.ofString( clientRequest.getBody() );
+        }
+        else
+        {
+            bodyPublisher = HttpRequest.BodyPublishers.noBody();
+        }
+
+        httpRequestBuilder.method( clientRequest.getMethod().name(), bodyPublisher );
+
+        if ( clientRequest.getHeaders() != null )
+        {
+            for ( final Map.Entry<String, String> headerEntry : clientRequest.getHeaders().entrySet() )
+            {
+                httpRequestBuilder.setHeader( headerEntry.getKey(), headerEntry.getValue() );
+            }
+        }
+
+        httpRequestBuilder.setHeader( HttpHeader.UserAgent.getHttpName(), PwmConstants.PWM_APP_NAME );
+
+        return httpRequestBuilder.build();
+    }
+
+    private static Map<String, String> convertResponseHeaders( final HttpHeaders httpHeaders )
+    {
+        final Map<String, String> returnHeaders = new HashMap<>();
+        if ( httpHeaders != null )
+        {
+            for ( final String name : httpHeaders.map().keySet() )
+            {
+                returnHeaders.put( name, httpHeaders.firstValue( name ).orElseThrow() );
+            }
+        }
+
+        return Collections.unmodifiableMap( returnHeaders );
+    }
+
+    private static void applyProxyConfig( final PwmApplication pwmApplication, final HttpClient.Builder builder )
+    {
+        final Configuration appConfig = pwmApplication.getConfig();
+        final String proxyUrl = appConfig.readSettingAsString( PwmSetting.HTTP_PROXY_URL );
+        if ( !StringUtil.isEmpty( proxyUrl ) )
+        {
+            final URI proxyURI = URI.create( proxyUrl );
+            final String host = proxyURI.getHost();
+            final int port = proxyURI.getPort();
+            final InetSocketAddress inetSocketAddress = new InetSocketAddress( host, port );
+            builder.proxy( ProxySelector.of( inetSocketAddress ) );
+        }
+    }
+
+    @Override
+    public InputStream streamForUrl( final String inputUrl )
+            throws IOException, PwmUnrecoverableException
+    {
+        final URL url = new URL( inputUrl );
+        if ( "file".equals( url.getProtocol() ) )
+        {
+            return url.openStream();
+        }
+
+        if ( "http".equals( url.getProtocol() ) || "https".equals( url.getProtocol() ) )
+        {
+            try
+            {
+                final HttpRequest httpRequest = makeJavaHttpRequest( PwmHttpClientRequest.builder().method( HttpMethod.GET ).url( inputUrl ).build() );
+                final HttpResponse<InputStream> response = httpClient.send( httpRequest, HttpResponse.BodyHandlers.ofInputStream() );
+                if ( response.statusCode() != HttpStatus.SC_OK )
+                {
+                    final String errorMsg = "error retrieving stream for url '" + inputUrl + "', remote response: " + response.statusCode();
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg );
+                    LOGGER.error( errorInformation );
+                    throw new PwmUnrecoverableException( errorInformation );
+                }
+                return response.body();
+            }
+            catch ( final IOException | InterruptedException exception )
+            {
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, exception.getMessage() );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+        }
+
+        throw new IllegalArgumentException( "unknown protocol type: " + url.getProtocol() );
+    }
+
+    @Override
+    public List<X509Certificate> readServerCertificates()
+            throws PwmUnrecoverableException
+    {
+        return PwmHttpClient.readServerCertificates( trustManagers );
+    }
+
+}

+ 13 - 526
server/src/main/java/password/pwm/svc/httpclient/PwmHttpClient.java

@@ -20,550 +20,38 @@
 
 package password.pwm.svc.httpclient;
 
-import org.apache.commons.io.input.CountingInputStream;
-import org.apache.http.Header;
-import org.apache.http.HeaderElement;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpHost;
-import org.apache.http.HttpRequest;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.auth.AuthScope;
-import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.CredentialsProvider;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPatch;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.config.Registry;
-import org.apache.http.config.RegistryBuilder;
-import org.apache.http.conn.HttpClientConnectionManager;
-import org.apache.http.conn.routing.HttpRoute;
-import org.apache.http.conn.routing.HttpRoutePlanner;
-import org.apache.http.conn.socket.ConnectionSocketFactory;
-import org.apache.http.conn.socket.PlainConnectionSocketFactory;
-import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.BasicCredentialsProvider;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.impl.client.ProxyAuthenticationStrategy;
-import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
-import org.apache.http.protocol.HttpContext;
-import org.apache.http.util.EntityUtils;
-import password.pwm.AppProperty;
-import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.HttpContentType;
-import password.pwm.http.HttpEntityDataType;
-import password.pwm.http.HttpHeader;
-import password.pwm.http.HttpMethod;
-import password.pwm.http.PwmURL;
-import password.pwm.http.bean.ImmutableByteArray;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogLevel;
-import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.CertificateReadingTrustManager;
 
-import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManager;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.security.SecureRandom;
 import java.security.cert.X509Certificate;
-import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicInteger;
 
-public class PwmHttpClient implements AutoCloseable
+public interface PwmHttpClient extends AutoCloseable
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmHttpClient.class );
+    void close()
+            throws Exception;
 
-    private static final AtomicInteger CLIENT_COUNTER = new AtomicInteger( 0 );
+    boolean isOpen();
 
-    private final int clientID = CLIENT_COUNTER.getAndIncrement();
-    private final PwmApplication pwmApplication;
-    private final PwmHttpClientConfiguration pwmHttpClientConfiguration;
-
-    private final TrustManager[] trustManagers;
-    private final CloseableHttpClient httpClient;
-
-    private volatile boolean open = true;
-
-    PwmHttpClient( final PwmApplication pwmApplication, final PwmHttpClientConfiguration pwmHttpClientConfiguration )
-            throws PwmUnrecoverableException
-    {
-        this.pwmApplication = pwmApplication;
-        this.pwmHttpClientConfiguration = pwmHttpClientConfiguration;
-
-        this.trustManagers = makeTrustManager( pwmApplication.getConfig(), pwmHttpClientConfiguration );
-        this.httpClient = makeHttpClient( pwmApplication, pwmHttpClientConfiguration, this.trustManagers );
-    }
-
-    @Override
-    public void close()
-            throws Exception
-    {
-        LOGGER.trace( () -> "closed client #" + clientID );
-        httpClient.close();
-        open = false;
-    }
-
-    boolean isClosed()
-    {
-        return !open;
-    }
-
-    private static TrustManager[] makeTrustManager(
-            final Configuration configuration,
-            final PwmHttpClientConfiguration pwmHttpClientConfiguration
-    )
-            throws PwmUnrecoverableException
-    {
-        final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( configuration, pwmHttpClientConfiguration );
-        return httpTrustManagerHelper.makeTrustManager();
-    }
-
-    private static CloseableHttpClient makeHttpClient(
-            final PwmApplication pwmApplication,
-            final PwmHttpClientConfiguration pwmHttpClientConfiguration,
-            final TrustManager[] trustManagers
-    )
-            throws PwmUnrecoverableException
-    {
-        final Configuration configuration = pwmApplication.getConfig();
-        final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
-        clientBuilder.setUserAgent( PwmConstants.PWM_APP_NAME );
-        final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( configuration, pwmHttpClientConfiguration );
-
-        try
-        {
-            final SSLContext sslContext = SSLContext.getInstance( "TLS" );
-            sslContext.init(
-                    null,
-                    trustManagers,
-                    new SecureRandom() );
-            final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( sslContext, httpTrustManagerHelper.hostnameVerifier() );
-            final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
-                    .register( "https", sslConnectionFactory )
-                    .register( "http", PlainConnectionSocketFactory.INSTANCE )
-                    .build();
-            final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager( registry );
-            clientBuilder.setSSLHostnameVerifier( httpTrustManagerHelper.hostnameVerifier() );
-            clientBuilder.setSSLContext( sslContext );
-            clientBuilder.setSSLSocketFactory( sslConnectionFactory );
-            clientBuilder.setConnectionManager( ccm );
-            clientBuilder.setConnectionManagerShared( true );
-        }
-        catch ( final Exception e )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "unexpected error creating promiscuous https client: " + e.getMessage() ) );
-        }
-
-        final String proxyUrl = configuration.readSettingAsString( PwmSetting.HTTP_PROXY_URL );
-        if ( proxyUrl != null && proxyUrl.length() > 0 )
-        {
-            final URI proxyURI = URI.create( proxyUrl );
-
-            final String host = proxyURI.getHost();
-            final int port = proxyURI.getPort();
-            final HttpHost proxyHost = new HttpHost( host, port );
-
-            final String userInfo = proxyURI.getUserInfo();
-            if ( userInfo != null && userInfo.length() > 0 )
-            {
-                final String[] parts = userInfo.split( ":" );
-
-                final String username = parts[ 0 ];
-                final String password = ( parts.length > 1 ) ? parts[ 1 ] : "";
-
-                final CredentialsProvider credsProvider = new BasicCredentialsProvider();
-                credsProvider.setCredentials( new AuthScope( host, port ), new UsernamePasswordCredentials( username, password ) );
-                clientBuilder.setDefaultCredentialsProvider( credsProvider );
-                clientBuilder.setProxyAuthenticationStrategy( new ProxyAuthenticationStrategy() );
-            }
-
-            clientBuilder.setRoutePlanner( new ProxyRoutePlanner( proxyHost, configuration ) );
-        }
-
-        clientBuilder.setDefaultRequestConfig( RequestConfig.copy( RequestConfig.DEFAULT )
-                .setSocketTimeout( Integer.parseInt( configuration.readAppProperty( AppProperty.HTTP_CLIENT_SOCKET_TIMEOUT_MS ) ) )
-                .setConnectTimeout( Integer.parseInt( configuration.readAppProperty( AppProperty.HTTP_CLIENT_CONNECT_TIMEOUT_MS ) ) )
-                .setConnectionRequestTimeout( Integer.parseInt( configuration.readAppProperty( AppProperty.HTTP_CLIENT_REQUEST_TIMEOUT_MS ) ) )
-                .build() );
-
-        return clientBuilder.build();
-    }
-
-    String entityToDebugString(
-            final String topLine,
-            final PwmHttpClientMessage pwmHttpClientMessage
-    )
-    {
-        final HttpEntityDataType dataType = pwmHttpClientMessage.getDataType();
-        final ImmutableByteArray binaryBody = pwmHttpClientMessage.getBinaryBody();
-        final String body = pwmHttpClientMessage.getBody();
-        final Map<String, String> headers = pwmHttpClientMessage.getHeaders();
-
-        final boolean isBinary = dataType == HttpEntityDataType.ByteArray;
-        final boolean emptyBody = isBinary
-                ? binaryBody == null || binaryBody.isEmpty()
-                : StringUtil.isEmpty( body );
-
-
-        final StringBuilder msg = new StringBuilder();
-        msg.append( topLine );
-        msg.append( " id=" ).append( pwmHttpClientMessage.getRequestID() ).append( ") " );
-
-        if ( emptyBody )
-        {
-            msg.append( " (no body)" );
-        }
-
-        if ( headers != null )
-        {
-            for ( final Map.Entry<String, String> headerEntry : headers.entrySet() )
-            {
-                msg.append( "\n" );
-                final HttpHeader httpHeader = HttpHeader.forHttpHeader( headerEntry.getKey() );
-                if ( httpHeader != null )
-                {
-                    final boolean sensitive = httpHeader.isSensitive();
-                    msg.append( "  header: " ).append( httpHeader.getHttpName() ).append( "=" );
-
-                    if ( sensitive )
-                    {
-                        msg.append( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
-                    }
-                    else
-                    {
-                        msg.append( headerEntry.getValue() );
-                    }
-                }
-                else
-                {
-                    // We encountered a header name that doesn't have a corresponding enum in HttpHeader,
-                    // so we can't check the sensitive flag.
-                    msg.append( "  header: " ).append( headerEntry.getKey() ).append( "=" ).append( headerEntry.getValue() );
-                }
-            }
-        }
-
-        if ( !emptyBody )
-        {
-            msg.append( "\n  body: " );
-
-            final boolean alwaysOutput = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_CLIENT_ALWAYS_LOG_ENTITIES ) );
-
-
-            if ( isBinary )
-            {
-                if ( binaryBody != null && !binaryBody.isEmpty() )
-                {
-                    msg.append( "[binary, " ).append( binaryBody.size() ).append( " bytes]" );
-                }
-                else
-                {
-                    msg.append( "[no data]" );
-                }
-            }
-            else
-            {
-                if ( StringUtil.isEmpty( body ) )
-                {
-                    msg.append( "[no data]" );
-                }
-                else
-                {
-                    msg.append( "[" ).append( body.length() ).append( " chars] " );
-
-                    if ( alwaysOutput || !pwmHttpClientConfiguration.isMaskBodyDebugOutput() )
-                    {
-                        msg.append( body );
-                    }
-                    else
-                    {
-                        msg.append( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
-                    }
-                }
-            }
-        }
-
-        return msg.toString();
-    }
-
-    public PwmHttpClientResponse makeRequest(
-            final PwmHttpClientRequest clientRequest,
-            final SessionLabel sessionLabel
-    )
-            throws PwmUnrecoverableException
-    {
-        try
-        {
-            return makeRequestImpl( clientRequest, sessionLabel );
-        }
-        catch ( final IOException e )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_UNREACHABLE, "error while making http request: " + e.getMessage() ), e );
-        }
-    }
-
-    private PwmHttpClientResponse makeRequestImpl(
-            final PwmHttpClientRequest clientRequest,
-            final SessionLabel sessionLabel
+    PwmHttpClientResponse makeRequest(
+            PwmHttpClientRequest clientRequest,
+            SessionLabel sessionLabel
     )
-            throws IOException, PwmUnrecoverableException
-    {
-        final Instant startTime = Instant.now();
-        if ( LOGGER.isEnabled( PwmLogLevel.TRACE ) )
-        {
-            final String sslDebugText;
-            if ( clientRequest.isHttps() )
-            {
-                final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( pwmApplication.getConfig(), pwmHttpClientConfiguration );
-                sslDebugText = "using " + httpTrustManagerHelper.debugText();
-            }
-            else
-            {
-                sslDebugText = "";
-            }
-
-            LOGGER.trace( sessionLabel, () -> "client #" + clientID + " preparing to send "
-                    + clientRequest.toDebugString( this, sslDebugText ) );
-        }
-
-        final HttpResponse httpResponse = executeRequest( clientRequest );
-
-        final PwmHttpClientResponse.PwmHttpClientResponseBuilder httpClientResponseBuilder = PwmHttpClientResponse.builder();
-        httpClientResponseBuilder.requestID( clientRequest.getRequestID() );
-
-        final Optional<HttpContentType> optionalHttpContentType = contentTypeForEntity( httpResponse.getEntity() );
-
-        if ( optionalHttpContentType.isPresent() && optionalHttpContentType.get().getDataType() ==  HttpEntityDataType.ByteArray )
-        {
-            httpClientResponseBuilder.binaryBody( readBinaryEntityBody( httpResponse.getEntity() ) );
-            httpClientResponseBuilder.dataType( HttpEntityDataType.ByteArray );
-        }
-        else
-        {
-            httpClientResponseBuilder.body( EntityUtils.toString( httpResponse.getEntity() ) );
-            httpClientResponseBuilder.dataType( HttpEntityDataType.String );
-        }
-
-        final Map<String, String> responseHeaders = new LinkedHashMap<>();
-        if ( httpResponse.getAllHeaders() != null )
-        {
-            Arrays.stream( httpResponse.getAllHeaders() ).forEach( header -> responseHeaders.put( header.getName(), header.getValue() ) );
-        }
-
-        final PwmHttpClientResponse httpClientResponse = httpClientResponseBuilder
-                .statusCode( httpResponse.getStatusLine().getStatusCode() )
-                .contentType( optionalHttpContentType.orElse( HttpContentType.plain ) )
-                .statusPhrase( httpResponse.getStatusLine().getReasonPhrase() )
-                .headers( Collections.unmodifiableMap( responseHeaders ) )
-                .build();
-
-        final TimeDuration duration = TimeDuration.fromCurrent( startTime );
-        LOGGER.trace( sessionLabel, () -> "client #" + clientID + " received response (id=" + clientRequest.getRequestID() + ") in "
-                + duration.asCompactString() + ": "
-                + httpClientResponse.toDebugString( this ) );
-        return httpClientResponse;
-    }
-
-    private HttpResponse executeRequest( final PwmHttpClientRequest clientRequest )
-            throws IOException, PwmUnrecoverableException
-    {
-        final String requestBody = clientRequest.getBody();
-
-        final HttpRequestBase httpRequest;
-        switch ( clientRequest.getMethod() )
-        {
-            case POST:
-            {
-                try
-                {
-                    httpRequest = new HttpPost( new URI( clientRequest.getUrl() ).toString() );
-                    if ( requestBody != null && !requestBody.isEmpty() )
-                    {
-                        ( ( HttpPost ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
-                    }
-                }
-                catch ( final URISyntaxException e )
-                {
-                    throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "malformed url: " + clientRequest.getUrl() + ", error: " + e.getMessage() );
-                }
-            }
-            break;
-
-            case PUT:
-                httpRequest = new HttpPut( clientRequest.getUrl() );
-                if ( clientRequest.getBody() != null && !clientRequest.getBody().isEmpty() )
-                {
-                    ( ( HttpPut ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
-                }
-                break;
-
-            case PATCH:
-                httpRequest = new HttpPatch( clientRequest.getUrl() );
-                if ( clientRequest.getBody() != null && !clientRequest.getBody().isEmpty() )
-                {
-                    ( ( HttpPatch ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
-                }
-                break;
-
-            case GET:
-                httpRequest = new HttpGet( clientRequest.getUrl() );
-                break;
-
-            case DELETE:
-                httpRequest = new HttpDelete( clientRequest.getUrl() );
-                break;
-
-            default:
-                throw new IllegalStateException( "http method not yet implemented" );
-        }
-
-        if ( clientRequest.getHeaders() != null )
-        {
-            for ( final String key : clientRequest.getHeaders().keySet() )
-            {
-                final String value = clientRequest.getHeaders().get( key );
-                httpRequest.addHeader( key, value );
-            }
-        }
-
-        return httpClient.execute( httpRequest );
-    }
-
-    public InputStream streamForUrl( final String inputUrl )
-            throws IOException, PwmUnrecoverableException
-    {
-        final URL url = new URL( inputUrl );
-        if ( "file".equals( url.getProtocol() ) )
-        {
-            return url.openStream();
-        }
-
-        if ( "http".equals( url.getProtocol() ) || "https".equals( url.getProtocol() ) )
-        {
-
-            final PwmHttpClientRequest pwmHttpClientRequest = PwmHttpClientRequest.builder()
-                    .method( HttpMethod.GET )
-                    .url( inputUrl )
-                    .build();
+                    throws PwmUnrecoverableException;
 
-            final HttpResponse httpResponse = executeRequest( pwmHttpClientRequest );
-            if ( httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK )
-            {
-                final String errorMsg = "error retrieving stream for url '" + inputUrl + "', remote response: " + httpResponse.getStatusLine().toString();
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_REMOTE_ERROR_VALUE, errorMsg );
-                LOGGER.error( errorInformation );
-                throw new PwmUnrecoverableException( errorInformation );
-            }
-            return httpResponse.getEntity().getContent();
-        }
-
-        throw new IllegalArgumentException( "unknown protocol type: " + url.getProtocol() );
-    }
-
-    private static class ProxyRoutePlanner implements HttpRoutePlanner
-    {
-        private final HttpHost proxyServer;
-        private final Configuration configuration;
-
-        ProxyRoutePlanner( final HttpHost proxyServer, final Configuration configuration )
-        {
-            this.proxyServer = proxyServer;
-            this.configuration = configuration;
-        }
-
-        @Override
-        public HttpRoute determineRoute(
-                final HttpHost target,
-                final HttpRequest request,
-                final HttpContext context
-        )
-        {
-            final String targetUri = target.toURI();
+    InputStream streamForUrl( String inputUrl )
+                            throws IOException, PwmUnrecoverableException;
 
-            final List<String> proxyExceptionUrls = configuration.readSettingAsStringArray( PwmSetting.HTTP_PROXY_EXCEPTIONS );
+    List<X509Certificate> readServerCertificates()
+                                    throws PwmUnrecoverableException;
 
-            if ( PwmURL.testIfUrlMatchesAllowedPattern( targetUri, proxyExceptionUrls, null ) )
-            {
-                return new HttpRoute( target );
-            }
-
-            final boolean secure = "https".equalsIgnoreCase( target.getSchemeName() );
-            return new HttpRoute( target, null, proxyServer, secure );
-        }
-    }
-
-    private static Optional<HttpContentType> contentTypeForEntity( final HttpEntity httpEntity )
-    {
-        if ( httpEntity != null )
-        {
-            final Header header = httpEntity.getContentType();
-            if ( header != null )
-            {
-                final HeaderElement[] headerElements = header.getElements();
-                if ( headerElements != null )
-                {
-                    for ( final HeaderElement headerElement : headerElements )
-                    {
-                        if ( headerElement != null )
-                        {
-                            final String name = headerElement.getName();
-                            if ( name != null )
-                            {
-                                final Optional<HttpContentType> httpContentType = HttpContentType.fromContentTypeHeader( name, null );
-                                if ( httpContentType.isPresent() )
-                                {
-                                    return httpContentType;
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        return Optional.empty();
-    }
-
-    private ImmutableByteArray readBinaryEntityBody( final HttpEntity httpEntity )
-            throws IOException
-    {
-        final long maxSize = JavaHelper.silentParseLong( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_CLIENT_RESPONSE_MAX_SIZE ), 100_000_000L );
-        try ( CountingInputStream contentStream = new CountingInputStream( httpEntity.getContent() ) )
-        {
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream(  );
-            JavaHelper.copyWhilePredicate( contentStream, baos, aLong -> contentStream.getByteCount() <= maxSize );
-            return ImmutableByteArray.of( baos.toByteArray() );
-        }
-    }
-
-    public List<X509Certificate> readServerCertificates()
+    static List<X509Certificate> readServerCertificates( final TrustManager[] trustManagers )
             throws PwmUnrecoverableException
     {
         final List<X509Certificate> returnList = new ArrayList<>(  );
@@ -580,4 +68,3 @@ public class PwmHttpClient implements AutoCloseable
         return Collections.unmodifiableList( returnList );
     }
 }
-

+ 31 - 0
server/src/main/java/password/pwm/svc/httpclient/PwmHttpClientProvider.java

@@ -0,0 +1,31 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed 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.
+ */
+
+package password.pwm.svc.httpclient;
+
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmUnrecoverableException;
+
+public interface PwmHttpClientProvider extends PwmHttpClient
+{
+    void init( PwmApplication pwmApplication, HttpClientService httpClientService, PwmHttpClientConfiguration pwmHttpClientConfiguration )
+            throws PwmUnrecoverableException;
+
+}

+ 18 - 2
server/src/main/java/password/pwm/svc/httpclient/PwmHttpClientRequest.java

@@ -50,12 +50,12 @@ public class PwmHttpClientRequest implements Serializable, PwmHttpClientMessage
 
     private final HttpEntityDataType dataType = HttpEntityDataType.String;
 
-    private final ImmutableByteArray binaryBody = null;
+    private final ImmutableByteArray binaryBody;
 
     @Singular
     private final Map<String, String> headers;
 
-    public String toDebugString( final PwmHttpClient pwmHttpClient, final String additionalText )
+    public String toDebugString( final ApachePwmHttpClient pwmHttpClient, final String additionalText )
     {
         final String topLine = "HTTP " + method + " request to " + url
                 + ( StringUtil.isEmpty( additionalText )
@@ -68,4 +68,20 @@ public class PwmHttpClientRequest implements Serializable, PwmHttpClientMessage
     {
         return "https".equals( URI.create( getUrl() ).getScheme() );
     }
+
+    public long size()
+    {
+        long size = 0;
+        size += method.toString().length();
+        size += url.length();
+        size += body == null ? 0 : body.length();
+        size += binaryBody == null ? 0 : binaryBody.size();
+        if ( headers != null )
+        {
+            size += headers.entrySet().stream()
+                    .map( e -> e.getValue().length() + e.getKey().length() )
+                    .reduce( 0, Integer::sum );
+        }
+        return size;
+    }
 }

+ 16 - 1
server/src/main/java/password/pwm/svc/httpclient/PwmHttpClientResponse.java

@@ -46,9 +46,24 @@ public class PwmHttpClientResponse implements Serializable, PwmHttpClientMessage
     private final String body;
     private final ImmutableByteArray binaryBody;
 
-    public String toDebugString( final PwmHttpClient pwmHttpClient )
+    public String toDebugString( final ApachePwmHttpClient pwmHttpClient )
     {
         final String topLine = "HTTP response status " + statusCode + " " + statusPhrase;
         return pwmHttpClient.entityToDebugString( topLine, this );
     }
+
+    public long size()
+    {
+        long size = 0;
+        size += statusPhrase == null ? 0 : statusPhrase.length();
+        size += body == null ? 0 : body.length();
+        size += binaryBody == null ? 0 : binaryBody.size();
+        if ( headers != null )
+        {
+            size += headers.entrySet().stream()
+                    .map( e -> e.getValue().length() + e.getKey().length() )
+                    .reduce( 0, Integer::sum );
+        }
+        return size;
+    }
 }

+ 1 - 2
server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java

@@ -54,7 +54,6 @@ import java.util.function.BooleanSupplier;
 abstract class AbstractWordlist implements Wordlist, PwmService
 {
     static final TimeDuration DEBUG_OUTPUT_FREQUENCY = TimeDuration.MINUTE;
-    private static final TimeDuration BUCKET_CHECK_LOG_WARNING_TIMEOUT = TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS );
 
     private WordlistConfiguration wordlistConfiguration;
     private WordlistBucket wordlistBucket;
@@ -201,7 +200,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
         getStatistics().getWordCheckTimeMS().update( timeDuration.asMillis() );
 
-        if ( timeDuration.isLongerThan( BUCKET_CHECK_LOG_WARNING_TIMEOUT ) )
+        if ( timeDuration.isLongerThan( this.wordlistConfiguration.getBucketCheckTimeWarningMs() ) )
         {
             getLogger().debug( () -> "wordlist search time for wordlist permutations was greater then 100ms: " + timeDuration.asCompactString() );
         }

+ 5 - 0
server/src/main/java/password/pwm/svc/wordlist/WordlistConfiguration.java

@@ -68,6 +68,8 @@ public class WordlistConfiguration implements Serializable
 
     private final TimeDuration autoImportRecheckDuration;
     private final TimeDuration importDurationGoal;
+    private final TimeDuration bucketCheckTimeWarningMs;
+
     private final int importMinTransactions;
     private final int importMaxTransactions;
     private final long importMaxChars;
@@ -128,6 +130,9 @@ public class WordlistConfiguration implements Serializable
                 .importDurationGoal( TimeDuration.of(
                         Long.parseLong( configuration.readAppProperty( AppProperty.WORDLIST_IMPORT_DURATION_GOAL_MS ) ),
                         TimeDuration.Unit.MILLISECONDS ) )
+                .bucketCheckTimeWarningMs( TimeDuration.of(
+                        Long.parseLong( configuration.readAppProperty( AppProperty.WORDLIST_BUCKET_CHECK_TIME_WARNING_MS ) ),
+                        TimeDuration.Unit.MILLISECONDS ) )
                 .importMinTransactions( Integer.parseInt( configuration.readAppProperty( AppProperty.WORDLIST_IMPORT_MIN_TRANSACTIONS ) ) )
                 .importMaxTransactions( Integer.parseInt( configuration.readAppProperty( AppProperty.WORDLIST_IMPORT_MAX_TRANSACTIONS ) ) )
                 .importMaxChars( JavaHelper.silentParseLong( configuration.readAppProperty( AppProperty.WORDLIST_IMPORT_MAX_CHARS_TRANSACTIONS ), 10_1024_1024 ) )

+ 2 - 0
server/src/main/resources/password/pwm/AppProperty.properties

@@ -133,6 +133,7 @@ http.client.requestTimeoutMs=60000
 http.client.response.maxSize=20000000
 http.client.enableHostnameVerification=true
 http.client.promiscuous.wordlist.enable=true
+http.client.implementation=password.pwm.svc.httpclient.ApachePwmHttpClient
 http.header.server=@PwmAppName@
 http.header.sendContentLanguage=true
 http.header.sendXAmb=true
@@ -357,6 +358,7 @@ wordlist.import.maxCharsTransactions=10485760
 wordlist.import.lineComments=!#comment:
 wordlist.inspector.frequencySeconds=300
 wordlist.testMode=false
+wordlist.bucket.checkTimeWarningMs=1000
 ws.restClient.pwRule.haltOnError=true
 ws.restServer.signing.form.timeoutSeconds=120
 ws.restServer.statistics.defaultHistoryDays=7