1 //------------------------------------------------------------------------------ 2 // <copyright file="AnonymousIdentificationModule.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 //------------------------------------------------------------------------------ 6 7 /* 8 * AnonymousIdentificationModule class 9 * 10 * Copyright (c) 1999 Microsoft Corporation 11 */ 12 13 namespace System.Web.Security { 14 using System.Web; 15 using System.Text; 16 using System.Web.Configuration; 17 using System.Web.Caching; 18 using System.Web.Handlers; 19 using System.Collections; 20 using System.Configuration.Provider; 21 using System.Web.Util; 22 using System.Security.Principal; 23 using System.Security.Permissions; 24 using System.Globalization; 25 using System.Web.Management; 26 using System.Web.Hosting; 27 using System.IO; 28 using System.Runtime.Serialization.Formatters.Binary; 29 using System.Web.Security.Cryptography; 30 31 /// <devdoc> 32 /// <para>[To be supplied.]</para> 33 /// </devdoc> 34 public sealed class AnonymousIdentificationModule : IHttpModule { 35 36 private const int MAX_ENCODED_COOKIE_STRING = 512; 37 private const int MAX_ID_LENGTH = 128; 38 39 /// <devdoc> 40 /// <para> 41 /// Initializes a new instance of the <see cref='System.Web.Security.AnonymousIdentificationModule'/> 42 /// class. 43 /// </para> 44 /// </devdoc> 45 [SecurityPermission(SecurityAction.Demand, Unrestricted=true)] AnonymousIdentificationModule()46 public AnonymousIdentificationModule() { 47 } 48 49 ///////////////////////////////////////////////////////////////////////////// 50 ///////////////////////////////////////////////////////////////////////////// 51 ///////////////////////////////////////////////////////////////////////////// 52 // Events 53 private AnonymousIdentificationEventHandler _CreateNewIdEventHandler; 54 55 public event AnonymousIdentificationEventHandler Creating { 56 add { _CreateNewIdEventHandler += value; } 57 remove { _CreateNewIdEventHandler -= value; } 58 } 59 ClearAnonymousIdentifier()60 public static void ClearAnonymousIdentifier() 61 { 62 if (!s_Initialized) 63 Initialize(); 64 65 HttpContext context = HttpContext.Current; 66 if (context == null) 67 return; 68 69 // VSWhidbey 418835: When this feature is enabled, prevent infinite loop when cookieless 70 // mode != Cookies and there was no cookie, also we cannot clear when current user is anonymous. 71 if (!s_Enabled || !context.Request.IsAuthenticated) { 72 throw new NotSupportedException(SR.GetString(SR.Anonymous_ClearAnonymousIdentifierNotSupported)); 73 } 74 75 //////////////////////////////////////////////////////////// 76 // Check if we need to clear the ticket stored in the URI 77 bool clearUri = false; 78 if (context.CookielessHelper.GetCookieValue('A') != null) { 79 context.CookielessHelper.SetCookieValue('A', null); // Always clear the uri-cookie 80 clearUri = true; 81 } 82 83 //////////////////////////////////////////////////////////// 84 // Clear cookie if cookies are supported by the browser 85 if (!CookielessHelperClass.UseCookieless(context, false, s_CookieMode) || context.Request.Browser.Cookies) 86 { // clear cookie if required 87 string cookieValue = String.Empty; 88 if (context.Request.Browser["supportsEmptyStringInCookieValue"] == "false") 89 cookieValue = "NoCookie"; 90 HttpCookie cookie = new HttpCookie(s_CookieName, cookieValue); 91 cookie.HttpOnly = true; 92 cookie.Path = s_CookiePath; 93 cookie.Secure = s_RequireSSL; 94 if (s_Domain != null) 95 cookie.Domain = s_Domain; 96 cookie.Expires = new System.DateTime(1999, 10, 12); 97 context.Response.Cookies.RemoveCookie(s_CookieName); 98 context.Response.Cookies.Add(cookie); 99 } 100 101 //////////////////////////////////////////////////////////// 102 // Redirect back to this page if we removed a URI ticket 103 if (clearUri) { 104 context.Response.Redirect(context.Request.RawUrl, false); 105 } 106 } 107 ///////////////////////////////////////////////////////////////////////////// 108 ///////////////////////////////////////////////////////////////////////////// 109 ///////////////////////////////////////////////////////////////////////////// 110 // Public functions 111 Dispose()112 public void Dispose() { } 113 Init(HttpApplication app)114 public void Init(HttpApplication app) { 115 // for IIS 7, skip event wireup altogether if this feature isn't 116 // enabled 117 if (!s_Initialized) { 118 Initialize(); 119 } 120 if (s_Enabled) { 121 app.PostAuthenticateRequest += new EventHandler(this.OnEnter); 122 } 123 } 124 125 //////////////////////////////////////////////////////////// 126 //////////////////////////////////////////////////////////// 127 //////////////////////////////////////////////////////////// 128 129 /// <devdoc> 130 /// <para>[To be supplied.]</para> 131 /// </devdoc> OnEnter(Object source, EventArgs eventArgs)132 private void OnEnter(Object source, EventArgs eventArgs) { 133 if (!s_Initialized) 134 Initialize(); 135 136 if (!s_Enabled) 137 return; 138 139 HttpApplication app; 140 HttpContext context; 141 HttpCookie cookie = null; 142 bool createCookie = false; 143 AnonymousIdData decodedValue = null; 144 bool cookieLess; 145 string encValue = null; 146 bool isAuthenticated = false; 147 148 app = (HttpApplication)source; 149 context = app.Context; 150 151 isAuthenticated = context.Request.IsAuthenticated; 152 153 if (isAuthenticated) { 154 cookieLess = CookielessHelperClass.UseCookieless(context, false /* no redirect */, s_CookieMode); 155 } else { 156 cookieLess = CookielessHelperClass.UseCookieless(context, true /* do redirect */, s_CookieMode); 157 //if (!cookieLess && s_RequireSSL && !context.Request.IsSecureConnection) 158 // throw new HttpException(SR.GetString(SR.Connection_not_secure_creating_secure_cookie)); 159 } 160 161 //////////////////////////////////////////////////////////////////////// 162 // Handle secure-cookies over non SSL 163 if (s_RequireSSL && !context.Request.IsSecureConnection) 164 { 165 if (!cookieLess) 166 { 167 cookie = context.Request.Cookies[s_CookieName]; 168 if (cookie != null) 169 { 170 cookie = new HttpCookie(s_CookieName, String.Empty); 171 cookie.HttpOnly = true; 172 cookie.Path = s_CookiePath; 173 cookie.Secure = s_RequireSSL; 174 if (s_Domain != null) 175 cookie.Domain = s_Domain; 176 cookie.Expires = new System.DateTime(1999, 10, 12); 177 178 if (context.Request.Browser["supportsEmptyStringInCookieValue"] == "false") 179 cookie.Value = "NoCookie"; 180 context.Response.Cookies.Add(cookie); 181 } 182 183 return; 184 } 185 } 186 187 188 //////////////////////////////////////////////////////////// 189 // Step 2: See if cookie, or cookie-header has the value 190 if (!cookieLess) 191 { 192 cookie = context.Request.Cookies[s_CookieName]; 193 if (cookie != null) 194 { 195 encValue = cookie.Value; 196 cookie.Path = s_CookiePath; 197 if (s_Domain != null) 198 cookie.Domain = s_Domain; 199 } 200 } 201 else 202 { 203 encValue = context.CookielessHelper.GetCookieValue('A'); 204 } 205 206 decodedValue = GetDecodedValue(encValue); 207 208 if (decodedValue != null && decodedValue.AnonymousId != null) { 209 // Copy existing value in Request 210 context.Request.AnonymousID = decodedValue.AnonymousId; 211 } 212 if (isAuthenticated) // For the authenticated case, we are done 213 return; 214 215 if (context.Request.AnonymousID == null) { 216 //////////////////////////////////////////////////////////// 217 // Step 3: Create new Identity 218 219 // Raise event 220 if (_CreateNewIdEventHandler != null) { 221 AnonymousIdentificationEventArgs e = new AnonymousIdentificationEventArgs(context); 222 _CreateNewIdEventHandler(this, e); 223 context.Request.AnonymousID = e.AnonymousID; 224 } 225 226 // Create from GUID 227 if (string.IsNullOrEmpty(context.Request.AnonymousID)) { 228 context.Request.AnonymousID = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture); 229 } else { 230 if (context.Request.AnonymousID.Length > MAX_ID_LENGTH) 231 throw new HttpException(SR.GetString(SR.Anonymous_id_too_long)); 232 } 233 if (s_RequireSSL && !context.Request.IsSecureConnection && !cookieLess) 234 return; // Don't create secure-cookie in un-secured connection 235 236 createCookie = true; 237 } 238 239 //////////////////////////////////////////////////////////// 240 // Step 4: Check if cookie has to be created 241 DateTime dtNow = DateTime.UtcNow; 242 if (!createCookie) { 243 if (s_SlidingExpiration) { 244 if (decodedValue == null || decodedValue.ExpireDate < dtNow) { 245 createCookie = true; 246 } else { 247 double secondsLeft = (decodedValue.ExpireDate - dtNow).TotalSeconds; 248 if (secondsLeft < (double) ((s_CookieTimeout*60)/2)) { 249 createCookie = true; 250 } 251 } 252 } 253 } 254 255 //////////////////////////////////////////////////////////// 256 // Step 4: Create new cookie or cookieless header 257 if (createCookie) { 258 DateTime dtExpireTime = dtNow.AddMinutes(s_CookieTimeout); 259 encValue = GetEncodedValue(new AnonymousIdData(context.Request.AnonymousID, dtExpireTime)); 260 if (encValue.Length > MAX_ENCODED_COOKIE_STRING) 261 throw new HttpException(SR.GetString(SR.Anonymous_id_too_long_2)); 262 263 if (!cookieLess) { 264 cookie = new HttpCookie(s_CookieName, encValue); 265 cookie.HttpOnly = true; 266 cookie.Expires = dtExpireTime; 267 cookie.Path = s_CookiePath; 268 cookie.Secure = s_RequireSSL; 269 if (s_Domain != null) 270 cookie.Domain = s_Domain; 271 context.Response.Cookies.Add(cookie); 272 } else { 273 context.CookielessHelper.SetCookieValue('A', encValue); 274 context.Response.Redirect(context.Request.RawUrl); 275 } 276 } 277 } 278 279 public static bool Enabled { 280 get { 281 if (!s_Initialized) { 282 Initialize(); 283 } 284 return s_Enabled; 285 } 286 } 287 288 ///////////////////////////////////////////////////////////////////////////// 289 ///////////////////////////////////////////////////////////////////////////// 290 ///////////////////////////////////////////////////////////////////////////// 291 // Static config settings 292 private static bool s_Initialized = false; 293 private static bool s_Enabled = false; 294 private static string s_CookieName = ".ASPXANONYMOUS"; 295 private static string s_CookiePath = "/"; 296 private static int s_CookieTimeout = 100000; 297 private static bool s_RequireSSL = false; 298 private static string s_Domain = null; 299 private static bool s_SlidingExpiration = true; 300 private static byte [] s_Modifier = null; 301 private static object s_InitLock = new object(); 302 private static HttpCookieMode s_CookieMode = HttpCookieMode.UseDeviceProfile; 303 private static CookieProtection s_Protection = CookieProtection.None; 304 305 ///////////////////////////////////////////////////////////////////////////// 306 ///////////////////////////////////////////////////////////////////////////// 307 ///////////////////////////////////////////////////////////////////////////// 308 // Static functions Initialize()309 private static void Initialize() { 310 311 if (s_Initialized) 312 return; 313 314 lock(s_InitLock) { 315 if (s_Initialized) 316 return; 317 318 AnonymousIdentificationSection settings = RuntimeConfig.GetAppConfig().AnonymousIdentification; 319 s_Enabled = settings.Enabled; 320 s_CookieName = settings.CookieName; 321 s_CookiePath = settings.CookiePath; 322 s_CookieTimeout = (int) settings.CookieTimeout.TotalMinutes; 323 s_RequireSSL = settings.CookieRequireSSL; 324 s_SlidingExpiration = settings.CookieSlidingExpiration; 325 s_Protection = settings.CookieProtection; 326 s_CookieMode = settings.Cookieless; 327 s_Domain = settings.Domain; 328 329 s_Modifier = Encoding.UTF8.GetBytes("AnonymousIdentification"); 330 if (s_CookieTimeout < 1) 331 s_CookieTimeout = 1; 332 if (s_CookieTimeout > 60 * 24 * 365 * 2) 333 s_CookieTimeout = 60 * 24 * 365 * 2; // 2 years 334 s_Initialized = true; 335 } 336 } 337 338 ///////////////////////////////////////////////////////////////////////////// 339 ///////////////////////////////////////////////////////////////////////////// GetEncodedValue(AnonymousIdData data)340 private static string GetEncodedValue(AnonymousIdData data){ 341 if (data == null) 342 return null; 343 byte [] bufId = Encoding.UTF8.GetBytes(data.AnonymousId); 344 byte [] bufIdLen = BitConverter.GetBytes(bufId.Length); 345 byte [] bufDate = BitConverter.GetBytes(data.ExpireDate.ToFileTimeUtc()); 346 byte [] buffer = new byte[12 + bufId.Length]; 347 348 Buffer.BlockCopy(bufDate, 0, buffer, 0, 8); 349 Buffer.BlockCopy(bufIdLen, 0, buffer, 8, 4); 350 Buffer.BlockCopy(bufId, 0, buffer, 12, bufId.Length); 351 return CookieProtectionHelper.Encode(s_Protection, buffer, Purpose.AnonymousIdentificationModule_Ticket); 352 } 353 354 ///////////////////////////////////////////////////////////////////////////// 355 ///////////////////////////////////////////////////////////////////////////// GetDecodedValue(string data)356 private static AnonymousIdData GetDecodedValue(string data){ 357 if (data == null || data.Length < 1 || data.Length > MAX_ENCODED_COOKIE_STRING) 358 return null; 359 360 try { 361 byte [] bBlob = CookieProtectionHelper.Decode(s_Protection, data, Purpose.AnonymousIdentificationModule_Ticket); 362 if (bBlob == null || bBlob.Length < 13) 363 return null; 364 DateTime expireDate = DateTime.FromFileTimeUtc(BitConverter.ToInt64(bBlob, 0)); 365 if (expireDate < DateTime.UtcNow) 366 return null; 367 int len = BitConverter.ToInt32(bBlob, 8); 368 if (len < 0 || len > bBlob.Length - 12) 369 return null; 370 string id = Encoding.UTF8.GetString(bBlob, 12, len); 371 if (id.Length > MAX_ID_LENGTH) 372 return null; 373 return new AnonymousIdData(id, expireDate); 374 } 375 catch {} 376 return null; 377 } 378 379 ///////////////////////////////////////////////////////////////////////////// 380 ///////////////////////////////////////////////////////////////////////////// 381 } 382 383 ///////////////////////////////////////////////////////////////////////////// 384 ///////////////////////////////////////////////////////////////////////////// 385 ///////////////////////////////////////////////////////////////////////////// 386 [Serializable] 387 internal class AnonymousIdData 388 { AnonymousIdData(string id, DateTime dt)389 internal AnonymousIdData(string id, DateTime dt) { 390 ExpireDate = dt; 391 AnonymousId = (dt > DateTime.UtcNow) ? id : null; // Ignore expired data 392 } 393 394 internal string AnonymousId; 395 internal DateTime ExpireDate; 396 } 397 398 ///////////////////////////////////////////////////////////////////////////// 399 ///////////////////////////////////////////////////////////////////////////// 400 ///////////////////////////////////////////////////////////////////////////// 401 AnonymousIdentificationEventHandler(object sender, AnonymousIdentificationEventArgs e)402 public delegate void AnonymousIdentificationEventHandler(object sender, AnonymousIdentificationEventArgs e); 403 404 ///////////////////////////////////////////////////////////////////////////// 405 ///////////////////////////////////////////////////////////////////////////// 406 ///////////////////////////////////////////////////////////////////////////// 407 408 public sealed class AnonymousIdentificationEventArgs : EventArgs { 409 410 public string AnonymousID { get { return _AnonymousId;} set { _AnonymousId = value;}} 411 412 public HttpContext Context { get { return _Context; }} 413 414 private string _AnonymousId; 415 private HttpContext _Context; 416 417 AnonymousIdentificationEventArgs(HttpContext context)418 public AnonymousIdentificationEventArgs(HttpContext context) { 419 _Context = context; 420 } 421 } 422 423 } 424 425 426