// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Web.HttpModules { using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using ImageProcessor.Web.Caching; using ImageProcessor.Web.Configuration; using ImageProcessor.Web.Extensions; using ImageProcessor.Web.Helpers; using ImageProcessor.Web.Services; /// /// Processes any image requests within the web application. /// public sealed class ImageProcessingModule : IHttpModule { #region Fields /// /// The key for storing the response type of the current image. /// private const string CachedResponseTypeKey = "CACHED_IMAGE_RESPONSE_TYPE_054F217C-11CF-49FF-8D2F-698E8E6EB58F"; /// /// The key for storing the cached path of the current image. /// private const string CachedPathKey = "CACHED_IMAGE_PATH_TYPE_E0741478-C17B-433D-96A8-6CDA797644E9"; /// /// The key for storing the file dependency of the current image. /// private const string CachedResponseFileDependency = "CACHED_IMAGE_DEPENDENCY_054F217C-11CF-49FF-8D2F-698E8E6EB58F"; /// /// The regular expression to search strings for. /// private static readonly Regex PresetRegex = new Regex(@"preset=[^&]+", RegexOptions.Compiled); /// /// The assembly version. /// private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); /// /// Whether to preserve exif meta data. /// private static bool? preserveExifMetaData; /// /// The locker for preventing duplicate requests. /// private readonly AsyncDuplicateLock locker = new AsyncDuplicateLock(); /// /// A value indicating whether this instance of the given entity has been disposed. /// /// if this instance has been disposed; otherwise, . /// /// If the entity is disposed, it must not be disposed a second /// time. The isDisposed field is set the first time the entity /// is disposed. If the isDisposed field is true, then the Dispose() /// method will not dispose again. This help not to prolong the entity's /// life in the Garbage Collector. /// private bool isDisposed; /// /// The image cache. /// private IImageCache imageCache; #endregion #region Destructors /// /// Finalizes an instance of the class. /// /// /// Use C# destructor syntax for finalization code. /// This destructor will run only if the Dispose method /// does not get called. /// It gives your base class the opportunity to finalize. /// Do not provide destructors in types derived from this class. /// ~ImageProcessingModule() { // Do not re-create Dispose clean-up code here. // Calling Dispose(false) is optimal in terms of // readability and maintainability. this.Dispose(false); } #endregion /// /// The process querystring event handler. /// /// /// The sender. /// /// /// The . /// /// Returns the processed querystring. public delegate string ProcessQuerystringEventHandler(object sender, ProcessQueryStringEventArgs e); /// /// The event that is called when a new image is processed. /// public static event EventHandler OnPostProcessing; /// /// The event that is called when a querystring is processed. /// public static event ProcessQuerystringEventHandler OnProcessQuerystring; #region IHttpModule Members /// /// Initializes a module and prepares it to handle requests. /// /// /// An that provides /// access to the methods, properties, and events common to all /// application objects within an ASP.NET application /// public void Init(HttpApplication context) { if (preserveExifMetaData == null) { preserveExifMetaData = ImageProcessorConfiguration.Instance.PreserveExifMetaData; } EventHandlerTaskAsyncHelper postAuthorizeHelper = new EventHandlerTaskAsyncHelper(this.PostAuthorizeRequest); context.AddOnPostAuthorizeRequestAsync(postAuthorizeHelper.BeginEventHandler, postAuthorizeHelper.EndEventHandler); EventHandlerTaskAsyncHelper postProcessHelper = new EventHandlerTaskAsyncHelper(this.PostProcessImage); context.AddOnEndRequestAsync(postProcessHelper.BeginEventHandler, postProcessHelper.EndEventHandler); context.PreSendRequestHeaders += this.ContextPreSendRequestHeaders; } /// /// Disposes of the resources (other than memory) used by the module that implements . /// public void Dispose() { this.Dispose(true); // This object will be cleaned up by the Dispose method. // Therefore, you should call GC.SuppressFinalize to // take this object off the finalization queue // and prevent finalization code for this object // from executing a second time. GC.SuppressFinalize(this); } /// /// Disposes the object and frees resources for the Garbage Collector. /// /// /// If true, the object gets disposed. /// private void Dispose(bool disposing) { if (this.isDisposed) { return; } if (disposing) { // Dispose of any managed resources here. } // Call the appropriate methods to clean up // unmanaged resources here. // Note disposing is done. this.isDisposed = true; } #endregion /// /// Occurs when the user for the current request has been authorized. /// /// /// The source of the event. /// /// /// An EventArgs that contains the event data. /// /// /// The . /// private Task PostAuthorizeRequest(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; return this.ProcessImageAsync(context); } /// /// Occurs when the ASP.NET event handler finishes execution. /// /// /// The source of the event. /// /// /// An EventArgs that contains the event data. /// /// /// The . /// private async Task PostProcessImage(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; object cachedPathObject = context.Items[CachedPathKey]; if (cachedPathObject != null) { // Trim the cache. await this.imageCache.TrimCacheAsync(); string cachedPath = cachedPathObject.ToString(); // Fire the post processing event. EventHandler handler = OnPostProcessing; if (handler != null) { context.Items[CachedPathKey] = null; await Task.Run(() => handler(this, new PostProcessingEventArgs { CachedImagePath = cachedPath })); } } } /// /// Occurs just before ASP.NET send HttpHeaders to the client. /// /// /// The source of the event. /// /// /// An EventArgs that contains the event data. /// private void ContextPreSendRequestHeaders(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; object responseTypeObject = context.Items[CachedResponseTypeKey]; object dependencyFileObject = context.Items[CachedResponseFileDependency]; string responseType = responseTypeObject as string; List dependencyFiles = dependencyFileObject as List; // Set the headers this.SetHeaders(context, responseType, dependencyFiles); } #region Private /// /// Processes the image. /// /// /// the HttpContext object that provides /// references to the intrinsic server objects /// /// /// The . /// private async Task ProcessImageAsync(HttpContext context) { HttpRequest request = context.Request; // Should we ignore this request? if (request.RawUrl.ToUpperInvariant().Contains("IPIGNORE=TRUE")) { return; } IImageService currentService = this.GetImageServiceForRequest(request); if (currentService != null) { bool isFileLocal = currentService.IsFileLocalService; bool hasMultiParams = request.Url.ToString().Count(f => f == '?') > 1; string requestPath = string.Empty; string queryString = string.Empty; string urlParameters = string.Empty; // Legacy support. I'd like to remove this asap. if (hasMultiParams) { // We need to split the querystring to get the actual values we want. string urlDecode = HttpUtility.UrlDecode(request.QueryString.ToString()); if (!string.IsNullOrWhiteSpace(urlDecode)) { // UrlDecode seems to mess up in some circumstance. if (urlDecode.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1) { urlDecode = urlDecode.Replace(":/", "://"); } string[] paths = urlDecode.Split('?'); requestPath = paths[0]; // Handle extension-less urls. if (paths.Length > 2) { queryString = paths[2]; urlParameters = paths[1]; } else if (paths.Length > 1) { queryString = paths[1]; } } } else { if (string.IsNullOrWhiteSpace(currentService.Prefix)) { requestPath = HostingEnvironment.MapPath(request.Path); queryString = HttpUtility.UrlDecode(request.QueryString.ToString()); } else { // Parse any protocol values from settings. string protocol = currentService.Settings.ContainsKey("Protocol") ? currentService.Settings["Protocol"] + "://" : string.Empty; requestPath = protocol + request.Path.Replace(currentService.Prefix, string.Empty).TrimStart('/'); queryString = HttpUtility.UrlDecode(request.QueryString.ToString()); } } // Replace any presets in the querystring with the actual value. queryString = this.ReplacePresetsInQueryString(queryString); // Execute the handler which can change the querystring queryString = this.CheckQuerystringHandler(queryString, request.RawUrl); // If the current service doesn't require a prefix, don't fetch it. // Let the static file handler take over. if (string.IsNullOrWhiteSpace(currentService.Prefix) && string.IsNullOrWhiteSpace(queryString)) { return; } string parts = !string.IsNullOrWhiteSpace(urlParameters) ? "?" + urlParameters : string.Empty; string fullPath = string.Format("{0}{1}?{2}", requestPath, parts, queryString); object resourcePath; // More legacy support code. if (hasMultiParams) { resourcePath = string.IsNullOrWhiteSpace(urlParameters) ? new Uri(requestPath, UriKind.RelativeOrAbsolute) : new Uri(requestPath + "?" + urlParameters, UriKind.RelativeOrAbsolute); } else { resourcePath = requestPath; } // Check whether the path is valid for other requests. if (resourcePath == null || !currentService.IsValidRequest(resourcePath.ToString())) { return; } // Create a new cache to help process and cache the request. this.imageCache = (IImageCache)ImageProcessorConfiguration.Instance .ImageCache.GetInstance(requestPath, fullPath, queryString); // Is the file new or updated? bool isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync(); string cachedPath = this.imageCache.CachedPath; // Only process if the file has been updated. if (isNewOrUpdated) { // Process the image. using (ImageFactory imageFactory = new ImageFactory(preserveExifMetaData != null && preserveExifMetaData.Value)) { using (await this.locker.LockAsync(cachedPath)) { byte[] imageBuffer = await currentService.GetImage(resourcePath); using (MemoryStream inStream = new MemoryStream(imageBuffer)) { // Process the Image using (MemoryStream outStream = new MemoryStream()) { imageFactory.Load(inStream).AutoProcess(queryString).Save(outStream); // Add to the cache. await this.imageCache.AddImageToCacheAsync(outStream, imageFactory.CurrentImageFormat.MimeType); } } // Store the cached path, response type, and cache dependency in the context for later retrieval. context.Items[CachedPathKey] = cachedPath; context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType; bool isFileCached = new Uri(cachedPath).IsFile; if (isFileLocal) { if (isFileCached) { // Some services might only provide filename so we can't monitor for the browser. context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? new List { cachedPath } : new List { requestPath, cachedPath }; } else { context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? null : new List { requestPath }; } } else if (isFileCached) { context.Items[CachedResponseFileDependency] = new List { cachedPath }; } } } } // The cached file is valid so just rewrite the path. this.imageCache.RewritePath(context); // Redirect if not a locally store file. if (!new Uri(cachedPath).IsFile) { context.ApplicationInstance.CompleteRequest(); } } } /// /// This will make the browser and server keep the output /// in its cache and thereby improve performance. /// /// /// the HttpContext object that provides /// references to the intrinsic server objects /// /// /// The HTTP MIME type to send. /// /// /// The dependency path for the cache dependency. /// private void SetHeaders(HttpContext context, string responseType, IEnumerable dependencyPaths) { if (this.imageCache != null) { HttpResponse response = context.Response; if (response.Headers["ImageProcessedBy"] == null) { response.AddHeader("ImageProcessedBy", "ImageProcessor.Web/" + AssemblyVersion); } HttpCachePolicy cache = response.Cache; cache.SetCacheability(HttpCacheability.Public); cache.VaryByHeaders["Accept-Encoding"] = true; if (!string.IsNullOrWhiteSpace(responseType)) { response.ContentType = responseType; } if (dependencyPaths != null) { context.Response.AddFileDependencies(dependencyPaths.ToArray()); cache.SetLastModifiedFromFileDependencies(); } int maxDays = this.imageCache.MaxDays; cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(maxDays)); cache.SetMaxAge(new TimeSpan(maxDays, 0, 0, 0)); cache.SetRevalidation(HttpCacheRevalidation.AllCaches); this.imageCache = null; } } /// /// Replaces preset values stored in the configuration in the querystring. /// /// /// The query string. /// /// /// The containing the updated querystring. /// private string ReplacePresetsInQueryString(string queryString) { if (!string.IsNullOrWhiteSpace(queryString)) { foreach (Match match in PresetRegex.Matches(queryString)) { if (match.Success) { string preset = match.Value.Split('=')[1]; // We use the processor config system to store the preset values. string replacements = ImageProcessorConfiguration.Instance.GetPresetSettings(preset); queryString = Regex.Replace(queryString, preset, replacements ?? string.Empty); } } } return queryString; } /// /// Checks if there is a handler that changes the querystring and executes that handler. /// /// /// The query string. /// /// /// The raw request url. /// /// /// The containing the updated querystring. /// private string CheckQuerystringHandler(string queryString, string rawUrl) { // Fire the process querystring event. ProcessQuerystringEventHandler handler = OnProcessQuerystring; if (handler != null) { ProcessQueryStringEventArgs args = new ProcessQueryStringEventArgs { Querystring = queryString ?? string.Empty, RawUrl = rawUrl ?? string.Empty }; queryString = handler(this, args); } return queryString; } /// /// Gets the correct for the given request. /// /// /// The current image request. /// /// /// The . /// private IImageService GetImageServiceForRequest(HttpRequest request) { IImageService imageService = null; IList services = ImageProcessorConfiguration.Instance.ImageServices; string path = request.Path.TrimStart('/'); foreach (IImageService service in services) { string key = service.Prefix; if (!string.IsNullOrWhiteSpace(key) && path.StartsWith(key, StringComparison.InvariantCultureIgnoreCase)) { imageService = service; } } if (imageService != null) { return imageService; } // Return the file based service return services.FirstOrDefault(s => string.IsNullOrWhiteSpace(s.Prefix) && s.IsValidRequest(path)); } #endregion } }