VersionCheckService.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /*
  2. * Password Management Servlets (PWM)
  3. * http://www.pwm-project.org
  4. *
  5. * Copyright (c) 2006-2009 Novell, Inc.
  6. * Copyright (c) 2009-2021 The PWM Project
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. package password.pwm.svc.version;
  21. import lombok.Builder;
  22. import lombok.Value;
  23. import password.pwm.AppAttribute;
  24. import password.pwm.AppProperty;
  25. import password.pwm.PwmApplication;
  26. import password.pwm.PwmConstants;
  27. import password.pwm.bean.DomainID;
  28. import password.pwm.bean.VersionNumber;
  29. import password.pwm.bean.pub.PublishVersionBean;
  30. import password.pwm.config.AppConfig;
  31. import password.pwm.config.PwmSetting;
  32. import password.pwm.error.ErrorInformation;
  33. import password.pwm.error.PwmError;
  34. import password.pwm.error.PwmException;
  35. import password.pwm.error.PwmUnrecoverableException;
  36. import password.pwm.health.HealthMessage;
  37. import password.pwm.health.HealthRecord;
  38. import password.pwm.http.HttpContentType;
  39. import password.pwm.http.HttpHeader;
  40. import password.pwm.svc.AbstractPwmService;
  41. import password.pwm.svc.PwmService;
  42. import password.pwm.svc.httpclient.PwmHttpClient;
  43. import password.pwm.svc.httpclient.PwmHttpClientRequest;
  44. import password.pwm.svc.httpclient.PwmHttpClientResponse;
  45. import password.pwm.util.i18n.LocaleHelper;
  46. import password.pwm.util.java.CollectionUtil;
  47. import password.pwm.util.java.JavaHelper;
  48. import password.pwm.util.java.StringUtil;
  49. import password.pwm.util.java.TimeDuration;
  50. import password.pwm.util.json.JsonFactory;
  51. import password.pwm.util.localdb.LocalDB;
  52. import password.pwm.util.logging.PwmLogger;
  53. import password.pwm.ws.server.RestResultBean;
  54. import java.lang.reflect.Type;
  55. import java.time.Instant;
  56. import java.time.temporal.ChronoUnit;
  57. import java.util.Collections;
  58. import java.util.EnumMap;
  59. import java.util.List;
  60. import java.util.Map;
  61. import java.util.Objects;
  62. public class VersionCheckService extends AbstractPwmService
  63. {
  64. private static final PwmLogger LOGGER = PwmLogger.forClass( VersionCheckService.class );
  65. private PwmApplication pwmApplication;
  66. private VersionCheckSettings settings;
  67. private VersionNumber runningVersion;
  68. private CacheHolder cacheHolder;
  69. private Instant nextScheduledCheck;
  70. private enum DebugKey
  71. {
  72. runningVersion,
  73. currentVersion,
  74. outdatedVersionFlag,
  75. lastCheckTime,
  76. nextCheckTime,
  77. lastError
  78. }
  79. @Override
  80. protected STATUS postAbstractInit( final PwmApplication pwmApplication, final DomainID domainID )
  81. throws PwmException
  82. {
  83. this.pwmApplication = Objects.requireNonNull( pwmApplication );
  84. this.settings = VersionCheckSettings.fromConfig( pwmApplication.getConfig() );
  85. initRunningVersion();
  86. if ( enabled() )
  87. {
  88. cacheHolder = new CacheHolder( pwmApplication );
  89. setStatus( STATUS.OPEN );
  90. scheduleNextCheck();
  91. return STATUS.OPEN;
  92. }
  93. return STATUS.CLOSED;
  94. }
  95. @Override
  96. protected void shutdownImpl()
  97. {
  98. }
  99. private Map<DebugKey, String> debugMap()
  100. {
  101. if ( status() != STATUS.OPEN )
  102. {
  103. return Collections.emptyMap();
  104. }
  105. final String notApplicable = LocaleHelper.valueNotApplicable( PwmConstants.DEFAULT_LOCALE );
  106. final VersionCheckInfoCache localCache = cacheHolder.getVersionCheckInfoCache();
  107. final Map<DebugKey, String> debugKeyMap = new EnumMap<>( DebugKey.class );
  108. debugKeyMap.put( DebugKey.runningVersion, runningVersion == null ? notApplicable : runningVersion.prettyVersionString() );
  109. debugKeyMap.put( DebugKey.currentVersion, localCache.getCurrentVersion() == null ? notApplicable : localCache.getCurrentVersion().prettyVersionString() );
  110. debugKeyMap.put( DebugKey.outdatedVersionFlag, LocaleHelper.valueBoolean( PwmConstants.DEFAULT_LOCALE, isOutdated() ) );
  111. debugKeyMap.put( DebugKey.lastError, localCache.getLastError() == null ? notApplicable : localCache.getLastError().toDebugStr() );
  112. debugKeyMap.put( DebugKey.lastCheckTime, localCache.getLastCheckTimestamp() == null ? notApplicable : StringUtil.toIsoDate( localCache.getLastCheckTimestamp() ) );
  113. debugKeyMap.put( DebugKey.nextCheckTime, nextScheduledCheck == null
  114. ? notApplicable
  115. : StringUtil.toIsoDate( nextScheduledCheck ) + " (" + TimeDuration.compactFromCurrent( nextScheduledCheck ) + ")" );
  116. return Collections.unmodifiableMap( debugKeyMap );
  117. }
  118. @Override
  119. public ServiceInfoBean serviceInfo()
  120. {
  121. return ServiceInfoBean.builder()
  122. .debugProperties( CollectionUtil.enumMapToStringMap( debugMap() ) )
  123. .build();
  124. }
  125. private void scheduleNextCheck()
  126. {
  127. if ( status() != PwmService.STATUS.OPEN )
  128. {
  129. return;
  130. }
  131. final VersionCheckInfoCache localCache = cacheHolder.getVersionCheckInfoCache();
  132. this.nextScheduledCheck = calculateNextScheduledCheck( localCache, settings );
  133. final TimeDuration delayUntilNextExecution = TimeDuration.fromCurrent( this.nextScheduledCheck );
  134. scheduleJob( new PeriodicCheck(), delayUntilNextExecution );
  135. LOGGER.trace( getSessionLabel(), () -> "scheduled next check execution at " + StringUtil.toIsoDate( nextScheduledCheck )
  136. + " in " + delayUntilNextExecution.asCompactString() );
  137. }
  138. private static Instant calculateNextScheduledCheck( final VersionCheckInfoCache localCache, final VersionCheckSettings settings )
  139. {
  140. final TimeDuration idealDurationUntilNextCheck = localCache.getLastError() != null && localCache.getCurrentVersion() == null
  141. ? settings.getCheckIntervalError()
  142. : settings.getCheckInterval();
  143. if ( localCache.getLastCheckTimestamp() == null )
  144. {
  145. return Instant.now().plus( 10, ChronoUnit.SECONDS );
  146. }
  147. else
  148. {
  149. final Instant nextIdealTimestamp = localCache.getLastCheckTimestamp().plus( idealDurationUntilNextCheck.asDuration() );
  150. return nextIdealTimestamp.isBefore( Instant.now() )
  151. ? Instant.now().plus( 10, ChronoUnit.SECONDS )
  152. : nextIdealTimestamp;
  153. }
  154. }
  155. private class PeriodicCheck implements Runnable
  156. {
  157. @Override
  158. public void run()
  159. {
  160. if ( status() != PwmService.STATUS.OPEN )
  161. {
  162. return;
  163. }
  164. try
  165. {
  166. processReturnedVersionBean( executeFetch() );
  167. }
  168. catch ( final PwmUnrecoverableException e )
  169. {
  170. cacheHolder.setVersionCheckInfoCache( VersionCheckInfoCache.builder()
  171. .lastError( e.getErrorInformation() )
  172. .lastCheckTimestamp( Instant.now() )
  173. .build() );
  174. }
  175. scheduleNextCheck();
  176. }
  177. }
  178. private void processReturnedVersionBean( final PublishVersionBean publishVersionBean )
  179. {
  180. final Instant startTime = Instant.now();
  181. final VersionNumber currentVersion = publishVersionBean.getVersions().get( PublishVersionBean.VersionKey.current );
  182. cacheHolder.setVersionCheckInfoCache( VersionCheckInfoCache.builder()
  183. .currentVersion( currentVersion )
  184. .lastCheckTimestamp( Instant.now() )
  185. .build() );
  186. LOGGER.trace( getSessionLabel(), () -> "successfully fetched current version information from cloud service: "
  187. + currentVersion, TimeDuration.fromCurrent( startTime ) );
  188. }
  189. private PublishVersionBean executeFetch()
  190. throws PwmUnrecoverableException
  191. {
  192. final Instant startTime = Instant.now();
  193. try
  194. {
  195. final PwmHttpClient pwmHttpClient = pwmApplication.getHttpClientService().getPwmHttpClient( getSessionLabel() );
  196. final PwmHttpClientRequest request = PwmHttpClientRequest.builder()
  197. .url( settings.getUrl() )
  198. .header( HttpHeader.ContentType.getHttpName(), HttpContentType.json.getHeaderValueWithEncoding() )
  199. .header( HttpHeader.Accept.getHttpName(), HttpContentType.json.getHeaderValueWithEncoding() )
  200. .body( JsonFactory.get().serialize( makeRequestBody() ) )
  201. .build();
  202. LOGGER.trace( getSessionLabel(), () -> "sending cloud version request to: " + settings.getUrl() );
  203. final PwmHttpClientResponse response = pwmHttpClient.makeRequest( request );
  204. if ( response.getStatusCode() == 200 )
  205. {
  206. final Type restResultBeanType = JsonFactory.get().newParameterizedType( RestResultBean.class, PublishVersionBean.class );
  207. final String body = response.getBody();
  208. final RestResultBean<PublishVersionBean> restResultBean = JsonFactory.get().deserialize( body, restResultBeanType );
  209. return restResultBean.getData();
  210. }
  211. else
  212. {
  213. LOGGER.debug( getSessionLabel(), () -> "error reading cloud current version information: " + response );
  214. final String msg = "error reading cloud current version information: " + response.getStatusLine();
  215. throw PwmUnrecoverableException.newException( PwmError.ERROR_UNREACHABLE_CLOUD_SERVICE, msg );
  216. }
  217. }
  218. catch ( final Exception e )
  219. {
  220. final ErrorInformation errorInformation;
  221. if ( e instanceof PwmException )
  222. {
  223. errorInformation = ( ( PwmUnrecoverableException ) e ).getErrorInformation();
  224. }
  225. else
  226. {
  227. final String errorMsg = "error reading current version from cloud service: " + e.getMessage();
  228. errorInformation = new ErrorInformation( PwmError.ERROR_UNREACHABLE_CLOUD_SERVICE, errorMsg );
  229. }
  230. LOGGER.debug( getSessionLabel(), () -> "error fetching current version from cloud: "
  231. + e.getMessage(), TimeDuration.fromCurrent( startTime ) );
  232. throw new PwmUnrecoverableException( errorInformation );
  233. }
  234. }
  235. private PublishVersionBean makeRequestBody()
  236. {
  237. VersionNumber versionNumber = VersionNumber.ZERO;
  238. try
  239. {
  240. versionNumber = VersionNumber.parse( PwmConstants.BUILD_VERSION );
  241. }
  242. catch ( final Exception e )
  243. {
  244. LOGGER.trace( getSessionLabel(), () -> "error reading local version number " + e.getMessage() );
  245. }
  246. return new PublishVersionBean( Collections.singletonMap( PublishVersionBean.VersionKey.current, versionNumber ) );
  247. }
  248. @Override
  249. protected List<HealthRecord> serviceHealthCheck()
  250. {
  251. if ( status() != PwmService.STATUS.OPEN )
  252. {
  253. return Collections.emptyList();
  254. }
  255. final VersionCheckInfoCache localCache = cacheHolder.getVersionCheckInfoCache();
  256. if ( isOutdated() )
  257. {
  258. return Collections.singletonList( HealthRecord.forMessage(
  259. DomainID.systemId(),
  260. HealthMessage.Version_OutOfDate,
  261. PwmConstants.PWM_APP_NAME,
  262. localCache.getCurrentVersion().prettyVersionString() ) );
  263. }
  264. if ( localCache.getLastError() != null )
  265. {
  266. return Collections.singletonList( HealthRecord.forMessage(
  267. DomainID.systemId(),
  268. HealthMessage.Version_Unreachable,
  269. localCache.getLastError().toDebugStr() ) );
  270. }
  271. return Collections.emptyList();
  272. }
  273. private boolean isOutdated()
  274. {
  275. if ( status() != PwmService.STATUS.OPEN )
  276. {
  277. return false;
  278. }
  279. final VersionCheckInfoCache localCache = cacheHolder.getVersionCheckInfoCache();
  280. if ( runningVersion == null || localCache.getCurrentVersion() == null )
  281. {
  282. return false;
  283. }
  284. final int comparisonInt = runningVersion.compareTo( localCache.getCurrentVersion() );
  285. return comparisonInt < 0;
  286. }
  287. @Value
  288. @Builder
  289. private static class VersionCheckInfoCache
  290. {
  291. private final Instant lastCheckTimestamp;
  292. private final ErrorInformation lastError;
  293. private final VersionNumber currentVersion;
  294. }
  295. @Value
  296. @Builder
  297. private static class VersionCheckSettings
  298. {
  299. private static final int DEFAULT_INTERVAL_SECONDS = 3801;
  300. private final String url;
  301. private final TimeDuration checkInterval;
  302. private final TimeDuration checkIntervalError;
  303. static VersionCheckSettings fromConfig( final AppConfig appConfig )
  304. {
  305. final int checkSeconds = JavaHelper.silentParseInt( appConfig.readAppProperty( AppProperty.VERSION_CHECK_CHECK_INTERVAL_SECONDS ), DEFAULT_INTERVAL_SECONDS );
  306. final int checkSecondsError = JavaHelper.silentParseInt(
  307. appConfig.readAppProperty( AppProperty.VERSION_CHECK_CHECK_INTERVAL_ERROR_SECONDS ), DEFAULT_INTERVAL_SECONDS );
  308. return VersionCheckSettings.builder()
  309. .url( appConfig.readAppProperty( AppProperty.VERSION_CHECK_URL ) )
  310. .checkInterval( TimeDuration.of( checkSeconds, TimeDuration.Unit.SECONDS ) )
  311. .checkIntervalError( TimeDuration.of( checkSecondsError, TimeDuration.Unit.SECONDS ) )
  312. .build();
  313. }
  314. }
  315. private void initRunningVersion()
  316. {
  317. try
  318. {
  319. this.runningVersion = VersionNumber.parse( PwmConstants.BUILD_VERSION );
  320. }
  321. catch ( final Exception e )
  322. {
  323. LOGGER.error( getSessionLabel(), () -> "error parsing internal running version number: " + e.getMessage() );
  324. }
  325. }
  326. private boolean enabled()
  327. {
  328. return pwmApplication.getLocalDB() != null
  329. && runningVersion != null
  330. && pwmApplication.getLocalDB().status() == LocalDB.Status.OPEN
  331. && !pwmApplication.getPwmEnvironment().isInternalRuntimeInstance()
  332. && pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.VERSION_CHECK_ENABLE );
  333. }
  334. private static class CacheHolder
  335. {
  336. private final PwmApplication pwmApplication;
  337. private VersionCheckInfoCache versionCheckInfoCache;
  338. CacheHolder( final PwmApplication pwmApplication )
  339. {
  340. this.pwmApplication = pwmApplication;
  341. this.versionCheckInfoCache = pwmApplication.readAppAttribute( AppAttribute.VERSION_CHECK_CACHE, VersionCheckInfoCache.class )
  342. .orElse( VersionCheckInfoCache.builder().build() );
  343. }
  344. public VersionCheckInfoCache getVersionCheckInfoCache()
  345. {
  346. return versionCheckInfoCache == null ? VersionCheckInfoCache.builder().build() : versionCheckInfoCache;
  347. }
  348. public void setVersionCheckInfoCache( final VersionCheckInfoCache versionCheckInfoCache )
  349. {
  350. this.versionCheckInfoCache = versionCheckInfoCache;
  351. pwmApplication.writeAppAttribute( AppAttribute.VERSION_CHECK_CACHE, versionCheckInfoCache );
  352. }
  353. }
  354. }