1 //------------------------------------------------------------------------------ 2 // <copyright file="SessionIDManager.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 //------------------------------------------------------------------------------ 6 7 /* 8 * SessionIDManager 9 * 10 * Copyright (c) 1998-1999, Microsoft Corporation 11 * 12 */ 13 14 namespace System.Web.SessionState { 15 16 using System; 17 using System.Collections; 18 using System.IO; 19 using System.Web.Util; 20 using System.Web.Configuration; 21 using System.Security.Cryptography; 22 using System.Globalization; 23 using System.Security.Permissions; 24 using System.Text; 25 using System.Web.Security; 26 using System.Web.Management; 27 using System.Web.Hosting; 28 29 public interface ISessionIDManager { 30 InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIDReissue)31 bool InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIDReissue); 32 33 // Get the session id from Context or Cookies. 34 // Called by session state module in AcquireState event. GetSessionID(HttpContext context)35 String GetSessionID(HttpContext context); 36 37 // Create a session id. CreateSessionID(HttpContext context)38 String CreateSessionID(HttpContext context); 39 40 // Save the session id to either URL or cookies. 41 // For URL case, the param "redirected" will be set 42 // to true, meaning the caller should call HttpApplication.CompleteRequest() 43 // and return. Called by session state module at the end of AcquireState event 44 // and if a session state is successfully retrieved. 45 SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded)46 void SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded); 47 48 // If cookie-ful, remove the session id from cookie. 49 // Called by session state module in the ReleaseState event if a new 50 // session was created but was unused 51 RemoveSessionID(HttpContext context)52 void RemoveSessionID(HttpContext context); 53 54 // Called by GetSessionID to make sure the ID sent by the browser is legitimate 55 Validate(String id)56 bool Validate(String id); 57 Initialize()58 void Initialize(); 59 } 60 61 /* 62 * The sesssion id manager provides session id services 63 * for an application. 64 */ 65 public class SessionIDManager : ISessionIDManager { 66 67 // cookieless vars 68 69 const int COOKIELESS_SESSION_LENGTH = SessionId.ID_LENGTH_CHARS + 2; 70 71 internal const String COOKIELESS_SESSION_KEY = "AspCookielessSession"; 72 internal const String COOKIELESS_BOOL_SESSION_KEY = "AspCookielessBoolSession"; 73 internal const String ASP_SESSIONID_MANAGER_INITIALIZEREQUEST_CALLED_KEY = "AspSessionIDManagerInitializeRequestCalled"; 74 75 static string s_appPath; 76 static int s_iSessionId; 77 78 79 internal const HttpCookieMode COOKIEMODE_DEFAULT = HttpCookieMode.UseCookies; 80 internal const String SESSION_COOKIE_DEFAULT = "ASP.NET_SessionId"; 81 internal const int SESSION_ID_LENGTH_LIMIT = 80; 82 83 #pragma warning disable 0649 84 static ReadWriteSpinLock s_lock; 85 #pragma warning restore 0649 86 static SessionStateSection s_config; 87 88 bool _isInherited; 89 90 RandomNumberGenerator _randgen; 91 SessionIDManager()92 public SessionIDManager() { 93 } 94 95 public static int SessionIDMaxLength { 96 get { return SESSION_ID_LENGTH_LIMIT; } 97 } 98 OneTimeInit()99 void OneTimeInit() { 100 SessionStateSection config = RuntimeConfig.GetAppConfig().SessionState; 101 102 s_appPath = HostingEnvironment.ApplicationVirtualPathObject.VirtualPathString; 103 104 // s_iSessionId is pointing to the starting "(" 105 s_iSessionId = s_appPath.Length; 106 107 s_config = config; 108 } 109 110 static SessionStateSection Config { 111 get { 112 if (s_config == null) { 113 throw new HttpException(SR.GetString(SR.SessionIDManager_uninit)); 114 } 115 116 return s_config; 117 } 118 } 119 Initialize()120 public void Initialize() { 121 if (s_config == null) { 122 s_lock.AcquireWriterLock(); 123 try { 124 if (s_config == null) { 125 OneTimeInit(); 126 } 127 } 128 finally { 129 s_lock.ReleaseWriterLock(); 130 } 131 } 132 133 _isInherited = !(this.GetType() == typeof(SessionIDManager)); 134 135 Debug.Trace("SessionIDManager", "cookieMode = " + Config.Cookieless + 136 ", cookieName = " + Config.CookieName + 137 ", inherited = " + _isInherited); 138 } 139 GetCookielessSessionID(HttpContext context, bool allowRedirect, out bool cookieless)140 internal void GetCookielessSessionID(HttpContext context, bool allowRedirect, out bool cookieless) { 141 HttpRequest request; 142 string id; 143 144 Debug.Trace("SessionIDManager", "Beginning SessionIDManager::GetCookielessSessionID"); 145 146 request = context.Request; 147 148 // Please note that even if the page doesn't require session state, we still need 149 // to read the session id because we have to update the state's timeout value 150 151 cookieless = CookielessHelperClass.UseCookieless(context, allowRedirect, Config.Cookieless); 152 context.Items[COOKIELESS_BOOL_SESSION_KEY] = cookieless; 153 154 Debug.Trace("SessionIDManager", "cookieless=" + cookieless); 155 156 if (cookieless) { 157 /* 158 * Check if it's cookie-less session id 159 */ 160 161 id = context.CookielessHelper.GetCookieValue('S'); 162 if (id == null) 163 id = String.Empty; 164 // Decode() is caled on all id's before saved to URL or cookie 165 id = Decode(id); 166 if (!ValidateInternal(id, false)) { 167 Debug.Trace("SessionIDManager", "No legitimate cookie on path\nReturning from SessionStateModule::GetCookielessSessionID"); 168 return; 169 } 170 171 context.Items.Add(COOKIELESS_SESSION_KEY, id); 172 173 Debug.Trace("SessionIDManager", "CookielessSessionModule found SessionId=" + id + 174 "\nReturning from SessionIDManager::GetCookielessSessionID"); 175 } 176 177 } 178 CreateSessionCookie(String id)179 static HttpCookie CreateSessionCookie(String id) { 180 HttpCookie cookie; 181 182 cookie = new HttpCookie(Config.CookieName, id); 183 cookie.Path = "/"; 184 185 // VSWhidbey 414687 Use HttpOnly to prevent client side script manipulation of cookie 186 cookie.HttpOnly = true; 187 188 return cookie; 189 } 190 CheckIdLength(string id, bool throwOnFail)191 internal static bool CheckIdLength(string id, bool throwOnFail) { 192 bool ret = true; 193 194 if (id.Length > SESSION_ID_LENGTH_LIMIT) { 195 if (throwOnFail) { 196 throw new HttpException( 197 SR.GetString(SR.Session_id_too_long, 198 SESSION_ID_LENGTH_LIMIT.ToString(CultureInfo.InvariantCulture), id)); 199 } 200 else { 201 ret = false; 202 } 203 } 204 205 return ret; 206 } 207 ValidateInternal(string id, bool throwOnIdCheck)208 private bool ValidateInternal(string id, bool throwOnIdCheck) { 209 return CheckIdLength(id, throwOnIdCheck) && Validate(id); 210 } 211 Validate(string id)212 public virtual bool Validate(string id) { 213 return SessionId.IsLegit(id); 214 } 215 216 /// <devdoc> 217 /// <para>[To be supplied.]</para> 218 /// </devdoc> Encode(String id)219 public virtual String Encode(String id) { 220 // Need to do UrlEncode if the session id could be custom created. 221 if (_isInherited) { 222 Debug.Trace("SessionIDManager", "Encode is doing UrlEncode "); 223 return HttpUtility.UrlEncode(id); 224 } 225 else { 226 Debug.Trace("SessionIDManager", "Encode is doing nothing "); 227 return id; 228 } 229 } 230 Decode(String id)231 public virtual String Decode(String id) { 232 // Need to do UrlDecode if the session id could be custom created. 233 if (_isInherited) { 234 Debug.Trace("SessionIDManager", "Decode is doing UrlDecode "); 235 return HttpUtility.UrlDecode(id); 236 } 237 else { 238 Debug.Trace("SessionIDManager", "Decode is doing nothing"); 239 return id.ToLower(CultureInfo.InvariantCulture); 240 } 241 } 242 UseCookieless(HttpContext context)243 internal bool UseCookieless(HttpContext context) { 244 Debug.Assert(context.Items[ASP_SESSIONID_MANAGER_INITIALIZEREQUEST_CALLED_KEY] != null); 245 246 if (Config.Cookieless == HttpCookieMode.UseCookies) { 247 return false; 248 } 249 else { 250 object o = context.Items[COOKIELESS_BOOL_SESSION_KEY]; 251 252 Debug.Assert(o != null, "GetCookielessSessionID should be run already"); 253 254 return (bool)o; 255 } 256 } 257 CheckInitializeRequestCalled(HttpContext context)258 void CheckInitializeRequestCalled(HttpContext context) { 259 if (context.Items[ASP_SESSIONID_MANAGER_INITIALIZEREQUEST_CALLED_KEY] == null) { 260 throw new HttpException(SR.GetString(SR.SessionIDManager_InitializeRequest_not_called)); 261 } 262 } 263 InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIDReissue)264 public bool InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIDReissue) { 265 // Note: We support cookie reissue only if we're using cookieless. VSWhidbey 384892 266 267 if (context.Items[ASP_SESSIONID_MANAGER_INITIALIZEREQUEST_CALLED_KEY] != null) { 268 supportSessionIDReissue = UseCookieless(context); 269 return false; 270 } 271 272 context.Items[ASP_SESSIONID_MANAGER_INITIALIZEREQUEST_CALLED_KEY] = true; 273 274 if (Config.Cookieless == HttpCookieMode.UseCookies) { 275 supportSessionIDReissue = false; 276 return false; 277 } 278 else { 279 bool cookieless; 280 GetCookielessSessionID(context, !suppressAutoDetectRedirect, out cookieless); 281 282 supportSessionIDReissue = cookieless; 283 return context.Response.IsRequestBeingRedirected; 284 } 285 } 286 287 // Get the session id from Context or Cookies. 288 // Called by session state module in AcquireState event. GetSessionID(HttpContext context)289 public String GetSessionID(HttpContext context) { 290 String s = null; 291 HttpCookie cookie; 292 293 CheckInitializeRequestCalled(context); 294 295 if (UseCookieless(context)) { 296 s = (String) context.Items[COOKIELESS_SESSION_KEY]; 297 } 298 else { 299 cookie = context.Request.Cookies[Config.CookieName]; 300 if (cookie != null && cookie.Value != null) { 301 s = Decode((String)cookie.Value); 302 if (s != null && !ValidateInternal(s, false)) { 303 s = null; 304 } 305 } 306 } 307 308 return s; 309 } 310 311 // Create a session id. CreateSessionID(HttpContext context)312 virtual public String CreateSessionID(HttpContext context) { 313 return SessionId.Create(ref _randgen); 314 } 315 316 // Save the session id to either URL or cookies. 317 // For URL case, the param "redirected" will be set 318 // to true, and we've called HttpApplication.CompleteRequest(). 319 // The caller should return. Called by session state module at the end of AcquireState event 320 // and if a session state is successfully retrieved. SaveSessionID(HttpContext context, String id, out bool redirected, out bool cookieAdded)321 public void SaveSessionID(HttpContext context, String id, out bool redirected, 322 out bool cookieAdded) 323 { 324 HttpCookie cookie; 325 String idEncoded; 326 327 redirected = false; 328 cookieAdded = false; 329 330 CheckInitializeRequestCalled(context); 331 332 if (context.Response.HeadersWritten) { 333 // We support setting the session ID in a cookie or by redirecting to a munged URL. 334 // Both techniques require that response headers haven't yet been flushed. 335 throw new HttpException( 336 SR.GetString(SR.Cant_save_session_id_because_response_was_flushed)); 337 } 338 339 if (!ValidateInternal(id, true)) { 340 // VSWhidbey 439376 341 throw new HttpException( 342 SR.GetString(SR.Cant_save_session_id_because_id_is_invalid, id)); 343 } 344 345 idEncoded = Encode(id); 346 347 if (!UseCookieless(context)) { 348 /* 349 * Set the cookie. 350 */ 351 Debug.Trace("SessionIDManager", 352 "Creating session cookie, id=" + id + ", idEncoded=" + idEncoded); 353 354 cookie = CreateSessionCookie(idEncoded); 355 context.Response.Cookies.Add(cookie); 356 cookieAdded = true; 357 } 358 else { 359 context.CookielessHelper.SetCookieValue('S', idEncoded); 360 361 /* 362 * Redirect. 363 */ 364 HttpRequest request = context.Request; 365 string path = request.Path; 366 string qs = request.QueryStringText; 367 if (!String.IsNullOrEmpty(qs)) { 368 path = path + "?" + qs; 369 } 370 371 Debug.Trace("SessionIDManager", 372 "Redirecting to create cookieless session, path=" + path + ", idEncoded=" + idEncoded + 373 "\nReturning from SessionIDManager::SaveSessionID"); 374 375 context.Response.Redirect(path, false); 376 context.ApplicationInstance.CompleteRequest(); 377 378 // Caller has to return immediately. 379 redirected = true; 380 } 381 382 return; 383 } 384 385 // If cookie-ful, remove the session id from cookie. 386 // Called by session state module in the ReleaseState event if a new 387 // session was created but was unused 388 389 // If cookieless, we can't do anything. RemoveSessionID(HttpContext context)390 public void RemoveSessionID(HttpContext context) { 391 Debug.Trace("SessionIDManager", "Removing session id cookie"); 392 context.Response.Cookies.RemoveCookie(Config.CookieName); 393 } 394 } 395 396 /* 397 * Provides and verifies the integrity of a session id. 398 * 399 * A session id is a logically 120 bit random number, 400 * represented in a string of 20 characters from a 401 * size 64 character set. The session id can be placed 402 * in a url without url-encoding. 403 */ 404 internal static class SessionId { 405 internal const int NUM_CHARS_IN_ENCODING = 32; 406 internal const int ENCODING_BITS_PER_CHAR = 5; 407 internal const int ID_LENGTH_BITS = 120; 408 internal const int ID_LENGTH_BYTES = (ID_LENGTH_BITS / 8 ); // 15 409 internal const int ID_LENGTH_CHARS = (ID_LENGTH_BITS / ENCODING_BITS_PER_CHAR); // 24 410 411 static char[] s_encoding = new char[NUM_CHARS_IN_ENCODING] 412 { 413 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 414 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 415 '0', '1', '2', '3', '4', '5' 416 }; 417 418 static bool[] s_legalchars; 419 SessionId()420 static SessionId() { 421 int i; 422 char ch; 423 424 s_legalchars = new bool[128]; 425 for (i = s_encoding.Length - 1; i >= 0; i--) { 426 ch = s_encoding[i]; 427 s_legalchars[ch] = true; 428 } 429 } 430 IsLegit(String s)431 internal static bool IsLegit(String s) { 432 int i; 433 char ch; 434 435 if (s == null || s.Length != ID_LENGTH_CHARS) 436 return false; 437 438 try { 439 i = ID_LENGTH_CHARS; 440 while (--i >= 0) { 441 ch = s[i]; 442 if (!s_legalchars[ch]) 443 return false; 444 } 445 446 return true; 447 } 448 catch (IndexOutOfRangeException) { 449 return false; 450 } 451 } 452 Encode(byte[] buffer)453 static String Encode(byte[] buffer) { 454 int i, j, k, n; 455 char[] chars = new char[ID_LENGTH_CHARS]; 456 457 Debug.Assert(buffer.Length == ID_LENGTH_BYTES); 458 459 j = 0; 460 for (i = 0; i < ID_LENGTH_BYTES; i += 5) { 461 n = (int) buffer[i] | 462 ((int) buffer[i+1] << 8) | 463 ((int) buffer[i+2] << 16) | 464 ((int) buffer[i+3] << 24); 465 466 k = (n & 0x0000001F); 467 chars[j++] = s_encoding[k]; 468 469 k = ((n >> 5) & 0x0000001F); 470 chars[j++] = s_encoding[k]; 471 472 k = ((n >> 10) & 0x0000001F); 473 chars[j++] = s_encoding[k]; 474 475 k = ((n >> 15) & 0x0000001F); 476 chars[j++] = s_encoding[k]; 477 478 k = ((n >> 20) & 0x0000001F); 479 chars[j++] = s_encoding[k]; 480 481 k = ((n >> 25) & 0x0000001F); 482 chars[j++] = s_encoding[k]; 483 484 n = ((n >> 30) & 0x00000003) | ((int)buffer[i + 4] << 2); 485 486 k = (n & 0x0000001F); 487 chars[j++] = s_encoding[k]; 488 489 k = ((n >> 5) & 0x0000001F); 490 chars[j++] = s_encoding[k]; 491 } 492 493 Debug.Assert(j == ID_LENGTH_CHARS); 494 495 return new String(chars); 496 } 497 Create(ref RandomNumberGenerator randgen)498 static internal String Create(ref RandomNumberGenerator randgen) { 499 byte[] buffer; 500 String encoding; 501 502 if (randgen == null) { 503 randgen = new RNGCryptoServiceProvider(); 504 } 505 506 buffer = new byte [15]; 507 randgen.GetBytes(buffer); 508 encoding = Encode(buffer); 509 return encoding; 510 } 511 } 512 513 } 514