Browse Source

telemetry service w/ ftp and http senders

Jason Rivard 8 years ago
parent
commit
6a8b6dbe2f

+ 3 - 0
import-control.xml

@@ -40,6 +40,9 @@
     <!-- chai -->
     <allow pkg="com.novell.ldapchai"/>
 
+    <!-- commons ftp client -->
+    <allow pkg="org.apache.commons.net.ftp"/>
+
     <!-- xml  -->
     <allow pkg="org.jdom2"/>
     <allow pkg="javax.xml"/>

+ 5 - 0
pom.xml

@@ -611,6 +611,11 @@
             <artifactId>ldapchai</artifactId>
             <version>0.6.9</version>
         </dependency>
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <version>3.6</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-csv</artifactId>

+ 6 - 1
src/main/java/password/pwm/AppProperty.java

@@ -29,7 +29,7 @@ import java.util.ResourceBundle;
  * by an associated {@code AppProperty.properties} file.  Properties can be overridden by the application administrator in
  * the configuration using the setting {@link password.pwm.config.PwmSetting#APP_PROPERTY_OVERRIDES}.
  */
-public enum     AppProperty {
+public enum AppProperty {
 
     APPLICATION_FILELOCK_FILENAME                   ("application.fileLock.filename"),
     APPLICATION_FILELOCK_WAIT_SECONDS               ("application.fileLock.waitSeconds"),
@@ -287,6 +287,11 @@ public enum     AppProperty {
     TOKEN_RESEND_DELAY_MS                           ("token.resend.delayMS"),
     TOKEN_REMOVE_ON_CLAIM                           ("token.removeOnClaim"),
     TOKEN_VERIFY_PW_MODIFY_TIME                     ("token.verifyPwModifyTime"),
+    TELEMETRY_SENDER_IMPLEMENTATION                 ("telemetry.senderImplementation"),
+    TELEMETRY_SENDER_SETTINGS                       ("telemetry.senderSettings"),
+    TELEMETRY_SEND_FREQUENCY_SECONDS                ("telemetry.sendFrequencySeconds"),
+    TELEMETRY_MIN_AUTHENTICATIONS                   ("telemetry.minimumAuthentications"),
+
 
 
     /** Regular expression to be used for matching URLs to be shortened by the URL Shortening Service Class. */

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

@@ -59,7 +59,7 @@ import password.pwm.svc.wordlist.SeedlistManager;
 import password.pwm.svc.wordlist.SharedHistoryManager;
 import password.pwm.svc.wordlist.WordlistManager;
 import password.pwm.util.PasswordData;
-import password.pwm.util.VersionChecker;
+import password.pwm.svc.telemetry.VersionChecker;
 import password.pwm.util.cli.commands.ExportHttpsTomcatConfigCommand;
 import password.pwm.util.db.DatabaseAccessor;
 import password.pwm.util.db.DatabaseService;
@@ -127,6 +127,8 @@ public class PwmApplication {
         CONFIG_LOGIN_HISTORY("config.loginHistory"),
         LOCALDB_LOGGER_STORAGE_FORMAT("localdb.logger.storage.format"),
 
+        TELEMETRY_LAST_PUBLISH_TIMESTAMP("telemetry.lastPublish.timestamp")
+
         ;
 
         private final String key;

+ 19 - 10
src/main/java/password/pwm/bean/StatsPublishBean.java → src/main/java/password/pwm/bean/TelemetryPublishBean.java

@@ -22,7 +22,8 @@
 
 package password.pwm.bean;
 
-import lombok.AllArgsConstructor;
+import com.novell.ldapchai.provider.ChaiProvider;
+import lombok.Builder;
 import lombok.Getter;
 
 import java.io.Serializable;
@@ -31,20 +32,28 @@ import java.util.List;
 import java.util.Map;
 
 @Getter
-@AllArgsConstructor
-public class StatsPublishBean implements Serializable {
+@Builder
+public class TelemetryPublishBean implements Serializable {
+    private final String id;
     private final String instanceID;
+    private final String siteDescription;
+    private final Instant installTime;
     private final Instant timestamp;
-    private final Map<String,String> totalStatistics;
+    private final List<ChaiProvider.DIRECTORY_VENDOR> ldapVendor;
+    private final Map<String,String> statistics;
     private final List<String> configuredSettings;
     private final String versionBuild;
     private final String versionVersion;
-    private final Map<String,String> otherInfo;
+    private final Environment environment;
 
-    public enum KEYS {
-        SITE_URL,
-        SITE_DESCRIPTION,
-        INSTALL_DATE,
-        LDAP_VENDOR
+    @Getter
+    @Builder
+    public static class Environment implements Serializable {
+        String osName;
+        String osVersion;
+        String javaVendor;
+        String javaName;
+        String javaVersion;
+        boolean appliance;
     }
 }

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

@@ -165,6 +165,7 @@ public enum PwmError {
     ERROR_PASSWORD_ONLY_BAD(        5089, "Error_PasswordOnlyBad",          null),
 
     ERROR_REMOTE_ERROR_VALUE(       6000, "Error_RemoteErrorValue",         null, ErrorFlag.Permanent),
+    ERROR_TELEMETRY_SEND_ERROR(     6001, "Error_TelemetrySendError",       null),
 
     ERROR_FIELD_REQUIRED(           5100, "Error_FieldRequired",            null),
     ERROR_FIELD_NOT_A_NUMBER(       5101, "Error_FieldNotANumber",          null),

+ 7 - 2
src/main/java/password/pwm/http/PwmSessionWrapper.java

@@ -64,7 +64,11 @@ public class PwmSessionWrapper {
         return returnSession;
     }
 
-    public static PwmSession readPwmSession(final HttpServletRequest httpRequest) throws PwmUnrecoverableException {
+    public static PwmSession readPwmSession(
+            final HttpServletRequest httpRequest
+    )
+            throws PwmUnrecoverableException
+    {
         return readPwmSession(httpRequest.getSession());
     }
 
@@ -72,7 +76,8 @@ public class PwmSessionWrapper {
             final PwmApplication pwmApplication,
             final PwmSession pwmSession,
             final HttpSession httpSession
-    ) throws PwmUnrecoverableException
+    )
+            throws PwmUnrecoverableException
     {
         final IdleTimeoutCalculator.MaxIdleTimeoutResult result = IdleTimeoutCalculator.figureMaxSessionTimeout(pwmApplication, pwmSession);
         if (httpSession.getMaxInactiveInterval() != result.getIdleTimeout().getTotalSeconds()) {

+ 87 - 0
src/main/java/password/pwm/svc/PwmServiceEnum.java

@@ -0,0 +1,87 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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;
+
+import password.pwm.util.java.JavaHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public enum PwmServiceEnum {
+    SecureService(          password.pwm.util.secure.SecureService.class,           Flag.StartDuringRuntimeInstance),
+    LdapConnectionService(  password.pwm.ldap.LdapConnectionService.class,          Flag.StartDuringRuntimeInstance),
+    DatabaseService(        password.pwm.util.db.DatabaseService.class,             Flag.StartDuringRuntimeInstance),
+    SharedHistoryManager(   password.pwm.svc.wordlist.SharedHistoryManager.class),
+    AuditService(           password.pwm.svc.event.AuditService.class),
+    StatisticsManager(      password.pwm.svc.stats.StatisticsManager.class),
+    WordlistManager(        password.pwm.svc.wordlist.WordlistManager.class),
+    SeedlistManager(        password.pwm.svc.wordlist.SeedlistManager.class),
+    EmailQueueManager(      password.pwm.util.queue.EmailQueueManager.class),
+    SmsQueueManager(        password.pwm.util.queue.SmsQueueManager.class),
+    UrlShortenerService(    password.pwm.svc.shorturl.UrlShortenerService.class),
+    TokenService(           password.pwm.svc.token.TokenService.class),
+    VersionChecker(         password.pwm.svc.telemetry.VersionChecker.class),
+    IntruderManager(        password.pwm.svc.intruder.IntruderManager.class),
+    CrService(              password.pwm.util.operations.CrService.class,           Flag.StartDuringRuntimeInstance),
+    OtpService(             password.pwm.util.operations.OtpService.class),
+    CacheService(           password.pwm.svc.cache.CacheService.class,              Flag.StartDuringRuntimeInstance),
+    HealthMonitor(          password.pwm.health.HealthMonitor.class),
+    ReportService(          password.pwm.svc.report.ReportService.class,            Flag.StartDuringRuntimeInstance),
+    ResourceServletService( password.pwm.http.servlet.resource.ResourceServletService.class),
+    SessionTrackService(    password.pwm.svc.sessiontrack.SessionTrackService.class),
+    SessionStateSvc(        password.pwm.http.state.SessionStateService.class),
+    UserSearchEngine(       password.pwm.ldap.search.UserSearchEngine.class,        Flag.StartDuringRuntimeInstance),
+    TelemetryService(       password.pwm.svc.telemetry.TelemetryService.class),
+    ClusterService(         password.pwm.svc.cluster.ClusterService.class),
+
+    ;
+
+    private final Class<? extends PwmService> clazz;
+    private final Flag[] flags;
+
+    private enum Flag {
+        StartDuringRuntimeInstance,
+    }
+
+    PwmServiceEnum(final Class<? extends PwmService> clazz, final Flag... flags) {
+        this.clazz = clazz;
+        this.flags = flags;
+    }
+
+    public boolean isInternalRuntime() {
+        return JavaHelper.enumArrayContainsValue(flags, Flag.StartDuringRuntimeInstance);
+    }
+
+    static List<Class<? extends PwmService>> allClasses() {
+        final List<Class<? extends PwmService>> pwmServiceClasses = new ArrayList<>();
+        for (final PwmServiceEnum enumClass : values()) {
+            pwmServiceClasses.add(enumClass.getPwmServiceClass());
+        }
+        return Collections.unmodifiableList(pwmServiceClasses);
+    }
+
+    public Class<? extends PwmService> getPwmServiceClass() {
+        return clazz;
+    }
+}

+ 2 - 79
src/main/java/password/pwm/svc/PwmServiceManager.java

@@ -28,32 +28,8 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.health.HealthMonitor;
-import password.pwm.http.servlet.resource.ResourceServletService;
-import password.pwm.http.state.SessionStateService;
-import password.pwm.ldap.LdapConnectionService;
-import password.pwm.ldap.search.UserSearchEngine;
-import password.pwm.svc.cache.CacheService;
-import password.pwm.svc.cluster.ClusterService;
-import password.pwm.svc.event.AuditService;
-import password.pwm.svc.intruder.IntruderManager;
-import password.pwm.svc.report.ReportService;
-import password.pwm.svc.sessiontrack.SessionTrackService;
-import password.pwm.svc.shorturl.UrlShortenerService;
-import password.pwm.svc.stats.StatisticsManager;
-import password.pwm.svc.token.TokenService;
-import password.pwm.svc.wordlist.SeedlistManager;
-import password.pwm.svc.wordlist.SharedHistoryManager;
-import password.pwm.svc.wordlist.WordlistManager;
-import password.pwm.util.VersionChecker;
-import password.pwm.util.db.DatabaseService;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.operations.CrService;
-import password.pwm.util.operations.OtpService;
-import password.pwm.util.queue.EmailQueueManager;
-import password.pwm.util.queue.SmsQueueManager;
-import password.pwm.util.secure.SecureService;
 
 import java.time.Instant;
 import java.util.ArrayList;
@@ -71,59 +47,6 @@ public class PwmServiceManager {
     private final Map<Class<? extends PwmService>, PwmService> runningServices = new HashMap<>();
     private boolean initialized;
 
-    public enum PwmServiceClassEnum {
-        SecureService(          SecureService.class,             true),
-        LdapConnectionService(  LdapConnectionService.class,     true),
-        DatabaseService(        DatabaseService.class,           true),
-        SharedHistoryManager(   SharedHistoryManager.class,      false),
-        AuditService(           AuditService.class,              false),
-        StatisticsManager(      StatisticsManager.class,         false),
-        WordlistManager(        WordlistManager.class,           false),
-        SeedlistManager(        SeedlistManager.class,           false),
-        EmailQueueManager(      EmailQueueManager.class,         false),
-        SmsQueueManager(        SmsQueueManager.class,           false),
-        UrlShortenerService(    UrlShortenerService.class,       false),
-        TokenService(           TokenService.class,              false),
-        VersionChecker(         VersionChecker.class,            false),
-        IntruderManager(        IntruderManager.class,           false),
-        CrService(              CrService.class,                 true),
-        OtpService(             OtpService.class,                false),
-        CacheService(           CacheService.class,              true),
-        HealthMonitor(          HealthMonitor.class,             false),
-        ReportService(          ReportService.class,             true),
-        ResourceServletService( ResourceServletService.class,    false),
-        SessionTrackService(    SessionTrackService.class,       false),
-        SessionStateSvc(        SessionStateService.class,       false),
-        UserSearchEngine(       UserSearchEngine.class,          true),
-        ClusterService(         ClusterService.class,            false),
-
-        ;
-
-        private final Class<? extends PwmService> clazz;
-        private final boolean internalRuntime;
-
-        PwmServiceClassEnum(final Class<? extends PwmService> clazz, final boolean internalRuntime) {
-            this.clazz = clazz;
-            this.internalRuntime = internalRuntime;
-        }
-
-        public boolean isInternalRuntime() {
-            return internalRuntime;
-        }
-
-        static List<Class<? extends PwmService>> allClasses() {
-            final List<Class<? extends PwmService>> pwmServiceClasses = new ArrayList<>();
-            for (final PwmServiceClassEnum enumClass : values()) {
-                pwmServiceClasses.add(enumClass.getPwmServiceClass());
-            }
-            return Collections.unmodifiableList(pwmServiceClasses);
-        }
-
-        public Class<? extends PwmService> getPwmServiceClass() {
-            return clazz;
-        }
-    }
-
     public PwmServiceManager(final PwmApplication pwmApplication) {
         this.pwmApplication = pwmApplication;
     }
@@ -139,7 +62,7 @@ public class PwmServiceManager {
         final boolean internalRuntimeInstance = pwmApplication.getPwmEnvironment().isInternalRuntimeInstance()
                 || pwmApplication.getPwmEnvironment().getFlags().contains(PwmEnvironment.ApplicationFlag.CommandLineInstance);
 
-        for (final PwmServiceClassEnum serviceClassEnum : PwmServiceClassEnum.values()) {
+        for (final PwmServiceEnum serviceClassEnum : PwmServiceEnum.values()) {
             boolean startService = true;
             if (internalRuntimeInstance && !serviceClassEnum.isInternalRuntime()) {
                 startService = false;
@@ -192,7 +115,7 @@ public class PwmServiceManager {
             return;
         }
 
-        final List<Class<? extends PwmService>> reverseServiceList = new ArrayList<>(PwmServiceClassEnum.allClasses());
+        final List<Class<? extends PwmService>> reverseServiceList = new ArrayList<>(PwmServiceEnum.allClasses());
         Collections.reverse(reverseServiceList);
         for (final Class<? extends PwmService> serviceClass : reverseServiceList) {
             if (runningServices.containsKey(serviceClass)) {

+ 11 - 106
src/main/java/password/pwm/svc/stats/StatisticsManager.java

@@ -23,22 +23,12 @@
 package password.pwm.svc.stats;
 
 import org.apache.commons.csv.CSVPrinter;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.StringEntity;
 import password.pwm.PwmApplication;
-import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
-import password.pwm.bean.StatsPublishBean;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.error.PwmException;
-import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
-import password.pwm.http.client.PwmHttpClient;
 import password.pwm.svc.PwmService;
 import password.pwm.util.AlertHandler;
 import password.pwm.util.java.JavaHelper;
@@ -47,13 +37,10 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmRandom;
 
 import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
@@ -67,8 +54,9 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TimeZone;
-import java.util.Timer;
 import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 public class StatisticsManager implements PwmService {
 
@@ -86,14 +74,13 @@ public class StatisticsManager implements PwmService {
 
     public static final String KEY_CURRENT = "CURRENT";
     public static final String KEY_CUMULATIVE = "CUMULATIVE";
-    public static final String KEY_CLOUD_PUBLISH_TIMESTAMP = "CLOUD_PUB_TIMESTAMP";
 
     private LocalDB localDB;
 
     private DailyKey currentDailyKey = new DailyKey(new Date());
     private DailyKey initialDailyKey = new DailyKey(new Date());
 
-    private Timer daemonTimer;
+    private ScheduledExecutorService executorService;
 
     private final StatisticsBundle statsCurrent = new StatisticsBundle();
     private StatisticsBundle statsDaily = new StatisticsBundle();
@@ -297,28 +284,10 @@ public class StatisticsManager implements PwmService {
         localDB.put(LocalDB.DB.PWM_STATS, DB_KEY_INITIAL_DAILY_KEY, initialDailyKey.toString());
 
         { // setup a timer to roll over at 0 Zula and one to write current stats every 10 seconds
-            final String threadName = JavaHelper.makeThreadName(pwmApplication, this.getClass()) + " timer";
-            daemonTimer = new Timer(threadName, true);
-            daemonTimer.schedule(new FlushTask(), 10 * 1000, DB_WRITE_FREQUENCY_MS);
-            daemonTimer.schedule(new NightlyTask(), Date.from(JavaHelper.nextZuluZeroTime()));
-        }
-
-        if (pwmApplication.getApplicationMode() == PwmApplicationMode.RUNNING) {
-            if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.PUBLISH_STATS_ENABLE)) {
-                long lastPublishTimestamp = pwmApplication.getInstallTime().toEpochMilli();
-                {
-                    final String lastPublishDateStr = localDB.get(LocalDB.DB.PWM_STATS,KEY_CLOUD_PUBLISH_TIMESTAMP);
-                    if (lastPublishDateStr != null && lastPublishDateStr.length() > 0) {
-                        try {
-                            lastPublishTimestamp = Long.parseLong(lastPublishDateStr);
-                        } catch (Exception e) {
-                            LOGGER.error("unexpected error reading last publish timestamp from PwmDB: " + e.getMessage());
-                        }
-                    }
-                }
-                final Date nextPublishTime = new Date(lastPublishTimestamp + PwmConstants.STATISTICS_PUBLISH_FREQUENCY_MS + (long) PwmRandom.getInstance().nextInt(3600 * 1000));
-                daemonTimer.schedule(new PublishTask(), nextPublishTime, PwmConstants.STATISTICS_PUBLISH_FREQUENCY_MS);
-            }
+            executorService = JavaHelper.makeSingleThreadExecutorService(pwmApplication, this.getClass());
+            executorService.scheduleAtFixedRate(new FlushTask(), 10 * 1000, DB_WRITE_FREQUENCY_MS, TimeUnit.MILLISECONDS);
+            final TimeDuration delayTillNextZulu = TimeDuration.fromCurrent(JavaHelper.nextZuluZeroTime());
+            executorService.scheduleAtFixedRate(new NightlyTask(), delayTillNextZulu.getTotalMilliseconds(), TimeUnit.DAYS.toMillis(1), TimeUnit.MILLISECONDS);
         }
 
         status = STATUS.OPEN;
@@ -375,9 +344,9 @@ public class StatisticsManager implements PwmService {
         } catch (Exception e) {
             LOGGER.error("unexpected error closing: " + e.getMessage());
         }
-        if (daemonTimer != null) {
-            daemonTimer.cancel();
-        }
+
+        JavaHelper.closeAndWaitExecutor(executorService, new TimeDuration(3, TimeUnit.SECONDS));
+
         status = STATUS.CLOSED;
     }
 
@@ -390,7 +359,6 @@ public class StatisticsManager implements PwmService {
         public void run() {
             writeDbValues();
             resetDailyStats();
-            daemonTimer.schedule(new NightlyTask(), Date.from(JavaHelper.nextZuluZeroTime()));
         }
     }
 
@@ -400,15 +368,7 @@ public class StatisticsManager implements PwmService {
         }
     }
 
-    private class PublishTask extends TimerTask {
-        public void run() {
-            try {
-                publishStatisticsToCloud();
-            } catch (Exception e) {
-                LOGGER.error("error publishing statistics to cloud: " + e.getMessage());
-            }
-        }
-    }
+
 
     public static class DailyKey {
         int year;
@@ -491,61 +451,6 @@ public class StatisticsManager implements PwmService {
         return epsMeterMap.get(type.toString() + duration.toString()).readEventRate();
     }
 
-    private void publishStatisticsToCloud()
-            throws URISyntaxException, IOException, PwmUnrecoverableException {
-        final StatsPublishBean statsPublishData;
-        {
-            final StatisticsBundle bundle = getStatBundleForKey(KEY_CUMULATIVE);
-            final Map<String,String> statData = new HashMap<>();
-            for (final Statistic loopStat : Statistic.values()) {
-                statData.put(loopStat.getKey(),bundle.getStatistic(loopStat));
-            }
-            final Configuration config = pwmApplication.getConfig();
-            final List<String> configuredSettings = new ArrayList<>();
-            for (final PwmSetting pwmSetting : config.nonDefaultSettings()) {
-                if (!pwmSetting.getCategory().hasProfiles() && !config.isDefaultValue(pwmSetting)) {
-                    configuredSettings.add(pwmSetting.getKey());
-                }
-            }
-            final Map<String,String> otherData = new HashMap<>();
-            otherData.put(StatsPublishBean.KEYS.SITE_URL.toString(),config.readSettingAsString(PwmSetting.PWM_SITE_URL));
-            otherData.put(StatsPublishBean.KEYS.SITE_DESCRIPTION.toString(),config.readSettingAsString(PwmSetting.PUBLISH_STATS_SITE_DESCRIPTION));
-            otherData.put(StatsPublishBean.KEYS.INSTALL_DATE.toString(), JavaHelper.toIsoDate(pwmApplication.getInstallTime()));
-
-            try {
-                otherData.put(StatsPublishBean.KEYS.LDAP_VENDOR.toString(),pwmApplication.getProxyChaiProvider(config.getDefaultLdapProfile().getIdentifier()).getDirectoryVendor().toString());
-            } catch (Exception e) {
-                LOGGER.trace("unable to read ldap vendor type for stats publication: " + e.getMessage());
-            }
-
-            statsPublishData = new StatsPublishBean(
-                    pwmApplication.getInstanceID(),
-                    Instant.now(),
-                    statData,
-                    configuredSettings,
-                    PwmConstants.BUILD_NUMBER,
-                    PwmConstants.BUILD_VERSION,
-                    otherData
-            );
-        }
-        final URI requestURI = new URI(PwmConstants.PWM_URL_CLOUD + "/rest/pwm/statistics");
-        final HttpPost httpPost = new HttpPost(requestURI.toString());
-        final String jsonDataString = JsonUtil.serialize(statsPublishData);
-        httpPost.setEntity(new StringEntity(jsonDataString));
-        httpPost.setHeader("Accept", PwmConstants.AcceptValue.json.getHeaderValue());
-        httpPost.setHeader("Content-Type", PwmConstants.ContentTypeValue.json.getHeaderValue());
-        LOGGER.debug("preparing to send anonymous statistics to " + requestURI.toString() + ", data to send: " + jsonDataString);
-        final HttpResponse httpResponse = PwmHttpClient.getHttpClient(pwmApplication.getConfig()).execute(httpPost);
-        if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
-            throw new IOException("http response error code: " + httpResponse.getStatusLine().getStatusCode());
-        }
-        LOGGER.info("published anonymous statistics to " + requestURI.toString());
-        try {
-            localDB.put(LocalDB.DB.PWM_STATS, KEY_CLOUD_PUBLISH_TIMESTAMP, String.valueOf(System.currentTimeMillis()));
-        } catch (LocalDBException e) {
-            LOGGER.error("unexpected error trying to save last statistics published time to LocalDB: " + e.getMessage());
-        }
-    }
 
     public int outputStatsToCsv(final OutputStream outputStream, final Locale locale, final boolean includeHeader)
             throws IOException

+ 207 - 0
src/main/java/password/pwm/svc/telemetry/FtpTelemetrySender.java

@@ -0,0 +1,207 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.telemetry;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPReply;
+import org.apache.commons.net.ftp.FTPSClient;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class FtpTelemetrySender implements TelemetrySender {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(FtpTelemetrySender.class);
+
+    private Settings settings;
+
+    @Override
+    public void init(final PwmApplication pwmApplication,final  String initString) {
+        settings = JsonUtil.deserialize(initString, Settings.class);
+    }
+
+    @Override
+    public void publish(final TelemetryPublishBean telemetryPublishBean) throws PwmUnrecoverableException
+    {
+        ftpPut(telemetryPublishBean);
+    }
+
+    private void ftpPut(final TelemetryPublishBean telemetryPublishBean) throws PwmUnrecoverableException
+    {
+        final FTPClient ftpClient;
+        switch (settings.getFtpMode()) {
+            case ftp:
+                ftpClient = new FTPClient();
+                break;
+
+            case ftps:
+                ftpClient = new FTPSClient();
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement(settings.getFtpMode());
+                throw new UnsupportedOperationException();
+        }
+
+
+        // connect
+        try {
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "establishing " + settings.getFtpMode() + " connection to " + settings.getHost());
+            ftpClient.connect(settings.getHost());
+
+            final int reply = ftpClient.getReplyCode();
+            if (!FTPReply.isPositiveCompletion(reply)) {
+                disconnectFtpClient(ftpClient);
+                final String msg = "error " + reply + " connecting to " + settings.getHost();
+                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+            }
+
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "connected to " + settings.getHost());
+        } catch (IOException e) {
+            disconnectFtpClient(ftpClient);
+            final String msg = "unable to connect to " + settings.getHost() + ", error: " + e.getMessage();
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+        }
+
+        // set modes
+        try {
+            ftpClient.enterLocalPassiveMode();
+            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+
+            final int reply = ftpClient.getReplyCode();
+            if (!FTPReply.isPositiveCompletion(reply)) {
+                disconnectFtpClient(ftpClient);
+                final String msg = "error setting file type mode to binary, error=" + reply;
+                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+            }
+        } catch (IOException e) {
+            disconnectFtpClient(ftpClient);
+            final String msg = "unable to connect to " + settings.getHost() + ", error: " + e.getMessage();
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+        }
+
+        // authenticate
+        try {
+            ftpClient.login(settings.getUsername(), settings.getPassword());
+
+            final int reply = ftpClient.getReplyCode();
+            if (!FTPReply.isPositiveCompletion(reply)) {
+                disconnectFtpClient(ftpClient);
+                final String msg = "error authenticating as " + settings.getUsername() + " to " + settings.getHost() + ", error=" + reply;
+                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+            }
+
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "authenticated to " + settings.getHost() + " as " + settings.getUsername());
+        } catch (IOException e) {
+            disconnectFtpClient(ftpClient);
+            final String msg = "error authenticating as " + settings.getUsername() + " to " + settings.getHost() + ", error: " + e.getMessage();
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+        }
+
+
+        // upload
+        try {
+            final String filePath = settings.getPath() + "/" + telemetryPublishBean.getId() + ".zip";
+            final byte[] fileBytes = dataToJsonZipFile(telemetryPublishBean);
+            final ByteArrayInputStream fileStream = new ByteArrayInputStream(fileBytes);
+
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "preparing to transfer " + fileBytes.length + " bytes to file path " + filePath);
+
+            final Instant startTime = Instant.now();
+            ftpClient.storeFile(filePath, fileStream);
+
+            final int reply = ftpClient.getReplyCode();
+            if (!FTPReply.isPositiveCompletion(reply)) {
+                disconnectFtpClient(ftpClient);
+                final String msg = "error uploading file  to " + settings.getHost() + ", error=" + reply;
+                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+            }
+
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "completed transfer of " + fileBytes.length + " in " + TimeDuration.compactFromCurrent(startTime));
+        } catch (IOException e) {
+            disconnectFtpClient(ftpClient);
+            final String msg = "error uploading file  to " + settings.getHost() + ", error: " +e.getMessage();
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+        }
+    }
+
+    private void disconnectFtpClient(final FTPClient ftpClient) {
+        if (ftpClient.isConnected()) {
+            try {
+                ftpClient.disconnect();
+                LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "disconnected");
+            } catch (IOException e) {
+                LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "error while disconnecting ftp client: " + e.getMessage());
+            }
+        }
+    }
+
+    @Getter
+    @AllArgsConstructor
+    private static class Settings implements Serializable {
+        private FTP_MODE ftpMode;
+        private String host;
+        private String username;
+        private String password;
+        private String path;
+
+        enum FTP_MODE {
+            ftp,
+            ftps,
+        }
+    }
+
+    private static byte[] dataToJsonZipFile(final TelemetryPublishBean telemetryPublishBean) throws IOException
+    {
+        final String jsonData = JsonUtil.serialize(telemetryPublishBean, JsonUtil.Flag.PrettyPrint);
+        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        final ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
+        final ZipEntry e = new ZipEntry(telemetryPublishBean.getId() + ".json");
+        zipOutputStream.putNextEntry(e);
+
+        final byte[] data = jsonData.getBytes(PwmConstants.DEFAULT_CHARSET);
+        zipOutputStream.write(data, 0, data.length);
+        zipOutputStream.closeEntry();
+        zipOutputStream.close();
+        return byteArrayOutputStream.toByteArray();
+    }
+
+}

+ 86 - 0
src/main/java/password/pwm/svc/telemetry/HttpTelemetrySender.java

@@ -0,0 +1,86 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.telemetry;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpHeader;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.client.PwmHttpClient;
+import password.pwm.http.client.PwmHttpClientConfiguration;
+import password.pwm.http.client.PwmHttpClientRequest;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HttpTelemetrySender implements TelemetrySender {
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass(HttpTelemetrySender.class);
+
+    private PwmApplication pwmApplication;
+    private Settings settings;
+
+    @Override
+    public void init(final PwmApplication pwmApplication, final String initString)
+    {
+        this.pwmApplication = pwmApplication;
+        settings = JsonUtil.deserialize(initString, HttpTelemetrySender.Settings.class);
+    }
+
+    @Override
+    public void publish(final TelemetryPublishBean statsPublishBean)
+            throws PwmUnrecoverableException
+    {
+        final PwmHttpClientConfiguration pwmHttpClientConfiguration = new PwmHttpClientConfiguration.Builder()
+                .setPromiscuous(true)
+                .create();
+        final PwmHttpClient pwmHttpClient = new PwmHttpClient(pwmApplication, SessionLabel.TELEMETRY_SESSION_LABEL, pwmHttpClientConfiguration);
+        final String body = JsonUtil.serialize(statsPublishBean);
+        final Map<String,String> headers = new HashMap<>();
+        headers.put(HttpHeader.Content_Type.getHttpName(), PwmConstants.ContentTypeValue.json.getHeaderValue());
+        headers.put(HttpHeader.Accept.getHttpName(), PwmConstants.AcceptValue.json.getHeaderValue());
+        final PwmHttpClientRequest pwmHttpClientRequest = new PwmHttpClientRequest(
+                HttpMethod.POST,
+                settings.getUrl(),
+                body,
+                headers
+        );
+        LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL,"preparing to send telemetry data to '" + settings.getUrl() + ")");
+        pwmHttpClient.makeRequest(pwmHttpClientRequest);
+        LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL,"sent telemetry data to '" + settings.getUrl() + ")");
+    }
+
+    @Getter
+    @AllArgsConstructor
+    private static class Settings implements Serializable {
+        private String url;
+    }
+}

+ 33 - 0
src/main/java/password/pwm/svc/telemetry/TelemetrySender.java

@@ -0,0 +1,33 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.telemetry;
+
+import password.pwm.PwmApplication;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.error.PwmUnrecoverableException;
+
+public interface TelemetrySender {
+    void init(PwmApplication pwmApplication, String initString);
+
+    void publish(TelemetryPublishBean statsPublishBean) throws PwmUnrecoverableException;
+}

+ 338 - 0
src/main/java/password/pwm/svc/telemetry/TelemetryService.java

@@ -0,0 +1,338 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.telemetry;
+
+import com.novell.ldapchai.provider.ChaiProvider;
+import lombok.Builder;
+import lombok.Getter;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmApplicationMode;
+import password.pwm.PwmConstants;
+import password.pwm.PwmEnvironment;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.LdapProfile;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.health.HealthRecord;
+import password.pwm.svc.PwmService;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsBundle;
+import password.pwm.svc.stats.StatisticsManager;
+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.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.secure.PwmRandom;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class TelemetryService implements PwmService {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(TelemetryService.class);
+
+    private ScheduledExecutorService executorService;
+    private PwmApplication pwmApplication;
+    private Settings settings;
+
+    private Instant lastPublishTime;
+    private ErrorInformation lastError;
+    private TelemetrySender sender;
+
+    private STATUS status = STATUS.NEW;
+
+
+    @Override
+    public STATUS status()
+    {
+        return null;
+    }
+
+    @Override
+    public void init(final PwmApplication pwmApplication) throws PwmException
+    {
+        status = STATUS.OPENING;
+        this.pwmApplication = pwmApplication;
+
+        if (pwmApplication.getApplicationMode() != PwmApplicationMode.RUNNING) {
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, app is not running");
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.PUBLISH_STATS_ENABLE)) {
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, publish stats not enabled");
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        if (pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN) {
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, localdb not enabled");
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        if (pwmApplication.getStatisticsManager().status() != STATUS.OPEN) {
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, statistics manager is not enabled");
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        settings = Settings.fromConfig(pwmApplication.getConfig());
+        try {
+            initSender();
+        } catch (PwmUnrecoverableException e) {
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, unable to init sender: " + e.getMessage());
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        {
+            final Instant storedLastPublishTimestamp = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.TELEMETRY_LAST_PUBLISH_TIMESTAMP, Instant.class);
+            lastPublishTime = storedLastPublishTimestamp != null ?
+                    storedLastPublishTimestamp :
+                    pwmApplication.getInstallTime();
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "last publish time was " + JavaHelper.toIsoDate(lastPublishTime));
+        }
+
+        executorService = JavaHelper.makeSingleThreadExecutorService(pwmApplication, TelemetryService.class);
+
+        scheduleNextJob();
+    }
+
+    private void initSender() throws PwmUnrecoverableException
+    {
+        if (StringUtil.isEmpty(settings.getSenderImplementation())) {
+            final String msg = "telemetry sender implementation not specified";
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
+        }
+
+        final TelemetrySender telemetrySender;
+        try {
+            final String senderClass = settings.getSenderImplementation();
+            final Class theClass = Class.forName(senderClass);
+            telemetrySender = (TelemetrySender) theClass.newInstance();
+        } catch (Exception e) {
+            final String msg = "unable to load implementation class: " + e.getMessage();
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, msg));
+        }
+
+        try {
+            final String macrodSettings = MacroMachine.forNonUserSpecific(pwmApplication, null).expandMacros(settings.getSenderSettings());
+            telemetrySender.init(pwmApplication, macrodSettings);
+        } catch (Exception e) {
+            final String msg = "unable to init implementation class: " + e.getMessage();
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, msg));
+        }
+        sender = telemetrySender;
+    }
+
+    private void executePublishJob() throws PwmUnrecoverableException, IOException, URISyntaxException
+    {
+        final String authValue = pwmApplication.getStatisticsManager().getStatBundleForKey(StatisticsManager.KEY_CUMULATIVE).getStatistic(Statistic.AUTHENTICATIONS);
+        if (StringUtil.isEmpty(authValue) || Integer.parseInt(authValue) < settings.getMinimumAuthentications()) {
+            LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "skipping telemetry send, authentication count is too low");
+        } else {
+            try {
+                final TelemetryPublishBean telemetryPublishBean = generatePublishableBean();
+                sender.publish(telemetryPublishBean);
+                LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "sent telemetry data: " + JsonUtil.serialize(telemetryPublishBean));
+            } catch (PwmException e) {
+                lastError = e.getErrorInformation();
+                LOGGER.error(SessionLabel.TELEMETRY_SESSION_LABEL, "error sending telemetry data: " + e.getMessage());
+            }
+        }
+
+        lastPublishTime = Instant.now();
+        pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.TELEMETRY_LAST_PUBLISH_TIMESTAMP, lastPublishTime);
+        scheduleNextJob();
+    }
+
+    private void scheduleNextJob() {
+        final TimeDuration durationUntilNextPublish = durationUntilNextPublish();
+        executorService.schedule(
+                new PublishJob(),
+                durationUntilNextPublish.getTotalMilliseconds(),
+                TimeUnit.MILLISECONDS);
+        LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "next publish time: " + durationUntilNextPublish().asCompactString());
+    }
+
+    private class PublishJob implements Runnable {
+        @Override
+        public void run()
+        {
+            try {
+                executePublishJob();
+            } catch (PwmException e) {
+                LOGGER.error(e.getErrorInformation());
+            } catch (Exception e) {
+                LOGGER.error("unexpected error during telemetry publish job: " + e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public void close()
+    {
+
+    }
+
+    @Override
+    public List<HealthRecord> healthCheck()
+    {
+        return null;
+    }
+
+    @Override
+    public ServiceInfoBean serviceInfo()
+    {
+        final Map<String,String> debugMap = new LinkedHashMap<>();
+        debugMap.put("lastPublishTime", JavaHelper.toIsoDate(lastPublishTime));
+        debugMap.put("lastError", lastError.toDebugStr());
+        return new ServiceInfoBean(null,Collections.unmodifiableMap(debugMap));
+    }
+
+
+    private TelemetryPublishBean generatePublishableBean()
+            throws URISyntaxException, IOException, PwmUnrecoverableException
+    {
+        final StatisticsBundle bundle = pwmApplication.getStatisticsManager().getStatBundleForKey(StatisticsManager.KEY_CUMULATIVE);
+        final Configuration config = pwmApplication.getConfig();
+
+        final Map<String,String> statData = new TreeMap<>();
+        for (final Statistic loopStat : Statistic.values()) {
+            statData.put(loopStat.getKey(),bundle.getStatistic(loopStat));
+        }
+
+        final List<String> configuredSettings = new ArrayList<>();
+        for (final PwmSetting pwmSetting : config.nonDefaultSettings()) {
+            if (!pwmSetting.getCategory().hasProfiles() && !config.isDefaultValue(pwmSetting)) {
+                configuredSettings.add(pwmSetting.getKey());
+            }
+        }
+
+        final Set<ChaiProvider.DIRECTORY_VENDOR> ldapVendors = new LinkedHashSet<>();
+        for (final LdapProfile ldapProfile : config.getLdapProfiles().values()) {
+            try {
+                ldapVendors.add(ldapProfile.getProxyChaiProvider(pwmApplication).getDirectoryVendor());
+            } catch (Exception e) {
+                LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "unable to read ldap vendor type for stats publication: " + e.getMessage());
+            }
+        }
+
+        final TelemetryPublishBean.TelemetryPublishBeanBuilder builder = TelemetryPublishBean.builder();
+        builder.timestamp(Instant.now());
+        builder.id(makeId(pwmApplication));
+        builder.instanceID(pwmApplication.getInstanceID());
+        builder.installTime(pwmApplication.getInstallTime());
+        builder.siteDescription(config.readSettingAsString(PwmSetting.PUBLISH_STATS_SITE_DESCRIPTION));
+        builder.versionBuild(PwmConstants.BUILD_NUMBER);
+        builder.versionVersion(PwmConstants.BUILD_VERSION);
+        builder.ldapVendor(Collections.unmodifiableList(new ArrayList<>(ldapVendors)));
+        builder.statistics(Collections.unmodifiableMap(statData));
+        builder.configuredSettings(Collections.unmodifiableList(configuredSettings));
+
+        final TelemetryPublishBean.Environment environment = TelemetryPublishBean.Environment.builder()
+                .appliance(pwmApplication.getPwmEnvironment().getFlags().contains(PwmEnvironment.ApplicationFlag.Appliance))
+                .javaVendor(System.getProperty("java.vm.vendor"))
+                .javaName(System.getProperty("java.vm.name"))
+                .javaVersion(System.getProperty("java.vm.version"))
+                .osName(System.getProperty("os.name"))
+                .osVersion(System.getProperty("os.version"))
+                .build();
+        builder.environment(environment);
+
+        return builder.build();
+    }
+
+    private static String makeId(final PwmApplication pwmApplication) throws PwmUnrecoverableException
+    {
+        final String SEPARATOR = "-";
+        final String DATETIME_PATTERN = "yyyyMMdd-HHmmss'Z'";
+        final String timestamp = DateTimeFormatter.ofPattern(DATETIME_PATTERN).format(ZonedDateTime.now(ZoneId.of("Zulu")));
+        return PwmConstants.PWM_APP_NAME.toLowerCase()
+                + SEPARATOR + instanceHash(pwmApplication)
+                + SEPARATOR + timestamp;
+
+    }
+
+    private static String instanceHash(final PwmApplication pwmApplication) throws PwmUnrecoverableException
+    {
+        final int MAX_HASH_LENGTH = 64;
+        final String instanceID = pwmApplication.getInstanceID();
+        final String hash = pwmApplication.getSecureService().hash(instanceID);
+        return hash.length() > 64
+                ? hash.substring(0, MAX_HASH_LENGTH)
+                : hash;
+    }
+
+    @Getter
+    @Builder
+    private static class Settings {
+        private TimeDuration publishFrequency;
+        private int minimumAuthentications;
+        private String senderImplementation;
+        private String senderSettings;
+
+        static Settings fromConfig(final Configuration config) {
+            return Settings.builder()
+                    .minimumAuthentications(Integer.parseInt(config.readAppProperty(AppProperty.TELEMETRY_MIN_AUTHENTICATIONS)))
+                    .publishFrequency(new TimeDuration(Integer.parseInt(config.readAppProperty(AppProperty.TELEMETRY_SEND_FREQUENCY_SECONDS)),TimeUnit.SECONDS))
+                    .senderImplementation(config.readAppProperty(AppProperty.TELEMETRY_SENDER_IMPLEMENTATION))
+                    .senderSettings(config.readAppProperty(AppProperty.TELEMETRY_SENDER_SETTINGS))
+                    .build();
+        }
+    }
+
+    private TimeDuration durationUntilNextPublish() {
+
+        final Instant nextPublishTime = settings.getPublishFrequency().incrementFromInstant(lastPublishTime);
+        final Instant minuteFromNow = TimeDuration.MINUTE.incrementFromInstant(Instant.now());
+        return nextPublishTime.isBefore(minuteFromNow)
+                ? TimeDuration.fromCurrent(minuteFromNow)
+                : TimeDuration.fromCurrent(nextPublishTime.toEpochMilli() + (PwmRandom.getInstance().nextInt(600) - 300));
+    }
+
+}

+ 3 - 2
src/main/java/password/pwm/util/VersionChecker.java → src/main/java/password/pwm/svc/telemetry/VersionChecker.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
+package password.pwm.svc.telemetry;
 
 import org.apache.http.HttpResponse;
 import org.apache.http.HttpStatus;
@@ -151,7 +151,7 @@ public class VersionChecker implements PwmService {
         if (PwmConstants.BUILD_NUMBER == null || PwmConstants.BUILD_NUMBER.length() < 1) {
             return true;
         }
-
+        /*
         try {
             final VersionCheckInfoCache versionCheckInfo = getVersionCheckInfo();
             final String currentBuild = versionCheckInfo.getCurrentBuild();
@@ -169,6 +169,7 @@ public class VersionChecker implements PwmService {
         } catch (Exception e) {
             LOGGER.error("unable to retrieve current version data from cloud: " + e.toString());
         }
+        */
         return true;
     }
 

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

@@ -182,6 +182,12 @@ public class TimeDuration implements Comparable, Serializable {
         return new TimeDuration(this.getTotalMilliseconds() + duration.getTotalMilliseconds());
     }
 
+    public Instant incrementFromInstant(final Instant input) {
+        final long inputMillis = input.toEpochMilli();
+        final long nextMills = inputMillis + this.getTotalMilliseconds();
+        return Instant.ofEpochMilli(nextMills);
+    }
+
     public long getTotalMilliseconds() {
         return ms;
     }

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

@@ -264,6 +264,10 @@ security.defaultEphemeralHashAlg=SHA512
 security.config.minSecurityKeyLength=32
 seedlist.builtin.path=/WEB-INF/seedlist.zip
 smtp.subjectEncodingCharset=UTF8
+telemetry.senderImplementation=
+telemetry.senderSettings=
+telemetry.sendFrequencySeconds=259203
+telemetry.minimumAuthentications=10
 token.maxUniqueCreateAttempts=100
 token.resend.enabled=true
 token.resend.delayMS=3000

+ 1 - 1
src/main/resources/password/pwm/config/PwmSetting.xml

@@ -66,7 +66,7 @@
         <example>https://www.example.com/example</example>
         <default/>
     </setting>
-    <setting hidden="false" key="pwm.versionCheck.enable" level="1" required="true">
+    <setting hidden="true" key="pwm.versionCheck.enable" level="1" required="true">
         <default>
             <value>false</value>
         </default>

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

@@ -35,6 +35,7 @@
 <%@ include file="fragment/header.jsp" %>
 <% response.setHeader("Content-Encoding",""); //remove gzip encoding header %>
 <% final int statusCode = pageContext.getErrorData().getStatusCode(); %>
+
 <body class="nihilo" data-jsp-page="error-http.jsp">
 <div id="wrapper">
     <jsp:include page="fragment/header-body.jsp">

+ 0 - 5
src/main/webapp/WEB-INF/web.xml

@@ -187,11 +187,6 @@
         <listener-class>password.pwm.http.HttpEventManager</listener-class>
     </listener>
     <error-page>
-        <error-code>404</error-code>
-        <location>/WEB-INF/jsp/error-http.jsp</location>
-    </error-page>
-    <error-page>
-        <error-code>500</error-code>
         <location>/WEB-INF/jsp/error-http.jsp</location>
     </error-page>
     <session-config>