private async Task ProcessImageAsync(HttpContext context) { HttpRequest request = context.Request; // Fixes issue 10. bool isRemote = request.Path.EndsWith(remotePrefix, StringComparison.OrdinalIgnoreCase); string requestPath = string.Empty; string queryString = string.Empty; bool validExtensionLessUrl = false; string urlParameters = ""; string extensionLessExtension = ""; if (isRemote) { // 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.Count() > 2) { queryString = paths[2]; urlParameters = paths[1]; } else if (paths.Length > 1) { queryString = paths[1]; } validExtensionLessUrl = RemoteFile.RemoteFileWhiteListExtensions.Any( x => x.ExtensionLess && requestPath.StartsWith(x.Url.AbsoluteUri)); if (validExtensionLessUrl) { extensionLessExtension = RemoteFile.RemoteFileWhiteListExtensions.First( x => x.ExtensionLess && requestPath.StartsWith(x.Url.AbsoluteUri)).ImageFormat; } } } else { requestPath = HostingEnvironment.MapPath(request.Path); queryString = HttpUtility.UrlDecode(request.QueryString.ToString()); } // Only process requests that pass our sanitizing filter. if ((ImageHelpers.IsValidImageExtension(requestPath) || validExtensionLessUrl) && !string.IsNullOrWhiteSpace(queryString)) { // Replace any presets in the querystring with the actual value. queryString = this.ReplacePresetsInQueryString(queryString); string fullPath = string.Format("{0}?{1}", requestPath, queryString); string imageName = Path.GetFileName(requestPath); if (validExtensionLessUrl && !string.IsNullOrWhiteSpace(extensionLessExtension)) { fullPath = requestPath; if (!string.IsNullOrWhiteSpace(urlParameters)) { string hashedUrlParameters = urlParameters.ToMD5Fingerprint(); // TODO: Add hash for querystring parameters. imageName += hashedUrlParameters; fullPath += hashedUrlParameters; } imageName += "." + extensionLessExtension; fullPath += extensionLessExtension + "?" + queryString; } // Create a new cache to help process and cache the request. DiskCache cache = new DiskCache(request, requestPath, fullPath, imageName, isRemote); // Since we are now rewriting the path we need to check again that the current user has access // to the rewritten path. // Get the user for the current request // If the user is anonymous or authentication doesn't work for this suffix avoid a NullReferenceException // in the UrlAuthorizationModule by creating a generic identity. string virtualCachedPath = cache.GetVirtualCachedPath(); IPrincipal user = context.User ?? new GenericPrincipal(new GenericIdentity(string.Empty, string.Empty), new string[0]); // Do we have permission to call UrlAuthorizationModule.CheckUrlAccessForPrincipal? PermissionSet permission = new PermissionSet(PermissionState.None); permission.AddPermission(new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted)); bool hasPermission = permission.IsSubsetOf(AppDomain.CurrentDomain.PermissionSet); bool isAllowed = true; // Run the rewritten path past the auth system again, using the result as the default "AllowAccess" value if (hasPermission && !context.SkipAuthorization) { isAllowed = UrlAuthorizationModule.CheckUrlAccessForPrincipal(virtualCachedPath, user, "GET"); } if (isAllowed) { // Is the file new or updated? bool isNewOrUpdated = await cache.IsNewOrUpdatedFileAsync(); // Only process if the file has been updated. if (isNewOrUpdated) { string cachedPath = cache.CachedPath; // Process the image. using (ImageFactory imageFactory = new ImageFactory()) { if (isRemote) { Uri uri = new Uri(requestPath + "?" + urlParameters); RemoteFile remoteFile = new RemoteFile(uri, false); // Prevent response blocking. WebResponse webResponse = await remoteFile.GetWebResponseAsync().ConfigureAwait(false); SemaphoreSlim semaphore = GetSemaphoreSlim(cachedPath); try { semaphore.Wait(); using (MemoryStream memoryStream = new MemoryStream()) { using (WebResponse response = webResponse) { using (Stream responseStream = response.GetResponseStream()) { if (responseStream != null) { responseStream.CopyTo(memoryStream); // Process the Image imageFactory.Load(memoryStream) .AddQueryString(queryString) .AutoProcess() .Save(cachedPath); // Store the response type in the context for later retrieval. context.Items[CachedResponseTypeKey] = imageFactory.MimeType; // Ensure that the LastWriteTime property of the source and cached file match. Tuple <DateTime, DateTime> creationAndLastWriteDateTimes = await cache.SetCachedLastWriteTimeAsync(); // Add to the cache. cache.AddImageToCache(creationAndLastWriteDateTimes); // Trim the cache. await cache.TrimCachedFolderAsync(cachedPath); } } } } } finally { semaphore.Release(); } } else { // Check to see if the file exists. // ReSharper disable once AssignNullToNotNullAttribute FileInfo fileInfo = new FileInfo(requestPath); if (!fileInfo.Exists) { throw new HttpException(404, "No image exists at " + fullPath); } SemaphoreSlim semaphore = GetSemaphoreSlim(cachedPath); try { semaphore.Wait(); // Process the Image imageFactory.Load(fullPath).AutoProcess().Save(cachedPath); // Store the response type in the context for later retrieval. context.Items[CachedResponseTypeKey] = imageFactory.MimeType; // Ensure that the LastWriteTime property of the source and cached file match. Tuple <DateTime, DateTime> creationAndLastWriteDateTimes = await cache.SetCachedLastWriteTimeAsync(); // Add to the cache. cache.AddImageToCache(creationAndLastWriteDateTimes); // Trim the cache. await cache.TrimCachedFolderAsync(cachedPath); } finally { semaphore.Release(); } } } } string incomingEtag = context.Request.Headers["If-None-Match"]; if (incomingEtag != null && !isNewOrUpdated) { // Explicitly set the Content-Length header so the client doesn't wait for // content but keeps the connection open for other requests context.Response.AddHeader("Content-Length", "0"); context.Response.StatusCode = (int)HttpStatusCode.NotModified; context.Response.SuppressContent = true; context.Response.AddFileDependency(context.Server.MapPath(cache.GetVirtualCachedPath())); this.SetHeaders(context, (string)context.Items[CachedResponseTypeKey]); if (!isRemote) { return; } } string virtualPath = cache.GetVirtualCachedPath(); // The cached file is valid so just rewrite the path. context.RewritePath(virtualPath, false); } else { throw new HttpException(403, "Access denied"); } } else if (isRemote) { // Just repoint to the external url. HttpContext.Current.Response.Redirect(requestPath); } }
/// <summary> /// Processes the image. /// </summary> /// <param name="context"> /// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that provides /// references to the intrinsic server objects /// </param> /// <returns> /// The <see cref="T:System.Threading.Tasks.Task"/>. /// </returns> 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; 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 { requestPath = 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; 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. DiskCache cache = new DiskCache(requestPath, fullPath, queryString); string cachedPath = cache.CachedPath; // Since we are now rewriting the path we need to check again that the current user has access // to the rewritten path. // Get the user for the current request // If the user is anonymous or authentication doesn't work for this suffix avoid a NullReferenceException // in the UrlAuthorizationModule by creating a generic identity. string virtualCachedPath = cache.VirtualCachedPath; IPrincipal user = context.User ?? new GenericPrincipal(new GenericIdentity(string.Empty, string.Empty), new string[0]); // Do we have permission to call UrlAuthorizationModule.CheckUrlAccessForPrincipal? PermissionSet permission = new PermissionSet(PermissionState.None); permission.AddPermission(new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted)); bool hasPermission = permission.IsSubsetOf(AppDomain.CurrentDomain.PermissionSet); bool isAllowed = true; // Run the rewritten path past the authorization system again. // We can then use the result as the default "AllowAccess" value if (hasPermission && !context.SkipAuthorization) { isAllowed = UrlAuthorizationModule.CheckUrlAccessForPrincipal(virtualCachedPath, user, "GET"); } if (isAllowed) { // Is the file new or updated? bool isNewOrUpdated = cache.IsNewOrUpdatedFile(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 memoryStream = new MemoryStream(imageBuffer)) { // Reset the position of the stream to ensure we're reading the correct part. memoryStream.Position = 0; // Process the Image imageFactory.Load(memoryStream).AutoProcess(queryString).Save(cachedPath); // Add to the cache. cache.AddImageToCache(cachedPath); // 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; context.Items[CachedResponseFileDependency] = new List <string> { cachedPath }; } } } } // Image is from the cache so the mime-type will need to be set. if (context.Items[CachedResponseTypeKey] == null) { string mimetype = ImageHelpers.GetMimeType(cachedPath); if (!string.IsNullOrEmpty(mimetype)) { context.Items[CachedResponseTypeKey] = mimetype; } } if (context.Items[CachedResponseFileDependency] == null) { if (isFileLocal) { // Some services might only provide filename so we can't monitor for the browser. context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? new List <string> { cachedPath } : new List <string> { requestPath, cachedPath }; } else { context.Items[CachedResponseFileDependency] = new List <string> { cachedPath }; } } // The cached file is valid so just rewrite the path. context.RewritePath(virtualCachedPath, false); } else { throw new HttpException(403, "Access denied"); } } }