using System; using System.Collections.Concurrent; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using EmbedIO.Utilities; namespace EmbedIO.Sessions { /// /// A simple session manager to handle in-memory sessions. /// Not for intensive use or for distributed applications. /// public partial class LocalSessionManager : ISessionManager { /// /// The default name for session cookies, i.e. "__session". /// public const string DefaultCookieName = "__session"; /// /// The default path for session cookies, i.e. "/". /// public const string DefaultCookiePath = "/"; /// /// The default HTTP-only flag for session cookies, i.e. . /// public const bool DefaultCookieHttpOnly = true; /// /// The default duration for session cookies, i.e. . /// public static readonly TimeSpan DefaultCookieDuration = TimeSpan.Zero; /// /// The default duration for sessions, i.e. 30 minutes. /// public static readonly TimeSpan DefaultSessionDuration = TimeSpan.FromMinutes(30); /// /// The default interval between automatic purges of expired and empty sessions, i.e. 30 seconds. /// public static readonly TimeSpan DefaultPurgeInterval = TimeSpan.FromSeconds(30); private readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(Session.KeyComparer); private string _cookieName = DefaultCookieName; private string _cookiePath = DefaultCookiePath; private TimeSpan _cookieDuration = DefaultCookieDuration; private bool _cookieHttpOnly = DefaultCookieHttpOnly; private TimeSpan _sessionDuration = DefaultSessionDuration; private TimeSpan _purgeInterval = DefaultPurgeInterval; /// /// Initializes a new instance of the class /// with default values for all properties. /// /// /// /// /// /// /// public LocalSessionManager() { } /// /// Gets or sets the duration of newly-created sessions. /// /// This property is being set after calling /// the method. /// public TimeSpan SessionDuration { get => _sessionDuration; set { EnsureConfigurationNotLocked(); _sessionDuration = value; } } /// /// Gets or sets the interval between purges of expired sessions. /// /// This property is being set after calling /// the method. /// public TimeSpan PurgeInterval { get => _purgeInterval; set { EnsureConfigurationNotLocked(); _purgeInterval = value; } } /// /// Gets or sets the name for session cookies. /// /// This property is being set after calling /// the method. /// This property is being set to . /// This property is being set and the provided value /// is not a valid URL path. /// public string CookieName { get => _cookieName; set { EnsureConfigurationNotLocked(); _cookieName = Validate.Rfc2616Token(nameof(value), value); } } /// /// Gets or sets the path for session cookies. /// /// This property is being set after calling /// the method. /// This property is being set to . /// This property is being set and the provided value /// is not a valid URL path. /// public string CookiePath { get => _cookiePath; set { EnsureConfigurationNotLocked(); _cookiePath = Validate.UrlPath(nameof(value), value, true); } } /// /// Gets or sets the duration of session cookies. /// /// This property is being set after calling /// the method. /// public TimeSpan CookieDuration { get => _cookieDuration; set { EnsureConfigurationNotLocked(); _cookieDuration = value; } } /// /// Gets or sets a value indicating whether session cookies are hidden from Javascript code running on a user agent. /// /// This property is being set after calling /// the method. /// public bool CookieHttpOnly { get => _cookieHttpOnly; set { EnsureConfigurationNotLocked(); _cookieHttpOnly = value; } } private bool ConfigurationLocked { get; set; } /// public void Start(CancellationToken cancellationToken) { ConfigurationLocked = true; Task.Run(async () => { try { while (!cancellationToken.IsCancellationRequested) { PurgeExpiredAndEmptySessions(); await Task.Delay(PurgeInterval, cancellationToken).ConfigureAwait(false); } } catch (TaskCanceledException) { // ignore } }, cancellationToken); } /// public ISession Create(IHttpContext context) { var id = context.Request.Cookies.FirstOrDefault(IsSessionCookie)?.Value.Trim(); SessionImpl session; lock (_sessions) { if (!string.IsNullOrEmpty(id) && _sessions.TryGetValue(id!, out session)) { session.BeginUse(); } else { id = UniqueIdGenerator.GetNext(); session = new SessionImpl(id, SessionDuration); _sessions.TryAdd(id, session); } } context.Request.Cookies.Add(BuildSessionCookie(id)); context.Response.Cookies.Add(BuildSessionCookie(id)); return session; } /// public void Delete(IHttpContext context, string id) { lock (_sessions) { if (_sessions.TryGetValue(id, out var session)) session.EndUse(() => _sessions.TryRemove(id, out _)); } context.Request.Cookies.Add(BuildSessionCookie(string.Empty)); context.Response.Cookies.Add(BuildSessionCookie(string.Empty)); } /// public void OnContextClose(IHttpContext context) { if (!context.Session.Exists) return; var id = context.Session.Id; lock (_sessions) { if (_sessions.TryGetValue(id, out var session)) { session.EndUse(() => _sessions.TryRemove(id, out _)); } } } private void EnsureConfigurationNotLocked() { if (ConfigurationLocked) throw new InvalidOperationException($"Cannot configure a {nameof(LocalSessionManager)} once it has been started."); } private bool IsSessionCookie(Cookie cookie) => cookie.Name.Equals(CookieName, StringComparison.OrdinalIgnoreCase) && !cookie.Expired; private Cookie BuildSessionCookie(string? id) { var cookie = new Cookie(CookieName, id, CookiePath) { HttpOnly = CookieHttpOnly, }; if (CookieDuration > TimeSpan.Zero) { cookie.Expires = DateTime.UtcNow.Add(CookieDuration); } return cookie; } private void PurgeExpiredAndEmptySessions() { string[] ids; lock (_sessions) { ids = _sessions.Keys.ToArray(); } foreach (var id in ids) { lock (_sessions) { if (!_sessions.TryGetValue(id, out var session)) return; session.UnregisterIfNeeded(() => _sessions.TryRemove(id, out _)); } } } private string GetSessionId(IHttpContext context) => context.Request.Cookies.FirstOrDefault(IsSessionCookie)?.Value.Trim() ?? string.Empty; } }