using C4IT.FASD.Base; using C4IT.Logging; using C4IT.Security; using System; using System.Collections.Generic; using System.Reflection; using System.Threading; using System.Threading.Tasks; using static C4IT.Logging.cLogManager; namespace C4IT.DataHistoryProvider { public class cDataHistoryCollectorTokenCache { private const int timerCleanupInterval = 15 * 60 * 1000; private System.Threading.Timer cleanupTimer; private cDataHistoryCollector Collector; private Dictionary> dicToken = null; public class cTokenInfo { public Guid id; public cF4SDTokenRegistration.enumTokenType type; public string name; public string name2; public string secret; public DateTime validUntil; public DateTime renewUntil; public bool IsValid() { if (id == Guid.Empty) return false; if (validUntil < DateTime.UtcNow + TimeSpan.FromMinutes(cDataHistoryCollector.constMinutesUntilTokenIsInvalid)) return false; return true; } public cTokenInfo GetNewInstance() { var _sec = secret; if (HasTokenTypeLongLifetime(type)) _sec = cSecurePassword.Instance.Decode(secret); var _ret = new cTokenInfo() { id = id, type = type, name = name, name2 = name2, secret = _sec, validUntil = validUntil, renewUntil = renewUntil }; return _ret; } } private static class TokenCacheCriticalSection { private static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1); public static async Task EnterAsync() { await Semaphore.WaitAsync(); } public static void Leave() { Semaphore.Release(); } } private static bool HasTokenTypeLongLifetime(cF4SDTokenRegistration.enumTokenType tokenType) { switch (tokenType) { case cF4SDTokenRegistration.enumTokenType.M42Api: return true; default: return false; } } public cDataHistoryCollectorTokenCache(cDataHistoryCollector Collector, cF4sdWebRequestInfo requestInfo, int LogDeep) { this.Collector = Collector; cleanupTimer = new System.Threading.Timer(async (x) => await DoCleanup(requestInfo, 0), null, timerCleanupInterval, timerCleanupInterval); } private bool privInsert(cTokenInfo info, bool overwite) { if (dicToken == null) return false; if (!dicToken.TryGetValue(info.id, out var _entyId)) { _entyId = new Dictionary(); dicToken.Add(info.id, _entyId); } if (!overwite && _entyId.ContainsKey(info.type)) return false; _entyId[info.type] = info; return true; } public async Task LoadAsync(cDbConnection DbConn, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep) { MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); } var _startTime = DateTime.UtcNow; try { if (dicToken == null) dicToken = new Dictionary>(); if (!DataHistorySqlHelper.GetWellKnownSqlStatement("GetAllExternalTokens", out var Query)) return; var sqlerror = 0; var sqlStartTime = DateTime.UtcNow; await TokenCacheCriticalSection.EnterAsync(); try { dicToken.Clear(); using (var _reader = await DataHistorySqlHelper.GetDataReaderAsync(DbConn, Query, null, Token)) { if (_reader != null) try { if (_reader.HasRows) { var _toDelete = new List(); while (await _reader.ReadAsync()) { try { var _entry = new cTokenInfo(); _entry.id = _reader.GetGuid(0); _entry.type = (cF4SDTokenRegistration.enumTokenType)_reader.GetInt32(1); _entry.name = _reader.IsDBNull(2) ? null : _reader.GetString(2); _entry.name2 = _reader.IsDBNull(3) ? null : _reader.GetString(3); var _secret = _reader.IsDBNull(4) ? null : _reader.GetString(4); if (HasTokenTypeLongLifetime(_entry.type)) _entry.secret = _secret; else _entry.secret = cSecurePassword.Instance.Decode(_secret); _entry.validUntil = _reader.IsDBNull(5) ? DateTime.MinValue : _reader.GetDateTime(5); _entry.renewUntil = _reader.IsDBNull(6) ? DateTime.MinValue : _reader.GetDateTime(6); if (_entry.IsValid()) { if (!privInsert(_entry, false)) _toDelete.Add(_entry); } else _toDelete.Add(_entry); } catch (Exception E) { cLogManager.DefaultLogger.LogException(E); } if (Token.IsCancellationRequested) return; } } } catch (Exception E) { cLogManager.DefaultLogger.LogException(E); } } } catch (Exception E) { sqlerror = E.HResult; cLogManager.DefaultLogger.LogException(E); } finally { TokenCacheCriticalSection.Leave(); if (DataHistorySqlHelper.LogSql) DataHistorySqlHelper.SaveSqlTimingEntry(Query.Name, sqlStartTime, "", sqlerror); } } catch (Exception E) { LogException(E); } finally { if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); } if (CM != null) LogMethodEnd(CM); } } public async Task SetAsync(cF4SDTokenRegistration _tokenRegistration, cF4SDTokenValidityPeriod validityPeriod, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep) { MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); } var _startTime = DateTime.UtcNow; try { // encryption of the secret for non-temporary tokens string _sec = _tokenRegistration.Secret; string _secEnc = cSecurePassword.Instance.Encode(_tokenRegistration.Secret); if (HasTokenTypeLongLifetime(_tokenRegistration.TokenType)) _sec = _secEnc; var _tokenInfo = new cTokenInfo() { id = _tokenRegistration.UserId, type = _tokenRegistration.TokenType, name = _tokenRegistration.Name, name2 = _tokenRegistration.Name2, secret = _sec, validUntil = validityPeriod.validUntil, renewUntil = validityPeriod.renewUntil }; if (!DataHistorySqlHelper.GetWellKnownSqlStatement("InsertOrUpdateExternalToken", out var Query)) return; var Params = new Dictionary() { { "Id", _tokenInfo.id }, { "Type", (int)_tokenInfo.type }, { "Name", _tokenInfo.name }, { "Name2", _tokenInfo.name2 }, { "Secret", _secEnc }, { "ValidUntil", _tokenInfo.validUntil }, { "RenewUntil", _tokenInfo.renewUntil }, }; var sqlerror = 0; var sqlStartTime = DateTime.UtcNow; await TokenCacheCriticalSection.EnterAsync(); try { privInsert(_tokenInfo, true); using (var Conn = new cDbConnection(Collector.mainDbConnection)) { if (!Conn.IsOpen) { LogEntry($"Could not open main database '{Collector.mainDbConnection.Database}', aborting query'", LogLevels.Error); return; } await DataHistorySqlHelper.ExecuteAsync(Conn, Query, Params, requestInfo, Token); } } catch (Exception E) { sqlerror = E.HResult; LogException(E); } finally { TokenCacheCriticalSection.Leave(); } } catch (Exception E) { LogException(E); } finally { if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); } if (CM != null) LogMethodEnd(CM); } } public async Task GetAsync(Guid UserId, cF4SDTokenRegistration.enumTokenType TokenType, cF4sdWebRequestInfo requestInfo, int LogDeep) { MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } if (dicToken == null) return null; var doCleanup = false; try { await TokenCacheCriticalSection.EnterAsync(); if (dicToken.TryGetValue(UserId, out var _entry)) if (_entry.TryGetValue(TokenType, out var _tokenInfo)) { if (_tokenInfo.validUntil < DateTime.UtcNow) return null; return _tokenInfo.GetNewInstance(); } } catch (Exception E) { LogException(E); } finally { TokenCacheCriticalSection.Leave(); if (doCleanup) await DoCleanup(requestInfo, LogDeep + 1); if (CM != null) LogMethodEnd(CM); } return null; } public async Task DoCleanup(cF4sdWebRequestInfo requestInfo, int LogDeep) { if (dicToken == null) return; MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); } var _startTime = DateTime.UtcNow; try { var _current = DateTime.UtcNow; // remove all expiered tokens in the cache await TokenCacheCriticalSection.EnterAsync(); cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); var _toRemove1 = new List(); foreach (var _entry in dicToken) { var _toRemove2 = new List(); foreach (var _tokenInfoPair in _entry.Value) { if (_tokenInfoPair.Value.validUntil < _current) _toRemove2.Add(_tokenInfoPair.Key); } foreach (var token in _toRemove2) _entry.Value.Remove(token); if (_entry.Value.Count == 0) _toRemove1.Add(_entry.Key); } foreach (var _entry in _toRemove1) dicToken.Remove(_entry); // synchronize the sql table with the cache if (!DataHistorySqlHelper.GetWellKnownSqlStatement("RemoveExpieredExternalTokens", out var Query)) return; // remove the expired tokens var Params = new Dictionary() { { "ValidUntil", _current } }; var sqlStartTime = DateTime.UtcNow; try { using (var Conn = new cDbConnection(Collector.mainDbConnection)) { if (!Conn.IsOpen) { LogEntry($"Could not open main database '{Collector.mainDbConnection.Database}', aborting query'", LogLevels.Error); return; } var _n = await DataHistorySqlHelper.ExecuteAsync(Conn, Query, Params, requestInfo, CancellationToken.None); } } catch (Exception E) { LogException(E); } } catch (Exception E) { LogException(E); } finally { TokenCacheCriticalSection.Leave(); cleanupTimer.Change(timerCleanupInterval, timerCleanupInterval); if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); } if (CM != null) LogMethodEnd(CM); } } } }