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;
}
}