/* * 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.util; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.util.EntityUtils; import password.pwm.PwmApplication; import password.pwm.PwmApplicationMode; import password.pwm.PwmConstants; import password.pwm.config.PwmSetting; import password.pwm.config.option.DataStorageMethod; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmUnrecoverableException; import password.pwm.health.HealthRecord; import password.pwm.health.HealthStatus; import password.pwm.health.HealthTopic; import password.pwm.http.client.PwmHttpClient; import password.pwm.i18n.Display; import password.pwm.svc.PwmService; import password.pwm.util.java.JsonUtil; 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 java.io.IOException; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; public class VersionChecker implements PwmService { private static final PwmLogger LOGGER = PwmLogger.forClass(VersionChecker.class); private static final String KEY_VERSION = "version"; private static final String KEY_BUILD = "build"; private static final String LOCALDB_KEY_VERSION_CHECK_INFO_CACHE = "versionCheckInfoCache"; private static final String VERSION_CHECK_URL = PwmConstants.PWM_URL_CLOUD + "/rest/pwm/current-version"; private PwmApplication pwmApplication; private VersionCheckInfoCache versionCheckInfoCache; private STATUS status = STATUS.CLOSED; public VersionChecker() { } public void init(final PwmApplication pwmApplication) { this.pwmApplication = pwmApplication; if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.VERSION_CHECK_ENABLE)) { status = STATUS.CLOSED; return; } if (pwmApplication.getLocalDB() != null && pwmApplication.getLocalDB().status() == LocalDB.Status.OPEN) { try { final String versionChkInfoJson = pwmApplication.getLocalDB().get(LocalDB.DB.PWM_META, LOCALDB_KEY_VERSION_CHECK_INFO_CACHE); if (versionChkInfoJson != null && versionChkInfoJson.length() > 0) { versionCheckInfoCache = JsonUtil.deserialize(versionChkInfoJson, VersionCheckInfoCache.class); } } catch (LocalDBException e) { LOGGER.error("error reading version check info out of LocalDB: " + e.getMessage()); } } if (pwmApplication.getApplicationMode() != PwmApplicationMode.RUNNING && pwmApplication.getApplicationMode() != PwmApplicationMode.CONFIGURATION ) { LOGGER.trace("skipping init due to application mode"); return; } if (versionCheckInfoCache != null && versionCheckInfoCache.getLastError() != null) { versionCheckInfoCache = null; } if (!isVersionCurrent()) { LOGGER.warn("this version of PWM is outdated, please check the project website for the current version"); } status = STATUS.OPEN; } public String currentVersion() { if (status() != STATUS.OPEN) { return Display.getLocalizedMessage(PwmConstants.DEFAULT_LOCALE, Display.Value_NotApplicable, pwmApplication.getConfig()); } try { final VersionCheckInfoCache versionCheckInfo = getVersionCheckInfo(); if (versionCheckInfo != null) { return versionCheckInfo.getCurrentVersion(); } } catch (Exception e) { LOGGER.error("unable to retrieve current version data from cloud: " + e.toString()); } return Display.getLocalizedMessage(PwmConstants.DEFAULT_LOCALE, Display.Value_NotApplicable, pwmApplication.getConfig()); } public Date lastReadTimestamp() { if (status() != STATUS.OPEN) { return null; } try { final VersionCheckInfoCache versionCheckInfo = getVersionCheckInfo(); if (versionCheckInfo != null) { return versionCheckInfo.getLastCheckTimestamp(); } } catch (Exception e) { LOGGER.error("unable to determine last read timestamp: " + e.toString()); } return null; } public boolean isVersionCurrent() { if (status() != STATUS.OPEN) { return true; } if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.VERSION_CHECK_ENABLE)) { return true; } if (PwmConstants.BUILD_NUMBER == null || PwmConstants.BUILD_NUMBER.length() < 1) { return true; } try { final VersionCheckInfoCache versionCheckInfo = getVersionCheckInfo(); final String currentBuild = versionCheckInfo.getCurrentBuild(); final int currentBuildNumber = Integer.parseInt(currentBuild); int localBuildNumber; try { localBuildNumber = Integer.parseInt(PwmConstants.BUILD_NUMBER); } catch (NumberFormatException e) { localBuildNumber = 0; } if (localBuildNumber < currentBuildNumber) { LOGGER.trace("current build " + currentBuildNumber + " is newer than local build (" + localBuildNumber + ")"); return false; } } catch (Exception e) { LOGGER.error("unable to retrieve current version data from cloud: " + e.toString()); } return true; } private synchronized VersionCheckInfoCache getVersionCheckInfo() { if (versionCheckInfoCache != null) { if (versionCheckInfoCache.getLastError() != null) { if (TimeDuration.fromCurrent(versionCheckInfoCache.getLastCheckTimestamp()).isLongerThan(PwmConstants.VERSION_CHECK_FAIL_RETRY_MS)) { versionCheckInfoCache = null; } } else if (TimeDuration.fromCurrent(versionCheckInfoCache.getLastCheckTimestamp()).isLongerThan(PwmConstants.VERSION_CHECK_FREQUENCEY_MS)) { versionCheckInfoCache = null; } } if (versionCheckInfoCache != null) { return versionCheckInfoCache; } try { final Map<String,String> responseMap = doCurrentVersionFetch(); versionCheckInfoCache = new VersionCheckInfoCache(null, responseMap.get(KEY_VERSION), responseMap.get(KEY_BUILD)); LOGGER.info("version check to " + VERSION_CHECK_URL +" completed, current-build=" + versionCheckInfoCache.getCurrentBuild() + ", current-version=" + versionCheckInfoCache.getCurrentVersion()); } catch (Exception e) { final String errorMsg = "unable to reach version check service '" + VERSION_CHECK_URL + "', error: " + e.getMessage(); final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNREACHABLE_CLOUD_SERVICE, errorMsg); LOGGER.error(errorInformation.toDebugStr()); versionCheckInfoCache = new VersionCheckInfoCache(errorInformation,"",""); } if (pwmApplication.getLocalDB() != null && pwmApplication.getLocalDB().status() == LocalDB.Status.OPEN) { try { final String jsonVersionInfo = JsonUtil.serialize(versionCheckInfoCache); pwmApplication.getLocalDB().put(LocalDB.DB.PWM_META, LOCALDB_KEY_VERSION_CHECK_INFO_CACHE,jsonVersionInfo); } catch (LocalDBException e) { LOGGER.error("error writing version check info out of LocalDB: " + e.getMessage()); } } return versionCheckInfoCache; } private Map<String,String> doCurrentVersionFetch() throws IOException, URISyntaxException, PwmUnrecoverableException { final URI requestURI = new URI(VERSION_CHECK_URL); final HttpGet httpGet = new HttpGet(requestURI.toString()); httpGet.setHeader("Accept", PwmConstants.ContentTypeValue.json.getHeaderValue()); LOGGER.trace("sending cloud version request to: " + VERSION_CHECK_URL); final HttpResponse httpResponse = PwmHttpClient.getHttpClient(pwmApplication.getConfig()).execute(httpGet); if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { throw new IOException("http response error code: " + httpResponse.getStatusLine().getStatusCode()); } final String responseBody = EntityUtils.toString(httpResponse.getEntity()); return JsonUtil.deserializeStringMap(responseBody); } public STATUS status() { return status; } public void close() { } public List<HealthRecord> healthCheck() { final ArrayList<HealthRecord> returnRecords = new ArrayList<>(); if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.VERSION_CHECK_ENABLE)) { // version checking final VersionCheckInfoCache checkInfoCache = getVersionCheckInfo(); if (checkInfoCache.getLastError() == null) { if (!isVersionCurrent()) { returnRecords.add(new HealthRecord(HealthStatus.CAUTION, HealthTopic.Application, "This version of " + PwmConstants.PWM_APP_NAME + " is out of date." + " The current version is " + versionCheckInfoCache.getCurrentVersion() + " (b" + versionCheckInfoCache.getCurrentBuild() + ")." + " Check the project page for more information.")); } } else { returnRecords.add(new HealthRecord(HealthStatus.WARN,HealthTopic.Application,"Unable to check current version: " + versionCheckInfoCache.getLastError().toDebugStr())); } } return returnRecords; } private static class VersionCheckInfoCache implements Serializable { private Date lastCheckTimestamp; private ErrorInformation lastError; private String currentVersion; private String currentBuild; private VersionCheckInfoCache(final ErrorInformation lastError, final String currentVersion, final String currentBuild) { this.lastCheckTimestamp = new Date(); this.lastError = lastError; this.currentVersion = currentVersion; this.currentBuild = currentBuild; } public Date getLastCheckTimestamp() { return lastCheckTimestamp; } public ErrorInformation getLastError() { return lastError; } public String getCurrentVersion() { return currentVersion; } public String getCurrentBuild() { return currentBuild; } } public ServiceInfo serviceInfo() { return new ServiceInfo(Collections.<DataStorageMethod>emptyList()); } }