using SharpChat.Channels; using SharpChat.Configuration; using SharpChat.Events; using SharpChat.Protocol; using SharpChat.Users; using System; using System.Collections.Generic; using System.Linq; using System.Net; namespace SharpChat.Sessions { public class SessionManager : IEventHandler { public const short DEFAULT_MAX_COUNT = 5; public const ushort DEFAULT_TIMEOUT = 5; private readonly object Sync = new(); private CachedValue MaxPerUser { get; } private CachedValue TimeOut { get; } private IEventDispatcher Dispatcher { get; } private string ServerId { get; } private UserManager Users { get; } private List Sessions { get; } = new(); private List LocalSessions { get; } = new(); public SessionManager(IEventDispatcher dispatcher, UserManager users, IConfig config, string serverId) { if(config == null) throw new ArgumentNullException(nameof(config)); Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); Users = users ?? throw new ArgumentNullException(nameof(users)); ServerId = serverId ?? throw new ArgumentNullException(nameof(serverId)); MaxPerUser = config.ReadCached(@"maxCount", DEFAULT_MAX_COUNT); TimeOut = config.ReadCached(@"timeOut", DEFAULT_TIMEOUT); } public bool HasTimedOut(ISession session) { if(session == null) throw new ArgumentNullException(nameof(session)); int timeOut = TimeOut; if(timeOut < 1) // avoid idiocy timeOut = DEFAULT_TIMEOUT; return session.GetIdleTime().TotalSeconds >= timeOut; } public void GetSession(Func predicate, Action callback) { if(predicate == null) throw new ArgumentNullException(nameof(predicate)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) { ISession session = Sessions.FirstOrDefault(predicate); if(session == null) return; callback(session); } } public void GetSession(string sessionId, Action callback) { if(sessionId == null) throw new ArgumentNullException(nameof(sessionId)); if(callback == null) throw new ArgumentNullException(nameof(callback)); if(string.IsNullOrWhiteSpace(sessionId)) { callback(null); return; } GetSession(s => sessionId.Equals(s.SessionId), callback); } public void GetSession(ISession session, Action callback) { if(session == null) throw new ArgumentNullException(nameof(session)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) { // Check if we have a local session if(session is Session && LocalSessions.Contains(session)) { callback(session); return; } // Check if we're already an instance if(Sessions.Contains(session)) { callback(session); return; } // Finde GetSession(session.Equals, callback); } } public void GetLocalSession(Func predicate, Action callback) { if(predicate == null) throw new ArgumentNullException(nameof(predicate)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) callback(LocalSessions.FirstOrDefault(predicate)); } public void GetLocalSession(ISession session, Action callback) { if(session == null) throw new ArgumentNullException(nameof(session)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) { if(session is Session && LocalSessions.Contains(session)) { callback(session); return; } GetLocalSession(session.Equals, callback); } } public void GetLocalSession(string sessionId, Action callback) { if(sessionId == null) throw new ArgumentNullException(nameof(sessionId)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetLocalSession(s => sessionId.Equals(s.SessionId), callback); } public void GetLocalSession(IConnection conn, Action callback) { if(conn == null) throw new ArgumentNullException(nameof(conn)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetLocalSession(s => s.HasConnection(conn), callback); } public void GetSessions(Func predicate, Action> callback) { if(predicate == null) throw new ArgumentNullException(nameof(predicate)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) callback(Sessions.Where(predicate)); } public void GetSessions(IUser user, Action> callback) { if(user == null) throw new ArgumentNullException(nameof(user)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetSessions(s => user.Equals(s.User), callback); } public void GetLocalSessions(Func predicate, Action> callback) { if(predicate == null) throw new ArgumentNullException(nameof(predicate)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) callback(LocalSessions.Where(predicate)); } public void GetLocalSessions(IUser user, Action> callback) { if(user == null) throw new ArgumentNullException(nameof(user)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetLocalSessions(s => user.Equals(s.User), callback); } public void GetLocalSessionsByUserId(long userId, Action> callback) { if(callback == null) throw new ArgumentNullException(nameof(callback)); GetLocalSessions(s => s.User.UserId == userId, callback); } public void GetLocalSessions(IEnumerable sessionIds, Action> callback) { if(sessionIds == null) throw new ArgumentNullException(nameof(sessionIds)); if(callback == null) throw new ArgumentNullException(nameof(callback)); if(!sessionIds.Any()) { callback(Enumerable.Empty()); return; } GetLocalSessions(s => sessionIds.Contains(s.SessionId), callback); } // i wonder what i'll think about this after sleeping a night on it // perhaps stick active sessions with the master User implementation again transparently. // session startups should probably be events as well public void GetSessions(IEnumerable users, Action> callback) { if(users == null) throw new ArgumentNullException(nameof(users)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetSessions(s => users.Any(s.User.Equals), callback); } public void GetLocalSessions(IEnumerable users, Action> callback) { if(users == null) throw new ArgumentNullException(nameof(users)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetLocalSessions(s => users.Any(s.User.Equals), callback); } public void GetActiveSessions(Action> callback) { if(callback == null) throw new ArgumentNullException(nameof(callback)); GetSessions(s => !HasTimedOut(s), callback); } public void GetActiveLocalSessions(Action> callback) { if(callback == null) throw new ArgumentNullException(nameof(callback)); GetLocalSessions(s => !HasTimedOut(s), callback); } public void GetDeadLocalSessions(Action> callback) { if(callback == null) throw new ArgumentNullException(nameof(callback)); GetLocalSessions(HasTimedOut, callback); } public void Create(IConnection conn, ILocalUser user, Action callback) { if(conn == null) throw new ArgumentNullException(nameof(conn)); if(user == null) throw new ArgumentNullException(nameof(user)); Session sess = null; lock(Sync) { sess = new Session(ServerId, RNG.NextString(Session.ID_LENGTH), conn.IsSecure, null, user, true, conn, conn.RemoteAddress); LocalSessions.Add(sess); Sessions.Add(sess); } Dispatcher.DispatchEvent(this, new SessionCreatedEvent(sess)); callback(sess); } public void DoKeepAlive(ISession session) { if(session == null) throw new ArgumentNullException(nameof(session)); lock(Sync) Dispatcher.DispatchEvent(this, new SessionPingEvent(session)); } public void SwitchChannel(ISession session, IChannel channel = null) { if(session == null) throw new ArgumentNullException(nameof(session)); lock(Sync) Dispatcher.DispatchEvent(this, new SessionChannelSwitchEvent(channel, session)); } public void Destroy(IConnection conn) { if(conn == null) throw new ArgumentNullException(nameof(conn)); lock(Sync) GetLocalSession(conn, session => { if(session == null) return; if(session is Session ls) LocalSessions.Remove(ls); Dispatcher.DispatchEvent(this, new SessionDestroyEvent(session)); }); } public void Destroy(ISession session) { if(session == null) throw new ArgumentNullException(nameof(session)); lock(Sync) GetSession(session, session => { if(session is Session ls) LocalSessions.Remove(ls); Dispatcher.DispatchEvent(this, new SessionDestroyEvent(session)); }); } public void HasSessions(IUser user, Action callback) { if(user == null) throw new ArgumentNullException(nameof(user)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) callback(Sessions.Any(s => user.Equals(s.User))); } public void GetSessionCount(IUser user, Action callback) { if(user == null) throw new ArgumentNullException(nameof(user)); if(callback == null) throw new ArgumentNullException(nameof(callback)); lock(Sync) callback(Sessions.Count(s => user.Equals(s.User))); } public void GetAvailableSessionCount(IUser user, Action callback) { if(user == null) throw new ArgumentNullException(nameof(user)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetSessionCount(user, sessionCount => callback(MaxPerUser - sessionCount)); } public void HasAvailableSessions(IUser user, Action callback) { if(user == null) throw new ArgumentNullException(nameof(user)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetAvailableSessionCount(user, availableSessionCount => callback(availableSessionCount > 0)); } public void GetRemoteAddresses(IUser user, Action> callback) { if(user == null) throw new ArgumentNullException(nameof(user)); if(callback == null) throw new ArgumentNullException(nameof(callback)); GetActiveSessions(sessions => { callback(sessions .Where(s => user.Equals(s.User)) .OrderByDescending(s => s.LastPing) .Select(s => s.RemoteAddress) .Distinct()); }); } public void CheckTimeOut() { GetDeadLocalSessions(sessions => { if(sessions?.Any() != true) return; Queue murder = new(sessions); while(murder.TryDequeue(out ISession session)) Destroy(session); }); } public void HandleEvent(object sender, IEvent evt) { switch(evt) { case SessionChannelSwitchEvent _: case SessionPingEvent _: case SessionResumeEvent _: case SessionSuspendEvent _: GetSession(evt.SessionId, session => session?.HandleEvent(sender, evt)); break; case SessionCreatedEvent sce: if(ServerId.Equals(sce.ServerId)) // we created the session break; lock(Sync) Users.GetUser(sce.UserId, user => { if(user != null) // if we get here and there's no user we've either hit a race condition or we're out of sync somehow Sessions.Add(new Session(sce.ServerId, sce.SessionId, sce.IsSecure, sce.LastPing, user, sce.IsConnected, null, sce.RemoteAddress)); }); break; case SessionDestroyEvent sde: GetSession(sde.SessionId, session => { Sessions.Remove(session); session.HandleEvent(sender, sde); }); break; /*case UserDisconnectEvent ude: GetLocalSessionsByUserId(ude.UserId, sessions => { foreach(ISession session in sessions) session.HandleEvent(sender, ude); }); break;*/ } } } }