//-------------------------------------------------------------------------------------------------------------------------- // // Copyright © Microsoft Corporation. All Rights Reserved. // //-------------------------------------------------------------------------------------------------------------------------- // @owner=alexgor, deliant //========================================================================================================================== // File: ChartHttpHandler.cs // // Namespace: Microsoft.Reporting.Chart.WebForms // // Classes: ChartHttpHandler // // Purpose: ChartHttpHandler is a static class which is responsible to handle with // chart images, interactive images, scripts and other resources. // // // Reviewed: DT // Reviewed: deliant on 4/14/2011 // MSRC#10470, VSTS#941768 http://vstfdevdiv:8080/web/wi.aspx?id=941768 // Please review information associated with MSRC#10470 before making any changes to this file. // - Fixes: // - Fixed Directory Traversal/Arbitrary File Read, Delete with malformed image key. // - Honor HttpContext.Current.Trace.IsEnabled when generate and deliver chart trace info. // - Handle empty guid parameter ("?g=") as invalid when enforcing privacy. // - Replaced the privacy byte array comparison with custom check (otherwise posible EOS marker can return 0 length string). // - Added fixed string to session key to avoid direct session access. // // Added: deliant on 4/48/2011 fix for VSTS: 3593 - ASP.Net chart under web farm exhibit fast performace degradation // Summary: Under large web farm setup ( ~16 processes and up) chart control image handler // soon starts to show performace degradation up to denial of service, when a file system is used as storage. // Issues: // - The image files in count over 2000 in one single folder causes exponentially growing slow response, // especially on the remote server. The fix places the Image files in separate subfolders for each process. // - Private protection seeks and read several times in the image file istead reading the image at once // and then check for privacy marker. Separate small network reads are expensive. // - Due missing lock in initialization stage the chart lock files number can grow more that process max // number which can create abandon chart image files //========================================================================================================================== #region Namespaces using System; using System.Collections.Generic; using System.Text; using System.Web; using System.Web.UI; using System.IO; using System.Web.Caching; using System.Collections; using System.Web.Configuration; using System.Resources; using System.Reflection; using System.Drawing; using System.Drawing.Imaging; using System.Threading; using System.Collections.Specialized; using System.Diagnostics; using System.Web.Hosting; using System.Web.SessionState; using System.Drawing.Drawing2D; using System.Runtime.InteropServices; using System.Globalization; using System.Diagnostics.CodeAnalysis; using System.Security.Permissions; using System.Security; using System.Security.Cryptography; using System.Collections.ObjectModel; using System.Web.UI.WebControls; #endregion //Namespaces namespace System.Web.UI.DataVisualization.Charting { /// /// ChartHttpHandler processes HTTP Web requests using, handles chart images, scripts and other resources. /// #if ASPPERM_35 [AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)] [AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] #endif public class ChartHttpHandler : Page, IRequiresSessionState, IHttpHandler { #region Fields // flag that indicates whether this chart handler is installed private static bool _installed = false; // flag that indicates whether this chart handler is installed private static bool _installChecked = false; // storage settings private static ChartHttpHandlerSettings _parameters = null; // machine hash key which is part in chart image file name private static string _machineHash = "_" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_"; // web gadren controller file. stays locked diring process lifetime. private static FileStream _controllerFileStream = null; private static string _controllerDirectory = null; private static object _initHandlerLock = new object(); // used for storing Guid key in context; internal static string ContextGuidKey = "{89FA5660-BD13-4f1b-8C7C-355CEC92CC7E}"; // web gadren controller file. stays locked diring process lifetime. private const string handlerCheckQry = "check"; #endregion //Fields #region Consts internal const string ChartHttpHandlerName = "ChartImg.axd"; internal const string ChartHttpHandlerAppSection = "ChartImageHandler"; internal const string DefaultConfigSettings = @"storage=file;timeout=20;dir=c:\TempImageFiles\;"; internal const string WebDevServerUseConfigSettings = "WebDevServerUseConfigSettings"; #endregion //Consts #region Constructors /// /// Ensures that the handler is initialized. /// /// if set to true then will be thrown all excepitons. private static void EnsureInitialized(bool hardCheck) { if (_installChecked) { return; } lock (_initHandlerLock) { if (_installChecked) { return; } if (HttpContext.Current != null) { try { using (TextWriter w = new StringWriter(CultureInfo.InvariantCulture)) { HttpContext.Current.Server.Execute(ChartHttpHandlerName + "?" + handlerCheckQry + "=0", w); } _installed = true; } catch (HttpException) { if (hardCheck) throw; } catch (SecurityException) { // under minimal configuration we assume that the hanlder is installed if app settings are present. _installed = !String.IsNullOrEmpty(WebConfigurationManager.AppSettings[ChartHttpHandlerAppSection]); } } if (_installed || hardCheck) { InitializeControllerFile(); } _installChecked = true; } } /// /// Initializes the storage settings /// //static ChartHttpHandler() private static ChartHttpHandlerSettings InitializeParameters() { ChartHttpHandlerSettings result = new ChartHttpHandlerSettings(); if (HttpContext.Current != null) { // Read settings from config; use DefaultConfigSettings in case when setting is not found string configSettings = WebConfigurationManager.AppSettings[ChartHttpHandlerAppSection]; if (String.IsNullOrEmpty(configSettings)) configSettings = DefaultConfigSettings; result = new ChartHttpHandlerSettings(configSettings); } else { result.PrepareDesignTime(); } return result; } private static void ResetControllerStream() { if (_controllerFileStream != null) _controllerFileStream.Dispose(); _controllerFileStream = null; _controllerDirectory = null; } private static void InitializeControllerFile() { if (Settings.StorageType == ChartHttpHandlerStorageType.File && _controllerFileStream == null) { byte[] data = System.Text.Encoding.UTF8.GetBytes("chart io controller file"); // 2048 processes max. for (Int32 i = 0; i < 2048; i++) { try { ResetControllerStream(); string controllerFileName = String.Format(CultureInfo.InvariantCulture, "{0}msc_cntr_{1}.txt", Settings.Directory, i); _controllerDirectory = String.Format(CultureInfo.InvariantCulture, "charts_{0}", i); _controllerFileStream = new System.IO.FileStream(controllerFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); _controllerFileStream.Lock(0, data.Length); _controllerFileStream.Write(data, 0, data.Length); _machineHash = "_" + i + "_"; if (!Directory.Exists(Settings.Directory + _controllerDirectory)) { Directory.CreateDirectory(Settings.Directory + _controllerDirectory); } else { TimeSpan lastWrite = DateTime.Now - Directory.GetLastWriteTime(Settings.Directory + _controllerDirectory); if (lastWrite.Seconds < Settings.Timeout.Seconds) { continue; } } return; } catch (IOException) { continue; } catch (Exception) { ResetControllerStream(); throw; } } ResetControllerStream(); throw new UnauthorizedAccessException(SR.ExceptionHttpHandlerTempDirectoryUnaccesible(Settings.Directory)); } } #endregion //Constructors #region Methods #region ChartImage /// /// Processes the saved image. /// /// The context. /// false if the image cannot be processed private static bool ProcessSavedChartImage(HttpContext context) { // image delivery doesn't depend if handler is intitilzed or not. String key = context.Request["i"]; CurrentGuidKey = context.Request["g"]; IChartStorageHandler handler = GetHandler(); try { Byte[] data = handler.Load(KeyToUnc(key)); if (data != null && data.Length > 0) { context.Response.Charset = ""; context.Response.ContentType = GetMime(key); context.Response.BinaryWrite(data); Diagnostics.TraceWrite(SR.DiagnosticChartImageServed(key), null); if (Settings.StorageType == ChartHttpHandlerStorageType.Session || Settings.DeleteAfterServicing) { handler.Delete(key); Diagnostics.TraceWrite(SR.DiagnosticChartImageDeleted(key), null); } return true; } if (!(handler is DefaultImageHandler)) { // the default handler will write more detailed message Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, SR.DiagnosticChartImageServedFailNotFound), null); } } catch (NullReferenceException nre) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, String.Empty), nre); throw; } catch (IOException ioe) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, String.Empty), ioe); throw; } catch (SecurityException se) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, String.Empty), se); throw; } return false; } #endregion //ChartImage #region Utilities /// /// Gets or sets the current GUID key. /// /// The current GUID key. internal static string CurrentGuidKey { get { if (HttpContext.Current != null) { return (string)HttpContext.Current.Items[ContextGuidKey]; } return String.Empty; } set { if (HttpContext.Current != null) { if (String.IsNullOrEmpty(value)) { HttpContext.Current.Items.Remove(ContextGuidKey); } else { HttpContext.Current.Items[ContextGuidKey] = value; } } } } /// /// Gets the chart image handler interface reference. /// /// private static IChartStorageHandler GetHandler() { return ChartHttpHandler.Settings.GetHandler(); } /// /// Determines whether this instance is installed. /// internal static void EnsureInstalled() { EnsureInitialized(true); EnsureSessionIsClean(); } /// /// Gets the handler URL. /// /// private static String GetHandlerUrl() { // the handler have to be executed in current cxecution path in order to get proper user identity String appDir = Path.GetDirectoryName(HttpContext.Current.Request.CurrentExecutionFilePath ?? "").Replace("\\","/"); if (!appDir.EndsWith("/", StringComparison.Ordinal)) { appDir += "/"; } return appDir + ChartHttpHandlerName + "?"; } /// /// Gets the MIME type by resource url. /// /// The resource URL. /// [SuppressMessage("Microsoft.Globalization", "CA1308", Justification = "No security decision is being made on the ToLowerInvariant() call. It is being used to ensure the file extension is lowercase")] private static String GetMime(String resourceUrl) { String ext = Path.GetExtension(resourceUrl); ext = ext.ToLowerInvariant(); if (ext == ".js") { return "text/javascript"; } else if (ext == ".htm") { return "text/html"; } else if (".css,.html,.xml".IndexOf(ext, StringComparison.Ordinal) != -1) { return "text/" + ext.Substring(1); } else if (".jpg;.jpeg;.gif;.png;.emf".IndexOf(ext, StringComparison.Ordinal) != -1) { string fmt = ext.Substring(1).Replace("jpg", "jpeg"); return "image/" + fmt; } return "text/plain"; } /// /// Generates the chart image file name (key). /// /// The ext. /// Name of the file. /// private static String GenerateKey(String ext) { String fmtKey = "chart" + _machineHash + "{0}." + ext; RingTimeTracker rt = RingTimeTrackerFactory.GetRingTracker(fmtKey); if (!String.IsNullOrEmpty(_controllerDirectory) && String.IsNullOrEmpty(Settings.FolderName)) { return _controllerDirectory + @"\" + rt.GetNextKey(); } return Settings.FolderName + rt.GetNextKey(); } private static String KeyToUnc(String key) { if (!String.IsNullOrEmpty(key)) { return key.Replace("/", @"\"); } return key; } private static String KeyFromUnc(String key) { if (!String.IsNullOrEmpty(key)) { return key.Replace(@"\", "/"); } return key; } /// /// Gets a URL by specified request query, file key. /// /// The query. /// The file key. /// The current GUID. /// private static String GetUrl(String query, String fileKey, string currentGuid) { return GetHandlerUrl() + query + "=" + KeyFromUnc(fileKey) + "&g=" + currentGuid; } /// /// Gets the image url. /// /// The stream. /// The image extention. /// Generated the image source URL [SuppressMessage("Microsoft.Globalization", "CA1308", Justification="No security decision is being made on the ToLowerInvariant() call. It is being used to ensure the file extension is lowercase")] internal static String GetChartImageUrl(MemoryStream stream, String imageExt) { EnsureInitialized(true); // generates new guid string guidKey = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); // set new guid in context CurrentGuidKey = guidKey; Int32 tryCounts = 10; while (tryCounts > 0) { tryCounts--; try { String key = GenerateKey(imageExt.ToLowerInvariant()); IChartStorageHandler handler = Settings.GetHandler(); handler.Save(key, stream.ToArray()); if (!(handler is DefaultImageHandler)) { Diagnostics.TraceWrite(SR.DiagnosticChartImageSaved(key), null); } Settings.FolderName = String.Empty; // clear guid so is not accessable out of the scope; CurrentGuidKey = String.Empty; return ChartHttpHandler.GetUrl("i", key, guidKey); } catch (IOException) { } catch { throw;} } throw new IOException(SR.ExceptionHttpHandlerCanNotSave); } /// /// Ensures the session is clean. /// private static void EnsureSessionIsClean() { if (!_installed) return; if (Settings.StorageType == ChartHttpHandlerStorageType.Session) { IChartStorageHandler handler = ChartHttpHandler.Settings.GetHandler(); foreach (RingTimeTracker tracker in RingTimeTrackerFactory.OpenedRingTimeTrackers()) { tracker.ForEach(true, delegate(RingItem item) { if (item.InUse && String.CompareOrdinal(Settings.ReadSessionKey(), item.SessionID) == 0) { handler.Delete(tracker.GetKey(item)); Diagnostics.TraceWrite(SR.DiagnosticChartImageDeleted(tracker.GetKey(item)), null); item.InUse = false; } } ); } } } #endregion //Utilities #region Diagnostics private static void DiagnosticWriteAll(HttpContext context) { HtmlTextWriter writer; using (TextWriter w = new StringWriter(CultureInfo.CurrentCulture)) { if (context.Request.Browser != null) writer = context.Request.Browser.CreateHtmlTextWriter(w); else writer = new Html32TextWriter(w); writer.Write("\n\r\n\r"); writer.Write("\r\n"); writer.Write("\r\n"); writer.Write("\r\n\r\n"); writer.Write("

" + SR.DiagnosticHeader + "

\r\n

\n\r"); DiagnosticWriteSettings(writer); writer.Write("
"); DiagnosticWriteActivity(writer); writer.Write("

\n\r"); try { writer.Write(typeof(Chart).AssemblyQualifiedName); } catch ( SecurityException ) {} writer.Write("\r\n\r\n"); context.Response.Write(w.ToString()); } } private static void DiagnosticWriteSettings(HtmlTextWriter writer) { writer.Write("

" + SR.DiagnosticSettingsConfig(WebConfigurationManager.AppSettings[ChartHttpHandlerAppSection]) + "

"); GridView grid = CreateGridView( true); grid.Caption = SR.DiagnosticSettingsHeader; BoundField field = new BoundField(); field.DataField = "Key"; field.HeaderText = SR.DiagnosticSettingsKey; field.HeaderStyle.HorizontalAlign = HorizontalAlign.Left; grid.Columns.Add(field); field = new BoundField(); field.DataField = "Value"; field.HeaderText = SR.DiagnosticSettingsValue; field.HeaderStyle.HorizontalAlign = HorizontalAlign.Left; grid.Columns.Add(field); Dictionary settings = new Dictionary(); settings.Add("StorageType", Settings.StorageType.ToString()); settings.Add("TimeOut", Settings.Timeout.ToString()); if (Settings.StorageType == ChartHttpHandlerStorageType.File) { settings.Add("Directory", Settings.Directory); } settings.Add("DeleteAfterServicing", Settings.DeleteAfterServicing.ToString()); settings.Add("PrivateImages", Settings.PrivateImages.ToString()); settings.Add("ImageOwnerKey", Settings.ImageOwnerKey.ToString()); settings.Add("CustomHandlerName", Settings.CustomHandlerName); settings.Add(ChartHttpHandler.WebDevServerUseConfigSettings, String.Equals(Settings[ChartHttpHandler.WebDevServerUseConfigSettings], "true", StringComparison.OrdinalIgnoreCase).ToString()); grid.DataSource = settings; grid.DataBind(); grid.RenderControl(writer); } private static void DiagnosticWriteActivity(HtmlTextWriter writer) { GridView grid = CreateGridView( true); grid.Caption = SR.DiagnosticActivityHeader; BoundField field = new BoundField(); field.DataField = "DateStamp"; field.ItemStyle.VerticalAlign = VerticalAlign.Top; field.HeaderText = SR.DiagnosticActivityTime; field.HeaderStyle.HorizontalAlign = HorizontalAlign.Left; field.HeaderStyle.Width = 150; grid.Columns.Add(field); field = new BoundField(); field.DataField = "Url"; field.HeaderText = SR.DiagnosticActivityMessage; field.HeaderStyle.HorizontalAlign = HorizontalAlign.Left; grid.Columns.Add(field); grid.RowDataBound += new GridViewRowEventHandler(DiagnosticActivityGrid_RowDataBound); grid.DataSource = Diagnostics.Messages; grid.DataBind(); grid.RenderControl(writer); } static void DiagnosticActivityGrid_RowDataBound(object sender, GridViewRowEventArgs e) { if (e.Row.RowType == DataControlRowType.DataRow) { Diagnostics.HandlerPageTraceInfo currentInfo = (Diagnostics.HandlerPageTraceInfo)e.Row.DataItem; TableCell cell = e.Row.Cells[1]; cell.Controls.Add(new Label() { Text = currentInfo.Verb + "," + currentInfo.Url }); GridView grid = CreateGridView(false); grid.Style[HtmlTextWriterStyle.MarginLeft] = "20px"; grid.ShowHeader = false; BoundField field = new BoundField(); field.DataField = "Text"; field.HeaderStyle.HorizontalAlign = HorizontalAlign.Left; grid.Columns.Add(field); grid.DataSource = currentInfo.Events; grid.DataBind(); cell.Controls.Add(grid); } } private static GridView CreateGridView(bool withAlternateStyle) { GridView result = new GridView(); result.AutoGenerateColumns = false; result.CellPadding = 4; result.Font.Names = new string[] { "Tahoma", "Ariel" }; result.Font.Size = new FontUnit(10, UnitType.Point); result.BorderWidth = 0; result.GridLines = GridLines.None; result.Width = new Unit(100, UnitType.Percentage); if (withAlternateStyle) { result.AlternatingRowStyle.BackColor = Color.White; result.RowStyle.BackColor = ColorTranslator.FromHtml("#efefef"); result.RowStyle.ForeColor = Color.Black; result.AlternatingRowStyle.ForeColor = Color.Black; } result.HeaderStyle.BackColor = Color.Gray; result.HeaderStyle.ForeColor = Color.White; result.HeaderStyle.Font.Bold = true; return result; } #endregion //Diagnostics #endregion //Methods #region Properties /// /// Gets the chart image storage settings registred in web.config file under ChartHttpHandler key. /// /// The settings. public static ChartHttpHandlerSettings Settings { get { if (_parameters == null) { _parameters = InitializeParameters(); } return _parameters; } } #endregion //Properties #region IHttpHandler Members /// /// Gets a value indicating whether the object can be reused. /// /// /// false in all cases. bool IHttpHandler.IsReusable { get { return true; } } /// /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the interface. /// /// An object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests. void IHttpHandler.ProcessRequest(HttpContext context) { if (context.Request["i"] != null && ProcessSavedChartImage(context)) { return; } else if (context.Request["trace"] != null && Diagnostics.IsTraceEnabled) { DiagnosticWriteAll(context); return; } else if (context.Request[handlerCheckQry] != null) { // handler execute test - returns no errors. return; } context.Response.StatusCode = 404; context.Response.StatusDescription = SR.ExceptionHttpHandlerImageNotFound; } #endregion } #region Enumerations /// /// Determines chart image storage medium /// public enum ChartHttpHandlerStorageType { /// /// Static into application memory /// InProcess, /// /// File system /// File, /// /// Using session as storage /// Session } /// /// Determines the image owner key for privacy protection. /// internal enum ImageOwnerKeyType { /// /// No privacy protection. /// None, /// /// The key will be automatically determined. /// Auto, /// /// The user name will be used as key. /// UserID, /// /// The AnonymousID will be used as key. /// AnonymousID, /// /// The SessionID will be used as key. /// SessionID } #endregion #region IChartStorageHandler interface /// /// Defines methods to manage rendered chart images in a storage. /// public interface IChartStorageHandler { /// /// Saves the data into external medium. /// /// Index key. /// Image data. void Save(String key, Byte[] data); /// /// Loads the data from external medium. /// /// Index key. /// A byte array with image data Byte[] Load(String key); /// /// Deletes the data from external medium. /// /// Index key. void Delete(String key); /// /// Checks for existence of data under specified key. /// /// Index key. /// True if data exists under specified key bool Exists(String key); } #endregion #region ChartHttpHandlerSettings Class /// /// Enables access to the chart image storage settings. /// #if ASPPERM_35 [AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)] [AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] #endif public class ChartHttpHandlerSettings { #region Fields private StorageSettingsCollection _ssCollection = new StorageSettingsCollection(); private string _sesionKey = "chartKey-" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); #endregion //Fields #region Properties private ChartHttpHandlerStorageType _chartImageStorage = ChartHttpHandlerStorageType.File; /// /// Gets or sets the chart image storage type. /// /// The chart image storage. public ChartHttpHandlerStorageType StorageType { get { return _chartImageStorage; } set { _chartImageStorage = value; } } private TimeSpan _timeout = TimeSpan.FromSeconds(30); /// /// Gets or sets the timeout. /// /// The timeout. public TimeSpan Timeout { get { return _timeout; } set { _timeout = value; } } private String _url = "~/"; /// /// Gets or sets the URL. /// /// The URL. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")] public String Url { get { return _url; } set { _url = value; } } private String _directory = String.Empty; /// /// Gets or sets the directory. /// /// The directory. public String Directory { get { return _directory; } set { _directory = value; } } private const String _folderKeyName = "{5FF3B636-70BA-4180-B7C5-FDD77D8FA525}"; /// /// Gets or sets the folder which will be used for storing images under . /// /// The folder name. public String FolderName { get { if (HttpContext.Current != null && HttpContext.Current.Items.Contains(_folderKeyName)) { return (string)HttpContext.Current.Items[_folderKeyName]; } return String.Empty; } set { if (!String.IsNullOrEmpty(value)) { if (!(value.EndsWith("/", StringComparison.Ordinal) || value.EndsWith("\\", StringComparison.Ordinal))) { value += "\\"; } this.ValidateUri(value); } if (HttpContext.Current != null) { HttpContext.Current.Items[_folderKeyName] = value; } } } internal void ValidateUri(string key) { if (this.StorageType == ChartHttpHandlerStorageType.File) { FileInfo fi = new FileInfo(this.Directory + key); Uri directory = new Uri(this.Directory); Uri combinedDirectory = new Uri(fi.FullName); if (directory.IsBaseOf(combinedDirectory)) { // it is fine. return; } throw new UnauthorizedAccessException(SR.ExceptionHttpHandlerInvalidLocation); } } private String _customHandlerName = typeof(DefaultImageHandler).FullName; /// /// Gets or sets the name of the custom handler. /// /// The name of the custom handler. public String CustomHandlerName { get { return _customHandlerName; } set { _customHandlerName = value; } } private Type _customHandlerType = null; /// /// Gets the type of the custom handler. /// /// The type of the custom handler. public Type HandlerType { get { if (this._customHandlerType == null) { this._customHandlerType = Type.GetType(this.CustomHandlerName, true); } return this._customHandlerType; } } /// /// Gets a value indicating whether the handler will utilize private images. /// /// true if the handler will utilize private images; otherwise, false. /// /// When PrivateImages is set the handler will not return images out of session scope and /// the client will not be able to download somebody else's images. This is default behavoiur. /// public bool PrivateImages { get { return ImageOwnerKey != ImageOwnerKeyType.None; } } /// /// Gets a settings parameter with the specified name registred in web.config file under ChartHttpHandler key. /// /// public string this[string name] { get { return this._ssCollection[name]; } } #endregion //Properties #region Constructors /// /// Initializes a new instance of the class. /// internal ChartHttpHandlerSettings() { ImageOwnerKey = ImageOwnerKeyType.Auto; } /// /// Initializes a new instance of the class. /// /// The parameters. internal ChartHttpHandlerSettings(String parameters) : this() { this.ParseParams(parameters); this._ssCollection.SetReadOnly(true); } #endregion //Constructors #region Methods private ConstructorInfo _handlerConstructor = null; IChartStorageHandler _storageHandler = null; /// /// Creates the handler instance. /// /// internal IChartStorageHandler GetHandler() { if (_storageHandler == null) { if (this._handlerConstructor == null) { this.InspectHandlerLoader(); } _storageHandler = this._handlerConstructor.Invoke(new object[0]) as IChartStorageHandler; } return _storageHandler; } /// /// Inspects the handler if it is valid. /// private void InspectHandlerLoader() { this._handlerConstructor = this.HandlerType.GetConstructor( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], new ParameterModifier[0]); if (this._handlerConstructor == null) { throw new InvalidOperationException( SR.ExceptionHttpHandlerCanNotLoadType( this.HandlerType.FullName )); } if (this.GetHandler() == null) { throw new InvalidOperationException(SR.ExceptionHttpHandlerImageHandlerInterfaceUnsupported(ChartHttpHandler.Settings.HandlerType.FullName)); } } /// /// Parses the params from web.config file key. /// /// The parameters. private void ParseParams(String parameters) { if (!String.IsNullOrEmpty(parameters)) { String[] pairs = parameters.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); for (int index = 0; index < pairs.Length; index++) { String item = pairs[index].Trim(); int eqPositon = item.IndexOf('='); if (eqPositon != -1) { String name = item.Substring(0, eqPositon).Trim(); String value = item.Substring(eqPositon + 1).Trim(); this._ssCollection.Add(name, value); if (name.StartsWith("stor", StringComparison.OrdinalIgnoreCase)) { if (value.StartsWith("inproc", StringComparison.OrdinalIgnoreCase) || value.StartsWith("memory", StringComparison.OrdinalIgnoreCase)) { this.StorageType = ChartHttpHandlerStorageType.InProcess; } else if (value.StartsWith("file", StringComparison.OrdinalIgnoreCase)) { this.StorageType = ChartHttpHandlerStorageType.File; } else if (value.StartsWith("session", StringComparison.OrdinalIgnoreCase)) { this.StorageType = ChartHttpHandlerStorageType.Session; } else { throw new System.Configuration.SettingsPropertyWrongTypeException(SR.ExceptionHttpHandlerParameterUnknown(name, value)); } } else if (name.StartsWith("url", StringComparison.OrdinalIgnoreCase)) { if (!value.EndsWith("/", StringComparison.Ordinal)) { value += "/"; } this.Url = value; } else if (name.StartsWith("dir", StringComparison.OrdinalIgnoreCase)) { this.Directory = value; } else if (name.StartsWith("time", StringComparison.OrdinalIgnoreCase)) { try { int seconds = Int32.Parse(value, CultureInfo.InvariantCulture); if (seconds < -1) { throw new System.Configuration.SettingsPropertyWrongTypeException(SR.ExceptionHttpHandlerValueInvalid); } if (seconds == -1) { this.Timeout = TimeSpan.MaxValue; } else { this.Timeout = TimeSpan.FromSeconds(seconds); } } catch (Exception exception) { throw new System.Configuration.SettingsPropertyWrongTypeException(SR.ExceptionHttpHandlerTimeoutParameterInvalid, exception); } } else if (name.StartsWith("handler", StringComparison.OrdinalIgnoreCase)) { this.CustomHandlerName = value; } else if (name.StartsWith("privateImages", StringComparison.OrdinalIgnoreCase)) { bool privateImg = true; if (Boolean.TryParse(value, out privateImg) && !privateImg) { ImageOwnerKey = ImageOwnerKeyType.None; } } else if (name.StartsWith("imageOwnerKey", StringComparison.OrdinalIgnoreCase)) { try { ImageOwnerKey = (ImageOwnerKeyType)Enum.Parse(typeof(ImageOwnerKeyType), value, true); } catch (ArgumentException) { throw new System.Configuration.SettingsPropertyWrongTypeException(SR.ExceptionHttpHandlerParameterInvalid(name, value)); } } } } } this.Inspect(); } /// /// Determines whether web dev server is active. /// /// /// true if web dev server active; otherwise, false. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "GetCurrentProcess will fail if there is no access. This is by design. ")] // VSTS: 5176 Security annotation violations in System.Web.DataVisualization.dll [SecuritySafeCritical] private static bool IsWebDevActive() { try { Process process = Process.GetCurrentProcess(); if (process.ProcessName.StartsWith("WebDev.WebServer", StringComparison.OrdinalIgnoreCase)) { return true; } if (process.ProcessName.StartsWith("ii----press", StringComparison.OrdinalIgnoreCase)) { return true; } } catch (SecurityException) { } return false; } /// /// Inspects and validates this instance after loading params. /// internal void Inspect() { switch (this.StorageType) { case ChartHttpHandlerStorageType.InProcess: break; case ChartHttpHandlerStorageType.File: if (IsWebDevActive() && !( String.Compare(this[ChartHttpHandler.WebDevServerUseConfigSettings], "true", StringComparison.OrdinalIgnoreCase) == 0)) { this.StorageType = ChartHttpHandlerStorageType.InProcess; break; } if (String.IsNullOrEmpty(this.Url)) { throw new ArgumentException(SR.ExceptionHttpHandlerUrlMissing); } String fileDirectory = this.Directory; if (String.IsNullOrEmpty(fileDirectory)) { try { fileDirectory = HttpContext.Current.Server.MapPath(this.Url); } catch (Exception exception) { throw new InvalidOperationException(SR.ExceptionHttpHandlerUrlInvalid, exception); } } fileDirectory = fileDirectory.Replace("/", "\\"); if (!fileDirectory.EndsWith("\\", StringComparison.Ordinal)) { fileDirectory += "\\"; } if (!System.IO.Directory.Exists(fileDirectory)) { throw new DirectoryNotFoundException(SR.ExceptionHttpHandlerTempDirectoryInvalid(fileDirectory)); } Exception thrown = null; try { String testFileName = fileDirectory + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); using (FileStream fileStream = File.Create(testFileName)) { } File.Delete(testFileName); } catch (DirectoryNotFoundException exception) { thrown = exception; } catch (NotSupportedException exception) { thrown = exception; } catch (PathTooLongException exception) { thrown = exception; } catch (UnauthorizedAccessException exception) { thrown = exception; } if (thrown != null) { throw new UnauthorizedAccessException(SR.ExceptionHttpHandlerTempDirectoryUnaccesible(fileDirectory)); } this.Directory = fileDirectory; break; } if (!String.IsNullOrEmpty(this.CustomHandlerName)) { this.InspectHandlerLoader(); } } /// /// Prepares the design time params. /// internal void PrepareDesignTime() { this.StorageType = ChartHttpHandlerStorageType.File; this.Timeout = TimeSpan.FromSeconds(3); ; this.Url = Path.GetTempPath(); this.Directory = Path.GetTempPath(); } internal string ReadSessionKey() { if (HttpContext.Current.Session != null) { // initialize session (if is empty any postsequent request will have different id); if (HttpContext.Current.Session.IsNewSession) { if (HttpContext.Current.Session.IsReadOnly) { return string.Empty; } HttpContext.Current.Session[this._sesionKey] = 0; } return HttpContext.Current.Session.SessionID; } return String.Empty; } internal string GetPrivacyKey( out ImageOwnerKeyType keyType ) { if (ImageOwnerKey == ImageOwnerKeyType.None) { keyType = ImageOwnerKeyType.None; return String.Empty; } if (HttpContext.Current != null) { switch (ImageOwnerKey) { case ImageOwnerKeyType.Auto: if (HttpContext.Current.User.Identity.IsAuthenticated) { keyType = ImageOwnerKeyType.UserID; return HttpContext.Current.User.Identity.Name; } if (!String.IsNullOrEmpty(HttpContext.Current.Request.AnonymousID)) { keyType = ImageOwnerKeyType.AnonymousID; return HttpContext.Current.Request.AnonymousID; } string sessionId = ReadSessionKey(); keyType = String.IsNullOrEmpty(sessionId) ? ImageOwnerKeyType.None : ImageOwnerKeyType.SessionID; return sessionId; case ImageOwnerKeyType.UserID: if (!HttpContext.Current.User.Identity.IsAuthenticated) { throw new InvalidOperationException(SR.ExceptionHttpHandlerPrivacyKeyInvalid("ImageOwnerKey", ImageOwnerKey.ToString())); } keyType = ImageOwnerKeyType.UserID; return HttpContext.Current.User.Identity.Name; case ImageOwnerKeyType.AnonymousID: if (String.IsNullOrEmpty(HttpContext.Current.Request.AnonymousID)) { throw new InvalidOperationException(SR.ExceptionHttpHandlerPrivacyKeyInvalid("ImageOwnerKey", ImageOwnerKey.ToString())); } keyType = ImageOwnerKeyType.AnonymousID; return HttpContext.Current.Request.AnonymousID; case ImageOwnerKeyType.SessionID: if (HttpContext.Current.Session == null) { throw new InvalidOperationException(SR.ExceptionHttpHandlerPrivacyKeyInvalid("ImageOwnerKey", ImageOwnerKey.ToString())); } keyType = ImageOwnerKeyType.SessionID; return ReadSessionKey(); default: Debug.Fail("Unknown ImageOwnerKeyType."); break; } } keyType = ImageOwnerKeyType.None; return string.Empty; } internal string PrivacyKey { get { ImageOwnerKeyType keyType; return GetPrivacyKey(out keyType); } } internal bool DeleteAfterServicing { get { // default, if is missing in config, is true. return !(String.Compare(this["DeleteAfterServicing"], "false", StringComparison.OrdinalIgnoreCase) == 0); } } /// /// Gets or sets the image owner key type. /// /// The image owner key. internal ImageOwnerKeyType ImageOwnerKey { get; set; } #endregion //Methods #region SettingsCollection Class private class StorageSettingsCollection : NameValueCollection { public StorageSettingsCollection() : base(StringComparer.OrdinalIgnoreCase) { } internal void SetReadOnly(bool flag) { this.IsReadOnly = flag; } } #endregion //SettingsCollection Class } #endregion ChartHttpHandlerParameters #region DefaultImageHandler Class /// /// Default implementation of ChartHttpHandler.IImageHandler interface /// internal class DefaultImageHandler : IChartStorageHandler { #region Fields // Hashtable for storage private static Hashtable _storageData = new Hashtable(); // lock object private static ReaderWriterLock _rwl = new ReaderWriterLock(); // max access timeout private const int accessTimeout = 10000; static string _privacyKeyName = "_pk"; static byte[] _privacyMarker = (new Guid("332E3AB032904bceA82B249C25E65CB6")).ToByteArray(); static string _sessionKeyPrefix = "chart-3ece47b3-9481-4b22-ab45-ab669972eb79"; #endregion //Fields #region Constructors /// /// Initializes a new instance of the class. /// internal DefaultImageHandler() { } #endregion //Constructors #region Members /// /// Nots the type of the supported storage. /// /// The settings. private void NotSupportedStorageType(ChartHttpHandlerSettings settings) { throw new NotSupportedException( SR.ExceptionHttpHandlerStorageTypeUnsupported( settings.StorageType.ToString() )); } #endregion //Members #region Methods /// /// Returns privacy hash which will be save in the file. /// /// A byte array of hash data private static byte[] GetHashData() { string currentGuid = ChartHttpHandler.CurrentGuidKey; string sessionID = ChartHttpHandler.Settings.PrivacyKey; if (String.IsNullOrEmpty(sessionID)) { return new byte[0]; } byte[] data = Encoding.UTF8.GetBytes(sessionID + "/" + currentGuid); using (SHA1 sha = new SHA1CryptoServiceProvider()) { return sha.ComputeHash(data); } } private static bool CompareBytes(byte[] a, byte[] b) { if (a.Length != b.Length) return false; for (int i = 0; i < a.Length; i++) { if (a[i] != b[i]) return false; } return true; } private static string GetSessionImageKey(string key) { // all session variables starts with _sessionKeyPrefix to avoid direct access to session by passing image key in Url query. return _sessionKeyPrefix + key; } #endregion //Methods #region ImageHandler Members /// /// Stores the data into external medium. /// /// The key. /// The data. void IChartStorageHandler.Save(String key, Byte[] data) { ChartHttpHandlerSettings settings = ChartHttpHandler.Settings; ImageOwnerKeyType imageOwnerKeyType = ImageOwnerKeyType.None; string privacyKey = settings.GetPrivacyKey(out imageOwnerKeyType); if (settings.StorageType == ChartHttpHandlerStorageType.InProcess) { _rwl.AcquireWriterLock(accessTimeout); try { _storageData[key] = data; if (settings.PrivateImages && !String.IsNullOrEmpty(privacyKey)) { _storageData[key + _privacyKeyName] = privacyKey; Diagnostics.TraceWrite( SR.DiagnosticChartImageSavedPrivate(key, imageOwnerKeyType.ToString()), null); } else Diagnostics.TraceWrite(SR.DiagnosticChartImageSaved(key), null); } finally { _rwl.ReleaseWriterLock(); } } else if (settings.StorageType == ChartHttpHandlerStorageType.File) { using (FileStream stream = File.Create(settings.Directory + key)) { stream.Write(data, 0, data.Length); if (settings.PrivateImages && !String.IsNullOrEmpty(privacyKey)) { byte[] privacyData = GetHashData(); stream.Write(privacyData, 0, privacyData.Length); // we will put a marker at the end of the file; stream.Write(_privacyMarker, 0, _privacyMarker.Length); Diagnostics.TraceWrite(SR.DiagnosticChartImageSavedPrivate(key, imageOwnerKeyType.ToString()), null); } else Diagnostics.TraceWrite(SR.DiagnosticChartImageSaved(key), null); } } else if (settings.StorageType == ChartHttpHandlerStorageType.Session) { HttpContext.Current.Session[GetSessionImageKey(key)] = data; Diagnostics.TraceWrite(SR.DiagnosticChartImageSaved(key), null); } else this.NotSupportedStorageType(settings); } /// /// Retrieves the data from external medium. /// /// The key. Byte[] IChartStorageHandler.Load( String key) { ChartHttpHandlerSettings settings = ChartHttpHandler.Settings; ImageOwnerKeyType imageOwnerKeyType = ImageOwnerKeyType.None; string privacyKey = settings.GetPrivacyKey(out imageOwnerKeyType); Byte[] data = new Byte[0]; if (settings.StorageType == ChartHttpHandlerStorageType.InProcess) { _rwl.AcquireReaderLock(accessTimeout); try { if (settings.PrivateImages) { if (!String.IsNullOrEmpty(privacyKey)) { if (!String.Equals((string)_storageData[key + _privacyKeyName], privacyKey, StringComparison.Ordinal)) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, SR.DiagnosticChartImageServedFailPrivacyFail(imageOwnerKeyType.ToString())), null); return data; } } else { if (!String.IsNullOrEmpty((string)_storageData[key + _privacyKeyName])) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, SR.DiagnosticChartImageServedFailPrivacyFail(imageOwnerKeyType.ToString())), null); return data; } } } data = (Byte[])_storageData[key]; if (data == null) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, SR.DiagnosticChartImageServedFailNotFound), null); } } finally { _rwl.ReleaseReaderLock(); } } else if (settings.StorageType == ChartHttpHandlerStorageType.File) { settings.ValidateUri(key); if (File.Exists(settings.Directory + key)) { using (FileStream fileStream = File.OpenRead(settings.Directory + key)) { byte[] fileData = new byte[fileStream.Length]; fileStream.Read(fileData, 0, fileData.Length); using (MemoryStream stream = new MemoryStream(fileData)) { int streamCut = 0; if (settings.PrivateImages) { // read the marker first byte[] privacyMarkerStream = new Byte[_privacyMarker.Length]; streamCut += _privacyMarker.Length; stream.Seek(stream.Length - streamCut, SeekOrigin.Begin); stream.Read(privacyMarkerStream, 0, privacyMarkerStream.Length); if (!String.IsNullOrEmpty(privacyKey)) { byte[] privacyData = GetHashData(); streamCut += privacyData.Length; byte[] privacyDataFromStream = new Byte[privacyData.Length]; stream.Seek(stream.Length - streamCut, SeekOrigin.Begin); stream.Read(privacyDataFromStream, 0, privacyDataFromStream.Length); if (!CompareBytes(privacyDataFromStream, privacyData)) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, SR.DiagnosticChartImageServedFailPrivacyFail(imageOwnerKeyType.ToString())), null); return data; } } else { // this image is marked as private - check end return null if fails if (String.Equals( Encoding.Unicode.GetString(privacyMarkerStream), Encoding.Unicode.GetString(_privacyMarker), StringComparison.Ordinal)) { Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, SR.DiagnosticChartImageServedFailPrivacyFail(imageOwnerKeyType.ToString())), null); return data; } // its fine ( no user is stored ) streamCut = 0; } } stream.Seek(0, SeekOrigin.Begin); data = new Byte[(int)stream.Length - streamCut]; stream.Read(data, 0, (int)data.Length); } } } else Diagnostics.TraceWrite(SR.DiagnosticChartImageServedFail(key, SR.DiagnosticChartImageServedFailNotFound), null); } else if (settings.StorageType == ChartHttpHandlerStorageType.Session) { data = (Byte[])HttpContext.Current.Session[GetSessionImageKey(key)]; } else this.NotSupportedStorageType(settings); return data; } /// /// Removes the data from external medium. /// /// The key. void IChartStorageHandler.Delete(String key) { ChartHttpHandlerSettings settings = ChartHttpHandler.Settings; if (settings.StorageType == ChartHttpHandlerStorageType.InProcess) { _rwl.AcquireWriterLock(accessTimeout); try { _storageData.Remove(key); _storageData.Remove(key + _privacyKeyName); } finally { _rwl.ReleaseWriterLock(); } } else if (settings.StorageType == ChartHttpHandlerStorageType.File) { File.Delete(settings.Directory + key); } else if (settings.StorageType == ChartHttpHandlerStorageType.Session) { HttpContext.Current.Session.Remove(GetSessionImageKey(key)); } else this.NotSupportedStorageType(settings); } /// /// Checks for existence the specified key. /// /// The key. /// bool IChartStorageHandler.Exists(String key) { ChartHttpHandlerSettings settings = ChartHttpHandler.Settings; if (settings.StorageType == ChartHttpHandlerStorageType.InProcess) { _rwl.AcquireReaderLock(accessTimeout); try { return _storageData.Contains(key); } finally { _rwl.ReleaseReaderLock(); } } else if (settings.StorageType == ChartHttpHandlerStorageType.File) { return File.Exists(settings.Directory + key); } else if (settings.StorageType == ChartHttpHandlerStorageType.Session) { return HttpContext.Current.Session[GetSessionImageKey(key)] is Byte[]; } else this.NotSupportedStorageType(settings); return false; } #endregion } #endregion //DefaultImageHandler Class #region RingTimeTracker class /// /// RingItem contains time span of creation timedate and index for key generation. /// internal class RingItem { internal Int32 Index; internal DateTime Created = DateTime.Now; internal string SessionID = String.Empty; internal bool InUse; /// /// Initializes a new instance of the class. /// /// The index. internal RingItem( int index) { this.Index = index; } } /// /// RingTimeTracker is a helper class for generating keys and tracking RingItem. /// Contains linked list queue and tracks exprired items. /// internal class RingTimeTracker { #region Fields // the item life span private TimeSpan _itemLifeTime = TimeSpan.FromSeconds(360); // last requested RingItem private LinkedListNode _current; // default key format to format names private String _keyFormat = String.Empty; // LinkedList with ring items private LinkedList _list = new LinkedList(); // Record session ID private bool _recordSessionID = false; #endregion //Fields #region Constructors /// /// Initializes a new instance of the class. /// /// The item life time. /// The key format. /// if set to true the session ID will be recorded. internal RingTimeTracker(TimeSpan itemLifeTime, String keyFormat, bool recordSessionID) { System.Diagnostics.Debug.Assert(!String.IsNullOrEmpty(keyFormat)); this._itemLifeTime = itemLifeTime; this._keyFormat = keyFormat; this._list.AddLast(new RingItem(_list.Count)); this._current = this._list.First; this._current.Value.Created = DateTime.Now - this._itemLifeTime - TimeSpan.FromSeconds(1); this._recordSessionID = recordSessionID; } #endregion //Constructors #region Methods /// /// Determines whether the specified item is expired. /// /// The item. /// The now. /// /// true if the specified item is expired; otherwise, false. /// internal bool IsExpired(RingItem item, DateTime now) { TimeSpan elapsed = (now - item.Created); return elapsed > this._itemLifeTime; } /// /// Gets the next key. /// /// internal String GetNextKey() { DateTime now = DateTime.Now; lock (this) { if ( !this.IsExpired(this._current.Value, now)) { if (this._current.Next == null) { if (!this.IsExpired(this._list.First.Value, now)) { this._list.AddLast(new RingItem(_list.Count)); this._current = this._list.Last; } else { this._current = this._list.First; } } else { if (!this.IsExpired(this._current.Next.Value, now)) { this._list.AddAfter(this._current, new RingItem(_list.Count)); } this._current = this._current.Next; } } this._current.Value.Created = now; if (this._recordSessionID) { this._current.Value.SessionID = ChartHttpHandler.Settings.ReadSessionKey(); this._current.Value.InUse = true; } return this.GetCurrentKey(); } } /// /// Gets the current key. /// /// internal String GetCurrentKey() { return String.Format( CultureInfo.InvariantCulture, this._keyFormat, this._current.Value.Index); } /// /// Gets the key. /// /// The ring item. /// internal String GetKey(RingItem ringItem) { return String.Format(CultureInfo.InvariantCulture, this._keyFormat, ringItem.Index); } /// /// Do Action for each item. /// /// if set to true do action for only expired items. /// The action. public void ForEach(bool onlyExpired, Action action) { if (action == null) { throw new ArgumentNullException("action"); } DateTime now = DateTime.Now; lock (this) { foreach (RingItem item in this._list) { if (onlyExpired) { if (this.IsExpired(item, now)) { action(item); } } else { action(item); } } } } #endregion //Methods } #endregion //RingTracker class #region RingTimeTrackerFactory Class /// /// RingTimeTrackerFactory contains static list of RingTimeTracker for each key formats /// internal static class RingTimeTrackerFactory { private static ListDictionary _ringTrackers = new ListDictionary(); private static Object _lockObject = new Object(); /// /// Gets the ring tracker by specified key format. /// /// The key format. /// internal static RingTimeTracker GetRingTracker(String keyFormat) { if (_ringTrackers.Contains(keyFormat)) { return (RingTimeTracker)_ringTrackers[keyFormat]; } lock (_lockObject) { if (_ringTrackers.Contains(keyFormat)) { return (RingTimeTracker)_ringTrackers[keyFormat]; } RingTimeTracker result = new RingTimeTracker(ChartHttpHandler.Settings.Timeout, keyFormat,ChartHttpHandler.Settings.StorageType == ChartHttpHandlerStorageType.Session); _ringTrackers.Add(keyFormat, result); return result; } } internal static IList OpenedRingTimeTrackers() { lock (_lockObject) { return new ArrayList(_ringTrackers.Values); } } } #endregion //RingTimeTrackerFactory Class #region Diagnostics class /// /// Contains helpres methods for diagnostics. /// internal static class Diagnostics { /// /// Trace category /// const string ChartCategory = "chart.handler"; /// /// Name of context item which contain the current trace item /// const string ContextID = "Trace-{89FA5660-BD13-4f1b-8C7C-355CEC92CC7E}"; /// /// Used for syncronizing. /// static object _lockObject = new object(); /// /// Limit of trace messages in the history. /// const int MessageLimit = 20; /// /// Collection of request messages. /// static List _messages = new List(MessageLimit); /// /// Contains request info /// public class HandlerPageTraceInfo { /// /// Events collection in this request. /// private List _events = new List(); /// /// Initializes a new instance of the class. /// public HandlerPageTraceInfo() { if (HttpContext.Current != null) { DateStamp = DateTime.Now; if (HttpContext.Current.Request != null) { Url = HttpContext.Current.Request.Url.ToString(); Verb = HttpContext.Current.Request.HttpMethod; } } } /// /// Gets or sets the date stamp. /// /// The date stamp. public DateTime DateStamp { get; private set; } /// /// Gets or sets the URL. /// /// The URL. public string Url { get; private set; } /// /// Gets or sets the verb. /// /// The verb. public string Verb { get; private set; } /// /// Gets the events. /// /// The events. public IList Events { get { return _events.AsReadOnly(); } } /// /// Adds a trace info item. /// /// The message. /// The error info. internal void AddTraceInfo(string message, string errorInfo) { lock (_events) { _events.Add(new ChartHandlerEvents() { Message = message, ErrorInfo = errorInfo } ); } } } /// /// Contains an event in particural request. /// public class ChartHandlerEvents { /// /// Gets or sets the message. /// /// The message. public string Message { get; set; } /// /// Gets or sets the error info. /// /// The error info. public string ErrorInfo { get; set; } /// /// Gets the text. /// /// The text. public string Text { get { return Message + ErrorInfo; } } } /// /// Writes message in the trace. /// /// The message. /// The error info. internal static void TraceWrite( string message, Exception errorInfo) { if (IsTraceEnabled) { HttpContext.Current.Trace.Write(ChartCategory, message, errorInfo); if (CurrentTraceInfo != null) { CurrentTraceInfo.AddTraceInfo(message, errorInfo != null ? errorInfo.ToString() : String.Empty); } } } /// /// Gets the current trace info. /// /// The current trace info. private static HandlerPageTraceInfo CurrentTraceInfo { get { lock (_lockObject) { if (HttpContext.Current != null) { if (HttpContext.Current.Items[Diagnostics.ContextID] == null) { HandlerPageTraceInfo pageTrace = new HandlerPageTraceInfo(); _messages.Add(pageTrace); if (_messages.Count > MessageLimit) { _messages.RemoveRange(0, _messages.Count - MessageLimit); } HttpContext.Current.Items[Diagnostics.ContextID] = pageTrace; } return (HandlerPageTraceInfo)HttpContext.Current.Items[Diagnostics.ContextID]; } } return null; } } /// /// Gets a value indicating whether this instance is trace enabled. /// /// /// true if this instance is trace enabled; otherwise, false. /// internal static bool IsTraceEnabled { get { return HttpContext.Current != null && HttpContext.Current.Trace.IsEnabled; } } /// /// Gets the messages collection. /// /// The messages. internal static ReadOnlyCollection Messages { get { List result; lock (_lockObject) { result = new List(_messages); } return result.AsReadOnly(); } } } #endregion //Diagnostics class }