ソースを参照

Merge branch 'master' into patch-11

atuc2 6 年 前
コミット
e335554f19
98 ファイル変更1562 行追加1081 行削除
  1. 1 0
      build/checkstyle-import.xml
  2. 9 0
      client/pom.xml
  3. 1 1
      data-service/pom.xml
  4. 2 5
      data-service/src/main/java/password/pwm/receiver/SummaryBean.java
  5. 1 1
      docker/pom.xml
  6. 1 1
      onejar/pom.xml
  7. 2 0
      onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java
  8. 2 2
      pom.xml
  9. 1 1
      server/pom.xml
  10. 3 1
      server/src/main/java/password/pwm/AppProperty.java
  11. 5 0
      server/src/main/java/password/pwm/bean/LocalSessionStateBean.java
  12. 1 1
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  13. 5 5
      server/src/main/java/password/pwm/config/value/FileValue.java
  14. 1 1
      server/src/main/java/password/pwm/health/HealthMessage.java
  15. 104 20
      server/src/main/java/password/pwm/health/HealthMonitor.java
  16. 35 40
      server/src/main/java/password/pwm/http/ContextManager.java
  17. 25 1
      server/src/main/java/password/pwm/http/HttpEventManager.java
  18. 1 1
      server/src/main/java/password/pwm/http/PwmRequest.java
  19. 1 1
      server/src/main/java/password/pwm/http/auth/CASFilterAuthenticationProvider.java
  20. 7 2
      server/src/main/java/password/pwm/http/bean/ImmutableByteArray.java
  21. 1 0
      server/src/main/java/password/pwm/http/bean/SetupResponsesBean.java
  22. 4 0
      server/src/main/java/password/pwm/http/filter/SessionFilter.java
  23. 9 4
      server/src/main/java/password/pwm/http/servlet/SetupResponsesServlet.java
  24. 2 2
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  25. 1 1
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  26. 2 1
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java
  27. 4 23
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java
  28. 2 2
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerWordlistServlet.java
  29. 206 138
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  30. 1 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  31. 1 1
      server/src/main/java/password/pwm/http/servlet/oauth/OAuthState.java
  32. 1 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java
  33. 1 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  34. 2 2
      server/src/main/java/password/pwm/http/servlet/resource/CacheEntry.java
  35. 2 2
      server/src/main/java/password/pwm/http/servlet/resource/MemoryFileResource.java
  36. 1 1
      server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java
  37. 2 2
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletConfiguration.java
  38. 64 52
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java
  39. 28 8
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java
  40. 7 2
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  41. 3 2
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  42. 28 9
      server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  43. 3 3
      server/src/main/java/password/pwm/svc/node/LDAPNodeDataService.java
  44. 2 2
      server/src/main/java/password/pwm/svc/node/NodeService.java
  45. 5 0
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java
  46. 1 1
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java
  47. 3 3
      server/src/main/java/password/pwm/svc/report/ReportService.java
  48. 4 0
      server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java
  49. 74 0
      server/src/main/java/password/pwm/svc/stats/AvgStatistic.java
  50. 79 0
      server/src/main/java/password/pwm/svc/stats/DailyKey.java
  51. 49 0
      server/src/main/java/password/pwm/svc/stats/EpsKey.java
  52. 10 21
      server/src/main/java/password/pwm/svc/stats/EpsStatistic.java
  53. 27 0
      server/src/main/java/password/pwm/svc/stats/StatKey.java
  54. 80 117
      server/src/main/java/password/pwm/svc/stats/Statistic.java
  55. 30 0
      server/src/main/java/password/pwm/svc/stats/StatisticType.java
  56. 63 106
      server/src/main/java/password/pwm/svc/stats/StatisticsBundle.java
  57. 41 177
      server/src/main/java/password/pwm/svc/stats/StatisticsManager.java
  58. 1 1
      server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java
  59. 0 2
      server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java
  60. 2 2
      server/src/main/java/password/pwm/svc/wordlist/WordlistSource.java
  61. 2 2
      server/src/main/java/password/pwm/svc/wordlist/WordlistZipReader.java
  62. 7 0
      server/src/main/java/password/pwm/util/CaptchaUtility.java
  63. 1 2
      server/src/main/java/password/pwm/util/EventRateMeter.java
  64. 13 10
      server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java
  65. 1 1
      server/src/main/java/password/pwm/util/PwmScheduler.java
  66. 4 2
      server/src/main/java/password/pwm/util/RandomPasswordGenerator.java
  67. 1 1
      server/src/main/java/password/pwm/util/db/DBConfiguration.java
  68. 6 0
      server/src/main/java/password/pwm/util/java/AtomicLoopIntIncrementer.java
  69. 60 0
      server/src/main/java/password/pwm/util/java/AtomicLoopLongIncrementer.java
  70. 43 0
      server/src/main/java/password/pwm/util/java/DebugOutputBuilder.java
  71. 109 82
      server/src/main/java/password/pwm/util/java/FileSystemUtility.java
  72. 12 0
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  73. 1 2
      server/src/main/java/password/pwm/util/localdb/LocalDBFactory.java
  74. 132 103
      server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java
  75. 3 3
      server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java
  76. 4 2
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  77. 1 1
      server/src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java
  78. 12 25
      server/src/main/java/password/pwm/util/secure/ChecksumInputStream.java
  79. 10 23
      server/src/main/java/password/pwm/util/secure/ChecksumOutputStream.java
  80. 1 1
      server/src/main/java/password/pwm/util/secure/X509Utils.java
  81. 1 1
      server/src/main/java/password/pwm/ws/server/RestServlet.java
  82. 19 9
      server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java
  83. 4 2
      server/src/main/resources/password/pwm/AppProperty.properties
  84. 3 0
      server/src/main/resources/password/pwm/i18n/Admin.properties
  85. 1 0
      server/src/main/resources/password/pwm/i18n/Display.properties
  86. 1 1
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  87. 18 7
      server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java
  88. 1 1
      server/src/test/java/password/pwm/util/localdb/LocalDBLoggerExtendedTest.java
  89. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/activateuser.jsp
  90. 22 9
      webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp
  91. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-search.jsp
  92. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/forgottenusername-search.jsp
  93. 1 1
      webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp
  94. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/login.jsp
  95. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/newuser.jsp
  96. 10 7
      webapp/src/main/webapp/public/resources/js/main.js
  97. 1 1
      webapp/src/main/webapp/public/resources/js/responses.js
  98. 3 3
      webapp/src/main/webapp/public/resources/js/uilibrary.js

+ 1 - 0
build/checkstyle-import.xml

@@ -38,6 +38,7 @@
     <allow pkg="java.security"/>
 
     <allow pkg="org.apache.catalina"/>
+    <allow pkg="org.apache.coyote"/>
 
     <!-- chai -->
     <allow pkg="com.novell.ldapchai"/>

+ 9 - 0
client/pom.xml

@@ -18,6 +18,15 @@
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
     </properties>
 
+    <profiles>
+        <profile>
+            <id>skip-frontend</id>
+            <properties>
+                <skip.npm>true</skip.npm>
+            </properties>
+        </profile>
+    </profiles>
+
     <build>
         <plugins>
             <plugin>

+ 1 - 1
data-service/pom.xml

@@ -140,7 +140,7 @@
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.8.1</version>
+            <version>3.9</version>
         </dependency>
         <dependency>
             <groupId>com.sun.mail</groupId>

+ 2 - 5
data-service/src/main/java/password/pwm/receiver/SummaryBean.java

@@ -125,11 +125,8 @@ public class SummaryBean
                     final Statistic statistic = Statistic.forKey( statKey );
                     if ( statistic != null )
                     {
-                        if ( statistic.getType() == Statistic.Type.INCREMENTER )
-                        {
-                            final int count = Integer.parseInt( bean.getStatistics().get( statKey ) );
-                            incrementCounterMap( statCount, statistic.getLabel( null ), count );
-                        }
+                        final int count = Integer.parseInt( bean.getStatistics().get( statKey ) );
+                        incrementCounterMap( statCount, statistic.getLabel( null ), count );
                     }
                 }
             }

+ 1 - 1
docker/pom.xml

@@ -34,7 +34,7 @@
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>1.0.2</version>
+                <version>1.1.0</version>
                 <executions>
                     <execution>
                         <id>make-docker-image</id>

+ 1 - 1
onejar/pom.xml

@@ -17,7 +17,7 @@
 
     <properties>
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
-        <tomcat.version>9.0.16</tomcat.version>
+        <tomcat.version>9.0.19</tomcat.version>
     </properties>
 
     <build>

+ 2 - 0
onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java

@@ -25,6 +25,7 @@ package password.pwm.onejar;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.util.ServerInfo;
+import org.apache.coyote.http2.Http2Protocol;
 
 import javax.servlet.ServletException;
 import java.io.BufferedReader;
@@ -159,6 +160,7 @@ public class TomcatOnejarRunner
         }
         connector.setSecure( true );
         connector.setScheme( "https" );
+        connector.addUpgradeProtocol( new Http2Protocol() );
         connector.setAttribute( "SSLEnabled", "true" );
         connector.setAttribute( "keystoreFile", onejarConfig.getKeystoreFile().getAbsolutePath() );
         connector.setAttribute( "keystorePass", onejarConfig.getKeystorePass() );

+ 2 - 2
pom.xml

@@ -247,7 +247,7 @@
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>3.1.12</version>
+                        <version>4.0.0-beta1</version>
                     </dependency>
                 </dependencies>
                 <configuration>
@@ -291,7 +291,7 @@
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>3.1.12</version>
+            <version>4.0.0-beta1</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>

+ 1 - 1
server/pom.xml

@@ -249,7 +249,7 @@
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.8.1</version>
+            <version>3.9</version>
         </dependency>
         <dependency>
             <groupId>commons-validator</groupId>

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

@@ -78,7 +78,7 @@ public enum AppProperty
     CONFIG_EDITOR_QUERY_FILTER_TEST_LIMIT           ( "configEditor.queryFilter.testLimit" ),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ( "configEditor.idleTimeoutSeconds" ),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ( "configGuide.idleTimeoutSeconds" ),
-    CONFIG_MANAGER_ZIPDEBUG_MAXLOGLINES             ( "configManager.zipDebug.maxLogLines" ),
+    CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES             ( "configManager.zipDebug.maxLogBytes" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ( "configManager.zipDebug.maxLogSeconds" ),
     CLUSTER_DB_ENABLE                               ( "cluster.db.enable" ),
     CLUSTER_DB_HEARTBEAT_SECONDS                    ( "cluster.db.heartbeatSeconds" ),
@@ -194,6 +194,8 @@ public enum AppProperty
     HEALTHCHECK_MIN_CHECK_INTERVAL                  ( "healthCheck.minimumCheckIntervalSeconds" ),
     HEALTHCHECK_MAX_RECORD_AGE                      ( "healthCheck.maximumRecordAgeSeconds" ),
     HEALTHCHECK_MAX_FORCE_WAIT                      ( "healthCheck.maximumForceCheckWaitSeconds" ),
+    HEALTH_SUPPORT_BUNDLE_WRITE_INTERVAL_SECONDS    ( "health.supportBundle.file.writeIntervalSeconds" ),
+    HEALTH_SUPPORT_BUNDLE_FILE_WRITE_COUNT          ( "health.supportBundle.file.writeRetentionCount" ),
     HEALTH_CERTIFICATE_WARN_SECONDS                 ( "health.certificate.warnSeconds" ),
     HEALTH_LDAP_CAUTION_DURATION_MS                 ( "health.ldap.cautionDurationMS" ),
     HEALTH_LDAP_PROXY_WARN_PW_EXPIRE_SECONDS        ( "health.ldap.proxy.pwExpireWarnSeconds" ),

+ 5 - 0
server/src/main/java/password/pwm/bean/LocalSessionStateBean.java

@@ -24,6 +24,8 @@ package password.pwm.bean;
 
 import lombok.Data;
 import password.pwm.ldap.UserInfoBean;
+import password.pwm.util.EventRateMeter;
+import password.pwm.util.java.TimeDuration;
 
 import java.io.Serializable;
 import java.time.Instant;
@@ -64,8 +66,11 @@ public class LocalSessionStateBean implements Serializable
 
     private boolean passwordModified;
     private boolean privateUrlAccessed;
+    private boolean captchaBypassedViaParameter;
 
     private final AtomicInteger intruderAttempts = new AtomicInteger( 0 );
+    private final AtomicInteger requestCount = new AtomicInteger( 0 );
+    private final EventRateMeter.MovingAverage avgRequestDuration = new EventRateMeter.MovingAverage( TimeDuration.DAY );
     private boolean oauthInProgress;
 
     private boolean sessionIdRecycleNeeded;

+ 1 - 1
server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -44,8 +44,8 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.PasswordData;
+import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;

+ 5 - 5
server/src/main/java/password/pwm/config/value/FileValue.java

@@ -89,30 +89,30 @@ public class FileValue extends AbstractValue implements StoredValue
                 throws IOException
         {
             final byte[] convertedBytes = StringUtil.base64Decode( input );
-            return new FileContent( new ImmutableByteArray( convertedBytes ) );
+            return new FileContent( ImmutableByteArray.of( convertedBytes ) );
         }
 
         public String toEncodedString( )
                 throws IOException
         {
-            return StringUtil.base64Encode( contents.getBytes(), StringUtil.Base64Options.GZIP );
+            return StringUtil.base64Encode( contents.copyOf(), StringUtil.Base64Options.GZIP );
         }
 
         public String md5sum( )
                 throws PwmUnrecoverableException
         {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.getBytes() ), PwmHashAlgorithm.MD5 );
+            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.MD5 );
         }
 
         public String sha1sum( )
                 throws PwmUnrecoverableException
         {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.getBytes() ), PwmHashAlgorithm.SHA1 );
+            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.SHA1 );
         }
 
         public int size( )
         {
-            return contents.getBytes().length;
+            return contents.copyOf().length;
         }
     }
 

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

@@ -44,7 +44,7 @@ public enum HealthMessage
     LDAP_TestUserReadPwError( HealthStatus.WARN, HealthTopic.LDAP ),
     LDAP_TestUserOK( HealthStatus.GOOD, HealthTopic.LDAP ),
     Email_SendFailure( HealthStatus.WARN, HealthTopic.Email ),
-    PwNotify_Failure( HealthStatus.WARN, HealthTopic.Email ),
+    PwNotify_Failure( HealthStatus.WARN, HealthTopic.Application ),
     MissingResource( HealthStatus.DEBUG, HealthTopic.Integrity ),
     BrokenMethod( HealthStatus.DEBUG, HealthTopic.Integrity ),
     Appliance_PendingUpdates( HealthStatus.CAUTION, HealthTopic.Appliance ),

+ 104 - 20
server/src/main/java/password/pwm/health/HealthMonitor.java

@@ -25,13 +25,22 @@ package password.pwm.health;
 import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
+import password.pwm.bean.SessionLabel;
 import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.servlet.configmanager.DebugItemGenerator;
 import password.pwm.svc.PwmService;
 import password.pwm.util.PwmScheduler;
+import password.pwm.util.java.FileSystemUtility;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.Serializable;
+import java.nio.file.Files;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -44,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.ZipOutputStream;
 
 public class HealthMonitor implements PwmService
 {
@@ -64,6 +74,7 @@ public class HealthMonitor implements PwmService
     }
 
     private ExecutorService executorService;
+    private ExecutorService supportZipWriterService;
     private HealthMonitorSettings settings;
 
     private Map<HealthMonitorFlag, Serializable> healthProperties = new ConcurrentHashMap<>();
@@ -82,6 +93,26 @@ public class HealthMonitor implements PwmService
     {
     }
 
+    public void init( final PwmApplication pwmApplication ) throws PwmException
+    {
+        status = STATUS.OPENING;
+        this.pwmApplication = pwmApplication;
+        settings = HealthMonitorSettings.fromConfiguration( pwmApplication.getConfig() );
+
+        if ( !Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTHCHECK_ENABLED ) ) )
+        {
+            LOGGER.debug( () -> "health monitor will remain inactive due to AppProperty " + AppProperty.HEALTHCHECK_ENABLED.getKey() );
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        supportZipWriterService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        scheduleNextZipOutput();
+
+        status = STATUS.OPEN;
+    }
+
     public Instant getLastHealthCheckTime( )
     {
         if ( status != STATUS.OPEN )
@@ -122,24 +153,6 @@ public class HealthMonitor implements PwmService
         return status;
     }
 
-    public void init( final PwmApplication pwmApplication ) throws PwmException
-    {
-        status = STATUS.OPENING;
-        this.pwmApplication = pwmApplication;
-        settings = HealthMonitorSettings.fromConfiguration( pwmApplication.getConfig() );
-
-        if ( !Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTHCHECK_ENABLED ) ) )
-        {
-            LOGGER.debug( () -> "health monitor will remain inactive due to AppProperty " + AppProperty.HEALTHCHECK_ENABLED.getKey() );
-            status = STATUS.CLOSED;
-            return;
-        }
-
-        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
-
-        status = STATUS.OPEN;
-    }
-
     public Set<HealthRecord> getHealthRecords( )
     {
         if ( status != STATUS.OPEN )
@@ -151,12 +164,12 @@ public class HealthMonitor implements PwmService
         {
             final Instant startTime = Instant.now();
             LOGGER.trace( () ->  "begin force immediate check" );
-            final Future future = pwmApplication.getPwmScheduler().scheduleFutureJob( new ImmediateJob(), executorService, TimeDuration.ZERO );
+            final Future future = pwmApplication.getPwmScheduler().scheduleJob( new ImmediateJob(), executorService, TimeDuration.ZERO );
             settings.getMaximumForceCheckWait().pause( future::isDone );
             LOGGER.trace( () ->  "exit force immediate check, done=" + future.isDone() + ", " + TimeDuration.compactFromCurrent( startTime ) );
         }
 
-        pwmApplication.getPwmScheduler().scheduleFutureJob( new UpdateJob(), executorService, settings.getNominalCheckInterval() );
+        pwmApplication.getPwmScheduler().scheduleJob( new UpdateJob(), executorService, settings.getNominalCheckInterval() );
 
         {
             final HealthData localHealthData = this.healthData;
@@ -175,6 +188,10 @@ public class HealthMonitor implements PwmService
         {
             executorService.shutdown();
         }
+        if ( supportZipWriterService != null )
+        {
+            supportZipWriterService.shutdown();
+        }
         healthData = emptyHealthData();
         status = STATUS.CLOSED;
     }
@@ -299,4 +316,71 @@ public class HealthMonitor implements PwmService
             return TimeDuration.fromCurrent( this.getTimeStamp() ).isLongerThan( settings.getMaximumRecordAge() );
         }
     }
+
+    private void scheduleNextZipOutput()
+    {
+        final int intervalSeconds = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_WRITE_INTERVAL_SECONDS ), 0 );
+        if ( intervalSeconds > 0 )
+        {
+            final TimeDuration intervalDuration = TimeDuration.of( intervalSeconds, TimeDuration.Unit.SECONDS );
+            pwmApplication.getPwmScheduler().scheduleJob( new SupportZipFileWriter( pwmApplication ), supportZipWriterService, intervalDuration );
+        }
+    }
+
+    private class SupportZipFileWriter implements Runnable
+    {
+        private final PwmApplication pwmApplication;
+
+        SupportZipFileWriter( final PwmApplication pwmApplication )
+        {
+            this.pwmApplication = pwmApplication;
+        }
+
+        @Override
+        public void run()
+        {
+            try
+            {
+                writeSupportZipToAppPath();
+            }
+            catch ( Exception e )
+            {
+                LOGGER.debug( SessionLabel.HEALTH_SESSION_LABEL, () -> "error writing support zip to file system: " + e.getMessage() );
+            }
+
+            scheduleNextZipOutput();
+        }
+
+        private void writeSupportZipToAppPath()
+                throws IOException, PwmUnrecoverableException
+        {
+            final File appPath = pwmApplication.getPwmEnvironment().getApplicationPath();
+            if ( !appPath.exists() )
+            {
+                return;
+            }
+
+            final int rotationCount = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_FILE_WRITE_COUNT ), 10 );
+            final DebugItemGenerator debugItemGenerator = new DebugItemGenerator( pwmApplication, SessionLabel.HEALTH_SESSION_LABEL );
+
+            final File supportPath = new File( appPath.getPath() + File.separator + "support" );
+
+            FileSystemUtility.mkdirs( supportPath );
+
+            final File supportFile = new File ( supportPath.getPath() + File.separator + debugItemGenerator.getFilename() );
+
+            FileSystemUtility.rotateBackups( supportFile, rotationCount );
+
+            final File newSupportFile = new File ( supportFile.getPath() + ".new" );
+            Files.deleteIfExists( newSupportFile.toPath() );
+
+            try ( ZipOutputStream zipOutputStream = new ZipOutputStream( new FileOutputStream( newSupportFile ) ) )
+            {
+                LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, () -> "beginning periodic support bundle filesystem output" );
+                debugItemGenerator.outputZipDebugFile( zipOutputStream );
+            }
+
+            Files.move( newSupportFile.toPath(), supportFile.toPath() );
+        }
+    }
 }

+ 35 - 40
server/src/main/java/password/pwm/http/ContextManager.java

@@ -59,9 +59,8 @@ import java.util.Map;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 
 public class ContextManager implements Serializable
 {
@@ -76,8 +75,8 @@ public class ContextManager implements Serializable
     private ErrorInformation startupErrorInformation;
 
     private final AtomicInteger restartCount = new AtomicInteger( 0 );
-    private TimeDuration readApplicationLockMaxWait = TimeDuration.SECONDS_30;
-    private final Lock restartLock = new ReentrantLock();
+    private TimeDuration readApplicationLockMaxWait = TimeDuration.of( 10, TimeDuration.Unit.SECONDS );
+    private final AtomicBoolean restartInProgressFlag = new AtomicBoolean();
 
     private String contextPath;
     private File applicationPath;
@@ -143,42 +142,32 @@ public class ContextManager implements Serializable
     public PwmApplication getPwmApplication( )
             throws PwmUnrecoverableException
     {
-        PwmApplication localApplication = this.pwmApplication;
+        final Instant startTime = Instant.now();
+        PwmApplication localApplication = pwmApplication;
 
-        if ( localApplication == null )
+        while (
+                ( restartInProgressFlag.get() || pwmApplication == null )
+                        &&  TimeDuration.fromCurrent( startTime ).isShorterThan( readApplicationLockMaxWait )
+        )
         {
-            try
-            {
-                final Instant startTime = Instant.now();
-                final boolean hasLock = restartLock.tryLock( readApplicationLockMaxWait.asMillis(), TimeUnit.MILLISECONDS );
-                if ( hasLock )
-                {
-                    localApplication = this.pwmApplication;
-                    if ( localApplication == null )
-                    {
-                        LOGGER.trace( () -> "could not read pwmApplication after waiting " + TimeDuration.compactFromCurrent( startTime ) );
-                    }
-                    else
-                    {
-                        LOGGER.trace( () -> "waited " + TimeDuration.compactFromCurrent( startTime )
-                                + " to read pwmApplication due to restart in progress" );
-                    }
-                }
-            }
-            catch ( InterruptedException e )
-            {
-                LOGGER.warn( "getPwmApplication restartLock unexpectedly interrupted" );
-            }
-            finally
-            {
-                restartLock.unlock();
-            }
+            TimeDuration.SECOND.pause();
+            localApplication = pwmApplication;
         }
 
         if ( localApplication != null )
         {
+            if ( TimeDuration.fromCurrent( startTime ).isLongerThan( TimeDuration.SECOND ) )
+            {
+                LOGGER.trace( () -> "waited " + TimeDuration.compactFromCurrent( startTime )
+                        + " to read pwmApplication due to restart in progress" );
+            }
             return localApplication;
         }
+        else
+        {
+            LOGGER.trace( () -> "could not read pwmApplication after waiting " + TimeDuration.compactFromCurrent( startTime ) );
+        }
+
 
         final ErrorInformation errorInformation;
         if ( startupErrorInformation != null )
@@ -484,6 +473,11 @@ public class ContextManager implements Serializable
         {
             final Instant startTime = Instant.now();
 
+            if ( restartInProgressFlag.get() )
+            {
+                return;
+            }
+
             if ( configReader != null && configReader.isSaveInProgress() )
             {
                 final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
@@ -492,12 +486,13 @@ public class ContextManager implements Serializable
                 return;
             }
 
-            restartLock.lock();
             final PwmApplication oldPwmApplication = pwmApplication;
             pwmApplication = null;
 
             try
             {
+                restartInProgressFlag.set( true );
+
                 waitForRequestsToComplete( oldPwmApplication );
 
                 {
@@ -542,7 +537,7 @@ public class ContextManager implements Serializable
             }
             finally
             {
-                restartLock.unlock();
+                restartInProgressFlag.set( false );
             }
         }
 
@@ -551,22 +546,22 @@ public class ContextManager implements Serializable
             final Instant startTime = Instant.now();
             final TimeDuration maxRequestWaitTime = TimeDuration.of(
                     Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.APPLICATION_RESTART_MAX_REQUEST_WAIT_MS ) ),
-                    TimeDuration.Unit.SECONDS );
-            final int startingRequetsInProgress = pwmApplication.getInprogressRequests().get();
+                    TimeDuration.Unit.MILLISECONDS );
+            final int startingRequestInProgress = pwmApplication.getInprogressRequests().get();
 
-            if ( startingRequetsInProgress == 0 )
+            if ( startingRequestInProgress == 0 )
             {
                 return;
             }
 
             LOGGER.trace( () -> "waiting up to " + maxRequestWaitTime.asCompactString()
-                    + " for " + startingRequetsInProgress  + " requests to complete." );
+                    + " for " + startingRequestInProgress  + " requests to complete." );
             maxRequestWaitTime.pause( TimeDuration.of( 10, TimeDuration.Unit.MILLISECONDS ), () -> pwmApplication.getInprogressRequests().get() == 0
             );
 
-            final int requestsInPrgoress = pwmApplication.getInprogressRequests().get();
+            final int requestsInProgress = pwmApplication.getInprogressRequests().get();
             final TimeDuration waitTime = TimeDuration.fromCurrent( startTime  );
-            LOGGER.trace( () -> "after " + waitTime.asCompactString() + ", " + requestsInPrgoress
+            LOGGER.trace( () -> "after " + waitTime.asCompactString() + ", " + requestsInProgress
                     + " requests in progress, proceeding with restart" );
         }
     }

+ 25 - 1
server/src/main/java/password/pwm/http/HttpEventManager.java

@@ -22,10 +22,13 @@
 
 package password.pwm.http;
 
+import com.novell.ldapchai.util.StringHelper;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.bean.LocalSessionStateBean;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.svc.stats.EpsStatistic;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 import javax.servlet.ServletContextEvent;
@@ -34,6 +37,9 @@ import javax.servlet.http.HttpSession;
 import javax.servlet.http.HttpSessionActivationListener;
 import javax.servlet.http.HttpSessionEvent;
 import javax.servlet.http.HttpSessionListener;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -82,17 +88,21 @@ public class HttpEventManager implements
         {
             if ( httpSession.getAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION ) != null )
             {
+                String debugMsg = "destroyed session";
                 final PwmSession pwmSession = PwmSessionWrapper.readPwmSession( httpSession );
                 if ( pwmSession != null )
                 {
+                    debugMsg += ": " + makeSessionDestroyedDebugMsg( pwmSession );
                     pwmSession.unauthenticateUser( null );
                 }
+
                 final PwmApplication pwmApplication = ContextManager.getPwmApplication( httpSession.getServletContext() );
                 if ( pwmApplication != null )
                 {
                     pwmApplication.getSessionTrackService().removeSessionData( pwmSession );
                 }
-                LOGGER.trace( pwmSession, () -> "destroyed session" );
+                final String outputMsg = debugMsg;
+                LOGGER.trace( pwmSession, () -> outputMsg );
             }
             else
             {
@@ -180,5 +190,19 @@ public class HttpEventManager implements
             LOGGER.error( "unable to activate (de-passivate) session: " + e.getMessage() );
         }
     }
+
+    private static String makeSessionDestroyedDebugMsg( final PwmSession pwmSession )
+    {
+        final LocalSessionStateBean sessionStateBean = pwmSession.getSessionStateBean();
+        final Map<String, String> debugItems = new LinkedHashMap<>();
+        debugItems.put( "requests", sessionStateBean.getRequestCount().toString() );
+        final Instant startTime = sessionStateBean.getSessionCreationTime();
+        final Instant lastAccessedTime = sessionStateBean.getSessionLastAccessedTime();
+        final TimeDuration timeDuration = TimeDuration.between( startTime, lastAccessedTime );
+        debugItems.put( "firstToLastRequestInterval", timeDuration.asCompactString() );
+        final TimeDuration avgReqDuration = TimeDuration.of( sessionStateBean.getAvgRequestDuration().getLastMillis(), TimeDuration.Unit.MILLISECONDS );
+        debugItems.put( "avgRequestDuration", avgReqDuration.asCompactString() );
+        return StringHelper.stringMapToString( debugItems, "," );
+    }
 }
 

+ 1 - 1
server/src/main/java/password/pwm/http/PwmRequest.java

@@ -270,7 +270,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
                     final FileUploadItem fileUploadItem = new FileUploadItem(
                             item.getName(),
                             item.getContentType(),
-                            new ImmutableByteArray( outputFile )
+                            ImmutableByteArray.of( outputFile )
                     );
                     returnObj.put( item.getFieldName(), fileUploadItem );
                 }

+ 1 - 1
server/src/main/java/password/pwm/http/auth/CASFilterAuthenticationProvider.java

@@ -225,7 +225,7 @@ public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticat
         if ( privatekey != null && !privatekey.isEmpty() )
         {
             final FileValue.FileContent fileContent = privatekey.values().iterator().next();
-            privateKeyBytes = fileContent.getContents().getBytes();
+            privateKeyBytes = fileContent.getContents().copyOf();
         }
         else
         {

+ 7 - 2
server/src/main/java/password/pwm/http/bean/ImmutableByteArray.java

@@ -29,12 +29,17 @@ public class ImmutableByteArray implements Serializable
 {
     private final byte[] bytes;
 
-    public ImmutableByteArray( final byte[] bytes )
+    private ImmutableByteArray( final byte[] bytes )
     {
         this.bytes = bytes == null ? null : Arrays.copyOf( bytes, bytes.length );
     }
 
-    public byte[] getBytes( )
+    public static ImmutableByteArray of( final byte[] bytes )
+    {
+        return new ImmutableByteArray( bytes );
+    }
+
+    public byte[] copyOf( )
     {
         return bytes == null ? null : Arrays.copyOf( bytes, bytes.length );
     }

+ 1 - 0
server/src/main/java/password/pwm/http/bean/SetupResponsesBean.java

@@ -45,6 +45,7 @@ public class SetupResponsesBean extends PwmSessionBean
     private boolean helpdeskResponsesSatisfied;
     private boolean confirmed;
     private Locale userLocale;
+    private boolean initialized;
 
     public Type getType( )
     {

+ 4 - 0
server/src/main/java/password/pwm/http/filter/SessionFilter.java

@@ -45,6 +45,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmResponse;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmURL;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -137,6 +138,9 @@ public class SessionFilter extends AbstractPwmFilter
 
         final TimeDuration requestExecuteTime = TimeDuration.fromCurrent( startTime );
         pwmRequest.debugHttpRequestToLog( "completed requestID=" + requestID + " in " + requestExecuteTime.asCompactString() );
+        pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_REQUEST_PROCESS_TIME, requestExecuteTime.asMillis() );
+        pwmRequest.getPwmSession().getSessionStateBean().getRequestCount().incrementAndGet();
+        pwmRequest.getPwmSession().getSessionStateBean().getAvgRequestDuration().update( requestExecuteTime.asMillis() );
     }
 
     private ProcessStatus handleStandardRequestOperations(

+ 9 - 4
server/src/main/java/password/pwm/http/servlet/SetupResponsesServlet.java

@@ -122,7 +122,13 @@ public class SetupResponsesServlet extends ControlledPwmServlet
 
     private SetupResponsesBean getSetupResponseBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     {
-        return pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, SetupResponsesBean.class );
+        final SetupResponsesBean setupResponsesBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, SetupResponsesBean.class );
+        if ( !setupResponsesBean.isInitialized() )
+        {
+            initializeBean( pwmRequest, setupResponsesBean );
+        }
+        return setupResponsesBean;
+
     }
 
     @Override
@@ -130,7 +136,6 @@ public class SetupResponsesServlet extends ControlledPwmServlet
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final SetupResponsesBean setupResponsesBean = getSetupResponseBean( pwmRequest );
 
         if ( !pwmSession.isAuthenticated() )
         {
@@ -161,10 +166,10 @@ public class SetupResponsesServlet extends ControlledPwmServlet
             pwmApplication.getSessionStateService().getBean( pwmRequest, SetupResponsesBean.class ).setUserLocale( pwmSession.getSessionStateBean().getLocale() );
         }
 
-        initializeBean( pwmRequest, setupResponsesBean );
-
         // check to see if the user has any challenges assigned
         final UserInfo uiBean = pwmSession.getUserInfo();
+        final SetupResponsesBean setupResponsesBean = getSetupResponseBean( pwmRequest );
+
         if ( setupResponsesBean.getResponseData().getChallengeSet() == null || setupResponsesBean.getResponseData().getChallengeSet().getChallenges().isEmpty() )
         {
             final String errorMsg = "no challenge sets configured for user " + uiBean.getUserIdentity();

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -53,7 +53,7 @@ import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
-import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PwmPasswordRuleValidator;
 import password.pwm.util.RandomPasswordGenerator;
@@ -366,7 +366,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
                 final TimeDuration totalTime = TimeDuration.fromCurrent( progressTracker.getBeginTime() );
                 try
                 {
-                    pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( Statistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
+                    pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
                     LOGGER.trace( pwmRequest, () -> "password sync process marked completed (" + totalTime.asCompactString() + ")" );
                 }
                 catch ( Exception e )

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

@@ -776,7 +776,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 }
 
                 final Map<String, PwmRequest.FileUploadItem> fileUploads = pwmRequest.readFileUploads( maxFileSize, 1 );
-                final ByteArrayInputStream fileIs = new ByteArrayInputStream( fileUploads.get( PwmConstants.PARAM_FILE_UPLOAD ).getContent().getBytes() );
+                final ByteArrayInputStream fileIs = new ByteArrayInputStream( fileUploads.get( PwmConstants.PARAM_FILE_UPLOAD ).getContent().copyOf() );
 
                 HttpsServerCertificateManager.importKey(
                         configManagerBean.getStoredConfiguration(),

+ 2 - 1
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java

@@ -38,6 +38,7 @@ import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ConfigGuideBean;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
@@ -129,7 +130,7 @@ public class ConfigGuideForm
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE_NAME, newValue, null );
         }
 
-        if ( configGuideBean.isUseConfiguredCerts() )
+        if ( configGuideBean.isUseConfiguredCerts() && !JavaHelper.isEmpty( configGuideBean.getLdapCertificates() ) )
         {
             final StoredValue newStoredValue = new X509CertificateValue( configGuideBean.getLdapCertificates() );
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_CERTS, LDAP_PROFILE_NAME, newStoredValue, null );

+ 4 - 23
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java

@@ -372,39 +372,20 @@ public class ConfigManagerServlet extends AbstractPwmServlet
     }
 
     private void doGenerateSupportZip( final PwmRequest pwmRequest )
-            throws IOException, ServletException
+            throws IOException, PwmUnrecoverableException
     {
+        final DebugItemGenerator debugItemGenerator = new DebugItemGenerator( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel() );
         final PwmResponse resp = pwmRequest.getPwmResponse();
         resp.setHeader( HttpHeader.ContentDisposition, "attachment;filename=" + PwmConstants.PWM_APP_NAME + "-Support.zip" );
         resp.setContentType( HttpContentType.zip );
-
-        final String pathPrefix = PwmConstants.PWM_APP_NAME + "-Support" + "/";
-
-        ZipOutputStream zipOutput = null;
-        try
+        try ( ZipOutputStream zipOutput = new ZipOutputStream( resp.getOutputStream(), PwmConstants.DEFAULT_CHARSET ) )
         {
-            zipOutput = new ZipOutputStream( resp.getOutputStream(), PwmConstants.DEFAULT_CHARSET );
-            DebugItemGenerator.outputZipDebugFile( pwmRequest, zipOutput, pathPrefix );
+            debugItemGenerator.outputZipDebugFile( zipOutput );
         }
         catch ( Exception e )
         {
             LOGGER.error( pwmRequest, "error during zip debug building: " + e.getMessage() );
         }
-        finally
-        {
-            if ( zipOutput != null )
-            {
-                try
-                {
-                    zipOutput.close();
-                }
-                catch ( Exception e )
-                {
-                    LOGGER.error( pwmRequest, "error during zip debug closing: " + e.getMessage() );
-                }
-            }
-        }
-
     }
 
 

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerWordlistServlet.java

@@ -259,8 +259,8 @@ public class ConfigManagerWordlistServlet extends AbstractPwmServlet
                         presentableValues.add( new DisplayElement(
                                 wordlistType.name() + "_sha256Hash",
                                 DisplayElement.Type.string,
-                                "SHA-256 Checksum Hash",
-                                StringUtil.truncate( wordlistStatus.getRemoteInfo().getChecksum(), 32 ) + "..." ) );
+                                "CRC Checksum",
+                                wordlistStatus.getRemoteInfo().getChecksum() ) );
                     }
                 }
                 if ( wordlist.getAutoImportError() != null )

+ 206 - 138
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -25,17 +25,18 @@ package password.pwm.http.servlet.configmanager;
 import lombok.Builder;
 import lombok.Value;
 import org.apache.commons.csv.CSVPrinter;
+import org.apache.commons.io.output.CountingOutputStream;
 import password.pwm.AppProperty;
 import password.pwm.PwmAboutProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.ContextManager;
-import password.pwm.http.PwmRequest;
 import password.pwm.http.servlet.admin.AppDashboardData;
 import password.pwm.http.servlet.admin.UserDebugDataBean;
 import password.pwm.http.servlet.admin.UserDebugDataReader;
@@ -43,7 +44,11 @@ import password.pwm.ldap.LdapDebugDataGenerator;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.node.NodeService;
+import password.pwm.svc.stats.EpsStatistic;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.LDAPPermissionCalculator;
+import password.pwm.util.java.DebugOutputBuilder;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -67,6 +72,7 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.lang.management.ManagementFactory;
 import java.lang.management.ThreadInfo;
+import java.math.BigDecimal;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -74,6 +80,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
@@ -106,54 +113,85 @@ public class DebugItemGenerator
             LdapRecentUserDebugGenerator.class,
             ClusterInfoDebugGenerator.class,
             CacheServiceDebugItemGenerator.class,
-            RootFileSystemDebugItemGenerator.class
+            RootFileSystemDebugItemGenerator.class,
+            StatisticsDataDebugItemGenerator.class,
+            StatisticsEpsDataDebugItemGenerator.class
     ) );
 
-    static void outputZipDebugFile(
-            final PwmRequest pwmRequest,
-            final ZipOutputStream zipOutput,
-            final String pathPrefix
-    )
-            throws IOException, PwmUnrecoverableException
+    private final PwmApplication pwmApplication;
+    private final Configuration obfuscatedConfiguration;
+    private final SessionLabel sessionLabel;
+
+    private static final Locale LOCALE = PwmConstants.DEFAULT_LOCALE;
+
+    public DebugItemGenerator( final PwmApplication pwmApplication, final SessionLabel sessionLabel )
+            throws PwmUnrecoverableException
     {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final String debugFileName = "zipDebugGeneration.csv";
+        this.pwmApplication = pwmApplication;
+        this.sessionLabel = sessionLabel;
 
-        final ByteArrayOutputStream debugGeneratorLogBaos = new ByteArrayOutputStream();
-        final CSVPrinter debugGeneratorLogFile = JavaHelper.makeCsvPrinter( debugGeneratorLogBaos );
+        final StoredConfigurationImpl storedConfiguration = StoredConfigurationImpl.copy( pwmApplication.getConfig().getStoredConfiguration() );
+        storedConfiguration.resetAllPasswordValues( "value removed from " + getFilenameBase() + " configuration export" );
+        this.obfuscatedConfiguration = new Configuration( storedConfiguration );
+    }
+
+    private String getFilenameBase()
+    {
+        return PwmConstants.PWM_APP_NAME + "-Support";
+    }
+
+    public String getFilename()
+    {
+        return getFilenameBase() + ".zip";
+    }
+
+    public void outputZipDebugFile( final ZipOutputStream zipOutput )
+            throws IOException
+    {
+        final String debugFileName = "zipDebugGeneration.csv";
+        final Instant startTime = Instant.now();
+        final DebugOutputBuilder debugGeneratorLogFile = new DebugOutputBuilder();
+        final DebugItemInput debugItemInput = new DebugItemInput( pwmApplication, sessionLabel, obfuscatedConfiguration );
+        debugGeneratorLogFile.appendLine( "beginning debug output" );
+        final String pathPrefix = getFilenameBase() + "/";
 
         for ( final Class<? extends DebugItemGenerator.Generator> serviceClass : DEBUG_ZIP_ITEM_GENERATORS )
         {
             try
             {
-                final Instant startTime = Instant.now();
-                LOGGER.trace( pwmRequest, () -> "beginning output of item " + serviceClass.getSimpleName() );
+                final Instant itemStartTime = Instant.now();
+                LOGGER.trace( sessionLabel, () -> "beginning output of item " + serviceClass.getSimpleName() );
                 final DebugItemGenerator.Generator newGeneratorItem = serviceClass.getDeclaredConstructor().newInstance();
                 zipOutput.putNextEntry( new ZipEntry( pathPrefix + newGeneratorItem.getFilename() ) );
-                newGeneratorItem.outputItem( pwmApplication, pwmRequest, zipOutput );
+                newGeneratorItem.outputItem( debugItemInput, zipOutput );
                 zipOutput.closeEntry();
                 zipOutput.flush();
                 final String finishMsg = "completed output of " + newGeneratorItem.getFilename()
-                        + " in " + TimeDuration.fromCurrent( startTime ).asCompactString();
-                LOGGER.trace( pwmRequest, () -> finishMsg );
-                debugGeneratorLogFile.printRecord( JavaHelper.toIsoDate( Instant.now() ), finishMsg );
+                        + " in " + TimeDuration.fromCurrent( itemStartTime ).asCompactString();
+                LOGGER.trace( sessionLabel, () -> finishMsg );
+                debugGeneratorLogFile.appendLine( finishMsg );
             }
             catch ( Throwable e )
             {
                 final String errorMsg = "unexpected error executing debug item output class '" + serviceClass.getName() + "', error: " + e.toString();
-                LOGGER.error( pwmRequest, errorMsg );
-                debugGeneratorLogFile.printRecord( JavaHelper.toIsoDate( Instant.now() ), errorMsg );
+                LOGGER.error( sessionLabel, errorMsg, e );
+                debugGeneratorLogFile.appendLine( errorMsg );
                 final Writer stackTraceOutput = new StringWriter();
                 e.printStackTrace( new PrintWriter( stackTraceOutput ) );
-                debugGeneratorLogFile.printRecord( stackTraceOutput );
+                debugGeneratorLogFile.appendLine( stackTraceOutput.toString() );
             }
         }
 
+        {
+            final String msg = "completed in " + TimeDuration.compactFromCurrent( startTime );
+            debugGeneratorLogFile.appendLine( msg );
+            LOGGER.trace( sessionLabel, () -> msg );
+        }
+
         try
         {
             zipOutput.putNextEntry( new ZipEntry( pathPrefix + debugFileName ) );
-            debugGeneratorLogFile.flush();
-            zipOutput.write( debugGeneratorLogBaos.toByteArray() );
+            zipOutput.write( debugGeneratorLogFile.toString().getBytes( PwmConstants.DEFAULT_CHARSET ) );
             zipOutput.closeEntry();
         }
         catch ( Exception e )
@@ -173,10 +211,9 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
-            storedConfiguration.resetAllPasswordValues( "value removed from " + PwmConstants.PWM_APP_NAME + "-Support configuration export" );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
             final String jsonOutput = JsonUtil.serialize( storedConfiguration.toJsonDebugObject(), JsonUtil.Flag.PrettyPrint );
             outputStream.write( jsonOutput.getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
@@ -191,10 +228,9 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
-            storedConfiguration.resetAllPasswordValues( "value removed from " + PwmConstants.PWM_APP_NAME + "-Support configuration export" );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
 
             final StringWriter writer = new StringWriter();
             writer.write( "Configuration Debug Output for "
@@ -205,7 +241,7 @@ public class DebugItemGenerator
 
             writer.write( "\n" );
             final Map<String, String> modifiedSettings = new TreeMap<>(
-                    storedConfiguration.getModifiedSettingDebugValues( PwmConstants.DEFAULT_LOCALE, true )
+                    storedConfiguration.getModifiedSettingDebugValues( LOCALE, true )
             );
 
             for ( final Map.Entry<String, String> entry : modifiedSettings.entrySet() )
@@ -232,15 +268,14 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
-            storedConfiguration.resetAllPasswordValues( "value removed from " + PwmConstants.PWM_APP_NAME + "-Support configuration export" );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
 
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            storedConfiguration.toXml( baos );
-            outputStream.write( baos.toByteArray() );
-        }
+            // temporary output stream required because .toXml closes stream.
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            storedConfiguration.toXml( byteArrayOutputStream );
+            outputStream.write( byteArrayOutputStream.toByteArray() );        }
     }
 
     static class AboutItemGenerator implements Generator
@@ -252,10 +287,10 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
             final Properties outputProps = new JavaHelper.SortedProperties();
-            final Map<PwmAboutProperty, String> infoBean = PwmAboutProperty.makeInfoBean( pwmApplication );
+            final Map<PwmAboutProperty, String> infoBean = PwmAboutProperty.makeInfoBean( debugItemInput.getPwmApplication() );
             outputProps.putAll( PwmAboutProperty.toStringMap( infoBean ) );
             outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
         }
@@ -270,7 +305,7 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
             final Properties outputProps = new JavaHelper.SortedProperties();
             outputProps.putAll( System.getenv() );
@@ -287,10 +322,10 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
 
-            final Configuration config = pwmRequest.getConfig();
+            final Configuration config = debugItemInput.getObfuscatedConfiguration();
             final Properties outputProps = new JavaHelper.SortedProperties();
 
             for ( final AppProperty appProperty : AppProperty.values() )
@@ -311,9 +346,9 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final LinkedHashMap<String, Object> outputMap = new LinkedHashMap<>();
 
             {
@@ -346,8 +381,9 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final Set<HealthRecord> records = pwmApplication.getHealthMonitor().getHealthRecords();
             final String recordJson = JsonUtil.serializeCollection( records, JsonUtil.Flag.PrettyPrint );
             outputStream.write( recordJson.getBytes( PwmConstants.DEFAULT_CHARSET ) );
@@ -363,18 +399,18 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
 
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            final PrintWriter writer = new PrintWriter( new OutputStreamWriter( baos, PwmConstants.DEFAULT_CHARSET ) );
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            final PrintWriter writer = new PrintWriter( new OutputStreamWriter( byteArrayOutputStream, PwmConstants.DEFAULT_CHARSET ) );
             final ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads( true, true );
             for ( final ThreadInfo threadInfo : threads )
             {
                 writer.write( JavaHelper.threadInfoToString( threadInfo ) );
             }
             writer.flush();
-            outputStream.write( baos.toByteArray() );
+            outputStream.write( byteArrayOutputStream.toByteArray() );
         }
     }
 
@@ -387,13 +423,13 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
             final List<LdapDebugDataGenerator.LdapDebugInfo> ldapDebugInfos = LdapDebugDataGenerator.makeLdapDebugInfos(
-                    pwmApplication,
-                    pwmRequest.getSessionLabel(),
-                    pwmApplication.getConfig(),
-                    pwmRequest.getLocale()
+                    debugItemInput.getPwmApplication(),
+                    debugItemInput.getSessionLabel(),
+                    debugItemInput.getObfuscatedConfiguration(),
+                    LOCALE
             );
             final Writer writer = new OutputStreamWriter( outputStream, PwmConstants.DEFAULT_CHARSET );
             writer.write( JsonUtil.serializeCollection( ldapDebugInfos, JsonUtil.Flag.PrettyPrint ) );
@@ -411,9 +447,10 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
             final List<FileSystemUtility.FileSummaryInformation> fileSummaryInformations = new ArrayList<>();
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final File applicationPath = pwmApplication.getPwmEnvironment().getApplicationPath();
 
             if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
@@ -433,7 +470,7 @@ public class DebugItemGenerator
                 }
                 catch ( Exception e )
                 {
-                    LOGGER.error( pwmRequest, "unable to generate webInfPath fileMd5sums during zip debug building: " + e.getMessage() );
+                    LOGGER.error( debugItemInput.getSessionLabel(), "unable to generate webInfPath fileMd5sums during zip debug building: " + e.getMessage() );
                 }
             }
 
@@ -445,7 +482,7 @@ public class DebugItemGenerator
                 }
                 catch ( Exception e )
                 {
-                    LOGGER.error( pwmRequest, "unable to generate appPath fileMd5sums during zip debug building: " + e.getMessage() );
+                    LOGGER.error( debugItemInput.getSessionLabel(), "unable to generate appPath fileMd5sums during zip debug building: " + e.getMessage() );
                 }
             }
 
@@ -457,7 +494,7 @@ public class DebugItemGenerator
                     headerRow.add( "Filename" );
                     headerRow.add( "Last Modified" );
                     headerRow.add( "Size" );
-                    headerRow.add( "sha1sum" );
+                    headerRow.add( "Checksum" );
                     csvPrinter.printComment( StringUtil.join( headerRow, "," ) );
                 }
                 for ( final FileSystemUtility.FileSummaryInformation fileSummaryInformation : fileSummaryInformations )
@@ -469,7 +506,7 @@ public class DebugItemGenerator
                         dataRow.add( fileSummaryInformation.getFilename() );
                         dataRow.add( JavaHelper.toIsoDate( fileSummaryInformation.getModified() ) );
                         dataRow.add( String.valueOf( fileSummaryInformation.getSize() ) );
-                        dataRow.add( fileSummaryInformation.getSha1sum() );
+                        dataRow.add( Long.toString( fileSummaryInformation.getChecksum() ) );
                         csvPrinter.printRecord( dataRow );
                     }
                     catch ( Exception e )
@@ -491,40 +528,36 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-
-            final int maxCount = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGLINES ) );
-            final int maxSeconds = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS ) );
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final long maxByteCount = JavaHelper.silentParseLong( pwmApplication.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES ), 10_000_000 );
+            final int maxSeconds = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS ), 60 );
             final LocalDBSearchQuery searchParameters = LocalDBSearchQuery.builder()
                     .minimumLevel( PwmLogLevel.TRACE )
-                    .maxEvents( maxCount )
+                    .maxEvents( Integer.MAX_VALUE )
                     .maxQueryTime( TimeDuration.of( maxSeconds, TimeDuration.Unit.SECONDS ) )
                     .build();
 
-            final LocalDBSearchResults searchResults = pwmApplication.getLocalDBLogger().readStoredEvents(
-                    searchParameters );
-            int counter = 0;
-            while ( searchResults.hasNext() )
+            final LocalDBSearchResults searchResults = pwmApplication.getLocalDBLogger().readStoredEvents( searchParameters );
+            final CountingOutputStream countingOutputStream = new CountingOutputStream( outputStream );
+
+            final Writer writer = new OutputStreamWriter( countingOutputStream, PwmConstants.DEFAULT_CHARSET );
             {
-                final PwmLogEvent event = searchResults.next();
-                outputStream.write( event.toLogString().getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                outputStream.write( "\n".getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                counter++;
-                if ( counter % 1000 == 0 )
+                while ( searchResults.hasNext() && countingOutputStream.getByteCount() < maxByteCount )
                 {
-                    outputStream.flush();
+                    final PwmLogEvent event = searchResults.next();
+                    writer.write( event.toLogString() );
+                    writer.write( "\n" );
                 }
-            }
 
-            {
-                final int finalCounter = counter;
-                LOGGER.trace( () -> "output " + finalCounter + " lines to " + this.getFilename() );
+                final String outputMsg = "debug output " + searchResults.getReturnedEvents() + " lines in " + searchResults.getSearchTime().asCompactString();
+                writer.write( "\n#" + outputMsg + "\n" );
+                LOGGER.trace( () ->  outputMsg );
             }
+
+            // do not close writer because underlying stream should not be closed.
+            writer.flush();
         }
     }
 
@@ -537,14 +570,10 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
 
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
             final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
 
             final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
@@ -581,13 +610,9 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final LocalDB localDB = pwmApplication.getLocalDB();
+            final LocalDB localDB = debugItemInput.getPwmApplication().getLocalDB();
             final Map<String, Serializable> serializableMap = localDB.debugInfo();
             outputStream.write( JsonUtil.serializeMap( serializableMap, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
@@ -602,14 +627,10 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            pwmApplication.getSessionTrackService().outputToCsv( pwmRequest.getLocale(), pwmRequest.getConfig(), outputStream );
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            pwmApplication.getSessionTrackService().outputToCsv( LOCALE, pwmApplication.getConfig(), outputStream );
         }
     }
 
@@ -621,14 +642,9 @@ public class DebugItemGenerator
             return "recentUserDebugData.json";
         }
 
-        @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final List<UserIdentity> recentUsers = pwmApplication.getSessionTrackService().getRecentLogins();
             final List<UserDebugDataBean> recentDebugBeans = new ArrayList<>();
 
@@ -636,8 +652,8 @@ public class DebugItemGenerator
             {
                 final UserDebugDataBean dataBean = UserDebugDataReader.readUserDebugData(
                         pwmApplication,
-                        pwmRequest.getLocale(),
-                        pwmRequest.getSessionLabel(),
+                        LOCALE,
+                        debugItemInput.getSessionLabel(),
                         userIdentity
                 );
                 recentDebugBeans.add( dataBean );
@@ -656,13 +672,9 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final NodeService nodeService = pwmApplication.getClusterService();
 
             final Map<String, Serializable> debugOutput = new LinkedHashMap<>();
@@ -687,13 +699,9 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final CacheService cacheService = pwmApplication.getCacheService();
 
             final Map<String, Serializable> debugOutput = new LinkedHashMap<>( cacheService.debugInfo() );
@@ -710,24 +718,82 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final ContextManager contextManager = ContextManager.getContextManager( pwmRequest );
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final ContextManager contextManager = pwmApplication.getPwmEnvironment().getContextManager();
             final AppDashboardData appDashboardData = AppDashboardData.makeDashboardData(
                     pwmApplication,
                     contextManager,
-                    pwmRequest.getLocale()
+                    LOCALE
             );
 
             outputStream.write( JsonUtil.serialize( appDashboardData, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
     }
 
+    static class StatisticsDataDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename()
+        {
+            return "statistics.csv";
+        }
+
+        @Override
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
+        {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final StatisticsManager statsManager = pwmApplication.getStatisticsManager();
+            statsManager.outputStatsToCsv( outputStream, LOCALE, true );
+        }
+    }
+
+    static class StatisticsEpsDataDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename()
+        {
+            return "statistics-eps.csv";
+        }
+
+        @Override
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
+        {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final StatisticsManager statsManager = pwmApplication.getStatisticsManager();
+            final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
+            {
+                final List<String> headerRow = new ArrayList<>();
+                headerRow.add( "Counter" );
+                headerRow.add( "Duration" );
+                headerRow.add( "Events/Second" );
+                csvPrinter.printComment( StringUtil.join( headerRow, "," ) );
+            }
+            for ( final EpsStatistic epsStatistic : EpsStatistic.values() )
+            {
+                for ( final Statistic.EpsDuration epsDuration : Statistic.EpsDuration.values() )
+                {
+                    try
+                    {
+                        final List<String> dataRow = new ArrayList<>();
+                        final BigDecimal value = statsManager.readEps( epsStatistic, epsDuration );
+                        final String sValue = value.toPlainString();
+                        dataRow.add( epsStatistic.getLabel( LOCALE ) );
+                        dataRow.add( epsDuration.getTimeDuration().asCompactString() );
+                        dataRow.add( sValue );
+                        csvPrinter.printRecord( dataRow );
+                    }
+                    catch ( Exception e )
+                    {
+                        LOGGER.trace( () -> "error generating csv-stats summary info: " + e.getMessage() );
+                    }
+                }
+            }
+            csvPrinter.flush();
+        }
+    }
+
     static class RootFileSystemDebugItemGenerator implements Generator
     {
         @Override
@@ -737,12 +803,7 @@ public class DebugItemGenerator
         }
 
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
             final Collection<RootFileSystemInfo> rootInfos = RootFileSystemInfo.forAllRootFileSystems();
             outputStream.write( JsonUtil.serializeCollection( rootInfos, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
@@ -782,10 +843,17 @@ public class DebugItemGenerator
         String getFilename( );
 
         void outputItem(
-                PwmApplication pwmApplication,
-                PwmRequest pwmRequest,
+                DebugItemInput debugItemInput,
                 OutputStream outputStream
         ) throws Exception;
     }
 
+    @Value
+    private static class DebugItemInput
+    {
+        private final PwmApplication pwmApplication;
+        private final SessionLabel sessionLabel;
+        private final Configuration obfuscatedConfiguration;
+    }
+
 }

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -1390,7 +1390,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
             resp.setContentType( photoData.getMimeType() );
 
-            outputStream.write( photoData.getContents().getBytes() );
+            outputStream.write( photoData.getContents().copyOf() );
         }
         return ProcessStatus.Halt;
     }

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/oauth/OAuthState.java

@@ -39,7 +39,7 @@ import java.time.Instant;
 @Builder
 class OAuthState implements Serializable
 {
-    private static final AtomicLoopIntIncrementer OAUTH_STATE_ID_COUNTER = new AtomicLoopIntIncrementer( Integer.MAX_VALUE );
+    private static final AtomicLoopIntIncrementer OAUTH_STATE_ID_COUNTER = new AtomicLoopIntIncrementer();
 
     @SerializedName( "c" )
     @Builder.Default

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java

@@ -42,7 +42,7 @@ public class PeopleSearchService implements PwmService
     @Override
     public STATUS status()
     {
-        return null;
+        return STATUS.OPEN;
     }
 
     @Override

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -272,7 +272,7 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
 
             if ( photoData.getContents() != null )
             {
-                outputStream.write( photoData.getContents().getBytes() );
+                outputStream.write( photoData.getContents().copyOf() );
             }
         }
         return ProcessStatus.Halt;

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/resource/CacheEntry.java

@@ -34,13 +34,13 @@ final class CacheEntry implements Serializable
 
     CacheEntry( final byte[] entity, final Map<String, String> headerStrings )
     {
-        this.entity = new ImmutableByteArray( entity );
+        this.entity = ImmutableByteArray.of( entity );
         this.headerStrings = headerStrings;
     }
 
     public byte[] getEntity( )
     {
-        return entity.getBytes();
+        return entity.copyOf();
     }
 
     public Map<String, String> getHeaderStrings( )

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/resource/MemoryFileResource.java

@@ -43,12 +43,12 @@ class MemoryFileResource implements FileResource
 
     public InputStream getInputStream( ) throws IOException
     {
-        return new ByteArrayInputStream( contents.getBytes() );
+        return new ByteArrayInputStream( contents.copyOf() );
     }
 
     public long length( )
     {
-        return contents.getBytes().length;
+        return contents.copyOf().length;
     }
 
     public long lastModified( )

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java

@@ -32,7 +32,7 @@ import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.servlet.PwmServlet;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JavaHelper;

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/resource/ResourceServletConfiguration.java

@@ -196,7 +196,7 @@ class ResourceServletConfiguration
     private static Map<String, FileResource> makeMemoryFileMapFromZipInput( final ImmutableByteArray content )
             throws IOException
     {
-        final ZipInputStream stream = new ZipInputStream( new ByteArrayInputStream( content.getBytes() ) );
+        final ZipInputStream stream = new ZipInputStream( new ByteArrayInputStream( content.copyOf() ) );
         final Map<String, FileResource> memoryMap = new HashMap<>();
 
         ZipEntry entry;
@@ -208,7 +208,7 @@ class ResourceServletConfiguration
                 final long lastModified = entry.getTime();
                 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                 IOUtils.copy( stream, byteArrayOutputStream );
-                final ImmutableByteArray contents = new ImmutableByteArray( byteArrayOutputStream.toByteArray() );
+                final ImmutableByteArray contents = ImmutableByteArray.of( byteArrayOutputStream.toByteArray() );
                 memoryMap.put( name, new MemoryFileResource( name, contents, lastModified ) );
                 {
                     final String finalEntry = entry.getName();

+ 64 - 52
server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java

@@ -32,8 +32,9 @@ import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.Percent;
@@ -41,7 +42,6 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.ChecksumOutputStream;
-import password.pwm.util.secure.PwmHashAlgorithm;
 
 import javax.servlet.ServletContext;
 import java.io.File;
@@ -175,7 +175,7 @@ public class ResourceServletService implements PwmService
     }
 
     private String makeResourcePathNonce( )
-            throws PwmUnrecoverableException, IOException
+            throws IOException
     {
         final int nonceLength = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_PATH_NONCE_LENGTH ) );
         final boolean enablePathNonce = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_ENABLE_PATH_NONCE ) );
@@ -185,56 +185,9 @@ public class ResourceServletService implements PwmService
         }
 
         final Instant startTime = Instant.now();
-        final ChecksumOutputStream checksumStream = new ChecksumOutputStream( PwmHashAlgorithm.SHA512, new NullOutputStream() );
+        final ImmutableByteArray checksumBytes = checksumAllResources( pwmApplication );
 
-        if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
-        {
-            try
-            {
-                final File webInfPath = pwmApplication.getPwmEnvironment().getContextManager().locateWebInfFilePath();
-                if ( webInfPath != null && webInfPath.exists() )
-                {
-                    final File basePath = webInfPath.getParentFile();
-                    if ( basePath != null && basePath.exists() )
-                    {
-                        final File resourcePath = new File( basePath.getAbsolutePath() + File.separator + "public" + File.separator + "resources" );
-                        if ( resourcePath.exists() )
-                        {
-                            for ( final FileSystemUtility.FileSummaryInformation fileSummaryInformation : FileSystemUtility.readFileInformation( resourcePath ) )
-                            {
-                                checksumStream.write( ( fileSummaryInformation.getSha1sum() ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                            }
-                        }
-                    }
-                }
-            }
-            catch ( Exception e )
-            {
-                LOGGER.error( "unable to generate resource path nonce: " + e.getMessage() );
-            }
-        }
-
-        for ( final FileResource fileResource : getResourceServletConfiguration().getCustomFileBundle().values() )
-        {
-            JavaHelper.copy( fileResource.getInputStream(), checksumStream );
-        }
-
-        if ( getResourceServletConfiguration().getZipResources() != null )
-        {
-            for ( final String key : getResourceServletConfiguration().getZipResources().keySet() )
-            {
-                final ZipFile zipFile = getResourceServletConfiguration().getZipResources().get( key );
-                checksumStream.write( key.getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                for ( Enumeration<? extends ZipEntry> zipEnum = zipFile.entries(); zipEnum.hasMoreElements(); )
-                {
-                    final ZipEntry entry = zipEnum.nextElement();
-                    JavaHelper.copy( zipFile.getInputStream( entry ), checksumStream );
-                }
-            }
-        }
-
-        final byte[] checksumBytes = checksumStream.getInProgressChecksum();
-        final String nonce = StringUtil.truncate( JavaHelper.byteArrayToHexString( checksumBytes ).toLowerCase(), nonceLength );
+        final String nonce = StringUtil.truncate( JavaHelper.byteArrayToHexString( checksumBytes.copyOf() ).toLowerCase(), nonceLength );
         LOGGER.debug( () -> "completed generation of nonce '" + nonce + "' in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
 
         final String noncePrefix = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_NONCE_PATH_PREFIX );
@@ -286,4 +239,63 @@ public class ResourceServletService implements PwmService
         LOGGER.debug( pwmRequest, () -> "check for theme validity of '" + themeName + "' returned false" );
         return false;
     }
+
+    private ImmutableByteArray checksumAllResources( final PwmApplication pwmApplication )
+            throws IOException
+    {
+        try ( ChecksumOutputStream checksumStream = new ChecksumOutputStream( new NullOutputStream() ) )
+        {
+            checksumResourceFilePath( pwmApplication, checksumStream );
+
+            for ( final FileResource fileResource : getResourceServletConfiguration().getCustomFileBundle().values() )
+            {
+                JavaHelper.copy( fileResource.getInputStream(), checksumStream );
+            }
+
+            if ( getResourceServletConfiguration().getZipResources() != null )
+            {
+                for ( final String key : getResourceServletConfiguration().getZipResources().keySet() )
+                {
+                    final ZipFile zipFile = getResourceServletConfiguration().getZipResources().get( key );
+                    checksumStream.write( key.getBytes( PwmConstants.DEFAULT_CHARSET ) );
+                    for ( Enumeration<? extends ZipEntry> zipEnum = zipFile.entries(); zipEnum.hasMoreElements(); )
+                    {
+                        final ZipEntry entry = zipEnum.nextElement();
+                        JavaHelper.copy( zipFile.getInputStream( entry ), checksumStream );
+                    }
+                }
+            }
+            return checksumStream.checksum();
+        }
+    }
+
+    private static void checksumResourceFilePath( final PwmApplication pwmApplication, final ChecksumOutputStream checksumStream )
+    {
+        if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
+        {
+            try
+            {
+                final File webInfPath = pwmApplication.getPwmEnvironment().getContextManager().locateWebInfFilePath();
+                if ( webInfPath != null && webInfPath.exists() )
+                {
+                    final File basePath = webInfPath.getParentFile();
+                    if ( basePath != null && basePath.exists() )
+                    {
+                        final File resourcePath = new File( basePath.getAbsolutePath() + File.separator + "public" + File.separator + "resources" );
+                        if ( resourcePath.exists() )
+                        {
+                            for ( final FileSystemUtility.FileSummaryInformation fileSummaryInformation : FileSystemUtility.readFileInformation( resourcePath ) )
+                            {
+                                checksumStream.write( JavaHelper.longToBytes( fileSummaryInformation.getChecksum() ) );
+                            }
+                        }
+                    }
+                }
+            }
+            catch ( Exception e )
+            {
+                LOGGER.error( "unable to generate resource path nonce: " + e.getMessage() );
+            }
+        }
+    }
 }

+ 28 - 8
server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java

@@ -45,6 +45,7 @@ import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.bean.UpdateProfileBean;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
@@ -368,28 +369,47 @@ public class UpdateProfileUtil
 
         LdapOperationsHelper.writeFormValuesToLdap( theUser, formMap, macroMachine, false );
 
-        final UserIdentity userIdentity = userInfo.getUserIdentity();
+        postUpdateActionsAndEmail( pwmApplication, sessionLabel, locale, userInfo.getUserIdentity(), updateProfileProfile );
+
+        // success, so forward to success page
+        pwmApplication.getStatisticsManager().incrementValue( Statistic.UPDATE_ATTRIBUTES );
+    }
+
+    private static void postUpdateActionsAndEmail(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final Locale locale,
+            final UserIdentity userIdentity,
+            final UpdateProfileProfile updateProfileProfile
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+    {
+        // obtain new macro machine (with a new UserInfo) so old cached values won't be used for next op
+        final UserInfo reloadedUserInfo = UserInfoFactory.newUserInfo(
+                pwmApplication,
+                sessionLabel,
+                locale,
+                userIdentity,
+                pwmApplication.getProxiedChaiUser( userIdentity ).getChaiProvider() );
+        final MacroMachine reloadedMacroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, reloadedUserInfo, null, null );
 
         {
             // execute configured actions
             final List<ActionConfiguration> actions = updateProfileProfile.readSettingAsAction( PwmSetting.UPDATE_PROFILE_WRITE_ATTRIBUTES );
             if ( actions != null && !actions.isEmpty() )
             {
-                LOGGER.debug( sessionLabel, () -> "executing configured actions to user " + userIdentity );
-
+                LOGGER.debug( sessionLabel, () -> "executing configured actions to user " + reloadedUserInfo.getUserIdentity() );
 
-                final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
+                final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, reloadedUserInfo.getUserIdentity() )
                         .setExpandPwmMacros( true )
-                        .setMacroMachine( macroMachine )
+                        .setMacroMachine( reloadedMacroMachine )
                         .createActionExecutor();
 
                 actionExecutor.executeActions( actions, sessionLabel );
             }
         }
-        sendProfileUpdateEmailNotice( pwmApplication, macroMachine, userInfo, locale, sessionLabel );
 
-        // success, so forward to success page
-        pwmApplication.getStatisticsManager().incrementValue( Statistic.UPDATE_ATTRIBUTES );
+        sendProfileUpdateEmailNotice( pwmApplication, reloadedMacroMachine, reloadedUserInfo, locale, sessionLabel );
     }
 
     static TokenDestinationItem tokenDestinationItemForCurrentValidation(

+ 7 - 2
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -55,6 +55,7 @@ import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CachePolicy;
+import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.i18n.LocaleHelper;
@@ -605,7 +606,7 @@ public class LdapOperationsHelper
     )
             throws ChaiUnavailableException, PwmUnrecoverableException
     {
-        return createChaiProvider(
+        final ChaiProvider chaiProvider = createChaiProvider(
                 pwmApplication.getLdapConnectionService().getChaiProviderFactory(),
                 sessionLabel,
                 ldapProfile,
@@ -613,6 +614,10 @@ public class LdapOperationsHelper
                 userDN,
                 userPassword
         );
+
+        pwmApplication.getStatisticsManager().updateEps( EpsStatistic.LDAP_BINDS, 1 );
+
+        return chaiProvider;
     }
 
     public static ChaiProvider createChaiProvider(
@@ -1039,7 +1044,7 @@ public class LdapOperationsHelper
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_INTERNAL, "error reading user photo ldap attribute: " + e.getMessage() ) );
         }
-        return new PhotoDataBean( mimeType, new ImmutableByteArray( photoData ) );
+        return new PhotoDataBean( mimeType, ImmutableByteArray.of( photoData ) );
     }
 
 

+ 3 - 2
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -52,6 +52,7 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.RecordType;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
@@ -86,7 +87,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
     private AuthenticationStrategy strategy = AuthenticationStrategy.BIND;
     private Instant startTime;
 
-    private static final AtomicLoopIntIncrementer OPERATION_COUNTER = new AtomicLoopIntIncrementer( 0 );
+    private static final AtomicLoopIntIncrementer OPERATION_COUNTER = new AtomicLoopIntIncrementer();
     private final int operationNumber;
 
 
@@ -349,7 +350,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
         final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager();
         statisticsManager.incrementValue( Statistic.AUTHENTICATIONS );
         statisticsManager.updateEps( EpsStatistic.AUTHENTICATION, 1 );
-        statisticsManager.updateAverageValue( Statistic.AVG_AUTHENTICATION_TIME,
+        statisticsManager.updateAverageValue( AvgStatistic.AVG_AUTHENTICATION_TIME,
                 TimeDuration.fromCurrent( startTime ).asMillis() );
 
 

+ 28 - 9
server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java

@@ -46,7 +46,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
@@ -66,6 +66,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -533,7 +534,7 @@ public class UserSearchEngine implements PwmService
 
         if ( pwmApplication.getStatisticsManager() != null && pwmApplication.getStatisticsManager().status() == PwmService.STATUS.OPEN )
         {
-            pwmApplication.getStatisticsManager().updateAverageValue( Statistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
+            pwmApplication.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
         }
 
         if ( results.isEmpty() )
@@ -558,21 +559,39 @@ public class UserSearchEngine implements PwmService
     private void validateSpecifiedContext( final LdapProfile profile, final String context )
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final Map<String, String> selectableContexts = profile.getSelectableContexts( pwmApplication );
-        if ( selectableContexts == null || selectableContexts.isEmpty() )
+        Objects.requireNonNull( profile, "ldapProfile can not be null for ldap search context validation" );
+        Objects.requireNonNull( context, "context can not be null for ldap search context validation" );
+
+        final String canonicalContext = profile.readCanonicalDN( pwmApplication, context );
+
         {
-            throw new PwmOperationalException( PwmError.ERROR_INTERNAL, "context specified, but no selectable contexts are configured" );
+            final Map<String, String> selectableContexts = profile.getSelectableContexts( pwmApplication );
+            if ( !JavaHelper.isEmpty( selectableContexts ) && selectableContexts.containsKey( canonicalContext ) )
+            {
+                // config pre-validates selectable contexts so this should be permitted
+                return;
+            }
         }
 
-        for ( final String loopContext : selectableContexts.keySet() )
         {
-            if ( loopContext.equals( context ) )
+            final List<String> rootContexts = profile.getRootContexts( pwmApplication );
+            if ( !JavaHelper.isEmpty( rootContexts ) )
             {
-                return;
+                for ( final String rootContext : rootContexts )
+                {
+                    if ( canonicalContext.endsWith( rootContext ) )
+                    {
+                        return;
+                    }
+                }
+
+                final String msg = "specified search context '" + canonicalContext + "' is not contained by a configured root context";
+                throw new PwmUnrecoverableException( PwmError.CONFIG_FORMAT_ERROR, msg );
             }
         }
 
-        throw new PwmOperationalException( PwmError.ERROR_INTERNAL, "context '" + context + "' is specified, but is not in configuration" );
+        final String msg = "specified search context '" + canonicalContext + "', but no selectable contexts or root are configured";
+        throw new PwmOperationalException( PwmError.ERROR_INTERNAL, msg );
     }
 
     private boolean checkIfStringIsDN(

+ 3 - 3
server/src/main/java/password/pwm/svc/node/LDAPNodeDataService.java

@@ -86,7 +86,7 @@ class LDAPNodeDataService implements NodeDataServiceProvider
         }
         catch ( ChaiException e )
         {
-            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error reading cluster data: " + e.getMessage() );
+            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error reading node service data: " + e.getMessage() );
         }
 
         return returnData;
@@ -116,7 +116,7 @@ class LDAPNodeDataService implements NodeDataServiceProvider
         }
         catch ( ChaiException e )
         {
-            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error writing cluster data: " + e.getMessage() );
+            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error writing node service data: " + e.getMessage() );
         }
 
     }
@@ -148,7 +148,7 @@ class LDAPNodeDataService implements NodeDataServiceProvider
                 }
                 catch ( ChaiException e )
                 {
-                    throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error purging cluster data: " + e.getMessage() );
+                    throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error purging node service data: " + e.getMessage() );
                 }
             }
         }

+ 2 - 2
server/src/main/java/password/pwm/svc/node/NodeService.java

@@ -84,7 +84,7 @@ public class NodeService implements PwmService
                 {
                     case DB:
                     {
-                        LOGGER.trace( () -> "starting database-backed cluster provider" );
+                        LOGGER.trace( () -> "starting database-backed node service provider" );
                         nodeServiceSettings = NodeServiceSettings.fromConfigForDB( pwmApplication.getConfig() );
                         clusterDataServiceProvider = new DatabaseNodeDataService( pwmApplication );
                     }
@@ -92,7 +92,7 @@ public class NodeService implements PwmService
 
                     case LDAP:
                     {
-                        LOGGER.trace( () -> "starting ldap-backed cluster provider" );
+                        LOGGER.trace( () -> "starting ldap-backed node service provider" );
                         nodeServiceSettings = NodeServiceSettings.fromConfigForLDAP( pwmApplication.getConfig() );
                         clusterDataServiceProvider = new LDAPNodeDataService( pwmApplication );
                     }

+ 5 - 0
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java

@@ -185,6 +185,11 @@ public class PwNotifyEngine
                     + ", sent " + noticeCount + " notices."
             );
         }
+        catch ( PwmUnrecoverableException | PwmOperationalException e )
+        {
+            log( "error while executing job: " + e.getMessage() );
+            throw e;
+        }
         finally
         {
             running = false;

+ 1 - 1
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java

@@ -259,7 +259,7 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
         if ( !isRunning() )
         {
             nextExecutionTime = Instant.now();
-            pwmApplication.getPwmScheduler().scheduleFutureJob( new PwNotifyJob(), executorService, TimeDuration.ZERO );
+            pwmApplication.getPwmScheduler().scheduleJob( new PwNotifyJob(), executorService, TimeDuration.ZERO );
         }
     }
 

+ 3 - 3
server/src/main/java/password/pwm/svc/report/ReportService.java

@@ -40,7 +40,7 @@ import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.TransactionSizeCalculator;
 import password.pwm.util.java.BlockingThreadPool;
@@ -398,7 +398,7 @@ public class ReportService implements PwmService
                         if ( executorService != null )
                         {
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background SearchLDAP, will retry; error: " + e.getMessage() );
-                            pwmApplication.getPwmScheduler().scheduleFutureJob( new ReadLDAPTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
+                            pwmApplication.getPwmScheduler().scheduleJob( new ReadLDAPTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
                             errorProcessed = true;
                         }
                     }
@@ -477,7 +477,7 @@ public class ReportService implements PwmService
                         if ( executorService != null )
                         {
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background ReadData, will retry; error: " + e.getMessage() );
-                            pwmApplication.getPwmScheduler().scheduleFutureJob( new ProcessWorkQueueTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
+                            pwmApplication.getPwmScheduler().scheduleJob( new ProcessWorkQueueTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
                         }
                     }
                     else

+ 4 - 0
server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java

@@ -32,9 +32,11 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.PwmRequest;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.IOException;
+import java.time.Instant;
 
 public class UserAgentUtils
 {
@@ -62,7 +64,9 @@ public class UserAgentUtils
 
     public static void initializeCache() throws PwmUnrecoverableException
     {
+        final Instant startTime = Instant.now();
         getUserAgentParser();
+        LOGGER.trace( () -> "loaded useragent parser in " + TimeDuration.compactFromCurrent( startTime ) );
     }
 
     public static void checkIfPreIE11( final PwmRequest pwmRequest ) throws PwmUnrecoverableException

+ 74 - 0
server/src/main/java/password/pwm/svc/stats/AvgStatistic.java

@@ -0,0 +1,74 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import password.pwm.i18n.Admin;
+import password.pwm.util.i18n.LocaleHelper;
+
+import java.util.Locale;
+
+public enum AvgStatistic
+{
+    AVG_PASSWORD_SYNC_TIME( "AvgPasswordSyncTime", null, "ms" ),
+    AVG_AUTHENTICATION_TIME( "AvgAuthenticationTime", null, "ms" ),
+    AVG_PASSWORD_STRENGTH( "AvgPasswordStrength", null, "" ),
+    AVG_LDAP_SEARCH_TIME( "AvgLdapSearchTime", null, "ms" ),
+    AVG_REQUEST_PROCESS_TIME( "AvgRequestProcessTime", null, "ms" ),;
+
+    private final String key;
+    private final Statistic.StatDetail statDetail;
+    private final String unit;
+
+    AvgStatistic(
+            final String key,
+            final Statistic.StatDetail statDetail,
+            final String unit
+    )
+    {
+        this.key = key;
+        this.statDetail = statDetail;
+        this.unit = unit;
+    }
+
+    public String getKey( )
+    {
+        return key;
+    }
+
+    public String getUnit()
+    {
+        return unit;
+    }
+
+    public String getLabel( final Locale locale )
+    {
+        final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
+    }
+
+    public String getDescription( final Locale locale )
+    {
+        final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
+    }
+}

+ 79 - 0
server/src/main/java/password/pwm/svc/stats/DailyKey.java

@@ -0,0 +1,79 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import lombok.Value;
+
+import java.time.LocalDate;
+
+@Value
+public class DailyKey
+{
+    private static final String DB_KEY_PREFIX_DAILY = "DAILY_";
+    private int year;
+    private int day;
+
+    private DailyKey()
+    {
+        final LocalDate localDate = LocalDate.now();
+        year = localDate.getYear();
+        day = localDate.getDayOfYear();
+    }
+
+    DailyKey( final String value )
+    {
+        final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length() );
+        final String[] splitValue = strippedValue.split( "_" );
+        year = Integer.parseInt( splitValue[ 0 ] );
+        day = Integer.parseInt( splitValue[ 1 ] );
+    }
+
+    private DailyKey( final int year, final int day )
+    {
+        this.year = year;
+        this.day = day;
+    }
+
+    @Override
+    public String toString( )
+    {
+        return DB_KEY_PREFIX_DAILY + year + "_" + day;
+    }
+
+    public static DailyKey forToday()
+    {
+        return new DailyKey( );
+    }
+
+    public DailyKey previous( )
+    {
+        final LocalDate thisDay = localDate();
+        final LocalDate previousDay = thisDay.minusDays( 1 );
+        return new DailyKey( previousDay.getYear(), previousDay.getDayOfYear() );
+    }
+
+    public LocalDate localDate()
+    {
+        return LocalDate.ofYearDay( year, day );
+    }
+}

+ 49 - 0
server/src/main/java/password/pwm/svc/stats/EpsKey.java

@@ -0,0 +1,49 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import lombok.Value;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Value
+class EpsKey
+{
+    static final String DB_KEY_PREFIX = "EPS-";
+    private EpsStatistic epsStatistic;
+    private Statistic.EpsDuration epsDuration;
+
+    static Set<EpsKey> allKeys()
+    {
+        final Set<EpsKey> returnSet = new HashSet<>();
+        for ( final EpsStatistic epsStatistic : EpsStatistic.values() )
+        {
+            for ( final Statistic.EpsDuration epsDuration : Statistic.EpsDuration.values() )
+            {
+                returnSet.add( new EpsKey( epsStatistic, epsDuration ) );
+            }
+        }
+        return returnSet;
+    }
+}

+ 10 - 21
server/src/main/java/password/pwm/svc/stats/EpsStatistic.java

@@ -29,27 +29,16 @@ import java.util.Locale;
 
 public enum EpsStatistic
 {
-    REQUESTS( null ),
-    SESSIONS( null ),
-    PASSWORD_CHANGES( Statistic.PASSWORD_CHANGES ),
-    AUTHENTICATION( Statistic.AUTHENTICATIONS ),
-    INTRUDER_ATTEMPTS( Statistic.INTRUDER_ATTEMPTS ),
-    PWMDB_WRITES( null ),
-    PWMDB_READS( null ),
-    DB_WRITES( null ),
-    DB_READS( null ),;
-
-    private Statistic relatedStatistic;
-
-    EpsStatistic( final Statistic relatedStatistic )
-    {
-        this.relatedStatistic = relatedStatistic;
-    }
-
-    public Statistic getRelatedStatistic( )
-    {
-        return relatedStatistic;
-    }
+    REQUESTS(),
+    SESSIONS(),
+    PASSWORD_CHANGES(),
+    AUTHENTICATION(),
+    INTRUDER_ATTEMPTS(),
+    PWMDB_WRITES(),
+    PWMDB_READS(),
+    DB_WRITES(),
+    DB_READS(),
+    LDAP_BINDS,;
 
     public String getLabel( final Locale locale )
     {

+ 27 - 0
server/src/main/java/password/pwm/svc/stats/StatKey.java

@@ -0,0 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+public interface StatKey
+{
+}

+ 80 - 117
server/src/main/java/password/pwm/svc/stats/Statistic.java

@@ -27,112 +27,101 @@ import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Admin;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
 
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Locale;
-import java.util.MissingResourceException;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
 public enum Statistic
 {
-    AUDIT_EVENTS( Type.INCREMENTER, "AuditEvents", null ),
-    AUTHENTICATIONS( Type.INCREMENTER, "Authentications", null ),
-    AUTHENTICATION_FAILURES( Type.INCREMENTER, "AuthenticationFailures", null ),
-    AUTHENTICATION_EXPIRED( Type.INCREMENTER, "Authentications_Expired", null ),
-    AUTHENTICATION_PRE_EXPIRED( Type.INCREMENTER, "Authentications_PreExpired", null ),
-    AUTHENTICATION_EXPIRED_WARNING( Type.INCREMENTER, "Authentications_ExpiredWarning", null ),
-    PWM_STARTUPS( Type.INCREMENTER, "PWM_Startups", null ),
-    PWM_UNKNOWN_ERRORS( Type.INCREMENTER, "PWM_UnknownErrors", null ),
-    PASSWORD_CHANGES( Type.INCREMENTER, "PasswordChanges", null ),
-    FORGOTTEN_USERNAME_FAILURES( Type.INCREMENTER, "ForgottenUsernameFailures", null ),
-    FORGOTTEN_USERNAME_SUCCESSES( Type.INCREMENTER, "ForgottenUsernameSuccesses", null ),
-    EMAIL_SEND_SUCCESSES( Type.INCREMENTER, "EmailSendSuccesses", null ),
-    EMAIL_SEND_FAILURES( Type.INCREMENTER, "EmailSendFailures", null ),
-    EMAIL_SEND_DISCARDS( Type.INCREMENTER, "EmailSendDiscards", null ),
-    SMS_SEND_SUCCESSES( Type.INCREMENTER, "SmsSendSuccesses", null ),
-    SMS_SEND_FAILURES( Type.INCREMENTER, "SmsSendFailures", null ),
-    SMS_SEND_DISCARDS( Type.INCREMENTER, "SmsSendDiscards", null ),
-    PASSWORD_RULE_CHECKS( Type.INCREMENTER, "PasswordRuleChecks", null ),
-    HTTP_REQUESTS( Type.INCREMENTER, "HttpRequests", null ),
-    HTTP_RESOURCE_REQUESTS( Type.INCREMENTER, "HttpResourceRequests", null ),
-    HTTP_SESSIONS( Type.INCREMENTER, "HttpSessions", null ),
-    ACTIVATED_USERS( Type.INCREMENTER, "ActivatedUsers", null ),
-    NEW_USERS( Type.INCREMENTER, "NewUsers", new ConfigSettingDetail( PwmSetting.NEWUSER_ENABLE ) ),
-    GUESTS( Type.INCREMENTER, "Guests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
-    UPDATED_GUESTS( Type.INCREMENTER, "UpdatedGuests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
-    LOCKED_USERS( Type.INCREMENTER, "LockedUsers", null ),
-    LOCKED_ADDRESSES( Type.INCREMENTER, "LockedAddresses", null ),
-    LOCKED_USERIDS( Type.INCREMENTER, "LockedUserDNs", null ),
-    LOCKED_ATTRIBUTES( Type.INCREMENTER, "LockedAttributes", null ),
-    LOCKED_TOKENDESTS( Type.INCREMENTER, "LockedTokenDests", null ),
-    CAPTCHA_SUCCESSES( Type.INCREMENTER, "CaptchaSuccessess", null ),
-    CAPTCHA_FAILURES( Type.INCREMENTER, "CaptchaFailures", null ),
-    CAPTCHA_PRESENTATIONS( Type.INCREMENTER, "CaptchaPresentations", null ),
-    LDAP_UNAVAILABLE_COUNT( Type.INCREMENTER, "LdapUnavailableCount", null ),
-    DB_UNAVAILABLE_COUNT( Type.INCREMENTER, "DatabaseUnavailableCount", null ),
-    SETUP_RESPONSES( Type.INCREMENTER, "SetupResponses", null ),
-    SETUP_OTP_SECRET( Type.INCREMENTER, "SetupOtpSecret", null ),
-    UPDATE_ATTRIBUTES( Type.INCREMENTER, "UpdateAttributes", new ConfigSettingDetail( PwmSetting.UPDATE_PROFILE_ENABLE ) ),
-    SHORTCUTS_SELECTED( Type.INCREMENTER, "ShortcutsSelected", new ConfigSettingDetail( PwmSetting.SHORTCUT_ENABLE ) ),
-    GENERATED_PASSWORDS( Type.INCREMENTER, "GeneratedPasswords", null ),
-    RECOVERY_SUCCESSES( Type.INCREMENTER, "RecoverySuccesses", null ),
-    RECOVERY_FAILURES( Type.INCREMENTER, "RecoveryFailures", null ),
-    TOKENS_SENT( Type.INCREMENTER, "TokensSent", null ),
-    TOKENS_PASSSED( Type.INCREMENTER, "TokensPassed", null ),
-    RECOVERY_TOKENS_SENT( Type.INCREMENTER, "RecoveryTokensSent", null ),
-    RECOVERY_TOKENS_PASSED( Type.INCREMENTER, "RecoveryTokensPassed", null ),
-    RECOVERY_TOKENS_FAILED( Type.INCREMENTER, "RecoveryTokensFailed", null ),
-    RECOVERY_OTP_PASSED( Type.INCREMENTER, "RecoveryOTPPassed", null ),
-    RECOVERY_OTP_FAILED( Type.INCREMENTER, "RecoveryOTPFailed", null ),
-    PEOPLESEARCH_CACHE_HITS( Type.INCREMENTER, "PeopleSearchCacheHits", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_CACHE_MISSES( Type.INCREMENTER, "PeopleSearchCacheMisses", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_SEARCHES( Type.INCREMENTER, "PeopleSearchSearches", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_DETAILS( Type.INCREMENTER, "PeopleSearchDetails", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_ORGCHART( Type.INCREMENTER, "PeopleSearchOrgChart", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PWNOTIFY_JOBS ( Type.INCREMENTER, "PwNotifyJobs", null ),
-    PWNOTIFY_JOB_ERRORS ( Type.INCREMENTER, "PwNotifyJobErrors", null ),
-    PWNOTIFY_EMAILS_SENT ( Type.INCREMENTER, "PwNotifyJobEmailsSent", null ),
-    HELPDESK_PASSWORD_SET( Type.INCREMENTER, "HelpdeskPasswordSet", null ),
-    HELPDESK_USER_LOOKUP( Type.INCREMENTER, "HelpdeskUserLookup", null ),
-    HELPDESK_TOKENS_SENT( Type.INCREMENTER, "HelpdeskTokenSent", null ),
-    HELPDESK_UNLOCK( Type.INCREMENTER, "HelpdeskUnlock", null ),
-    HELPDESK_VERIFY_OTP( Type.INCREMENTER, "HelpdeskVerifyOTP", null ),
-    REST_STATUS( Type.INCREMENTER, "RestStatus", null ),
-    REST_CHECKPASSWORD( Type.INCREMENTER, "RestCheckPassword", null ),
-    REST_SETPASSWORD( Type.INCREMENTER, "RestSetPassword", null ),
-    REST_RANDOMPASSWORD( Type.INCREMENTER, "RestRandomPassword", null ),
-    REST_PROFILE( Type.INCREMENTER, "RestProfile", null ),
-    REST_SIGNING_FORM( Type.INCREMENTER, "RestSigningForm", null ),
-    REST_CHALLENGES( Type.INCREMENTER, "RestChallenges", null ),
-    REST_HEALTH( Type.INCREMENTER, "RestHealth", null ),
-    REST_STATISTICS( Type.INCREMENTER, "RestStatistics", null ),
-    REST_VERIFYCHALLENGES( Type.INCREMENTER, "RestVerifyChallenges", null ),
-    REST_VERIFYOTP( Type.INCREMENTER, "RestVerifyOTP", null ),
-    INTRUDER_ATTEMPTS( Type.INCREMENTER, "IntruderAttempts", null ),
-    FOREIGN_SESSIONS_ACCEPTED( Type.INCREMENTER, "ForeignSessionsAccepted", null ),
-    OBSOLETE_URL_REQUESTS( Type.INCREMENTER, "ObsoleteUrlRequests", null ),
-    SYSLOG_MESSAGES_SENT( Type.INCREMENTER, "SyslogMessagesSent", null ),
+    AUDIT_EVENTS( "AuditEvents", null ),
+    AUTHENTICATIONS( "Authentications", null ),
+    AUTHENTICATION_FAILURES( "AuthenticationFailures", null ),
+    AUTHENTICATION_EXPIRED( "Authentications_Expired", null ),
+    AUTHENTICATION_PRE_EXPIRED( "Authentications_PreExpired", null ),
+    AUTHENTICATION_EXPIRED_WARNING( "Authentications_ExpiredWarning", null ),
+    PWM_STARTUPS( "PWM_Startups", null ),
+    PWM_UNKNOWN_ERRORS( "PWM_UnknownErrors", null ),
+    PASSWORD_CHANGES( "PasswordChanges", null ),
+    FORGOTTEN_USERNAME_FAILURES( "ForgottenUsernameFailures", null ),
+    FORGOTTEN_USERNAME_SUCCESSES( "ForgottenUsernameSuccesses", null ),
+    EMAIL_SEND_SUCCESSES( "EmailSendSuccesses", null ),
+    EMAIL_SEND_FAILURES( "EmailSendFailures", null ),
+    EMAIL_SEND_DISCARDS( "EmailSendDiscards", null ),
+    SMS_SEND_SUCCESSES( "SmsSendSuccesses", null ),
+    SMS_SEND_FAILURES( "SmsSendFailures", null ),
+    SMS_SEND_DISCARDS( "SmsSendDiscards", null ),
+    PASSWORD_RULE_CHECKS( "PasswordRuleChecks", null ),
+    HTTP_REQUESTS( "HttpRequests", null ),
+    HTTP_RESOURCE_REQUESTS( "HttpResourceRequests", null ),
+    HTTP_SESSIONS( "HttpSessions", null ),
+    ACTIVATED_USERS( "ActivatedUsers", null ),
+    NEW_USERS( "NewUsers", new ConfigSettingDetail( PwmSetting.NEWUSER_ENABLE ) ),
+    GUESTS( "Guests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
+    UPDATED_GUESTS( "UpdatedGuests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
+    LOCKED_USERS( "LockedUsers", null ),
+    LOCKED_ADDRESSES( "LockedAddresses", null ),
+    LOCKED_USERIDS( "LockedUserDNs", null ),
+    LOCKED_ATTRIBUTES( "LockedAttributes", null ),
+    LOCKED_TOKENDESTS( "LockedTokenDests", null ),
+    CAPTCHA_SUCCESSES( "CaptchaSuccessess", null ),
+    CAPTCHA_FAILURES( "CaptchaFailures", null ),
+    CAPTCHA_PRESENTATIONS( "CaptchaPresentations", null ),
+    LDAP_UNAVAILABLE_COUNT( "LdapUnavailableCount", null ),
+    DB_UNAVAILABLE_COUNT( "DatabaseUnavailableCount", null ),
+    SETUP_RESPONSES( "SetupResponses", null ),
+    SETUP_OTP_SECRET( "SetupOtpSecret", null ),
+    UPDATE_ATTRIBUTES( "UpdateAttributes", new ConfigSettingDetail( PwmSetting.UPDATE_PROFILE_ENABLE ) ),
+    SHORTCUTS_SELECTED( "ShortcutsSelected", new ConfigSettingDetail( PwmSetting.SHORTCUT_ENABLE ) ),
+    GENERATED_PASSWORDS( "GeneratedPasswords", null ),
+    RECOVERY_SUCCESSES( "RecoverySuccesses", null ),
+    RECOVERY_FAILURES( "RecoveryFailures", null ),
+    TOKENS_SENT( "TokensSent", null ),
+    TOKENS_PASSSED( "TokensPassed", null ),
+    RECOVERY_TOKENS_SENT( "RecoveryTokensSent", null ),
+    RECOVERY_TOKENS_PASSED( "RecoveryTokensPassed", null ),
+    RECOVERY_TOKENS_FAILED( "RecoveryTokensFailed", null ),
+    RECOVERY_OTP_PASSED( "RecoveryOTPPassed", null ),
+    RECOVERY_OTP_FAILED( "RecoveryOTPFailed", null ),
+    PEOPLESEARCH_CACHE_HITS( "PeopleSearchCacheHits", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_CACHE_MISSES( "PeopleSearchCacheMisses", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_SEARCHES( "PeopleSearchSearches", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_DETAILS( "PeopleSearchDetails", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_ORGCHART( "PeopleSearchOrgChart", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PWNOTIFY_JOBS ( "PwNotifyJobs", null ),
+    PWNOTIFY_JOB_ERRORS ( "PwNotifyJobErrors", null ),
+    PWNOTIFY_EMAILS_SENT ( "PwNotifyJobEmailsSent", null ),
+    HELPDESK_PASSWORD_SET( "HelpdeskPasswordSet", null ),
+    HELPDESK_USER_LOOKUP( "HelpdeskUserLookup", null ),
+    HELPDESK_TOKENS_SENT( "HelpdeskTokenSent", null ),
+    HELPDESK_UNLOCK( "HelpdeskUnlock", null ),
+    HELPDESK_VERIFY_OTP( "HelpdeskVerifyOTP", null ),
+    REST_STATUS( "RestStatus", null ),
+    REST_CHECKPASSWORD( "RestCheckPassword", null ),
+    REST_SETPASSWORD( "RestSetPassword", null ),
+    REST_RANDOMPASSWORD( "RestRandomPassword", null ),
+    REST_PROFILE( "RestProfile", null ),
+    REST_SIGNING_FORM( "RestSigningForm", null ),
+    REST_CHALLENGES( "RestChallenges", null ),
+    REST_HEALTH( "RestHealth", null ),
+    REST_STATISTICS( "RestStatistics", null ),
+    REST_VERIFYCHALLENGES( "RestVerifyChallenges", null ),
+    REST_VERIFYOTP( "RestVerifyOTP", null ),
+    INTRUDER_ATTEMPTS( "IntruderAttempts", null ),
+    FOREIGN_SESSIONS_ACCEPTED( "ForeignSessionsAccepted", null ),
+    OBSOLETE_URL_REQUESTS( "ObsoleteUrlRequests", null ),
+    SYSLOG_MESSAGES_SENT( "SyslogMessagesSent", null ),;
 
-    AVG_PASSWORD_SYNC_TIME( Type.AVERAGE, "AvgPasswordSyncTime", null ),
-    AVG_AUTHENTICATION_TIME( Type.AVERAGE, "AvgAuthenticationTime", null ),
-    AVG_PASSWORD_STRENGTH( Type.AVERAGE, "AvgPasswordStrength", null ),
-    AVG_LDAP_SEARCH_TIME( Type.AVERAGE, "AvgLdapSearchTime", null ),;
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( Statistic.class );
-    private final Type type;
     private final String key;
     private final StatDetail statDetail;
 
     Statistic(
-            final Type type,
             final String key,
             final StatDetail statDetail
     )
     {
-        this.type = type;
         this.key = key;
         this.statDetail = statDetail;
     }
@@ -142,11 +131,6 @@ public enum Statistic
         return key;
     }
 
-    public Type getType( )
-    {
-        return type;
-    }
-
     public boolean isActive( final PwmApplication pwmApplication )
     {
         if ( statDetail == null )
@@ -164,37 +148,16 @@ public enum Statistic
         return set;
     }
 
-    public enum Type
-    {
-        INCREMENTER,
-        AVERAGE,
-    }
-
     public String getLabel( final Locale locale )
     {
-        try
-        {
-            final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
-            return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
-        }
-        catch ( MissingResourceException e )
-        {
-            return "MISSING STATISTIC LABEL for " + this.getKey();
-        }
+        final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
     }
 
     public String getDescription( final Locale locale )
     {
         final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
-        try
-        {
-            return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( "unable to load localization for " + keyName + ", error: " + e.getMessage() );
-            return "missing localization for " + keyName;
-        }
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
     }
 
     public enum EpsDuration

+ 30 - 0
server/src/main/java/password/pwm/svc/stats/StatisticType.java

@@ -0,0 +1,30 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+public enum StatisticType
+{
+    INCREMENTER,
+    AVERAGE,
+    EPS,
+}

+ 63 - 106
server/src/main/java/password/pwm/svc/stats/StatisticsBundle.java

@@ -22,153 +22,105 @@
 
 package password.pwm.svc.stats;
 
+import password.pwm.util.java.AtomicLoopLongIncrementer;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
 import java.math.BigInteger;
-import java.text.SimpleDateFormat;
-import java.util.HashMap;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.TimeZone;
 
 public class StatisticsBundle
 {
+    private final Map<Statistic, AtomicLoopLongIncrementer> incrementerMap = new EnumMap<>( Statistic.class );
+    private final Map<AvgStatistic, AverageBean> avgMap = new EnumMap<>( AvgStatistic.class );
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StatisticsBundle.class );
-
-    static final SimpleDateFormat STORED_DATETIME_FORMATTER = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss Z" );
-
-    static
-    {
-        STORED_DATETIME_FORMATTER.setTimeZone( TimeZone.getTimeZone( "Zulu" ) );
-    }
-
-
-    private final Map<Statistic, String> valueMap = new HashMap<>();
-
-    public StatisticsBundle( )
+    StatisticsBundle( )
     {
+        for ( final Statistic statistic : Statistic.values() )
+        {
+            incrementerMap.put( statistic, new AtomicLoopLongIncrementer( 0, Long.MAX_VALUE ) );
+        }
+        for ( final AvgStatistic avgStatistic : AvgStatistic.values() )
+        {
+            avgMap.put( avgStatistic, new AverageBean() );
+        }
     }
 
     public String output( )
     {
-        return JsonUtil.serializeMap( valueMap );
-    }
+        final Map<String, String> outputMap = new LinkedHashMap<>();
 
-    public static StatisticsBundle input( final String inputString )
-    {
-        final Map<Statistic, String> srcMap = new HashMap<>();
-        final Map<String, String> loadedMap = JsonUtil.deserializeStringMap( inputString );
-        for ( final Map.Entry<String, String> entry : loadedMap.entrySet() )
+        for ( final Statistic statistic : Statistic.values() )
         {
-            final String key = entry.getKey();
-            try
+            final long currentValue = incrementerMap.get( statistic ).get();
+            if ( currentValue > 0 )
             {
-                srcMap.put( Statistic.valueOf( key ), entry.getValue() );
-            }
-            catch ( IllegalArgumentException e )
-            {
-                LOGGER.error( "error parsing statistic key '" + key + "', reason: " + e.getMessage() );
+                outputMap.put( statistic.name(), Long.toString( currentValue ) );
             }
         }
-        final StatisticsBundle bundle = new StatisticsBundle();
-
-        for ( final Statistic loopStat : Statistic.values() )
+        for ( final AvgStatistic epsStatistic : AvgStatistic.values() )
         {
-            final String value = srcMap.get( loopStat );
-            if ( !StringUtil.isEmpty( value ) )
+            final AverageBean averageBean = avgMap.get( epsStatistic );
+            if ( !averageBean.isZero() )
             {
-                bundle.valueMap.put( loopStat, value );
+                outputMap.put( epsStatistic.name(), JsonUtil.serialize( averageBean ) );
             }
         }
 
-        return bundle;
+        return JsonUtil.serializeMap( outputMap );
     }
 
-    public synchronized void incrementValue( final Statistic statistic )
+    public static StatisticsBundle input( final String inputString )
     {
-        if ( Statistic.Type.INCREMENTER != statistic.getType() )
-        {
-            LOGGER.error( "attempt to increment non-counter/incremental stat " + statistic );
-            return;
-        }
+        final Map<String, String> loadedMap = JsonUtil.deserializeStringMap( inputString );
+        final StatisticsBundle bundle = new StatisticsBundle();
 
-        BigInteger currentValue = BigInteger.ZERO;
-        try
+        for ( final Statistic loopStat : Statistic.values() )
         {
-            if ( valueMap.containsKey( statistic ) )
-            {
-                currentValue = new BigInteger( valueMap.get( statistic ) );
-            }
-            else
+            final String value = loadedMap.get( loopStat.name() );
+            if ( !StringUtil.isEmpty( value ) )
             {
-                currentValue = BigInteger.ZERO;
+                final long longValue = JavaHelper.silentParseLong( value, 0 );
+                final AtomicLoopLongIncrementer incrementer = new AtomicLoopLongIncrementer( longValue, Long.MAX_VALUE );
+                bundle.incrementerMap.put( loopStat, incrementer );
             }
         }
-        catch ( NumberFormatException e )
-        {
-            LOGGER.error( "error reading counter/incremental stat " + statistic );
-        }
-        final BigInteger newValue = currentValue.add( BigInteger.ONE );
-        valueMap.put( statistic, newValue.toString() );
-    }
-
-    public synchronized void updateAverageValue( final Statistic statistic, final long timeDuration )
-    {
-        if ( Statistic.Type.AVERAGE != statistic.getType() )
-        {
-            LOGGER.error( "attempt to update average value of non-average stat " + statistic );
-            return;
-        }
 
-        final String avgStrValue = valueMap.get( statistic );
-
-        AverageBean avgBean = new AverageBean();
-        if ( avgStrValue != null && avgStrValue.length() > 0 )
+        for ( final AvgStatistic loopStat : AvgStatistic.values() )
         {
-            try
-            {
-                avgBean = JsonUtil.deserialize( avgStrValue, AverageBean.class );
-            }
-            catch ( Exception e )
+            final String value = loadedMap.get( loopStat.name() );
+            if ( !StringUtil.isEmpty( value ) )
             {
-                LOGGER.trace( () -> "unable to parse statistics value for stat " + statistic.toString() + ", value=" + avgStrValue );
+                final AverageBean avgBean = JsonUtil.deserialize( value, AverageBean.class );
+                bundle.avgMap.put( loopStat, avgBean );
             }
         }
 
-        avgBean.appendValue( timeDuration );
-        valueMap.put( statistic, JsonUtil.serialize( avgBean ) );
+        return bundle;
+    }
+
+    void incrementValue( final Statistic statistic )
+    {
+        incrementerMap.get( statistic ).incrementAndGet();
+    }
+
+    void updateAverageValue( final AvgStatistic statistic, final long timeDuration )
+    {
+        avgMap.get( statistic ).appendValue( timeDuration );
     }
 
     public String getStatistic( final Statistic statistic )
     {
-        switch ( statistic.getType() )
-        {
-            case INCREMENTER:
-                return valueMap.containsKey( statistic ) ? valueMap.get( statistic ) : "0";
-
-            case AVERAGE:
-                final String avgStrValue = valueMap.get( statistic );
-
-                AverageBean avgBean = new AverageBean();
-                if ( avgStrValue != null && avgStrValue.length() > 0 )
-                {
-                    try
-                    {
-                        avgBean = JsonUtil.deserialize( avgStrValue, AverageBean.class );
-                    }
-                    catch ( Exception e )
-                    {
-                        LOGGER.trace( () ->  "unable to parse statistics value for stat " + statistic.toString() + ", value=" + avgStrValue );
-                    }
-                }
-                return avgBean.getAverage().toString();
-
-            default:
-                return "";
-        }
+        return Long.toString( incrementerMap.get( statistic ).get() );
+    }
+
+    public String getAvgStatistic( final AvgStatistic statistic )
+    {
+        return avgMap.get( statistic ).getAverage().toString();
     }
 
     private static class AverageBean implements Serializable
@@ -180,7 +132,7 @@ public class StatisticsBundle
         {
         }
 
-        BigInteger getAverage( )
+        synchronized BigInteger getAverage( )
         {
             if ( BigInteger.ZERO.equals( count ) )
             {
@@ -190,10 +142,15 @@ public class StatisticsBundle
             return total.divide( count );
         }
 
-        void appendValue( final long value )
+        synchronized void appendValue( final long value )
         {
             count = count.add( BigInteger.ONE );
             total = total.add( BigInteger.valueOf( value ) );
         }
+
+        synchronized boolean isZero()
+        {
+            return total.equals( BigInteger.ZERO );
+        }
     }
 }

+ 41 - 177
server/src/main/java/password/pwm/svc/stats/StatisticsManager.java

@@ -30,9 +30,10 @@ import password.pwm.error.PwmException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.svc.PwmService;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
@@ -41,19 +42,14 @@ import password.pwm.util.logging.PwmLogger;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.TimeZone;
 import java.util.TimerTask;
 import java.util.concurrent.ExecutorService;
 
@@ -68,7 +64,6 @@ public class StatisticsManager implements PwmService
     private static final String DB_KEY_VERSION = "STATS_VERSION";
     private static final String DB_KEY_CUMULATIVE = "CUMULATIVE";
     private static final String DB_KEY_INITIAL_DAILY_KEY = "INITIAL_DAILY_KEY";
-    private static final String DB_KEY_PREFIX_DAILY = "DAILY_";
     private static final String DB_KEY_TEMP = "TEMP_KEY";
 
     private static final String DB_VALUE_VERSION = "1";
@@ -78,15 +73,15 @@ public class StatisticsManager implements PwmService
 
     private LocalDB localDB;
 
-    private DailyKey currentDailyKey = new DailyKey( new Date() );
-    private DailyKey initialDailyKey = new DailyKey( new Date() );
+    private DailyKey currentDailyKey = DailyKey.forToday();
+    private DailyKey initialDailyKey = DailyKey.forToday();
 
     private ExecutorService executorService;
 
     private final StatisticsBundle statsCurrent = new StatisticsBundle();
     private StatisticsBundle statsDaily = new StatisticsBundle();
     private StatisticsBundle statsCummulative = new StatisticsBundle();
-    private Map<String, EventRateMeter> epsMeterMap = new HashMap<>();
+    private Map<EpsKey, EventRateMeter> epsMeterMap = new HashMap<>();
 
     private PwmApplication pwmApplication;
 
@@ -104,16 +99,20 @@ public class StatisticsManager implements PwmService
 
     public StatisticsManager( )
     {
+        for ( final EpsKey epsKey : EpsKey.allKeys() )
+        {
+            epsMeterMap.put( epsKey, new EventRateMeter( epsKey.getEpsDuration().getTimeDuration() ) );
+        }
     }
 
-    public synchronized void incrementValue( final Statistic statistic )
+    public void incrementValue( final Statistic statistic )
     {
         statsCurrent.incrementValue( statistic );
         statsDaily.incrementValue( statistic );
         statsCummulative.incrementValue( statistic );
     }
 
-    public synchronized void updateAverageValue( final Statistic statistic, final long value )
+    public void updateAverageValue( final AvgStatistic statistic, final long value )
     {
         statsCurrent.updateAverageValue( statistic, value );
         statsDaily.updateAverageValue( statistic, value );
@@ -130,7 +129,7 @@ public class StatisticsManager implements PwmService
             final StatisticsBundle bundle = getStatBundleForKey( loopKey.toString() );
             if ( bundle != null )
             {
-                final String key = ( new SimpleDateFormat( "MMM dd" ) ).format( loopKey.calendar().getTime() );
+                final String key = loopKey.toString();
                 final String value = bundle.getStatistic( statistic );
                 returnMap.put( key, value );
             }
@@ -171,7 +170,7 @@ public class StatisticsManager implements PwmService
         {
             final String storedStat = localDB.get( LocalDB.DB.PWM_STATS, key );
             final StatisticsBundle returnBundle;
-            if ( storedStat != null && storedStat.length() > 0 )
+            if ( !StringUtil.isEmpty( storedStat ) )
             {
                 returnBundle = StatisticsBundle.input( storedStat );
             }
@@ -192,13 +191,9 @@ public class StatisticsManager implements PwmService
 
     public Map<DailyKey, String> getAvailableKeys( final Locale locale )
     {
-        final DateFormat dateFormatter = SimpleDateFormat.getDateInstance( SimpleDateFormat.DEFAULT, locale );
-        final Map<DailyKey, String> returnMap = new LinkedHashMap<DailyKey, String>();
-
-        // add current time;
-        returnMap.put( currentDailyKey, dateFormatter.format( new Date() ) );
+        final Map<DailyKey, String> returnMap = new LinkedHashMap<>();
 
-        // if now historical data then we're done
+        // if no historical data then we're done
         if ( currentDailyKey.equals( initialDailyKey ) )
         {
             return returnMap;
@@ -208,8 +203,7 @@ public class StatisticsManager implements PwmService
         int safetyCounter = 0;
         while ( !loopKey.equals( initialDailyKey ) && safetyCounter < 5000 )
         {
-            final Calendar c = loopKey.calendar();
-            final String display = dateFormatter.format( c.getTime() );
+            final String display = loopKey.toString();
             returnMap.put( loopKey, display );
             loopKey = loopKey.previous();
             safetyCounter++;
@@ -239,14 +233,6 @@ public class StatisticsManager implements PwmService
 
     public void init( final PwmApplication pwmApplication ) throws PwmException
     {
-        for ( final EpsStatistic type : EpsStatistic.values() )
-        {
-            for ( final Statistic.EpsDuration duration : Statistic.EpsDuration.values() )
-            {
-                epsMeterMap.put( type.toString() + duration.toString(), new EventRateMeter( duration.getTimeDuration() ) );
-            }
-        }
-
         status = STATUS.OPENING;
         this.localDB = pwmApplication.getLocalDB();
         this.pwmApplication = pwmApplication;
@@ -259,56 +245,32 @@ public class StatisticsManager implements PwmService
         }
 
         {
-            final String storedCummulativeBundleStr = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE );
-            if ( storedCummulativeBundleStr != null && storedCummulativeBundleStr.length() > 0 )
+            final String storedCumulativeBundleSir = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE );
+            if ( !StringUtil.isEmpty( storedCumulativeBundleSir ) )
             {
                 try
                 {
-                    statsCummulative = StatisticsBundle.input( storedCummulativeBundleStr );
+                    statsCummulative = StatisticsBundle.input( storedCumulativeBundleSir );
                 }
                 catch ( Exception e )
                 {
-                    LOGGER.warn( "error loading saved stored statistics: " + e.getMessage() );
+                    LOGGER.warn( "error loading saved stored cumulative statistics: " + e.getMessage() );
                 }
             }
         }
 
-        {
-            for ( final EpsStatistic loopEpsType : EpsStatistic.values() )
-            {
-                for ( final EpsStatistic loopEpsDuration : EpsStatistic.values() )
-                {
-                    final String key = "EPS-" + loopEpsType.toString() + loopEpsDuration.toString();
-                    final String storedValue = localDB.get( LocalDB.DB.PWM_STATS, key );
-                    if ( storedValue != null && storedValue.length() > 0 )
-                    {
-                        try
-                        {
-                            final EventRateMeter eventRateMeter = JsonUtil.deserialize( storedValue, EventRateMeter.class );
-                            epsMeterMap.put( loopEpsType.toString() + loopEpsDuration.toString(), eventRateMeter );
-                        }
-                        catch ( Exception e )
-                        {
-                            LOGGER.error( "unexpected error reading last EPS rate for " + loopEpsType + " from LocalDB: " + e.getMessage() );
-                        }
-                    }
-                }
-            }
-
-        }
-
         {
             final String storedInitialString = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_INITIAL_DAILY_KEY );
-            if ( storedInitialString != null && storedInitialString.length() > 0 )
+            if ( !StringUtil.isEmpty( storedInitialString ) )
             {
                 initialDailyKey = new DailyKey( storedInitialString );
             }
         }
 
         {
-            currentDailyKey = new DailyKey( new Date() );
+            currentDailyKey = DailyKey.forToday();
             final String storedDailyStr = localDB.get( LocalDB.DB.PWM_STATS, currentDailyKey.toString() );
-            if ( storedDailyStr != null && storedDailyStr.length() > 0 )
+            if ( !StringUtil.isEmpty( storedDailyStr ) )
             {
                 statsDaily = StatisticsBundle.input( storedDailyStr );
             }
@@ -316,7 +278,7 @@ public class StatisticsManager implements PwmService
 
         try
         {
-            localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_TEMP, JavaHelper.toIsoDate( new Date() ) );
+            localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_TEMP, JavaHelper.toIsoDate( Instant.now() ) );
         }
         catch ( IllegalStateException e )
         {
@@ -340,30 +302,20 @@ public class StatisticsManager implements PwmService
 
     private void writeDbValues( )
     {
-        if ( localDB != null )
+        if ( localDB != null && status == STATUS.OPEN )
         {
             try
             {
-                localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE, statsCummulative.output() );
-                localDB.put( LocalDB.DB.PWM_STATS, currentDailyKey.toString(), statsDaily.output() );
-
-                for ( final EpsStatistic loopEpsType : EpsStatistic.values() )
-                {
-                    for ( final Statistic.EpsDuration loopEpsDuration : Statistic.EpsDuration.values() )
-                    {
-                        final String key = "EPS-" + loopEpsType.toString();
-                        final String mapKey = loopEpsType.toString() + loopEpsDuration.toString();
-                        final String value = JsonUtil.serialize( this.epsMeterMap.get( mapKey ) );
-                        localDB.put( LocalDB.DB.PWM_STATS, key, value );
-                    }
-                }
+                final Map<String, String> dbData = new LinkedHashMap<>();
+                dbData.put( DB_KEY_CUMULATIVE, statsCummulative.output() );
+                dbData.put( currentDailyKey.toString(), statsDaily.output() );
+                localDB.putAll( LocalDB.DB.PWM_STATS, dbData );
             }
             catch ( LocalDBException e )
             {
                 LOGGER.error( "error outputting pwm statistics: " + e.getMessage() );
             }
         }
-
     }
 
     public Map<String, String> dailyStatisticsAsLabelValueMap()
@@ -381,7 +333,7 @@ public class StatisticsManager implements PwmService
 
     private void resetDailyStats( )
     {
-        currentDailyKey = new DailyKey( new Date() );
+        currentDailyKey = DailyKey.forToday();
         statsDaily = new StatisticsBundle();
         LOGGER.debug( () -> "reset daily statistics" );
     }
@@ -431,102 +383,19 @@ public class StatisticsManager implements PwmService
         }
     }
 
-
-    public static class DailyKey
-    {
-        int year;
-        int day;
-
-        public DailyKey( final Date date )
-        {
-            final Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
-            calendar.setTime( date );
-            year = calendar.get( Calendar.YEAR );
-            day = calendar.get( Calendar.DAY_OF_YEAR );
-        }
-
-        public DailyKey( final String value )
-        {
-            final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length() );
-            final String[] splitValue = strippedValue.split( "_" );
-            year = Integer.parseInt( splitValue[ 0 ] );
-            day = Integer.parseInt( splitValue[ 1 ] );
-        }
-
-        private DailyKey( )
-        {
-        }
-
-        @Override
-        public String toString( )
-        {
-            return DB_KEY_PREFIX_DAILY + year + "_" + day;
-        }
-
-        public DailyKey previous( )
-        {
-            final Calendar calendar = calendar();
-            calendar.add( Calendar.HOUR, -24 );
-            final DailyKey newKey = new DailyKey();
-            newKey.year = calendar.get( Calendar.YEAR );
-            newKey.day = calendar.get( Calendar.DAY_OF_YEAR );
-            return newKey;
-        }
-
-        public Calendar calendar( )
-        {
-            final Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
-            calendar.set( Calendar.YEAR, year );
-            calendar.set( Calendar.DAY_OF_YEAR, day );
-            return calendar;
-        }
-
-        @Override
-        public boolean equals( final Object o )
-        {
-            if ( this == o )
-            {
-                return true;
-            }
-            if ( o == null || getClass() != o.getClass() )
-            {
-                return false;
-            }
-
-            final DailyKey key = ( DailyKey ) o;
-
-            if ( day != key.day )
-            {
-                return false;
-            }
-            if ( year != key.year )
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        @Override
-        public int hashCode( )
-        {
-            int result = year;
-            result = 31 * result + day;
-            return result;
-        }
-    }
-
     public void updateEps( final EpsStatistic type, final int itemCount )
     {
         for ( final Statistic.EpsDuration duration : Statistic.EpsDuration.values() )
         {
-            epsMeterMap.get( type.toString() + duration.toString() ).markEvents( itemCount );
+            final EpsKey epsKey = new EpsKey( type, duration );
+            epsMeterMap.get( epsKey ).markEvents( itemCount );
         }
     }
 
     public BigDecimal readEps( final EpsStatistic type, final Statistic.EpsDuration duration )
     {
-        return epsMeterMap.get( type.toString() + duration.toString() ).readEventRate();
+        final EpsKey epsKey = new EpsKey( type, duration );
+        return epsMeterMap.get( epsKey ).readEventRate();
     }
 
 
@@ -553,15 +422,15 @@ public class StatisticsManager implements PwmService
         }
 
         int counter = 0;
-        final Map<StatisticsManager.DailyKey, String> keys = statsManger.getAvailableKeys( PwmConstants.DEFAULT_LOCALE );
-        for ( final StatisticsManager.DailyKey loopKey : keys.keySet() )
+        final Map<DailyKey, String> keys = statsManger.getAvailableKeys( PwmConstants.DEFAULT_LOCALE );
+        for ( final DailyKey loopKey : keys.keySet() )
         {
             counter++;
             final StatisticsBundle bundle = statsManger.getStatBundleForKey( loopKey.toString() );
             final List<String> lineOutput = new ArrayList<>();
             lineOutput.add( loopKey.toString() );
-            lineOutput.add( String.valueOf( loopKey.year ) );
-            lineOutput.add( String.valueOf( loopKey.day ) );
+            lineOutput.add( String.valueOf( loopKey.getYear() ) );
+            lineOutput.add( String.valueOf( loopKey.getDay() ) );
             for ( final Statistic stat : Statistic.values() )
             {
                 lineOutput.add( bundle.getStatistic( stat ) );
@@ -580,14 +449,9 @@ public class StatisticsManager implements PwmService
 
     public ServiceInfoBean serviceInfo( )
     {
-        if ( status() == STATUS.OPEN )
-        {
-            return new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ) );
-        }
-        else
-        {
-            return new ServiceInfoBean( Collections.<DataStorageMethod>emptyList() );
-        }
+        return status() == STATUS.OPEN
+                ? new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ) )
+                : new ServiceInfoBean( Collections.emptyList() );
     }
 
     public static void incrementStat(

+ 1 - 1
server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java

@@ -215,7 +215,7 @@ public class TelemetryService implements PwmService
     private void scheduleNextJob( )
     {
         final TimeDuration durationUntilNextPublish = durationUntilNextPublish();
-        pwmApplication.getPwmScheduler().scheduleFutureJob( new PublishJob(), executorService, durationUntilNextPublish );
+        pwmApplication.getPwmScheduler().scheduleJob( new PublishJob(), executorService, durationUntilNextPublish );
         LOGGER.trace( SessionLabel.TELEMETRY_SESSION_LABEL, () -> "next publish time: " + durationUntilNextPublish().asCompactString() );
     }
 

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

@@ -40,7 +40,6 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmHashAlgorithm;
 
 import java.io.InputStream;
 import java.time.Instant;
@@ -53,7 +52,6 @@ import java.util.function.BooleanSupplier;
 
 abstract class AbstractWordlist implements Wordlist, PwmService
 {
-    static final PwmHashAlgorithm CHECKSUM_HASH_ALG = PwmHashAlgorithm.SHA256;
     static final TimeDuration DEBUG_OUTPUT_FREQUENCY = TimeDuration.MINUTE;
 
     private WordlistConfiguration wordlistConfiguration;

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

@@ -141,7 +141,7 @@ class WordlistSource
 
             inputStream = this.streamProvider.getInputStream();
 
-            final ChecksumInputStream checksumInputStream = new ChecksumInputStream( AbstractWordlist.CHECKSUM_HASH_ALG, inputStream );
+            final ChecksumInputStream checksumInputStream = new ChecksumInputStream( inputStream );
             final CountingInputStream countingInputStream = new CountingInputStream( checksumInputStream );
 
             final ConditionalTaskExecutor debugOutputter = new ConditionalTaskExecutor(
@@ -162,7 +162,7 @@ class WordlistSource
                 return null;
             }
 
-            final String hash = JavaHelper.binaryArrayToHex( checksumInputStream.closeAndFinalChecksum() );
+            final String hash = JavaHelper.binaryArrayToHex( checksumInputStream.readUntilEndAndChecksum().copyOf() );
 
             final WordlistSourceInfo wordlistSourceInfo = new WordlistSourceInfo(
                     hash,

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

@@ -57,7 +57,7 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
     WordlistZipReader( final InputStream inputStream ) throws PwmUnrecoverableException
     {
-        checksumInputStream = new ChecksumInputStream( AbstractWordlist.CHECKSUM_HASH_ALG, inputStream );
+        checksumInputStream = new ChecksumInputStream( inputStream );
         countingInputStream = new CountingInputStream( checksumInputStream );
 
         zipStream = new ZipInputStream( countingInputStream );
@@ -161,6 +161,6 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
     String getChecksum()
     {
-        return JavaHelper.binaryArrayToHex( checksumInputStream.getInProgressChecksum() );
+        return JavaHelper.binaryArrayToHex( checksumInputStream.checksum().copyOf() );
     }
 }

+ 7 - 0
server/src/main/java/password/pwm/util/CaptchaUtility.java

@@ -331,6 +331,12 @@ public class CaptchaUtility
     private static boolean checkIfCaptchaParamPresent( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException
     {
+        if ( pwmRequest.getPwmSession().getSessionStateBean().isCaptchaBypassedViaParameter() )
+        {
+            LOGGER.trace( pwmRequest, () -> "valid skipCaptcha value previously received in session, skipping captcha check" );
+            return true;
+        }
+
         final String skipCaptcha = pwmRequest.readParameterAsString( PwmConstants.PARAM_SKIP_CAPTCHA );
         if ( skipCaptcha != null && skipCaptcha.length() > 0 )
         {
@@ -338,6 +344,7 @@ public class CaptchaUtility
             if ( configValue != null && configValue.equals( skipCaptcha ) )
             {
                 LOGGER.trace( pwmRequest, () -> "valid skipCaptcha value in request, skipping captcha check for this session" );
+                pwmRequest.getPwmSession().getSessionStateBean().setCaptchaBypassedViaParameter( true );
                 return true;
             }
             else

+ 1 - 2
server/src/main/java/password/pwm/svc/stats/EventRateMeter.java → server/src/main/java/password/pwm/util/EventRateMeter.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.svc.stats;
+package password.pwm.util;
 
 import password.pwm.util.java.TimeDuration;
 
@@ -29,7 +29,6 @@ import java.math.BigDecimal;
 
 public class EventRateMeter implements Serializable
 {
-
     private final TimeDuration maxDuration;
 
     private MovingAverage movingAverage;

+ 13 - 10
server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java

@@ -57,7 +57,6 @@ import password.pwm.util.operations.PasswordUtility;
 import password.pwm.ws.client.rest.RestClientHelper;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -77,28 +76,34 @@ public class PwmPasswordRuleValidator
     private final PwmApplication pwmApplication;
     private final PwmPasswordPolicy policy;
     private final Locale locale;
+    private final Flag[] flags;
+
 
     public enum Flag
     {
         FailFast,
+        BypassLdapRuleCheck,
     }
 
-    public PwmPasswordRuleValidator( final PwmApplication pwmApplication, final PwmPasswordPolicy policy )
+    public PwmPasswordRuleValidator( final PwmApplication pwmApplication, final PwmPasswordPolicy policy, final Flag... flags )
     {
         this.pwmApplication = pwmApplication;
         this.policy = policy;
         this.locale = PwmConstants.DEFAULT_LOCALE;
+        this.flags = flags;
     }
 
     public PwmPasswordRuleValidator(
             final PwmApplication pwmApplication,
             final PwmPasswordPolicy policy,
-            final Locale locale
+            final Locale locale,
+            final Flag... flags
     )
     {
         this.pwmApplication = pwmApplication;
         this.policy = policy;
         this.locale = locale;
+        this.flags = flags;
     }
 
     public boolean testPassword(
@@ -116,7 +121,7 @@ public class PwmPasswordRuleValidator
             throw new PwmDataValidationException( errorResults.iterator().next() );
         }
 
-        if ( user != null )
+        if ( user != null && !JavaHelper.enumArrayContainsValue( flags, Flag.BypassLdapRuleCheck ) )
         {
             try
             {
@@ -183,26 +188,24 @@ public class PwmPasswordRuleValidator
     public List<ErrorInformation> internalPwmPolicyValidator(
             final PasswordData password,
             final PasswordData oldPassword,
-            final UserInfo userInfo,
-            final Flag... flags
+            final UserInfo userInfo
     )
             throws PwmUnrecoverableException
     {
         final String passwordString = password == null ? "" : password.getStringValue();
         final String oldPasswordString = oldPassword == null ? null : oldPassword.getStringValue();
-        return internalPwmPolicyValidator( passwordString, oldPasswordString, userInfo, flags );
+        return internalPwmPolicyValidator( passwordString, oldPasswordString, userInfo );
     }
 
     @SuppressWarnings( "checkstyle:MethodLength" )
     public List<ErrorInformation> internalPwmPolicyValidator(
             final String passwordString,
             final String oldPasswordString,
-            final UserInfo userInfo,
-            final Flag... flags
+            final UserInfo userInfo
     )
             throws PwmUnrecoverableException
     {
-        final boolean failFast = flags != null && Arrays.asList( flags ).contains( Flag.FailFast );
+        final boolean failFast = JavaHelper.enumArrayContainsValue( flags, Flag.FailFast );
 
         // null check
         if ( passwordString == null )

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

@@ -90,7 +90,7 @@ public class PwmScheduler
         scheduleFixedRateJob( runnable, executorService, delayTillNextOFfiset, TimeDuration.DAY );
     }
 
-    public Future scheduleFutureJob(
+    public Future scheduleJob(
             final Runnable runnable,
             final ExecutorService executor,
             final TimeDuration delay

+ 4 - 2
server/src/main/java/password/pwm/util/RandomPasswordGenerator.java

@@ -197,7 +197,7 @@ public class RandomPasswordGenerator
         password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
 
         // read a rule validator
-        final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator( pwmApplication, randomGenPolicy );
+
 
         // modify until it passes all the rules
         final int maxTryCount = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS ) );
@@ -214,8 +214,9 @@ public class RandomPasswordGenerator
                 password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
             }
 
+            final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator( pwmApplication, randomGenPolicy, PwmPasswordRuleValidator.Flag.FailFast );
             final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
-                    password.toString(), null, null, PwmPasswordRuleValidator.Flag.FailFast );
+                    password.toString(), null, null );
             if ( errors != null && !errors.isEmpty() )
             {
                 validPassword = false;
@@ -232,6 +233,7 @@ public class RandomPasswordGenerator
         // report outcome
         {
             final TimeDuration td = TimeDuration.fromCurrent( startTime );
+            final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator( pwmApplication, randomGenPolicy );
             if ( validPassword )
             {
                 final int finalTryCount = tryCount;

+ 1 - 1
server/src/main/java/password/pwm/util/db/DBConfiguration.java

@@ -77,7 +77,7 @@ public class DBConfiguration implements Serializable
         if ( fileValue != null && !fileValue.isEmpty() )
         {
             final FileValue.FileContent fileContent = fileValue.values().iterator().next();
-            jdbcDriverBytes = fileContent.getContents().getBytes();
+            jdbcDriverBytes = fileContent.getContents().copyOf();
         }
         else
         {

+ 6 - 0
server/src/main/java/password/pwm/util/java/AtomicLoopIntIncrementer.java

@@ -33,6 +33,12 @@ public class AtomicLoopIntIncrementer
     private final int ceiling;
     private final int floor;
 
+    public AtomicLoopIntIncrementer()
+    {
+        this.ceiling = Integer.MAX_VALUE;
+        this.floor = 0;
+    }
+
     public AtomicLoopIntIncrementer( final int ceiling )
     {
         this.ceiling = ceiling;

+ 60 - 0
server/src/main/java/password/pwm/util/java/AtomicLoopLongIncrementer.java

@@ -0,0 +1,60 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.java;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Thread safe rotating int incrementer with configurable floor and ceiling values.
+ */
+public class AtomicLoopLongIncrementer
+{
+    private final AtomicLong incrementer;
+    private final long ceiling;
+    private final long floor;
+
+    public AtomicLoopLongIncrementer( final long initialValue, final long ceiling )
+    {
+        this.ceiling = ceiling;
+        this.floor = 0;
+        incrementer = new AtomicLong( JavaHelper.rangeCheck( floor, ceiling, initialValue ) );
+    }
+
+    public long get()
+    {
+        return incrementer.get();
+    }
+
+    public long incrementAndGet( )
+    {
+        return incrementer.getAndUpdate( operand ->
+        {
+            operand++;
+            if ( operand >= ceiling )
+            {
+                operand = floor;
+            }
+            return operand;
+        } );
+    }
+}

+ 43 - 0
server/src/main/java/password/pwm/util/java/DebugOutputBuilder.java

@@ -0,0 +1,43 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.java;
+
+import java.time.Instant;
+
+public class DebugOutputBuilder
+{
+    private final StringBuilder stringBuilder = new StringBuilder();
+
+    public void appendLine( final CharSequence charSequence )
+    {
+        stringBuilder.append( JavaHelper.toIsoDate( Instant.now() ) );
+        stringBuilder.append( " " );
+        stringBuilder.append( charSequence );
+        stringBuilder.append( "\n" );
+    }
+
+    public String toString()
+    {
+        return stringBuilder.toString();
+    }
+}

+ 109 - 82
server/src/main/java/password/pwm/util/java/FileSystemUtility.java

@@ -22,87 +22,109 @@
 
 package password.pwm.util.java;
 
+import lombok.Value;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmHashAlgorithm;
-import password.pwm.util.secure.SecureEngine;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.Serializable;
 import java.lang.reflect.Method;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.RecursiveTask;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.zip.CRC32;
 
 public class FileSystemUtility
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( FileSystemUtility.class );
 
+    private static final AtomicLoopIntIncrementer OP_COUNTER = new AtomicLoopIntIncrementer();
+
     public static List<FileSummaryInformation> readFileInformation( final File rootFile )
-            throws PwmUnrecoverableException, IOException
     {
-        final AtomicInteger fileCounter = new AtomicInteger();
-        final AtomicLong byteCounter = new AtomicLong();
         final Instant startTime = Instant.now();
-        final ConditionalTaskExecutor debugLogger = ConditionalTaskExecutor.forPeriodicTask(
-                () -> LOGGER.trace( () -> "file info reading for path " + rootFile.getAbsolutePath() + " in progress, "
-                        + fileCounter.get() + " files, "
-                        + StringUtil.formatDiskSizeforDebug( byteCounter.get() )
-                        + " bytes, scanned in " + TimeDuration.compactFromCurrent( startTime )
-                ),
-                TimeDuration.SECONDS_10
-        );
-
-        final List<FileSummaryInformation> results = readFileInformation( rootFile, "", debugLogger, fileCounter, byteCounter );
-        LOGGER.trace( () -> "completed file info reading for path " + rootFile.getAbsolutePath() + ", "
-                + fileCounter.get() + " files, "
-                + StringUtil.formatDiskSizeforDebug( byteCounter.get() )
-                + " bytes, scanned in " + TimeDuration.compactFromCurrent( startTime ) );
-        return results;
+        final int operation = OP_COUNTER.next();
+        LOGGER.trace( () -> "begin file summary load for file '" + rootFile.getAbsolutePath() + ", operation=" + operation );
+        final ForkJoinPool pool = new ForkJoinPool();
+        final RecursiveFileReaderTask task = new RecursiveFileReaderTask( rootFile );
+        final List<FileSummaryInformation> fileSummaryInformations = pool.invoke( task );
+        final AtomicLong byteCount = new AtomicLong( 0 );
+        final AtomicInteger fileCount = new AtomicInteger( 0 );
+        fileSummaryInformations.forEach( fileSummaryInformation -> byteCount.addAndGet( fileSummaryInformation.getSize() ) );
+        fileSummaryInformations.forEach( fileSummaryInformation -> fileCount.incrementAndGet() );
+        final Map<String, String> debugInfo = new LinkedHashMap<>();
+        debugInfo.put( "operation", Integer.toString( operation ) );
+        debugInfo.put( "bytes", StringUtil.formatDiskSizeforDebug( byteCount.get() ) );
+        debugInfo.put( "files", Integer.toString( fileCount.get() ) );
+        debugInfo.put( "duration", TimeDuration.compactFromCurrent( startTime ) );
+        LOGGER.trace( () -> "completed file summary load for file '" + rootFile.getAbsolutePath() + ", " + StringUtil.mapToString( debugInfo ) );
+        return fileSummaryInformations;
     }
 
-    private static List<FileSummaryInformation> readFileInformation(
-            final File rootFile,
-            final String relativePath,
-            final ConditionalTaskExecutor debugLogger,
-            final AtomicInteger fileCounter,
-            final AtomicLong byteCounter
-    )
-            throws PwmUnrecoverableException
+    private static class RecursiveFileReaderTask extends RecursiveTask<List<FileSummaryInformation>>
     {
-        final ArrayList<FileSummaryInformation> results = new ArrayList<>();
-        final File[] files = rootFile.listFiles();
-        if ( files != null )
+        private final File theFile;
+
+        RecursiveFileReaderTask( final File theFile )
+        {
+            Objects.requireNonNull( theFile );
+            this.theFile = theFile;
+        }
+
+        @Override
+        protected List<FileSummaryInformation> compute()
         {
-            for ( final File loopFile : files )
+            final List<FileSummaryInformation> results = new ArrayList<>();
+
+            if ( theFile.isDirectory() )
+            {
+                final File[] subFiles = theFile.listFiles();
+                if ( subFiles != null )
+                {
+                    final List<RecursiveFileReaderTask> tasks = new ArrayList<>();
+                    for ( final File file : subFiles )
+                    {
+                        final RecursiveFileReaderTask newTask = new RecursiveFileReaderTask( file );
+                        newTask.fork();
+                        tasks.add( newTask );
+                    }
+                    tasks.forEach( recursiveFileReaderTask -> results.addAll( recursiveFileReaderTask.join() ) );
+                }
+            }
+            else
             {
-                final String path = relativePath + loopFile.getName();
-                if ( loopFile.isDirectory() )
+                try
                 {
-                    final String subPath = path + File.separator;
-                    results.addAll( readFileInformation( loopFile, subPath, debugLogger, fileCounter, byteCounter ) );
+                    results.add( fileInformationForFile( theFile ) );
                 }
-                else
+                catch ( Exception e )
                 {
-                    final FileSummaryInformation fileInformation = fileInformationForFile( loopFile );
-                    results.add( fileInformation );
-                    fileCounter.incrementAndGet();
-                    byteCounter.addAndGet( fileInformation.getSize() );
+                    LOGGER.debug( () -> "error executing file summary reader: " + e.getMessage() );
                 }
-
-                debugLogger.conditionallyExecuteTask();
             }
+
+            return Collections.unmodifiableList( results );
         }
-        return results;
     }
 
     private static FileSummaryInformation fileInformationForFile( final File file )
-            throws PwmUnrecoverableException
+            throws IOException
     {
         if ( file == null || !file.exists() )
         {
@@ -113,7 +135,7 @@ public class FileSystemUtility
                 file.getParentFile().getAbsolutePath(),
                 Instant.ofEpochMilli( file.lastModified() ),
                 file.length(),
-                SecureEngine.hash( file, PwmHashAlgorithm.SHA1 )
+                crc32( file )
         );
     }
 
@@ -222,47 +244,14 @@ public class FileSystemUtility
         }
     }
 
+    @Value
     public static class FileSummaryInformation implements Serializable
     {
         private final String filename;
         private final String filepath;
         private final Instant modified;
         private final long size;
-        private final String sha1sum;
-
-        public FileSummaryInformation( final String filename, final String filepath, final Instant modified, final long size, final String sha1sum )
-        {
-            this.filename = filename;
-            this.filepath = filepath;
-            this.modified = modified;
-            this.size = size;
-            this.sha1sum = sha1sum;
-        }
-
-        public String getFilename( )
-        {
-            return filename;
-        }
-
-        public String getFilepath( )
-        {
-            return filepath;
-        }
-
-        public Instant getModified( )
-        {
-            return modified;
-        }
-
-        public long getSize( )
-        {
-            return size;
-        }
-
-        public String getSha1sum( )
-        {
-            return sha1sum;
-        }
+        private final long checksum;
     }
 
     public static void deleteDirectoryContents( final File path ) throws IOException
@@ -270,7 +259,7 @@ public class FileSystemUtility
         deleteDirectoryContents( path, false );
     }
 
-    public static void deleteDirectoryContents( final File path, final boolean deleteThisLevel )
+    private static void deleteDirectoryContents( final File path, final boolean deleteThisLevel )
             throws IOException
     {
         if ( !path.exists() )
@@ -303,4 +292,42 @@ public class FileSystemUtility
             }
         }
     }
+
+    private static long crc32( final File file )
+            throws IOException
+    {
+        final CRC32 crc32 = new CRC32();
+        final FileInputStream fileInputStream = new FileInputStream( file );
+        final FileChannel fileChannel = fileInputStream.getChannel();
+        final ByteBuffer byteBuffer = ByteBuffer.allocateDirect( 1024 );
+
+        while ( fileChannel.read( byteBuffer ) > 0 )
+        {
+            // redundant cast to buffer to solve jdk8/9 inter-op issue
+            ( ( Buffer ) byteBuffer ).flip();
+
+            crc32.update( byteBuffer );
+
+            // redundant cast to buffer to solve jdk8/9 inter-op issue
+            ( ( Buffer ) byteBuffer ).clear();
+        }
+
+        return crc32.getValue();
+    }
+
+    public static void mkdirs( final File file )
+            throws PwmUnrecoverableException
+    {
+        if ( !file.exists() )
+        {
+            if ( !file.mkdirs() )
+            {
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to create directory: " + file.getAbsolutePath() );
+            }
+        }
+        else if ( !file.isDirectory() )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to create directory, file already exists: " + file.getAbsolutePath() );
+        }
+    }
 }

+ 12 - 0
server/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -641,4 +641,16 @@ public class JavaHelper
                 && input <= Long.MAX_VALUE
                 && input >= Long.MIN_VALUE;
     }
+
+    public static byte[] longToBytes( final long input )
+    {
+        final byte[] result = new byte[Byte.SIZE];
+        long shift = input;
+        for ( int i = Byte.SIZE - 1; i >= 0; i-- )
+        {
+            result[i] = (byte) ( shift & 0xFF );
+            shift >>= Byte.SIZE;
+        }
+        return result;
+    }
 }

+ 1 - 2
server/src/main/java/password/pwm/util/localdb/LocalDBFactory.java

@@ -94,8 +94,7 @@ public class LocalDBFactory
             if ( localDBUtility.readImportInprogressFlag() )
             {
                 LOGGER.error( "previous database import process did not complete successfully, clearing all data" );
-                localDBUtility.prepareForImport();
-                localDBUtility.markImportComplete();
+                localDBUtility.cancelImportProcess();
             }
         }
 

+ 132 - 103
server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java

@@ -24,16 +24,17 @@ package password.pwm.util.localdb;
 
 import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.csv.CSVRecord;
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.input.CountingInputStream;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.ProgressInfo;
 import password.pwm.util.TransactionSizeCalculator;
+import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -45,7 +46,7 @@ import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.io.Reader;
-import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.text.DecimalFormat;
 import java.time.Instant;
 import java.util.Date;
@@ -62,10 +63,10 @@ public class LocalDBUtility
 {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( LocalDBUtility.class );
+    private static final String IN_PROGRESS_STATUS_VALUE = "in-progress";
 
     private final LocalDB localDB;
     private int exportLineCounter;
-    private int importLineCounter;
 
     private static final int GZIP_BUFFER_SIZE = 1024 * 512;
 
@@ -221,18 +222,20 @@ public class LocalDBUtility
     private void importLocalDB( final InputStream inputStream, final Appendable out, final long totalBytes )
             throws PwmOperationalException, IOException
     {
-        this.prepareForImport();
 
-        importLineCounter = 0;
-        if ( totalBytes > 0 )
-        {
-            writeStringToOut( out, "total bytes in localdb import source: " + totalBytes );
-        }
-
-        writeStringToOut( out, "beginning localdb import..." );
+        final ImportLocalDBMachine importLocalDBMachine = new ImportLocalDBMachine( localDB, totalBytes, out );
+        importLocalDBMachine.doImport( inputStream );
+    }
 
-        final Instant startTime = Instant.now();
-        final TransactionSizeCalculator transactionCalculator = new TransactionSizeCalculator(
+    private static class ImportLocalDBMachine
+    {
+        private int lineReaderCounter;
+        private long byteReaderCounter;
+        private int recordImportCounter;
+        private final Instant startTime = Instant.now();
+        final Map<LocalDB.DB, Map<String, String>> transactionMap = new HashMap<>();
+        private final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
+        private final TransactionSizeCalculator transactionCalculator = new TransactionSizeCalculator(
                 TransactionSizeCalculator.Settings.builder()
                         .durationGoal( TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS ) )
                         .minTransactions( 50 )
@@ -240,96 +243,139 @@ public class LocalDBUtility
                         .build()
         );
 
-        final Map<LocalDB.DB, Map<String, String>> transactionMap = new HashMap<>();
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-        {
-            transactionMap.put( loopDB, new TreeMap<>() );
-        }
+        private final long totalBytes;
+        private final Appendable debugOutput;
+        private final LocalDB localDB;
 
-        final CountingInputStream countingInputStream = new CountingInputStream( inputStream );
-        final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
+        private final ConditionalTaskExecutor debugOutputWriter;
 
-        final Timer statTimer = new Timer( true );
-        statTimer.scheduleAtFixedRate( new TimerTask()
+        ImportLocalDBMachine( final LocalDB localDB, final long totalBytes, final Appendable debugOutput )
         {
-            @Override
-            public void run( )
+            this.localDB = localDB;
+            this.totalBytes = totalBytes;
+            this.debugOutput = debugOutput;
+
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
             {
-                String output = "";
-                if ( totalBytes > 0 )
-                {
-                    final ProgressInfo progressInfo = new ProgressInfo( startTime, totalBytes, countingInputStream.getByteCount() );
-                    output += progressInfo.debugOutput();
-                }
-                else
-                {
-                    output += "recordsImported=" + importLineCounter;
-                }
-                output += ", avgTransactionSize=" + transactionCalculator.getTransactionSize()
-                        + ", recordsPerMinute=" + eventRateMeter.readEventRate().setScale( 2, BigDecimal.ROUND_DOWN );
-                writeStringToOut( out, output );
+                transactionMap.put( loopDB, new TreeMap<>() );
             }
-        }, 30 * 1000, 30 * 1000 );
 
+            this.debugOutputWriter = ConditionalTaskExecutor.forPeriodicTask( () ->
+            {
+                writeStringToOut( debugOutput, debugStatsString() );
+            }, TimeDuration.of( 30, TimeDuration.Unit.SECONDS ) );
+        }
 
-        Reader csvReader = null;
-        try
+        void doImport( final InputStream inputStream )
+                throws IOException, LocalDBException
         {
-            csvReader = new InputStreamReader( new GZIPInputStream( countingInputStream, GZIP_BUFFER_SIZE ), PwmConstants.DEFAULT_CHARSET );
-            for ( final CSVRecord record : PwmConstants.DEFAULT_CSV_FORMAT.parse( csvReader ) )
+            this.prepareForImport();
+
+            if ( totalBytes > 0 )
             {
-                importLineCounter++;
-                eventRateMeter.markEvents( 1 );
-                final String dbNameRecordStr = record.get( 0 );
-                final LocalDB.DB db = JavaHelper.readEnumFromString( LocalDB.DB.class, null, dbNameRecordStr );
-                final String key = record.get( 1 );
-                final String value = record.get( 2 );
-                if ( db == null )
-                {
-                    writeStringToOut( out, "ignoring localdb import record #" + importLineCounter + ", invalid DB name '" + dbNameRecordStr + "'" );
-                }
-                else
+                writeStringToOut( debugOutput, "total bytes in localdb import source: " + totalBytes );
+            }
+
+            writeStringToOut( debugOutput, "beginning localdb import..." );
+
+            try ( CountingInputStream countingInputStream = new CountingInputStream( inputStream ) )
+            {
+                try ( Reader csvReader = new InputStreamReader( new GZIPInputStream( countingInputStream, GZIP_BUFFER_SIZE ), PwmConstants.DEFAULT_CHARSET ) )
                 {
-                    transactionMap.get( db ).put( key, value );
                     int cachedTransactions = 0;
-                    for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-                    {
-                        cachedTransactions += transactionMap.get( loopDB ).size();
-                    }
-                    if ( cachedTransactions >= transactionCalculator.getTransactionSize() )
+                    for ( final CSVRecord record : PwmConstants.DEFAULT_CSV_FORMAT.parse( csvReader ) )
                     {
-                        final long startTxnTime = System.currentTimeMillis();
-                        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+                        lineReaderCounter++;
+                        eventRateMeter.markEvents( 1 );
+                        byteReaderCounter = countingInputStream.getByteCount();
+                        final String dbNameRecordStr = record.get( 0 );
+                        final LocalDB.DB db = JavaHelper.readEnumFromString( LocalDB.DB.class, null, dbNameRecordStr );
+                        final String key = record.get( 1 );
+                        final String value = record.get( 2 );
+                        if ( db == null )
                         {
-                            localDB.putAll( loopDB, transactionMap.get( loopDB ) );
-                            transactionMap.get( loopDB ).clear();
+                            writeStringToOut( debugOutput, "ignoring localdb import record #" + lineReaderCounter + ", invalid DB name '" + dbNameRecordStr + "'" );
                         }
-                        transactionCalculator.recordLastTransactionDuration( TimeDuration.fromCurrent( startTxnTime ) );
+                        else
+                        {
+                            transactionMap.get( db ).put( key, value );
+                            cachedTransactions++;
+                            if ( cachedTransactions >= transactionCalculator.getTransactionSize() )
+                            {
+                                flushCachedTransactions();
+                                cachedTransactions = 0;
+                            }
+                        }
+                        debugOutputWriter.conditionallyExecuteTask();
                     }
                 }
             }
+
+            flushCachedTransactions();
+            this.markImportComplete();
+
+            final String completeMsg = "import process completed: " + debugStatsString();
+            LOGGER.info( () -> completeMsg );
+            writeStringToOut( debugOutput, completeMsg );
+        }
+
+        private void flushCachedTransactions( )
+                throws LocalDBException
+        {
+            final Instant startTxnTime = Instant.now();
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+            {
+                localDB.putAll( loopDB, transactionMap.get( loopDB ) );
+                recordImportCounter += transactionMap.get( loopDB ).size();
+                transactionMap.get( loopDB ).clear();
+            }
+            transactionCalculator.recordLastTransactionDuration( TimeDuration.fromCurrent( startTxnTime ) );
         }
-        finally
+
+        private void prepareForImport( )
+                throws LocalDBException
         {
-            LOGGER.trace( () -> "import process completed" );
-            statTimer.cancel();
-            IOUtils.closeQuietly( csvReader );
-            IOUtils.closeQuietly( countingInputStream );
+            LOGGER.info( () -> "preparing LocalDB for import procedure" );
+            localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), IN_PROGRESS_STATUS_VALUE );
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+            {
+                if ( loopDB != LocalDB.DB.PWM_META )
+                {
+                    localDB.truncate( loopDB );
+                }
+            }
+
+            // save meta for last so flag is cleared last.
+            localDB.truncate( LocalDB.DB.PWM_META );
+            localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), IN_PROGRESS_STATUS_VALUE  );
         }
 
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+        private void markImportComplete()
+                throws LocalDBException
         {
-            localDB.putAll( loopDB, transactionMap.get( loopDB ) );
-            transactionMap.get( loopDB ).clear();
+            LOGGER.info( () -> "marking LocalDB import procedure completed" );
+            localDB.remove( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() );
         }
 
-        this.markImportComplete();
+        private String debugStatsString()
+        {
+            final Map<String, String> stats = new LinkedHashMap<>();
+            if ( totalBytes > 0 && byteReaderCounter > 0 )
+            {
+                final ProgressInfo progressInfo = new ProgressInfo( startTime, totalBytes, byteReaderCounter );
+                stats.put( "progress", progressInfo.debugOutput() );
+            }
 
-        writeStringToOut( out, "restore complete, restored " + importLineCounter + " records in " + TimeDuration.fromCurrent( startTime ).asLongString() );
-        statTimer.cancel();
+            stats.put( "linesRead", Integer.toString( lineReaderCounter ) );
+            stats.put( "bytesRead", Long.toString( byteReaderCounter ) );
+            stats.put( "recordsImported", Integer.toString( recordImportCounter ) );
+            stats.put( "avgTransactionSize", Integer.toString( transactionCalculator.getTransactionSize() ) );
+            stats.put( "recordsPerMinute", eventRateMeter.readEventRate().setScale( 2, RoundingMode.DOWN ).toString() );
+            stats.put( "duration", TimeDuration.compactFromCurrent( startTime ) );
+            return StringUtil.mapToString( stats );
+        }
     }
 
-
     public static Map<StatsKey, Object> dbStats(
             final LocalDB localDB,
             final LocalDB.DB db
@@ -381,35 +427,10 @@ public class LocalDBUtility
         AVG_VALUE_LENGTH,
     }
 
-    public void prepareForImport( )
-            throws LocalDBException
-    {
-        LOGGER.info( () -> "preparing LocalDB for import procedure" );
-        localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), "inprogress" );
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-        {
-            if ( loopDB != LocalDB.DB.PWM_META )
-            {
-                localDB.truncate( loopDB );
-            }
-        }
-
-        // save meta for last so flag is cleared last.
-        localDB.truncate( LocalDB.DB.PWM_META );
-        localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), "inprogress" );
-    }
-
-    public void markImportComplete( )
-            throws LocalDBException
-    {
-        LOGGER.info( () -> "marking LocalDB import procedure completed" );
-        localDB.remove( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() );
-    }
-
     public boolean readImportInprogressFlag( )
             throws LocalDBException
     {
-        return "inprogress".equals(
+        return IN_PROGRESS_STATUS_VALUE.equals(
                 localDB.get( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() ) );
     }
 
@@ -417,4 +438,12 @@ public class LocalDBUtility
     {
         return parameters != null && parameters.containsKey( parameter ) && Boolean.parseBoolean( parameters.get( parameter ) );
     }
+
+    public void cancelImportProcess()
+            throws LocalDBException
+    {
+        final ImportLocalDBMachine importLocalDBMachine = new ImportLocalDBMachine( localDB, 0, new StringBuilder() );
+        importLocalDBMachine.prepareForImport();
+        importLocalDBMachine.markImportComplete();
+    }
 }

+ 3 - 3
server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java

@@ -29,7 +29,7 @@ import password.pwm.PwmApplication;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.JavaHelper;
@@ -72,12 +72,12 @@ public final class WorkQueueProcessor<W extends Serializable>
 
     private volatile WorkerThread workerThread;
 
-    private final AtomicLoopIntIncrementer idGenerator = new AtomicLoopIntIncrementer( 0 );
+    private final AtomicLoopIntIncrementer idGenerator = new AtomicLoopIntIncrementer();
     private Instant eldestItem = null;
 
     private ThreadPoolExecutor executorService;
 
-    private final EventRateMeter.MovingAverage avgLagTime = new EventRateMeter.MovingAverage( 60 * 60 * 1000 );
+    private final EventRateMeter.MovingAverage avgLagTime = new EventRateMeter.MovingAverage( TimeDuration.HOUR );
     private final EventRateMeter sendRate = new EventRateMeter( TimeDuration.HOUR );
 
     private final AtomicInteger preQueueSubmit = new AtomicInteger( 0 );

+ 4 - 2
server/src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -76,6 +76,7 @@ import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.HelpdeskAuditRecord;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.PasswordCharCounter;
@@ -395,7 +396,8 @@ public class PasswordUtility
 
             final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator(
                     pwmApplication,
-                    passwordPolicy
+                    passwordPolicy,
+                    PwmPasswordRuleValidator.Flag.BypassLdapRuleCheck
             );
 
             pwmPasswordRuleValidator.testPassword( newPassword, null, userInfo, theUser );
@@ -465,7 +467,7 @@ public class PasswordUtility
         pwmApplication.getStatisticsManager().updateEps( EpsStatistic.PASSWORD_CHANGES, 1 );
 
         final int passwordStrength = PasswordUtility.judgePasswordStrength( pwmApplication.getConfig(), newPassword.getStringValue() );
-        pwmApplication.getStatisticsManager().updateAverageValue( Statistic.AVG_PASSWORD_STRENGTH, passwordStrength );
+        pwmApplication.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_STRENGTH, passwordStrength );
 
         // at this point the password has been changed, so log it.
         final String msg = ( bindIsSelf

+ 1 - 1
server/src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java

@@ -116,7 +116,7 @@ public class NMASCrOperator implements CrOperator
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( NMASCrOperator.class );
 
-    private final AtomicLoopIntIncrementer threadCounter = new AtomicLoopIntIncrementer( Integer.MAX_VALUE );
+    private final AtomicLoopIntIncrementer threadCounter = new AtomicLoopIntIncrementer();
     private final List<NMASSessionThread> sessionMonitorThreads = Collections.synchronizedList( new ArrayList<NMASSessionThread>() );
     private final PwmApplication pwmApplication;
     private final TimeDuration maxThreadIdleTime;

+ 12 - 25
server/src/main/java/password/pwm/util/secure/ChecksumInputStream.java

@@ -22,34 +22,21 @@
 
 package password.pwm.util.secure;
 
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.JavaHelper;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.zip.CRC32;
 
 public class ChecksumInputStream extends InputStream
 {
-    private final MessageDigest messageDigest;
+    private final CRC32 crc32 = new CRC32();
     private final InputStream wrappedStream;
 
-    public ChecksumInputStream( final PwmHashAlgorithm hash, final InputStream wrappedStream ) throws PwmUnrecoverableException
+    public ChecksumInputStream( final InputStream wrappedStream )
     {
         this.wrappedStream = wrappedStream;
-
-        try
-        {
-            messageDigest = MessageDigest.getInstance( hash.getAlgName() );
-        }
-        catch ( NoSuchAlgorithmException e )
-        {
-            final String errorMsg = "missing hash algorithm: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CRYPT_ERROR, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
     }
 
     @Override
@@ -58,7 +45,7 @@ public class ChecksumInputStream extends InputStream
         final int value = wrappedStream.read();
         if ( value >= 0 )
         {
-            messageDigest.update( ( byte ) value );
+            crc32.update( ( byte ) value );
         }
         return value;
     }
@@ -69,7 +56,7 @@ public class ChecksumInputStream extends InputStream
         final int length = wrappedStream.read( b );
         if ( length > 0 )
         {
-            messageDigest.update( b, 0, length );
+            crc32.update( b, 0, length );
         }
         return length;
     }
@@ -80,7 +67,7 @@ public class ChecksumInputStream extends InputStream
         final int length = wrappedStream.read( b, off, len );
         if ( length > 0 )
         {
-            messageDigest.update( b, off, length );
+            crc32.update( b, off, length );
         }
         return length;
     }
@@ -121,12 +108,12 @@ public class ChecksumInputStream extends InputStream
         return false;
     }
 
-    public byte[] getInProgressChecksum( )
+    public ImmutableByteArray checksum( )
     {
-        return messageDigest.digest();
+        return ImmutableByteArray.of( JavaHelper.longToBytes( crc32.getValue() ) );
     }
 
-    public byte[] closeAndFinalChecksum( ) throws IOException
+    public ImmutableByteArray readUntilEndAndChecksum( ) throws IOException
     {
         final byte[] buffer = new byte[ 1024 ];
 
@@ -135,6 +122,6 @@ public class ChecksumInputStream extends InputStream
             // read out the remainder of the stream contents
         }
 
-        return getInProgressChecksum();
+        return checksum();
     }
 }

+ 10 - 23
server/src/main/java/password/pwm/util/secure/ChecksumOutputStream.java

@@ -22,34 +22,21 @@
 
 package password.pwm.util.secure;
 
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.JavaHelper;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.zip.CRC32;
 
 public class ChecksumOutputStream extends OutputStream
 {
-    private final MessageDigest messageDigest;
+    private final CRC32 crc32 = new CRC32();
     private final OutputStream wrappedStream;
 
-    public ChecksumOutputStream( final PwmHashAlgorithm hash, final OutputStream wrappedStream ) throws PwmUnrecoverableException
+    public ChecksumOutputStream( final OutputStream wrappedStream )
     {
         this.wrappedStream = wrappedStream;
-
-        try
-        {
-            messageDigest = MessageDigest.getInstance( hash.getAlgName() );
-        }
-        catch ( NoSuchAlgorithmException e )
-        {
-            final String errorMsg = "missing hash algorithm: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CRYPT_ERROR, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
     }
 
     @Override
@@ -61,7 +48,7 @@ public class ChecksumOutputStream extends OutputStream
     @Override
     public void write( final byte[] b ) throws IOException
     {
-        messageDigest.update( b );
+        crc32.update( b );
         wrappedStream.write( b );
     }
 
@@ -70,7 +57,7 @@ public class ChecksumOutputStream extends OutputStream
     {
         if ( len > 0 )
         {
-            messageDigest.update( b, off, len );
+            crc32.update( b, off, len );
         }
 
         wrappedStream.write( b, off, len );
@@ -85,12 +72,12 @@ public class ChecksumOutputStream extends OutputStream
     @Override
     public void write( final int b ) throws IOException
     {
-        messageDigest.update( ( byte ) b );
+        crc32.update( ( byte ) b );
         wrappedStream.write( b );
     }
 
-    public byte[] getInProgressChecksum( )
+    public ImmutableByteArray checksum( )
     {
-        return messageDigest.digest();
+        return ImmutableByteArray.of( JavaHelper.longToBytes( crc32.getValue() ) );
     }
 }

+ 1 - 1
server/src/main/java/password/pwm/util/secure/X509Utils.java

@@ -302,7 +302,7 @@ public abstract class X509Utils
                 {
                     try
                     {
-                        LOGGER.warn( sessionLabel, "blind trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
+                        LOGGER.debug( sessionLabel, () -> "promiscuous trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
                     }
                     catch ( Exception e )
                     {

+ 1 - 1
server/src/main/java/password/pwm/ws/server/RestServlet.java

@@ -67,7 +67,7 @@ import java.util.Locale;
 
 public abstract class RestServlet extends HttpServlet
 {
-    private static final AtomicLoopIntIncrementer REQUEST_COUNTER = new AtomicLoopIntIncrementer( Integer.MAX_VALUE );
+    private static final AtomicLoopIntIncrementer REQUEST_COUNTER = new AtomicLoopIntIncrementer();
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestServlet.class );
 

+ 19 - 9
server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java

@@ -34,8 +34,11 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpContentType;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.svc.stats.AvgStatistic;
+import password.pwm.svc.stats.DailyKey;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticType;
 import password.pwm.svc.stats.StatisticsBundle;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JavaHelper;
@@ -54,9 +57,7 @@ import java.math.RoundingMode;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -194,7 +195,7 @@ public class RestStatisticsServer extends RestServlet
         {
             final List<HistoryData> outerOutput = new ArrayList<>();
 
-            StatisticsManager.DailyKey dailyKey = new StatisticsManager.DailyKey( new Date() );
+            DailyKey dailyKey = DailyKey.forToday();
 
             for ( int daysAgo = 0; daysAgo < days; daysAgo++ )
             {
@@ -210,10 +211,10 @@ public class RestStatisticsServer extends RestServlet
                 final HistoryData historyData = HistoryData.builder()
                         .name( dailyKey.toString() )
                         .date( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ).withZone( ZoneOffset.UTC )
-                                .format( dailyKey.calendar().toInstant() ) )
-                        .year( dailyKey.calendar().get( Calendar.YEAR ) )
-                        .month( dailyKey.calendar().get( Calendar.MONTH ) )
-                        .day( dailyKey.calendar().get( Calendar.DAY_OF_MONTH ) )
+                                .format( dailyKey.localDate() ) )
+                        .year( dailyKey.localDate().getYear() )
+                        .month( dailyKey.localDate().getMonthValue() )
+                        .day( dailyKey.localDate().getDayOfMonth() )
                         .daysAgo( daysAgo )
                         .data( statValues )
                         .build();
@@ -250,7 +251,16 @@ public class RestStatisticsServer extends RestServlet
                 final StatLabelData statLabelData = new StatLabelData(
                         statistic.name(),
                         statistic.getLabel( locale ),
-                        statistic.getType().name(),
+                        StatisticType.INCREMENTER.name(),
+                        statistic.getDescription( locale ) );
+                output.put( statistic.name(), statLabelData );
+            }
+            for ( final AvgStatistic statistic : AvgStatistic.values() )
+            {
+                final StatLabelData statLabelData = new StatLabelData(
+                        statistic.name(),
+                        statistic.getLabel( locale ),
+                        StatisticType.AVERAGE.name(),
                         statistic.getDescription( locale ) );
                 output.put( statistic.name(), statLabelData );
             }
@@ -262,7 +272,7 @@ public class RestStatisticsServer extends RestServlet
                     final StatLabelData statLabelData = new StatLabelData(
                             name,
                             loopEps.getLabel( locale ),
-                            "EPS",
+                            StatisticType.EPS.name(),
                             null );
                     output.put( name, statLabelData );
                 }

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

@@ -76,8 +76,8 @@ configEditor.blockOldIE=true
 configEditor.queryFilter.testLimit=5000
 configEditor.idleTimeoutSeconds=900
 configGuide.idleTimeoutSeconds=3600
-configManager.zipDebug.maxLogLines=100000
-configManager.zipDebug.maxLogSeconds=30
+configManager.zipDebug.maxLogBytes=50000000
+configManager.zipDebug.maxLogSeconds=120
 db.jdbcLoadStrategy=AppPathFileLoader,Classpath
 db.connections.max=5
 db.connections.timeoutMs=30000
@@ -98,6 +98,8 @@ healthCheck.nominalCheckIntervalSeconds=60
 healthCheck.minimumCheckIntervalSeconds=10
 healthCheck.maximumRecordAgeSeconds=300
 healthCheck.maximumForceCheckWaitSeconds=30
+health.supportBundle.file.writeIntervalSeconds=0
+health.supportBundle.file.writeRetentionCount=10
 health.certificate.warnSeconds=2592000
 health.ldap.cautionDurationMS=10800000
 health.ldap.proxy.pwExpireWarnSeconds=2592000

+ 3 - 0
server/src/main/resources/password/pwm/i18n/Admin.properties

@@ -222,6 +222,8 @@ Statistic_Label.AvgPasswordSyncTime=Average Password Sync Time
 Statistic_Description.AvgPasswordSyncTime=Average time (in milliseconds) users spend waiting for the password sync progress to complete.
 Statistic_Label.AvgAuthenticationTime=Average Authentication Time
 Statistic_Description.AvgAuthenticationTime=Average time (in milliseconds) for authentications of all types to complete.
+Statistic_Label.AvgRequestProcessTime=Average Request Process Time
+Statistic_Description.AvgRequestProcessTime=Average time (in milliseconds) for page requests (not including resources) to process.
 Statistic_Label.RecoveryTokensSent=Forgotten Password Tokens Sent
 Statistic_Description.RecoveryTokensSent=Number of tokens used for forgotten password process issued and sent via email or SMS.
 Statistic_Label.RecoveryTokensPassed=Forgotten Password Tokens Passed
@@ -296,6 +298,7 @@ Statistic_Label.ObsoleteUrlRequests=Obsolete URL Requests
 Statistic_Description.ObsoleteUrlRequests=Number of web requests to obsolete URLs.
 Statistic_Label.SyslogMessagesSent=Syslog Messages Sent
 Statistic_Description.SyslogMessagesSent=Number of successfully sent syslog messages.
+EpsStatistic_Label.LDAP_BINDS=LDAP Binds
 EpsStatistic_Label.REQUESTS=Requests
 EpsStatistic_Label.SESSIONS=Sessions
 EpsStatistic_Label.PASSWORD_CHANGES=Password Changes

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

@@ -355,6 +355,7 @@ Title_TitleBarAuthenticated=@User:ID@  Password Self Service
 Title_TitleBar=Password Self Service
 Title_UpdateProfile=Update Profile
 Title_UpdateProfileConfirm=Confirm Profile Data
+Title_Upload=Upload
 Title_UploadPhoto=Upload Photo
 Title_UserData=My Data
 Title_UserEventHistory=Password History

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

@@ -714,7 +714,7 @@ Setting_Description_token.characters=Specify the available characters for the em
 Setting_Description_token.ldap.attribute=Specify the attribute that @PwmAppName@ uses when you enable the LDAP Token Storage Method to store and search for tokens.
 Setting_Description_token.length=Specify the length of the email token
 Setting_Description_token.lifetime=Specify the default lifetime an token is valid (in seconds). The default is one hour.  This default may be overridden by module specific settings.
-Setting_Description_token.storageMethod=Select the storage method @PwmAppName@ uses to save issued tokens.<table style\="width\: 400px"><tr><td>Method</td><td>Description</td></tr><tr><td>LocalDB</td><td>Stores the tokens in the local embedded LocalDB database.  Tokens are not common across multiple application instances.</td></tr><tr><td>DB</td><td>Store the tokens in a configured, remote database.  Tokens work across multiple application instances.</td></tr><tr><td>Crypto</td><td>Use crypto to create and read tokens, they are not stored locally.  Tokens work across multiple application instances if they have the same Security Key.  Crypto tokens ignore the length rules and might be too long to use for SMS purposes.</td></tr><tr><td>LDAP</td><td>Use the LDAP directory to store tokens.  Tokens work across multiple application instances.  You cannot use LDAP tokens as New User Registration tokens.</td></tr></table>
+Setting_Description_token.storageMethod=Select the storage method @PwmAppName@ uses to save issued tokens.<table style\="width\: 400px"><tr><td>Method</td><td>Description</td></tr><tr><td>LocalDB</td><td>Stores the tokens in the local embedded LocalDB database.  Tokens are not common across multiple application instances.</td></tr><tr><td>DB</td><td>Store the tokens in a configured, remote database.  Tokens work across multiple application instances.</td></tr><tr><td>Crypto</td><td>Use crypto to create and read tokens, they are not stored locally.  Tokens work across multiple application instances if they have the same Security Key.  Crypto tokens ignore the length and character rules and might be too long to use for SMS purposes.</td></tr><tr><td>LDAP</td><td>Use the LDAP directory to store tokens.  Tokens work across multiple application instances.  You cannot use LDAP tokens as New User Registration tokens.</td></tr></table>
 Setting_Description_token.valueMasking.enable=Enable this option to mask token destination display values (email addresses and telephone numbers).
 Setting_Description_updateAttributes.check.queryMatch=When you use the "checkProfile" or "checkAll" parameter with the command servlet, @PwmAppName@ uses this query match to determine if the user is required to populate the parameter values. <br/><br/>If this value is blank, then @PwmAppName@ checks the user's current values against the form requirements.
 Setting_Description_updateAttributes.email.verification=Enable this option to send an email to the user's email address before @PwmAppName@ updates the account. The user's email must change to cause this verification email to be sent. The user must verify receipt of the email before @PwmAppName@ updates the account.

+ 18 - 7
server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java

@@ -25,9 +25,11 @@ package password.pwm.i18n;
 import org.junit.Assert;
 import org.junit.Test;
 import password.pwm.PwmConstants;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.ResourceBundle;
 import java.util.Set;
@@ -48,13 +50,22 @@ public class AdminPropertyKeysTest
                     password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
                     password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
             };
-            for ( final String key : keys )
-            {
-                expectedKeys.add( key );
-                Assert.assertTrue(
-                        "Admin.properties missing record for " + key,
-                        resourceBundle.containsKey( key ) );
-            }
+            Collections.addAll( expectedKeys, keys );
+        }
+        for ( final AvgStatistic statistic : AvgStatistic.values() )
+        {
+            final String[] keys = new String[] {
+                    password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
+                    password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
+            };
+            Collections.addAll( expectedKeys, keys );
+        }
+
+        for ( final String key : expectedKeys )
+        {
+            Assert.assertTrue(
+                    "Admin.properties missing record for " + key,
+                    resourceBundle.containsKey( key ) );
         }
 
         final Set<String> extraKeys = new HashSet<>( resourceBundle.keySet() );

+ 1 - 1
server/src/test/java/password/pwm/util/localdb/LocalDBLoggerExtendedTest.java

@@ -30,7 +30,7 @@ import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigurationImpl;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.Percent;

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/activateuser.jsp

@@ -50,7 +50,6 @@
                 <input type="hidden" name="processAction" value="activate"/>
                 <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
             </div>
         </form>
     </div>

+ 22 - 9
webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp

@@ -20,16 +20,19 @@
  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 --%>
 
-<%@ page import="password.pwm.error.PwmError" %>
 <%@ page import="password.pwm.error.PwmException" %>
 <%@ page import="password.pwm.i18n.Admin" %>
+<%@ page import="password.pwm.svc.stats.AvgStatistic" %>
+<%@ page import="password.pwm.svc.stats.DailyKey" %>
 <%@ page import="password.pwm.svc.stats.Statistic" %>
+<%@ page import="password.pwm.svc.stats.StatisticType" %>
 <%@ page import="password.pwm.svc.stats.StatisticsBundle" %>
 <%@ page import="password.pwm.svc.stats.StatisticsManager" %>
 <%@ page import="password.pwm.util.java.JavaHelper" %>
-<%@ page import="java.text.DateFormat" %>
 <%@ page import="java.util.Locale" %>
 <%@ page import="java.util.Map" %>
+<%@ page import="password.pwm.util.java.StringUtil" %>
+<%@ page import="java.time.format.DateTimeFormatter" %>
 
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
@@ -182,16 +185,16 @@
                                             <select name="statsPeriodSelect"
                                                     style="width: 350px;">
                                                 <option value="<%=StatisticsManager.KEY_CUMULATIVE%>" <%= StatisticsManager.KEY_CUMULATIVE.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    since installation - <%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getInstallTime()) %>
+                                                    since installation - <span class="timestamp"><%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getInstallTime()) %></span>
                                                 </option>
                                                 <option value="<%=StatisticsManager.KEY_CURRENT%>" <%= StatisticsManager.KEY_CURRENT.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    since startup - <%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getStartupTime()) %>
+                                                    since startup - <span class="timestamp"><%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getStartupTime()) %></span>
                                                 </option>
-                                                <% final Map<StatisticsManager.DailyKey, String> availableKeys = statsManager.getAvailableKeys(locale); %>
-                                                <% for (final Map.Entry<StatisticsManager.DailyKey, String> entry : availableKeys.entrySet()) { %>
-                                                <% final StatisticsManager.DailyKey key = entry.getKey(); %>
+                                                <% final Map<DailyKey, String> availableKeys = statsManager.getAvailableKeys(locale); %>
+                                                <% for (final Map.Entry<DailyKey, String> entry : availableKeys.entrySet()) { %>
+                                                <% final DailyKey key = entry.getKey(); %>
                                                 <option value="<%=key%>" <%= key.toString().equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    <%= entry.getValue() %>
+                                                    <%=key.localDate().format(DateTimeFormatter.ISO_LOCAL_DATE)%>
                                                 </option>
                                                 <% } %>
                                             </select>
@@ -208,7 +211,17 @@
                                         <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
                                     </td>
                                     <td>
-                                        <%= stats.getStatistic(loopStat) %><%= loopStat.getType() == Statistic.Type.AVERAGE && loopStat != Statistic.AVG_PASSWORD_STRENGTH ? " ms" : "" %>
+                                        <%= stats.getStatistic(loopStat) %>
+                                    </td>
+                                </tr>
+                                <% } %>
+                                <% for (final AvgStatistic loopStat : AvgStatistic.values()) { %>
+                                <tr>
+                                    <td >
+                                        <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
+                                    </td>
+                                    <td>
+                                        <%= stats.getAvgStatistic(loopStat) %><%= loopStat.getUnit() %>
                                     </td>
                                 </tr>
                                 <% } %>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-search.jsp

@@ -58,7 +58,6 @@
                     </button>
                 </pwm:if>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
             </div>
         </form>
     </div>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/forgottenusername-search.jsp

@@ -51,7 +51,6 @@
                 </button>
                 <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
             </div>
         </form>
     </div>

+ 1 - 1
webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp

@@ -128,7 +128,7 @@
                 </script>
             </pwm:script>
             <% if (!StringUtil.isEmpty( currentValue) ) { %>
-            <button type="submit" id="button-deletePhoto-<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>" class="btn" title="<pwm:display key="Button_Delete"/>" form="form-deletePhoto-<%=loopConfiguration.getName()%>">
+            <button type="button" id="button-deletePhoto-<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>" class="btn" title="<pwm:display key="Button_Delete"/>">
                 <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
                 <pwm:display key="Button_Delete"/>
             </button>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/login.jsp

@@ -71,7 +71,6 @@
                         <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                     </pwm:if>
                     <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
-                    <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
                 </div>
             </div>
         </form>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/newuser.jsp

@@ -54,7 +54,6 @@
                     <pwm:display key="Button_Continue"/>
                 </button>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
 
                 <% if ((Boolean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.NewUser_FormShowBackButton)) { %>
                 <button type="button" id="button-goBack" name="button-goBack" class="btn" >

+ 10 - 7
webapp/src/main/webapp/public/resources/js/main.js

@@ -285,9 +285,10 @@ PWM_MAIN.applyFormAttributes = function() {
     require(["dojo"], function (dojo) {
         if(dojo.isIE){
             PWM_MAIN.doQuery("button[type=submit][form]",function(element){
-                PWM_MAIN.log('added event handler for submit button with form attribute ' + element.id);
+                PWM_MAIN.log('added IE event handler for submit button with form attribute ' + element.id);
                 PWM_MAIN.addEventHandler(element,'click',function(e){
-                    PWM_MAIN.stopEvent(e);
+                    e.preventDefault();
+                    PWM_MAIN.log('IE event handler intercepted submit for referenced form attribute ' + element.id);
                     var formID = element.getAttribute('form');
                     PWM_MAIN.handleFormSubmit(PWM_MAIN.getObject(formID));
                 });
@@ -424,11 +425,13 @@ PWM_MAIN.handleFormSubmit = function(form, event) {
     PWM_MAIN.cancelEvent(event);
 
     PWM_GLOBAL['idle_suspendTimeout'] = true;
-    var formElements = form.elements;
-    for (var i = 0; i < formElements.length; i++) {
-        formElements[i].readOnly = true;
-        if (formElements[i].type === 'button' || formElements[i].type === 'submit') {
-            formElements[i].disabled = true;
+    if ( form.elements ) {
+        var formElements = form.elements;
+        for (var i = 0; i < formElements.length; i++) {
+            formElements[i].readOnly = true;
+            if (formElements[i].type === 'button' || formElements[i].type === 'submit') {
+                formElements[i].disabled = true;
+            }
         }
     }
 

+ 1 - 1
webapp/src/main/webapp/public/resources/js/responses.js

@@ -57,7 +57,7 @@ PWM_RESPONSES.validateResponses=function() {
 };
 
 PWM_RESPONSES.updateDisplay=function(resultInfo) {
-    if (resultInfo === null) {
+    if (!resultInfo) {
         PWM_MAIN.getObject("button-setResponses").disabled = false;
         return;
     }

+ 3 - 3
webapp/src/main/webapp/public/resources/js/uilibrary.js

@@ -454,12 +454,12 @@ UILibrary.uploadFileDialog = function(options) {
     body += '<div id="fileList"></div>';
     body += '<input style="width:80%" class="btn" name="uploadFile" type="file" label="Select File" id="uploadFile"/>';
     body += '<div class="buttonbar">';
-    body += '<button class="btn" type="button" id="uploadButton" name="Upload" disabled><span class="pwm-icon pwm-icon-upload"></span> Upload</button>';
-    body += '</div></div>';
+    body += '<button class="btn" type="button" id="uploadButton" name="Upload" disabled><span class="pwm-icon pwm-icon-upload"></span>';
+    body +=  PWM_MAIN.showString('Button_Upload') + '</button></div></div>';
 
     var currentUrl = window.location.pathname;
     var uploadUrl = 'url' in options ? options['url'] : currentUrl;
-    var title = 'title' in options ? options['title'] : 'Upload File';
+    var title = 'title' in options ? options['title'] : PWM_MAIN.showString('Title_Upload');
 
     uploadUrl = PWM_MAIN.addPwmFormIDtoURL(uploadUrl);