// ----------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // ----------------------------------------------------------------------- namespace ImageProcessor.Web.HttpModules { #region Using using System; using System.IO; using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using ImageProcessor.Helpers.Extensions; using ImageProcessor.Imaging; using ImageProcessor.Web.Caching; using ImageProcessor.Web.Config; using ImageProcessor.Web.Helpers; #endregion /// /// 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 value to prefix any remote image requests with to ensure they get captured. /// private static readonly string RemotePrefix = ImageProcessorConfig.Instance.RemotePrefix; /// /// The assembly version. /// private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); /// /// The value that acts as a basis to check that the startup code has only been ran once. /// private static int initCheck; /// /// A value indicating whether the application has started. /// private readonly bool hasModuleInitialized = initCheck == 1; #endregion #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 (!this.hasModuleInitialized) { Interlocked.CompareExchange(ref initCheck, 1, 0); DiskCache.CreateDirectories(); } context.BeginRequest += this.ContextBeginRequest; context.PreSendRequestHeaders += this.ContextPreSendRequestHeaders; } /// /// Disposes of the resources (other than memory) used by the module that implements . /// public void Dispose() { // Nothing to dispose. } #endregion /// /// Occurs as the first event in the HTTP pipeline chain of execution when ASP.NET responds to a request. /// /// The source of the event. /// An EventArgs that contains the event data. private async void ContextBeginRequest(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; await this.ProcessImageAsync(context); } /// /// 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]; if (responseTypeObject != null) { string responseType = (string)responseTypeObject; // Set the headers this.SetHeaders(context, responseType); context.Items[CachedResponseTypeKey] = null; } } #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; bool isRemote = request.Path.Equals(RemotePrefix, StringComparison.OrdinalIgnoreCase); string requestPath = string.Empty; string queryString = string.Empty; if (isRemote) { // We need to split the querystring to get the actual values we want. string urlDecode = HttpUtility.UrlDecode(request.QueryString.ToString()); if (urlDecode != null) { string[] paths = urlDecode.Split('?'); requestPath = paths[0]; if (paths.Length > 1) { queryString = paths[1]; } } } else { requestPath = HostingEnvironment.MapPath(request.Path); queryString = HttpUtility.UrlDecode(request.QueryString.ToString()); } // Only process requests that pass our sanitizing filter. if (ImageUtils.IsValidImageExtension(requestPath) && !string.IsNullOrWhiteSpace(queryString)) { string fullPath = string.Format("{0}?{1}", requestPath, queryString); string imageName = Path.GetFileName(requestPath); // Create a new cache to help process and cache the request. DiskCache cache = new DiskCache(request, requestPath, fullPath, imageName, isRemote); // Is the file new or updated? bool isNewOrUpdated = await cache.IsNewOrUpdatedFileAsync(); // Only process if the file has been updated. if (isNewOrUpdated) { // Process the image. using (ImageFactory imageFactory = new ImageFactory()) { if (isRemote) { Uri uri = new Uri(requestPath); RemoteFile remoteFile = new RemoteFile(uri, false); // Prevent response blocking. WebResponse webResponse = await remoteFile.GetWebResponseAsync().ConfigureAwait(false); using (MemoryStream memoryStream = new MemoryStream()) { using (WebResponse response = webResponse) { using (Stream responseStream = response.GetResponseStream()) { if (responseStream != null) { // Trim the cache. await cache.TrimCachedFoldersAsync(); responseStream.CopyTo(memoryStream); imageFactory.Load(memoryStream) .AddQueryString(queryString) .Format(ImageUtils.GetImageFormat(imageName)) .AutoProcess().Save(cache.CachedPath); // Ensure that the LastWriteTime property of the source and cached file match. DateTime dateTime = await cache.SetCachedLastWriteTimeAsync(); // Add to the cache. await cache.AddImageToCacheAsync(dateTime); } } } } } else { // Trim the cache. await cache.TrimCachedFoldersAsync(); imageFactory.Load(fullPath).AutoProcess().Save(cache.CachedPath); // Ensure that the LastWriteTime property of the source and cached file match. DateTime dateTime = await cache.SetCachedLastWriteTimeAsync(); // Add to the cache. await cache.AddImageToCacheAsync(dateTime); } } } // Store the response type in the context for later retrieval. context.Items[CachedResponseTypeKey] = ImageUtils.GetResponseType(imageName).ToDescription(); // The cached file is valid so just rewrite the path. context.RewritePath(cache.GetVirtualCachedPath(), false); } } /// /// This will make the browser and server keep the output /// in its cache and thereby improve performance. /// See http://en.wikipedia.org/wiki/HTTP_ETag /// /// /// the HttpContext object that provides /// references to the intrinsic server objects /// /// The HTTP MIME type to to send. private void SetHeaders(HttpContext context, string responseType) { HttpResponse response = context.Response; response.ContentType = responseType; response.AddHeader("Image-Served-By", "ImageProcessor.Web/" + AssemblyVersion); HttpCachePolicy cache = response.Cache; cache.VaryByHeaders["Accept-Encoding"] = true; int maxDays = DiskCache.MaxFileCachedDuration; cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(maxDays)); cache.SetMaxAge(new TimeSpan(maxDays, 0, 0, 0)); cache.SetRevalidation(HttpCacheRevalidation.AllCaches); string incomingEtag = context.Request.Headers["If-None-Match"]; cache.SetCacheability(HttpCacheability.Public); if (incomingEtag == null) { return; } response.Clear(); response.StatusCode = (int)HttpStatusCode.NotModified; response.SuppressContent = true; } #endregion } }