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