//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.SessionState { using System; using System.Collections.Specialized; using System.Configuration; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Web; using System.Web.Configuration; using System.Web.Management; using System.Web.Security.Cryptography; using System.Web.Util; internal sealed class OutOfProcSessionStateStore : SessionStateStoreProviderBase { internal static readonly IntPtr INVALID_SOCKET = UnsafeNativeMethods.INVALID_HANDLE_VALUE; internal static readonly int WHIDBEY_MAJOR_VERSION = 2; internal const int STATE_NETWORK_TIMEOUT_DEFAULT = 10; // in sec static string s_uribase; static int s_networkTimeout; #pragma warning disable 0649 static ReadWriteSpinLock s_lock; #pragma warning restore 0649 static bool s_oneTimeInited; static StateServerPartitionInfo s_singlePartitionInfo; static PartitionManager s_partitionManager; static bool s_usePartition; static EventHandler s_onAppDomainUnload; // We keep these info because we don't want to hold on to the config object. static string s_configPartitionResolverType; static string s_configStateConnectionString; static string s_configStateConnectionStringFileName; static int s_configStateConnectionStringLineNumber; static bool s_configCompressionEnabled; // Per request info IPartitionResolver _partitionResolver; StateServerPartitionInfo _partitionInfo; internal override void Initialize(string name, NameValueCollection config, IPartitionResolver partitionResolver) { _partitionResolver = partitionResolver; Initialize(name, config); } public override void Initialize(string name, NameValueCollection config) { if (String.IsNullOrEmpty(name)) name = "State Server Session State Provider"; base.Initialize(name, config); if (!s_oneTimeInited) { s_lock.AcquireWriterLock(); try { if (!s_oneTimeInited) { OneTimeInit(); } } finally { s_lock.ReleaseWriterLock(); } } if (!s_usePartition) { // For single partition, the connection info won't change from request to request Debug.Assert(s_partitionManager == null); _partitionInfo = s_singlePartitionInfo; } } void OneTimeInit() { SessionStateSection config = RuntimeConfig.GetAppConfig().SessionState; s_configPartitionResolverType = config.PartitionResolverType; s_configStateConnectionString = config.StateConnectionString; s_configStateConnectionStringFileName = config.ElementInformation.Properties["stateConnectionString"].Source; s_configStateConnectionStringLineNumber = config.ElementInformation.Properties["stateConnectionString"].LineNumber; s_configCompressionEnabled = config.CompressionEnabled; if (_partitionResolver == null) { String stateConnectionString = config.StateConnectionString; SessionStateModule.ReadConnectionString(config, ref stateConnectionString, "stateConnectionString"); s_singlePartitionInfo = (StateServerPartitionInfo)CreatePartitionInfo(stateConnectionString); } else { s_usePartition = true; s_partitionManager = new PartitionManager(new CreatePartitionInfo(CreatePartitionInfo)); } s_networkTimeout = (int)config.StateNetworkTimeout.TotalSeconds; string appId = HttpRuntime.AppDomainAppId; string idHash = Convert.ToBase64String(CryptoUtil.ComputeSHA256Hash(Encoding.UTF8.GetBytes(appId))); // Make sure that we have a absolute URI, some hosts(Cassini) don't provide this. if (appId.StartsWith("/", StringComparison.Ordinal)) { s_uribase = appId + "(" + idHash + ")/"; } else { s_uribase = "/" + appId + "(" + idHash + ")/"; } // We only need to do this in one instance s_onAppDomainUnload = new EventHandler(OnAppDomainUnload); Thread.GetDomain().DomainUnload += s_onAppDomainUnload; s_oneTimeInited = true; } void OnAppDomainUnload(Object unusedObject, EventArgs unusedEventArgs) { Debug.Trace("OutOfProcSessionStateStore", "OnAppDomainUnload called"); Thread.GetDomain().DomainUnload -= s_onAppDomainUnload; if (_partitionResolver == null) { if (s_singlePartitionInfo != null) { s_singlePartitionInfo.Dispose(); } } else { if (s_partitionManager != null) { s_partitionManager.Dispose(); } } } internal IPartitionInfo CreatePartitionInfo(string stateConnectionString) { string server; bool serverIsIpv6NumericAddress; int port; int hr; try { ParseStateConnectionString(stateConnectionString, out server, out serverIsIpv6NumericAddress, out port); // At v1, we won't accept server name that has non-ascii characters for (int i = 0; i < server.Length; ++i) { if (server[i] > 0x7F) { throw new ArgumentException("stateConnectionString"); } } } catch { if (s_usePartition) { throw new HttpException( SR.GetString(SR.Error_parsing_state_server_partition_resolver_string, s_configPartitionResolverType)); } else { throw new ConfigurationErrorsException( SR.GetString(SR.Invalid_value_for_sessionstate_stateConnectionString, s_configStateConnectionString), s_configStateConnectionStringFileName, s_configStateConnectionStringLineNumber); } } hr = UnsafeNativeMethods.SessionNDConnectToService(server); if (hr != 0) { throw CreateConnectionException(server, port, hr); } return new StateServerPartitionInfo( new ResourcePool(new TimeSpan(0, 0, 5), int.MaxValue), server: server, serverIsIPv6NumericAddress: serverIsIpv6NumericAddress, port: port); } private static Regex _ipv6ConnectionStringFormat = new Regex(@"^\[(?.*)\]:(?\d*)$"); [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = @"The exception is never bubbled up to the user.")] internal static void ParseStateConnectionString(string stateConnectionString, out string server, out bool serverIsIPv6NumericAddress, out int port) { /* * stateConnection string has the following format: * * "tcpip=:" * "tcpip=[IPv6-address]:port", per RFC 3986, Sec. 3.2.2 */ // chop off the "tcpip=" part if (!stateConnectionString.StartsWith("tcpip=", StringComparison.Ordinal)) { throw new ArgumentException("stateConnectionString"); } stateConnectionString = stateConnectionString.Substring("tcpip=".Length); // is this an IPv6 address? Match ipv6RegexMatch = _ipv6ConnectionStringFormat.Match(stateConnectionString); if (ipv6RegexMatch != null && ipv6RegexMatch.Success) { string ipv6AddressString = ipv6RegexMatch.Groups["ipv6Address"].Value; IPAddress ipv6Address = IPAddress.Parse(ipv6AddressString); if (ipv6Address.AddressFamily != AddressFamily.InterNetworkV6) { throw new ArgumentException("stateConnectionString"); } server = ipv6AddressString; serverIsIPv6NumericAddress = true; port = UInt16.Parse(ipv6RegexMatch.Groups["port"].Value, CultureInfo.InvariantCulture); return; } // not an IPv6 address; assume "host:port" string[] parts = stateConnectionString.Split(':'); if (parts.Length != 2) { throw new ArgumentException("stateConnectionString"); } server = parts[0]; serverIsIPv6NumericAddress = false; port = UInt16.Parse(parts[1], CultureInfo.InvariantCulture); } internal static HttpException CreateConnectionException(string server, int port, int hr) { if (s_usePartition) { return new HttpException( SR.GetString(SR.Cant_make_session_request_partition_resolver, s_configPartitionResolverType, server, port.ToString(CultureInfo.InvariantCulture)), hr); } else { return new HttpException( SR.GetString(SR.Cant_make_session_request), hr); } } public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) { return false; } public override void Dispose() { } public override void InitializeRequest(HttpContext context) { if (s_usePartition) { // For multiple partition case, the connection info can change from request to request Debug.Assert(_partitionResolver != null); _partitionInfo = null; } } void MakeRequest( UnsafeNativeMethods.StateProtocolVerb verb, String id, UnsafeNativeMethods.StateProtocolExclusive exclusiveAccess, int extraFlags, int timeout, int lockCookie, byte[] buf, int cb, int networkTimeout, out UnsafeNativeMethods.SessionNDMakeRequestResults results) { int hr; string uri; OutOfProcConnection conn = null; HandleRef socketHandle; bool checkVersion = false; Debug.Assert(timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES, "item.Timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES"); SessionIDManager.CheckIdLength(id, true /* throwOnFail */); if (_partitionInfo == null) { Debug.Assert(s_partitionManager != null); Debug.Assert(_partitionResolver != null); _partitionInfo = (StateServerPartitionInfo)s_partitionManager.GetPartition(_partitionResolver, id); // If its still null, we give up if (_partitionInfo == null) { throw new HttpException(SR.GetString(SR.Bad_partition_resolver_connection_string, "PartitionManager")); } } // Need to make sure we dispose the connection if anything goes wrong try { conn = (OutOfProcConnection)_partitionInfo.RetrieveResource(); if (conn != null) { socketHandle = new HandleRef(this, conn._socketHandle.Handle); } else { socketHandle = new HandleRef(this, INVALID_SOCKET); } if (_partitionInfo.StateServerVersion == -1) { // We don't need locking here because it's okay to have two // requests initializing s_stateServerVersion. checkVersion = true; } Debug.Trace("OutOfProcSessionStateStoreMakeRequest", "Calling MakeRequest, " + "socket=" + (IntPtr)socketHandle.Handle + "verb=" + verb + " id=" + id + " exclusiveAccess=" + exclusiveAccess + " timeout=" + timeout + " buf=" + ((buf != null) ? "non-null" : "null") + " cb=" + cb + " checkVersion=" + checkVersion + " extraFlags=" + extraFlags); // Have to UrlEncode id because it may contain non-URL-safe characters uri = HttpUtility.UrlEncode(s_uribase + id); hr = UnsafeNativeMethods.SessionNDMakeRequest( socketHandle, _partitionInfo.Server, _partitionInfo.Port, _partitionInfo.ServerIsIPv6NumericAddress /* forceIPv6 */, networkTimeout, verb, uri, exclusiveAccess, extraFlags, timeout, lockCookie, buf, cb, checkVersion, out results); Debug.Trace("OutOfProcSessionStateStoreMakeRequest", "MakeRequest returned: " + "hr=" + hr + " socket=" + (IntPtr)results.socket + " httpstatus=" + results.httpStatus + " timeout=" + results.timeout + " contentlength=" + results.contentLength + " uri=" + (IntPtr)results.content + " lockCookie=" + results.lockCookie + " lockDate=" + string.Format("{0:x}", results.lockDate) + " lockAge=" + results.lockAge + " stateServerMajVer=" + results.stateServerMajVer + " actionFlags=" + results.actionFlags); if (conn != null) { if (results.socket == INVALID_SOCKET) { conn.Detach(); conn = null; } else if (results.socket != socketHandle.Handle) { // The original socket is no good. We've got a new one. // Pleae note that EnsureConnected has closed the bad // one already. conn._socketHandle = new HandleRef(this, results.socket); } } else if (results.socket != INVALID_SOCKET) { conn = new OutOfProcConnection(results.socket); } if (conn != null) { _partitionInfo.StoreResource(conn); } } catch { // We just need to dispose the connection if anything bad happened if (conn != null) { conn.Dispose(); } throw; } if (hr != 0) { HttpException e = CreateConnectionException(_partitionInfo.Server, _partitionInfo.Port, hr); string phase = null; switch (results.lastPhase) { case (int)UnsafeNativeMethods.SessionNDMakeRequestPhase.Initialization: phase = SR.GetString(SR.State_Server_detailed_error_phase0); break; case (int)UnsafeNativeMethods.SessionNDMakeRequestPhase.Connecting: phase = SR.GetString(SR.State_Server_detailed_error_phase1); break; case (int)UnsafeNativeMethods.SessionNDMakeRequestPhase.SendingRequest: phase = SR.GetString(SR.State_Server_detailed_error_phase2); break; case (int)UnsafeNativeMethods.SessionNDMakeRequestPhase.ReadingResponse: phase = SR.GetString(SR.State_Server_detailed_error_phase3); break; default: Debug.Assert(false, "Unknown results.lastPhase: " + results.lastPhase); break; } WebBaseEvent.RaiseSystemEvent(SR.GetString(SR.State_Server_detailed_error, phase, "0x" + hr.ToString("X08", CultureInfo.InvariantCulture), cb.ToString(CultureInfo.InvariantCulture)), this, WebEventCodes.WebErrorOtherError, WebEventCodes.StateServerConnectionError, e); throw e; } if (results.httpStatus == 400) { if (s_usePartition) { throw new HttpException( SR.GetString(SR.Bad_state_server_request_partition_resolver, s_configPartitionResolverType, _partitionInfo.Server, _partitionInfo.Port.ToString(CultureInfo.InvariantCulture))); } else { throw new HttpException( SR.GetString(SR.Bad_state_server_request)); } } if (checkVersion) { _partitionInfo.StateServerVersion = results.stateServerMajVer; if (_partitionInfo.StateServerVersion < WHIDBEY_MAJOR_VERSION) { // We won't work with versions lower than Whidbey if (s_usePartition) { throw new HttpException( SR.GetString(SR.Need_v2_State_Server_partition_resolver, s_configPartitionResolverType, _partitionInfo.Server, _partitionInfo.Port.ToString(CultureInfo.InvariantCulture))); } else { throw new HttpException( SR.GetString(SR.Need_v2_State_Server)); } } } } [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] internal SessionStateStoreData DoGet(HttpContext context, String id, UnsafeNativeMethods.StateProtocolExclusive exclusiveAccess, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { SessionStateStoreData item = null; UnmanagedMemoryStream stream = null; int contentLength; UnsafeNativeMethods.SessionNDMakeRequestResults results; // Set default return values locked = false; lockId = null; lockAge = TimeSpan.Zero; actionFlags = 0; results.content = IntPtr.Zero; try { MakeRequest(UnsafeNativeMethods.StateProtocolVerb.GET, id, exclusiveAccess, 0, 0, 0, null, 0, s_networkTimeout, out results); switch (results.httpStatus) { case 200: /* item found, deserialize it */ contentLength = results.contentLength; if (contentLength > 0) { try { unsafe { stream = new UnmanagedMemoryStream((byte*)results.content, contentLength); } item = SessionStateUtility.DeserializeStoreData(context, stream, s_configCompressionEnabled); } finally { if(stream != null) { stream.Close(); } } lockId = results.lockCookie; actionFlags = (SessionStateActions) results.actionFlags; } break; case 423: /* state locked, return lock information */ if (0 <= results.lockAge) { if (results.lockAge < Sec.ONE_YEAR) { lockAge = new TimeSpan(0, 0, results.lockAge); } else { lockAge = TimeSpan.Zero; } } else { DateTime now = DateTime.Now; if (0 < results.lockDate && results.lockDate < now.Ticks) { lockAge = now - new DateTime(results.lockDate); } else { lockAge = TimeSpan.Zero; } } locked = true; lockId = results.lockCookie; Debug.Assert((results.actionFlags & (int)SessionStateActions.InitializeItem) == 0, "(results.actionFlags & (int)SessionStateActions.InitializeItem) == 0; uninitialized item cannot be locked"); break; } } finally { if (results.content != IntPtr.Zero) { UnsafeNativeMethods.SessionNDFreeBody(new HandleRef(this, results.content)); } } return item; } public override SessionStateStoreData GetItem(HttpContext context, String id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { Debug.Trace("OutOfProcSessionStateStore", "Calling Get, id=" + id); return DoGet(context, id, UnsafeNativeMethods.StateProtocolExclusive.NONE, out locked, out lockAge, out lockId, out actionFlags); } public override SessionStateStoreData GetItemExclusive(HttpContext context, String id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { Debug.Trace("OutOfProcSessionStateStore", "Calling GetExlusive, id=" + id); return DoGet(context, id, UnsafeNativeMethods.StateProtocolExclusive.ACQUIRE, out locked, out lockAge, out lockId, out actionFlags); } public override void ReleaseItemExclusive(HttpContext context, String id, object lockId) { Debug.Assert(lockId != null, "lockId != null"); UnsafeNativeMethods.SessionNDMakeRequestResults results; int lockCookie = (int)lockId; Debug.Trace("OutOfProcSessionStateStore", "Calling ReleaseExclusive, id=" + id); MakeRequest(UnsafeNativeMethods.StateProtocolVerb.GET, id, UnsafeNativeMethods.StateProtocolExclusive.RELEASE, 0, 0, lockCookie, null, 0, s_networkTimeout, out results); } public override void SetAndReleaseItemExclusive(HttpContext context, String id, SessionStateStoreData item, object lockId, bool newItem) { UnsafeNativeMethods.SessionNDMakeRequestResults results; byte[] buf; int length; int lockCookie; Debug.Assert(item.Items != null, "item.Items != null"); Debug.Assert(item.StaticObjects != null, "item.StaticObjects != null"); Debug.Trace("OutOfProcSessionStateStore", "Calling Set, id=" + id + " sessionItems=" + item.Items + " timeout=" + item.Timeout); try { SessionStateUtility.SerializeStoreData(item, 0, out buf, out length, s_configCompressionEnabled); } catch { if (!newItem) { ((SessionStateStoreProviderBase)this).ReleaseItemExclusive(context, id, lockId); } throw; } // Save it to the store if (lockId == null) { lockCookie = 0; } else { lockCookie = (int)lockId; } MakeRequest(UnsafeNativeMethods.StateProtocolVerb.PUT, id, UnsafeNativeMethods.StateProtocolExclusive.NONE, 0, item.Timeout, lockCookie, buf, length, s_networkTimeout, out results); } public override void RemoveItem(HttpContext context, String id, object lockId, SessionStateStoreData item) { Debug.Assert(lockId != null, "lockId != null"); Debug.Trace("OutOfProcSessionStateStore", "Calling Remove, id=" + id); UnsafeNativeMethods.SessionNDMakeRequestResults results; int lockCookie = (int)lockId; MakeRequest(UnsafeNativeMethods.StateProtocolVerb.DELETE, id, UnsafeNativeMethods.StateProtocolExclusive.NONE, 0, 0, lockCookie, null, 0, s_networkTimeout, out results); } public override void ResetItemTimeout(HttpContext context, String id) { UnsafeNativeMethods.SessionNDMakeRequestResults results; Debug.Trace("OutOfProcSessionStateStore", "Calling ResetTimeout, id=" + id); MakeRequest(UnsafeNativeMethods.StateProtocolVerb.HEAD, id, UnsafeNativeMethods.StateProtocolExclusive.NONE, 0, 0, 0, null, 0, s_networkTimeout, out results); } public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) { Debug.Assert(timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES, "item.Timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES"); return SessionStateUtility.CreateLegitStoreData(context, null, null, timeout); } public override void CreateUninitializedItem(HttpContext context, String id, int timeout) { UnsafeNativeMethods.SessionNDMakeRequestResults results; byte[] buf; int length; Debug.Trace("OutOfProcSessionStateStore", "Calling CreateUninitializedItem, id=" + id + " timeout=" + timeout); // Create an empty item SessionStateUtility.SerializeStoreData(CreateNewStoreData(context, timeout), 0, out buf, out length, s_configCompressionEnabled); // Save it to the store MakeRequest(UnsafeNativeMethods.StateProtocolVerb.PUT, id, UnsafeNativeMethods.StateProtocolExclusive.NONE, (int)SessionStateItemFlags.Uninitialized, timeout, 0, buf, length, s_networkTimeout, out results); } // Called during EndRequest event public override void EndRequest(HttpContext context) { } class StateServerPartitionInfo : PartitionInfo { string _server; bool _serverIsIPv6NumericAddress; int _port; int _stateServerVersion; internal StateServerPartitionInfo(ResourcePool rpool, string server, bool serverIsIPv6NumericAddress, int port) : base(rpool) { _server = server; _serverIsIPv6NumericAddress = serverIsIPv6NumericAddress; _port = port; _stateServerVersion = -1; Debug.Trace("PartitionInfo", "Created a new info, server=" + server + ", port=" + port); } internal string Server { get { return _server; } } internal bool ServerIsIPv6NumericAddress { get { return _serverIsIPv6NumericAddress; } } internal int Port { get { return _port; } } internal int StateServerVersion { get { return _stateServerVersion; } set { _stateServerVersion = value; } } protected override string TracingPartitionString { get { // only add the brackets if the server is an IPv6 address, per the URI specification string formatString = (ServerIsIPv6NumericAddress) ? "[{0}]:{1}" : "{0}:{1}"; return String.Format(CultureInfo.InvariantCulture, formatString, Server, Port); } } } class OutOfProcConnection : IDisposable { internal HandleRef _socketHandle; internal OutOfProcConnection(IntPtr socket) { Debug.Assert(socket != OutOfProcSessionStateStore.INVALID_SOCKET, "socket != OutOfProcSessionStateStore.INVALID_SOCKET"); _socketHandle = new HandleRef(this, socket); PerfCounters.IncrementCounter(AppPerfCounter.SESSION_STATE_SERVER_CONNECTIONS); } ~OutOfProcConnection() { Dispose(false); } public void Dispose() { Debug.Trace("ResourcePool", "Disposing OutOfProcConnection"); Dispose(true); System.GC.SuppressFinalize(this); } private void Dispose(bool dummy) { if (_socketHandle.Handle != OutOfProcSessionStateStore.INVALID_SOCKET) { UnsafeNativeMethods.SessionNDCloseConnection(_socketHandle); _socketHandle = new HandleRef(this, OutOfProcSessionStateStore.INVALID_SOCKET); PerfCounters.DecrementCounter(AppPerfCounter.SESSION_STATE_SERVER_CONNECTIONS); } } internal void Detach() { _socketHandle = new HandleRef(this, OutOfProcSessionStateStore.INVALID_SOCKET); } } } }