瀏覽代碼

fix configeditor UI setting testers and add async/timeout functionality

Jason Rivard 2 年之前
父節點
當前提交
ad2ecc848e

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

@@ -80,6 +80,7 @@ public enum AppProperty
     CONFIG_EDITOR_BLOCK_OLD_IE                      ( "configEditor.blockOldIE" ),
     CONFIG_EDITOR_USER_PERMISSION_MATCH_LIMIT       ( "configEditor.userPermission.matchResultsLimit" ),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ( "configEditor.idleTimeoutSeconds" ),
+    CONFIG_EDITOR_SETTING_FUNCTION_TIMEOUT_MS       ( "configEditor.settingFunction.timeoutMs" ),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ( "configGuide.idleTimeoutSeconds" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES             ( "configManager.zipDebug.maxLogBytes" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ( "configManager.zipDebug.maxLogSeconds" ),

+ 1 - 3
server/src/main/java/password/pwm/PwmApplication.java

@@ -454,7 +454,6 @@ public class PwmApplication
 
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         {
-            LOGGER.debug( () -> "error retrieving key '" + appAttribute.getKey() + "', localDB unavailable: " );
             return Optional.empty();
         }
 
@@ -569,7 +568,6 @@ public class PwmApplication
 
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         {
-            LOGGER.error( () -> "error writing key '" + appAttribute.getKey() + "', localDB unavailable: " );
             return;
         }
 
@@ -592,7 +590,7 @@ public class PwmApplication
         }
         catch ( final Exception e )
         {
-            LOGGER.error( () -> "error retrieving key '" + appAttribute.getKey() + "' installation date from localDB: " + e.getMessage() );
+            LOGGER.error( () -> "error retrieving key '" + appAttribute.getKey() + "' from localDB: " + e.getMessage() );
             try
             {
                 localDB.remove( LocalDB.DB.PWM_META, appAttribute.getKey() );

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

@@ -309,6 +309,8 @@ public enum PwmError
             5094, "Error_WordlistImportError", Collections.emptySet() ),
     ERROR_PWNOTIFY_SERVICE_ERROR(
             5095, "Error_PwNotifyServiceError", Collections.emptySet() ),
+    ERROR_TIMEOUT(
+            5096, "Error_Timeout", Collections.emptySet() ),
 
     ERROR_REMOTE_ERROR_VALUE(
             6000, "Error_RemoteErrorValue", Collections.emptySet(), ErrorFlag.Permanent ),

+ 1 - 1
server/src/main/java/password/pwm/health/LDAPHealthChecker.java

@@ -186,7 +186,7 @@ public class LDAPHealthChecker implements HealthSupplier
 
         final List<HealthRecord> returnRecords = new ArrayList<>();
 
-        if ( testUserDN == null || testUserDN.length() < 1 )
+        if ( StringUtil.isEmpty( testUserDN ) )
         {
             return returnRecords;
         }

+ 40 - 48
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -67,7 +67,6 @@ import password.pwm.http.servlet.configeditor.data.NavTreeItem;
 import password.pwm.http.servlet.configeditor.data.NavTreeSettings;
 import password.pwm.http.servlet.configeditor.data.SettingData;
 import password.pwm.http.servlet.configeditor.data.SettingDataMaker;
-import password.pwm.http.servlet.configeditor.function.SettingUIFunction;
 import password.pwm.http.servlet.configmanager.ConfigManagerServlet;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Message;
@@ -76,6 +75,7 @@ import password.pwm.ldap.LdapBrowser;
 import password.pwm.svc.email.EmailServer;
 import password.pwm.svc.email.EmailServerUtil;
 import password.pwm.svc.email.EmailService;
+import password.pwm.svc.httpclient.PwmHttpClientResponse;
 import password.pwm.svc.sms.SmsQueueService;
 import password.pwm.util.PasswordData;
 import password.pwm.util.SampleDataGenerator;
@@ -94,7 +94,6 @@ import password.pwm.ws.server.rest.bean.PublicHealthData;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import java.io.IOException;
-import java.io.Serializable;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
@@ -250,36 +249,15 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final Map<String, String> requestMap = pwmRequest.readBodyAsJsonStringMap();
         final PwmSetting pwmSetting = PwmSetting.forKey( requestMap.get( "setting" ) )
                 .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
+
         final String functionName = requestMap.get( "function" );
         final ProfileID profileID = pwmSetting.getCategory().hasProfiles() ? ProfileID.create( pwmRequest.readParameterAsString( REQ_PARAM_PROFILE ) ) : null;
         final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( pwmSetting );
         final String extraData = requestMap.get( "extraData" );
 
-        try
-        {
-            final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profileID, domainID );
-            final Class implementingClass = Class.forName( functionName );
-            final SettingUIFunction function = ( SettingUIFunction ) implementingClass.getDeclaredConstructor().newInstance();
-            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
-            final Serializable result = function.provideFunction( pwmRequest, modifier, key, extraData );
-            configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
-            final RestResultBean restResultBean = RestResultBean.forSuccessMessage( result, pwmRequest, Message.Success_Unknown );
-            pwmRequest.outputJsonResult( restResultBean );
-        }
-        catch ( final Exception e )
-        {
-            final RestResultBean restResultBean;
-            if ( e instanceof PwmException )
-            {
-                restResultBean = RestResultBean.fromError( ( ( PwmException ) e ).getErrorInformation(), pwmRequest, true );
-            }
-            else
-            {
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, "error performing user search: " + e.getMessage() );
-                restResultBean = RestResultBean.fromError( errorInformation, pwmRequest );
-            }
-            pwmRequest.outputJsonResult( restResultBean );
-        }
+        final RestResultBean<?> restResultBean = ConfigEditorServletUtils.executeSettingFunction(
+                pwmRequest, configManagerBean, pwmSetting, functionName, profileID, domainID, extraData );
+        pwmRequest.outputJsonResult( restResultBean );
 
         return ProcessStatus.Halt;
     }
@@ -581,16 +559,18 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final ProfileID profileID = ProfileID.create( pwmRequest.readParameterAsString( REQ_PARAM_PROFILE ) );
         final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( PwmSetting.LDAP_SERVER_URLS );
         final DomainConfig config = AppConfig.forStoredConfig( configManagerBean.getStoredConfiguration() ).getDomainConfigs().get( domainID );
-        final PublicHealthData healthData = LDAPHealthChecker.healthForNewConfiguration(
-                pwmRequest.getLabel(),
-                pwmRequest.getPwmDomain(),
-                config,
-                pwmRequest.getLocale(),
-                profileID,
-                true,
-                true );
 
-        final RestResultBean restResultBean = RestResultBean.withData( healthData, PublicHealthData.class );
+        final PublicHealthData healthData = ConfigEditorServletUtils.timeoutExecutor( pwmRequest, () ->
+                LDAPHealthChecker.healthForNewConfiguration(
+                        pwmRequest.getLabel(),
+                        pwmRequest.getPwmDomain(),
+                        config,
+                        pwmRequest.getLocale(),
+                        profileID,
+                        true,
+                        true ) );
+
+        final RestResultBean<PublicHealthData> restResultBean = RestResultBean.withData( healthData, PublicHealthData.class );
 
         pwmRequest.outputJsonResult( restResultBean );
         LOGGER.debug( pwmRequest, () -> "completed restLdapHealthCheck in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
@@ -629,7 +609,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( PwmSetting.LDAP_SERVER_URLS );
         final DomainConfig config = AppConfig.forStoredConfig( configManagerBean.getStoredConfiguration() ).getDomainConfigs().get( domainID );
         final StringBuilder output = new StringBuilder();
-        output.append( "beginning SMS send process:\n" );
+        output.append( "beginning SMS send process.\n" );
 
         if ( !config.getAppConfig().isSmsConfigured() )
         {
@@ -641,14 +621,20 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             final SmsItemBean testSmsItem = new SmsItemBean( testParams.get( "to" ), testParams.get( "message" ), pwmRequest.getLabel() );
             try
             {
-                final String responseBody = SmsQueueService.sendDirectMessage(
+                final PwmHttpClientResponse responseBody = SmsQueueService.sendDirectMessage(
                         pwmRequest.getPwmDomain(),
                         config,
                         pwmRequest.getLabel(),
                         testSmsItem
                 );
-                output.append( "message sent:\n" );
-                output.append( "response body: \n" ).append( StringUtil.escapeHtml( responseBody ) );
+                output.append( "message sent.\n" );
+                output.append( "response status: " ).append( responseBody.getStatusLine() ).append( "\n" );
+                if ( responseBody.getHeaders() != null )
+                {
+                    responseBody.getHeaders().forEach( ( key, value ) ->
+                            output.append( "response header: " ).append( key ).append( ": " ).append( value ).append( "\n" ) );
+                }
+                output.append( "response body: \n" ).append( StringUtil.escapeHtml( responseBody.getBody() ) );
             }
             catch ( final PwmException e )
             {
@@ -656,7 +642,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             }
         }
 
-        final RestResultBean restResultBean = RestResultBean.withData( output.toString(), String.class );
+        final RestResultBean<String> restResultBean = RestResultBean.withData( output.toString(), String.class );
         pwmRequest.outputJsonResult( restResultBean );
         LOGGER.debug( pwmRequest, () -> "completed restSmsHealthCheck in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
         return ProcessStatus.Halt;
@@ -683,7 +669,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 null );
 
         final StringBuilder output = new StringBuilder();
-        output.append( "beginning EMail send process:\n" );
+        output.append( "Beginning EMail send process.\n" );
 
         final AppConfig testDomainConfig = AppConfig.forStoredConfig( configManagerBean.getStoredConfiguration() );
 
@@ -697,21 +683,27 @@ public class ConfigEditorServlet extends ControlledPwmServlet
 
                 try
                 {
-                    EmailService.sendEmailSynchronous( emailServer.get(), testDomainConfig, testEmailItem, macroRequest, pwmRequest.getLabel() );
-                    output.append( "message delivered" );
+                    ConfigEditorServletUtils.timeoutExecutor( pwmRequest,
+                            () ->
+                            {
+                                EmailService.sendEmailSynchronous( emailServer.get(), testDomainConfig, testEmailItem, macroRequest, pwmRequest.getLabel() );
+                                return Boolean.FALSE;
+                            } );
+
+                    output.append( "Test message delivered to server.\n" );
                 }
-                catch ( final PwmException e )
+                catch ( final Throwable e )
                 {
-                    output.append( "error: " ).append( StringUtil.escapeHtml( JavaHelper.readHostileExceptionMessage( e ) ) );
+                    output.append( "error: " ).append( StringUtil.escapeHtml( JavaHelper.readHostileExceptionMessage( e ) ) ).append( "\n" );
                 }
             }
         }
         else
         {
-            output.append( "smtp service is not configured." );
+            output.append( "EMail service is not configured.\n" );
         }
 
-        final RestResultBean restResultBean = RestResultBean.withData( output.toString(), String.class );
+        final RestResultBean<String> restResultBean = RestResultBean.withData( output.toString(), String.class );
         pwmRequest.outputJsonResult( restResultBean );
         LOGGER.debug( pwmRequest, () -> "completed restEmailHealthCheck in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
         return ProcessStatus.Halt;

+ 55 - 0
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java

@@ -46,9 +46,11 @@ import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestUtil;
 import password.pwm.http.bean.ConfigManagerBean;
+import password.pwm.http.servlet.configeditor.function.SettingUIFunction;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.HttpsServerCertificateManager;
@@ -57,6 +59,7 @@ import password.pwm.ws.server.RestResultBean;
 import javax.servlet.ServletException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.Serializable;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -70,6 +73,7 @@ import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.TreeMap;
+import java.util.concurrent.Callable;
 
 public class ConfigEditorServletUtils
 {
@@ -349,4 +353,55 @@ public class ConfigEditorServletUtils
         final String profileID = setting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( ConfigEditorServlet.REQ_PARAM_PROFILE ) : null;
         return StoredConfigKey.forSetting( setting, profileID == null ? null : ProfileID.create( profileID ), domainID );
     }
+
+    static RestResultBean<?> executeSettingFunction(
+            final PwmRequest pwmRequest,
+            final ConfigManagerBean configManagerBean,
+            final PwmSetting pwmSetting,
+            final String functionName,
+            final ProfileID profileID,
+            final DomainID domainID,
+            final String extraData
+    )
+    {
+        try
+        {
+            final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profileID, domainID );
+            final Class<?> implementingClass = Class.forName( functionName );
+            final SettingUIFunction function = ( SettingUIFunction ) implementingClass.getDeclaredConstructor().newInstance();
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
+
+            final Serializable result = timeoutExecutor( pwmRequest,
+                    () -> function.provideFunction( pwmRequest, modifier, key, extraData ) );
+
+            configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
+            return RestResultBean.forSuccessMessage( result, pwmRequest, Message.Success_Unknown );
+        }
+        catch ( final Exception e )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, "error running operation: " + e.getMessage() );
+            return RestResultBean.fromError( errorInformation, pwmRequest );
+        }
+    }
+
+    static <T> T timeoutExecutor( final PwmRequest pwmRequest, final Callable<T> callable )
+            throws PwmUnrecoverableException
+    {
+        final ConfigEditorSettings configEditorSettings = ConfigEditorSettings.fromAppConfig( pwmRequest.getAppConfig() );
+
+        try
+        {
+            return PwmScheduler.timeoutExecutor( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), configEditorSettings.getMaxWaitSettingsFunction(), callable );
+        }
+        catch ( final PwmUnrecoverableException e )
+        {
+            throw e;
+        }
+        catch ( final Throwable t )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, "error running operation: : " + t.getMessage() );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+    }
+
 }

+ 42 - 0
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorSettings.java

@@ -0,0 +1,42 @@
+/*
+ * 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.http.servlet.configeditor;
+
+import lombok.Value;
+import password.pwm.AppProperty;
+import password.pwm.config.AppConfig;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.TimeDuration;
+
+@Value
+public class ConfigEditorSettings
+{
+    private TimeDuration maxWaitSettingsFunction;
+
+    static ConfigEditorSettings fromAppConfig( final AppConfig appConfig )
+    {
+        final TimeDuration waitTime = TimeDuration.of( JavaHelper.silentParseLong(
+                        appConfig.readAppProperty( AppProperty.CONFIG_EDITOR_SETTING_FUNCTION_TIMEOUT_MS ), 30_000 ),
+                TimeDuration.Unit.MILLISECONDS );
+
+        return new ConfigEditorSettings( waitTime );
+    }
+}

+ 1 - 2
server/src/main/java/password/pwm/http/servlet/configeditor/function/UserMatchViewerFunction.java

@@ -81,7 +81,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
 
         final Instant startSearchTime = Instant.now();
         final int maxResultSize = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.CONFIG_EDITOR_USER_PERMISSION_MATCH_LIMIT ) );
-        final Collection<UserIdentity> users = discoverMatchingUsers(
+        final List<UserIdentity> users = discoverMatchingUsers(
                 pwmRequest.getLabel(),
                 pwmDomain,
                 maxResultSize,
@@ -126,7 +126,6 @@ public class UserMatchViewerFunction implements SettingUIFunction
         final List<UserIdentity> sortedResults = new ArrayList<>( CollectionUtil.iteratorToList( matches ) );
         Collections.sort( sortedResults );
         return Collections.unmodifiableList ( sortedResults );
-
     }
 
     private static void validateUserPermissionLdapValues(

+ 11 - 0
server/src/main/java/password/pwm/svc/AbstractPwmService.java

@@ -24,7 +24,9 @@ import password.pwm.PwmApplication;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthRecord;
 import password.pwm.util.PwmScheduler;
@@ -127,6 +129,15 @@ public abstract class AbstractPwmService implements PwmService
         return startupError;
     }
 
+    protected void checkOpenStatus()
+            throws PwmUnrecoverableException
+    {
+        if ( this.status() != STATUS.OPEN )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, name() + " service is not open" );
+        }
+    }
+
     public final List<HealthRecord> healthCheck( )
     {
         final List<HealthRecord> returnRecords = new ArrayList<>(  );

+ 13 - 1
server/src/main/java/password/pwm/svc/cr/CrService.java

@@ -61,13 +61,14 @@ import password.pwm.svc.AbstractPwmService;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.wordlist.WordlistService;
 import password.pwm.util.java.CollectionUtil;
-import password.pwm.util.json.JsonFactory;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
+import password.pwm.util.json.JsonFactory;
 import password.pwm.util.logging.PwmLogger;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -130,6 +131,8 @@ public class CrService extends AbstractPwmService implements PwmService
     )
             throws PwmUnrecoverableException
     {
+        checkOpenStatus();
+
         final DomainConfig config = pwmDomain.getConfig();
         final long methodStartTime = System.currentTimeMillis();
 
@@ -298,6 +301,8 @@ public class CrService extends AbstractPwmService implements PwmService
     )
             throws PwmDataValidationException, PwmUnrecoverableException
     {
+        checkOpenStatus();
+
         //strip null keys from responseMap;
         responseMap.keySet().removeIf( Objects::isNull );
 
@@ -427,6 +432,8 @@ public class CrService extends AbstractPwmService implements PwmService
     )
             throws ChaiUnavailableException, PwmUnrecoverableException
     {
+        checkOpenStatus();
+
         final DomainConfig config = pwmDomain.getConfig();
 
         LOGGER.trace( sessionLabel, () -> "beginning read of user response sequence" );
@@ -723,4 +730,9 @@ public class CrService extends AbstractPwmService implements PwmService
 
         return ServiceInfoBean.builder().storageMethods( usedStorageMethods ).build();
     }
+
+    protected Set<PwmApplication.Condition> openConditions()
+    {
+        return EnumSet.noneOf( PwmApplication.Condition.class );
+    }
 }

+ 7 - 7
server/src/main/java/password/pwm/svc/sms/SmsQueueService.java

@@ -487,7 +487,7 @@ public class SmsQueueService extends AbstractPwmService implements PwmService
         private final PwmApplication pwmApplication;
         private final AppConfig config;
 
-        private String lastResponseBody;
+        private PwmHttpClientResponse lastResponse;
 
         private SmsSendEngine(
                 final PwmApplication pwmApplication,
@@ -501,7 +501,7 @@ public class SmsQueueService extends AbstractPwmService implements PwmService
         protected void sendSms( final String to, final String message, final SessionLabel sessionLabel )
                 throws PwmUnrecoverableException, PwmOperationalException
         {
-            lastResponseBody = null;
+            lastResponse = null;
 
             final String requestData = makeRequestData( to, message, sessionLabel );
 
@@ -514,9 +514,9 @@ public class SmsQueueService extends AbstractPwmService implements PwmService
                 final PwmHttpClient pwmHttpClient = makePwmHttpClient( sessionLabel );
                 final PwmHttpClientResponse pwmHttpClientResponse = pwmHttpClient.makeRequest( pwmHttpClientRequest );
                 final int resultCode = pwmHttpClientResponse.getStatusCode();
+                lastResponse = pwmHttpClientResponse;
 
                 final String responseBody = pwmHttpClientResponse.getBody();
-                lastResponseBody = responseBody;
 
                 determineIfResultSuccessful( config, resultCode, responseBody );
                 LOGGER.debug( sessionLabel, () -> "SMS send successful, HTTP status: " + resultCode );
@@ -675,13 +675,13 @@ public class SmsQueueService extends AbstractPwmService implements PwmService
                     .build();
         }
 
-        public String getLastResponseBody( )
+        private PwmHttpClientResponse getLastResponse( )
         {
-            return lastResponseBody;
+            return lastResponse;
         }
     }
 
-    public static String sendDirectMessage(
+    public static PwmHttpClientResponse sendDirectMessage(
             final PwmDomain pwmDomain,
             final DomainConfig domainConfig,
             final SessionLabel sessionLabel,
@@ -692,7 +692,7 @@ public class SmsQueueService extends AbstractPwmService implements PwmService
     {
         final SmsSendEngine smsSendEngine = new SmsSendEngine( pwmDomain.getPwmApplication(), domainConfig.getAppConfig() );
         smsSendEngine.sendSms( smsItemBean.getTo(), smsItemBean.getMessage(), sessionLabel );
-        return smsSendEngine.getLastResponseBody();
+        return smsSendEngine.getLastResponse();
     }
 
     public int queueSize( )

+ 52 - 0
server/src/main/java/password/pwm/util/PwmScheduler.java

@@ -24,6 +24,7 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.error.PwmError;
+import password.pwm.error.PwmInternalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.StringUtil;
@@ -50,6 +51,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 public class PwmScheduler
@@ -339,4 +341,54 @@ public class PwmScheduler
         nextZuluMidnight.add( Calendar.HOUR, 24 );
         return nextZuluMidnight.toInstant();
     }
+
+    /**
+     * Execute a task within the time period specified by {@code maxWaitDuration}.  If the task exceeds the time allotted, it is
+     * cancelled and the results are discarded.  The calling thread will block until the callable returns
+     * a result or the {@code maxWaitDuration} is reached, whichever occurs first.
+     * @param pwmApplication application to use for thread naming and other housekeeping.
+     *
+     * @param label thread labels.
+     * @param maxWaitDuration maximum time to wait for result.
+     * @param callable task to execute.
+     * @param <T> return value of the callable.
+     * @return The {@code callable}'s return value.
+     * @throws PwmUnrecoverableException if the task times out.  Uses {@link PwmError#ERROR_TIMEOUT}.
+     * @throws Throwable any throwable generated by the {@code callable}
+     */
+    public static <T> T timeoutExecutor(
+            final PwmApplication pwmApplication,
+            final SessionLabel label,
+            final TimeDuration maxWaitDuration,
+            final Callable<T> callable
+    )
+            throws PwmUnrecoverableException, Throwable
+    {
+
+        final ThreadPoolExecutor executor = PwmScheduler.makeMultiThreadExecutor(
+                1, pwmApplication, label, callable.getClass() );
+
+        try
+        {
+            final Future<T> future = executor.submit( callable );
+
+            return future.get( maxWaitDuration.asMillis(), TimeUnit.MILLISECONDS );
+        }
+        catch ( final ExecutionException e )
+        {
+            throw e.getCause();
+        }
+        catch ( final TimeoutException e )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TIMEOUT, "operation timed out: " + e.getMessage() );
+        }
+        catch ( final Exception e )
+        {
+            throw PwmInternalException.fromPwmException( e );
+        }
+        finally
+        {
+            executor.shutdownNow();
+        }
+    }
 }

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

@@ -75,6 +75,7 @@ config.enableJbCryptPwLibrary=true
 configEditor.blockOldIE=true
 configEditor.userPermission.matchResultsLimit=5000
 configEditor.idleTimeoutSeconds=900
+configEditor.settingFunction.timeoutMs=5000
 configGuide.idleTimeoutSeconds=3600
 configManager.zipDebug.maxLogBytes=50000000
 configManager.zipDebug.maxLogSeconds=120

+ 1 - 0
server/src/main/resources/password/pwm/i18n/Error.properties

@@ -166,6 +166,7 @@ Error_RemoteErrorValue=Remote Error: %1%
 Error_WordlistImportError=An error occurred importing the wordlist: %1%
 Error_PwNotifyServiceError=An error occurred while running the password notify service: %1%
 Error_HttpError=An error occurred while connecting to remote HTTP service.
+Error_Timeout=The requested operation has timed out
 
 Error_ConfigUploadSuccess=File uploaded successfully
 Error_ConfigUploadFailure=File failed to upload.

+ 27 - 33
webapp/src/main/webapp/public/resources/js/configeditor.js

@@ -868,50 +868,44 @@ PWM_CFGEDIT.httpsCertificateView = function() {
 };
 
 PWM_CFGEDIT.smsHealthCheck = function() {
-    let dialogBody = '<p>' + PWM_CONFIG.showString('Warning_SmsTestData') + '</p><form id="smsCheckParametersForm"><table>';
-    dialogBody += '<tr><td>To</td><td><input name="to" type="text" value="555-1212"/></td></tr>';
-    dialogBody += '<tr><td>Message</td><td><input name="message" type="text" value="Test Message"/></td></tr>';
-    dialogBody += '</table></form>';
-    PWM_MAIN.showDialog({text:dialogBody,showCancel:true,title:'Test SMS connection',closeOnOk:false,okAction:function(){
-            const formElement = PWM_MAIN.getObject("smsCheckParametersForm");
-            const formData = PWM_MAIN.JSLibrary.formToValueMap(formElement);
-            const url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction', 'smsHealthCheck');
-            PWM_MAIN.showWaitDialog({loadFunction:function(){
-                    const loadFunction = function(data) {
-                        if (data['error']) {
-                            PWM_MAIN.showErrorDialog(data);
-                        } else {
-                            const bodyText = PWM_ADMIN.makeHealthHtml(data['data'],false,false);
-                            const titleText = 'SMS Send Message Status';
-                            PWM_MAIN.showDialog({text:bodyText,title:titleText,showCancel:true});
-                        }
+    const title = 'Test SMS Settings'
 
-                    };
-                    PWM_MAIN.ajaxRequest(url,loadFunction,{content:formData});
-                }});
-        }});
+    const dialogFormRows = '<p>' + PWM_CONFIG.showString('Warning_SmsTestData') +'</p>'
+    + '<tr><td>To</td><td><input name="to" type="text" value="555-1212"/></td></tr>'
+     + '<tr><td>Message</td><td><input name="message" type="text" value="Test Message"/></td></tr>';
+
+    const actionParam = 'smsHealthCheck';
+
+    PWM_CFGEDIT.healthCheckImpl(dialogFormRows,title,actionParam);
 };
 
 PWM_CFGEDIT.emailHealthCheck = function() {
-    let dialogBody = '<p>' + PWM_CONFIG.showString('Warning_EmailTestData') + '</p><form id="emailCheckParametersForm"><table>';
-    dialogBody += '<tr><td>To</td><td><input name="to" type="text" value="test@example.com"/></td></tr>';
-    dialogBody += '<tr><td>From</td><td><input name="from" type="text" value="@DefaultEmailFromAddress@"/></td></tr>';
-    dialogBody += '<tr><td>Subject</td><td><input name="subject" type="text" value="Test Email"/></td></tr>';
-    dialogBody += '<tr><td>Body</td><td><input name="body" type="text" value="Test Email""/></td></tr>';
-    dialogBody += '</table></form>';
-    PWM_MAIN.showDialog({text:dialogBody,showCancel:true,title:'Test Email Connection',closeOnOk:false,okAction:function(){
-            const formElement = PWM_MAIN.getObject("emailCheckParametersForm");
+    const title =  PWM_CONFIG.showString('Warning_EmailTestData');
+
+    const dialogFormRows = '<tr><td>To</td><td><input name="to" type="text" value="test@example.com"/></td></tr>'
+     + '<tr><td>From</td><td><input name="from" type="text" value="@DefaultEmailFromAddress@"/></td></tr>'
+     + '<tr><td>Subject</td><td><input name="subject" type="text" value="Test Email"/></td></tr>'
+     + '<tr><td>Body</td><td><input name="body" type="text" value="Test Email""/></td></tr>';
+
+    const actionParam = 'emailHealthCheck';
+
+    PWM_CFGEDIT.healthCheckImpl(dialogFormRows,title,actionParam);
+};
+
+PWM_CFGEDIT.healthCheckImpl = function(dialogFormRows, title, actionParam) {
+    const formBody = '<form id="parametersForm"><table>' + dialogFormRows + '</table></form>';
+    PWM_MAIN.showDialog({text:formBody,showCancel:true,title:title,closeOnOk:false,okAction:function(){
+            const formElement = PWM_MAIN.getObject("parametersForm");
             const formData = PWM_MAIN.JSLibrary.formToValueMap(formElement);
-            let url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction', 'emailHealthCheck');
+            let url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction', actionParam);
             url = PWM_MAIN.addParamToUrl(url,'profile',PWM_CFGEDIT.readCurrentProfile());
             PWM_MAIN.showWaitDialog({loadFunction:function(){
                     const loadFunction = function(data) {
                         if (data['error']) {
                             PWM_MAIN.showErrorDialog(data);
                         } else {
-                            const bodyText = PWM_ADMIN.makeHealthHtml(data['data'],false,false);
-                            const titleText = 'Email Send Message Status';
-                            PWM_MAIN.showDialog({text:bodyText,title:titleText,showCancel:true});
+                            const bodyText = '<div class="logViewer">' + data['data'] + '</div>';
+                            PWM_MAIN.showDialog({text:bodyText,title:title,showCancel:true,dialogClass:'wide'});
                         }
                     };
                     PWM_MAIN.ajaxRequest(url,loadFunction,{content:formData});