mirror of https://github.com/SixLabors/ImageSharp
7 changed files with 1060 additions and 193 deletions
@ -0,0 +1,64 @@ |
|||||
|
// License: CPOL at http://www.codeproject.com/info/cpol10.aspx
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Net; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace System.IO |
||||
|
{ |
||||
|
public static class AsyncIoExtensions |
||||
|
{ |
||||
|
public static Task<Stream> GetRequestStreamAsync(this WebRequest webRequest) |
||||
|
{ |
||||
|
return Task.Factory.FromAsync( |
||||
|
webRequest.BeginGetRequestStream, |
||||
|
ar => webRequest.EndGetRequestStream(ar), |
||||
|
null); |
||||
|
} |
||||
|
|
||||
|
public static Task<WebResponse> GetResponseAsync(this WebRequest webRequest) |
||||
|
{ |
||||
|
return Task.Factory.FromAsync( |
||||
|
webRequest.BeginGetResponse, |
||||
|
ar => webRequest.EndGetResponse(ar), |
||||
|
null); |
||||
|
} |
||||
|
|
||||
|
public static Task<int> ReadAsync(this Stream input, Byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
return Task.Factory.FromAsync( |
||||
|
input.BeginRead, |
||||
|
(Func<IAsyncResult, int>)input.EndRead, |
||||
|
buffer, offset, count, |
||||
|
null); |
||||
|
} |
||||
|
|
||||
|
public static Task WriteAsync(this Stream input, Byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
return Task.Factory.FromAsync( |
||||
|
input.BeginWrite, |
||||
|
input.EndWrite, |
||||
|
buffer, offset, count, |
||||
|
null); |
||||
|
} |
||||
|
|
||||
|
public static /*async*/ Task CopyToAsync(this Stream input, Stream output, CancellationToken cancellationToken = default(CancellationToken)) |
||||
|
{ |
||||
|
return CopyToAsyncTasks(input, output, cancellationToken).ToTask(); |
||||
|
} |
||||
|
private static IEnumerable<Task> CopyToAsyncTasks(Stream input, Stream output, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
byte[] buffer = new byte[0x1000]; // 4 KiB
|
||||
|
while (true) |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
var readTask = input.ReadAsync(buffer, 0, buffer.Length); |
||||
|
yield return readTask; |
||||
|
if (readTask.Result == 0) break; |
||||
|
|
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
yield return output.WriteAsync(buffer, 0, readTask.Result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,347 @@ |
|||||
|
// -----------------------------------------------------------------------
|
||||
|
// <copyright file="RemoteFile.cs" company="James South">
|
||||
|
// Copyright (c) James South.
|
||||
|
// Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
|
// </copyright>
|
||||
|
// -----------------------------------------------------------------------
|
||||
|
|
||||
|
namespace ImageProcessor.Web.Helpers |
||||
|
{ |
||||
|
#region Using
|
||||
|
using System; |
||||
|
using System.Diagnostics.Contracts; |
||||
|
using System.Globalization; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Net; |
||||
|
using System.Security; |
||||
|
using System.Text; |
||||
|
using ImageProcessor.Web.Config; |
||||
|
#endregion
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encapsulates methods used to download files from a website address.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// <para>
|
||||
|
/// The purpose of this class is so there's one core way of downloading remote files with url[s] that are from
|
||||
|
/// outside users. There's various areas in application where an attacker could supply an external url to the server
|
||||
|
/// and tie up resources.
|
||||
|
/// </para>
|
||||
|
/// For example, the ImageProcessingModule accepts off-server addresses as a path. An attacker could, for instance, pass the url
|
||||
|
/// to a file that's a few gigs in size, causing the server to get out-of-memory exceptions or some other errors. An attacker
|
||||
|
/// could also use this same method to use one application instance to hammer another site by, again, passing an off-server
|
||||
|
/// address of the victims site to the ImageProcessingModule.
|
||||
|
/// This class will not throw an exception if the Uri supplied points to a resource local to the running application instance.
|
||||
|
/// <para>
|
||||
|
/// There shouldn't be any security issues there, as the internal WebRequest instance is still calling it remotely.
|
||||
|
/// Any local files that shouldn't be accessed by this won't be allowed by the remote call.
|
||||
|
/// </para>
|
||||
|
/// Adapted from <see cref="http://blogengine.codeplex.com">BlogEngine.Net</see>
|
||||
|
/// </remarks>
|
||||
|
internal sealed class RemoteFile |
||||
|
{ |
||||
|
#region Fields
|
||||
|
/// <summary>
|
||||
|
/// The white-list of url[s] from which to download remote files.
|
||||
|
/// </summary>
|
||||
|
private static readonly Uri[] RemoteFileWhiteList = ImageProcessorConfig.Instance.RemoteFileWhiteList; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The length of time, in milliseconds, that a remote file download attempt can last before timing out.
|
||||
|
/// </summary>
|
||||
|
private static readonly int TimeoutMilliseconds = ImageProcessorConfig.Instance.Timeout; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum size, in bytes, that a remote file download attempt can download.
|
||||
|
/// </summary>
|
||||
|
private static readonly int MaxBytes = ImageProcessorConfig.Instance.MaxBytes; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Whether to allow remote downloads.
|
||||
|
/// </summary>
|
||||
|
private static readonly bool AllowRemoteDownloads = ImageProcessorConfig.Instance.AllowRemoteDownloads; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Whether this RemoteFile instance is ignoring remote download rules set in the current application
|
||||
|
/// instance.
|
||||
|
/// </summary>
|
||||
|
private readonly bool ignoreRemoteDownloadSettings; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The <see cref="T:System.Uri">Uri</see> of the remote file being downloaded.
|
||||
|
/// </summary>
|
||||
|
private readonly Uri url; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum allowable download size in bytes.
|
||||
|
/// </summary>
|
||||
|
private readonly int maxDownloadSize; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The length of time, in milliseconds, that a remote file download attempt can last before timing out.
|
||||
|
/// </summary>
|
||||
|
private int timeoutLength; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The <see cref="T:System.Net.WebResponse">WebResponse</see> object used internally for this RemoteFile instance.
|
||||
|
/// </summary>
|
||||
|
private WebRequest webRequest; |
||||
|
#endregion
|
||||
|
|
||||
|
#region Constructors
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="T:ImageProcessor.Web.Helpers.RemoteFile">RemoteFile</see> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="filePath">The url of the file to be downloaded.</param>
|
||||
|
/// <param name="ignoreRemoteDownloadSettings">
|
||||
|
/// If set to <see langword="true"/>, then RemoteFile should ignore the current the applications instance's remote download settings; otherwise,<see langword="false"/>.
|
||||
|
/// </param>
|
||||
|
internal RemoteFile(Uri filePath, bool ignoreRemoteDownloadSettings) |
||||
|
{ |
||||
|
Contract.Requires(filePath != null); |
||||
|
|
||||
|
this.url = filePath; |
||||
|
this.ignoreRemoteDownloadSettings = ignoreRemoteDownloadSettings; |
||||
|
this.timeoutLength = TimeoutMilliseconds; |
||||
|
this.maxDownloadSize = MaxBytes; |
||||
|
} |
||||
|
#endregion
|
||||
|
|
||||
|
#region Properties
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether this RemoteFile instance is ignoring remote download rules set in the
|
||||
|
/// current application instance.
|
||||
|
/// <remarks>
|
||||
|
/// This should only be set to true if the supplied url is a verified resource. Use at your own risk.
|
||||
|
/// </remarks>
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// <see langword="true"/> if this RemoteFile instance is ignoring remote download rules set in the current
|
||||
|
/// application instance; otherwise, <see langword="false"/>.
|
||||
|
/// </value>
|
||||
|
public bool IgnoreRemoteDownloadSettings |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return this.ignoreRemoteDownloadSettings; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Uri of the remote file being downloaded.
|
||||
|
/// </summary>
|
||||
|
public Uri Uri |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return this.url; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the length of time, in milliseconds, that a remote file download attempt can
|
||||
|
/// last before timing out.
|
||||
|
/// <remarks>
|
||||
|
/// <para>
|
||||
|
/// This value can only be set if the instance is supposed to ignore the remote download settings set
|
||||
|
/// in the current application instance.
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// Set this value to 0 if there should be no timeout.
|
||||
|
/// </para>
|
||||
|
/// </remarks>
|
||||
|
/// </summary>
|
||||
|
public int TimeoutLength |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return this.IgnoreRemoteDownloadSettings ? this.timeoutLength : TimeoutMilliseconds; |
||||
|
} |
||||
|
|
||||
|
set |
||||
|
{ |
||||
|
if (!this.IgnoreRemoteDownloadSettings) |
||||
|
{ |
||||
|
throw new SecurityException("Timeout length can not be adjusted on remote files that are abiding by remote download rules"); |
||||
|
} |
||||
|
|
||||
|
if (value < 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException("TimeoutLength"); |
||||
|
} |
||||
|
|
||||
|
this.timeoutLength = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the maximum download size, in bytes, that a remote file download attempt can be.
|
||||
|
/// <remarks>
|
||||
|
/// <para>
|
||||
|
/// This value can only be set if the instance is supposed to ignore the remote download settings set
|
||||
|
/// in the current application instance.
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// Set this value to 0 if there should be no timeout.
|
||||
|
/// </para>
|
||||
|
/// </remarks>
|
||||
|
/// </summary>
|
||||
|
public int MaxDownloadSize |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return this.IgnoreRemoteDownloadSettings ? this.maxDownloadSize : MaxBytes; |
||||
|
} |
||||
|
|
||||
|
set |
||||
|
{ |
||||
|
if (!this.IgnoreRemoteDownloadSettings) |
||||
|
{ |
||||
|
throw new SecurityException("Max Download Size can not be adjusted on remote files that are abiding by remote download rules"); |
||||
|
} |
||||
|
|
||||
|
if (value < 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException("MaxDownloadSize"); |
||||
|
} |
||||
|
|
||||
|
this.timeoutLength = value; |
||||
|
} |
||||
|
} |
||||
|
#endregion
|
||||
|
|
||||
|
#region Methods
|
||||
|
#region Public
|
||||
|
/// <summary>
|
||||
|
/// Returns the <see cref="T:System.Net.WebResponse">WebResponse</see> used to download this file.
|
||||
|
/// <remarks>
|
||||
|
/// <para>
|
||||
|
/// This method is meant for outside users who need specific access to the WebResponse this class
|
||||
|
/// generates. They're responsible for disposing of it.
|
||||
|
/// </para>
|
||||
|
/// </remarks>
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="T:System.Net.WebResponse">WebResponse</see> used to download this file.</returns>
|
||||
|
public WebResponse GetWebResponse() |
||||
|
{ |
||||
|
WebResponse response = this.GetWebRequest().GetResponse(); |
||||
|
|
||||
|
long contentLength = response.ContentLength; |
||||
|
|
||||
|
// WebResponse.ContentLength doesn't always know the value, it returns -1 in this case.
|
||||
|
if (contentLength == -1) |
||||
|
{ |
||||
|
// Response headers may still have the Content-Length inside of it.
|
||||
|
string headerContentLength = response.Headers["Content-Length"]; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(headerContentLength)) |
||||
|
{ |
||||
|
contentLength = long.Parse(headerContentLength, CultureInfo.InvariantCulture); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// We don't need to check the url here since any external urls are available only from the web.config.
|
||||
|
if ((this.MaxDownloadSize > 0) && (contentLength > this.MaxDownloadSize)) |
||||
|
{ |
||||
|
response.Close(); |
||||
|
throw new SecurityException("An attempt to download a remote file has been halted because the file is larger than allowed."); |
||||
|
} |
||||
|
|
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the remote file as a String.
|
||||
|
/// <remarks>
|
||||
|
/// This returns the resulting stream as a string as passed through a StreamReader.
|
||||
|
/// </remarks>
|
||||
|
/// </summary>
|
||||
|
/// <returns>The remote file as a String.</returns>
|
||||
|
public string GetFileAsString() |
||||
|
{ |
||||
|
using (WebResponse response = this.GetWebResponse()) |
||||
|
{ |
||||
|
Stream responseStream = response.GetResponseStream(); |
||||
|
|
||||
|
if (responseStream != null) |
||||
|
{ |
||||
|
// Pipe the stream to a stream reader with the required encoding format.
|
||||
|
using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8)) |
||||
|
{ |
||||
|
return reader.ReadToEnd(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return string.Empty; |
||||
|
} |
||||
|
} |
||||
|
#endregion
|
||||
|
|
||||
|
#region Private
|
||||
|
/// <summary>
|
||||
|
/// Performs a check to see whether the application is able to download remote files.
|
||||
|
/// </summary>
|
||||
|
private void CheckCanDownload() |
||||
|
{ |
||||
|
if (!this.IgnoreRemoteDownloadSettings && !AllowRemoteDownloads) |
||||
|
{ |
||||
|
throw new SecurityException("application is not configured to allow remote file downloads."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the WebRequest object used internally for this RemoteFile instance.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// <para>
|
||||
|
/// The WebRequest should not be passed outside of this instance, as it will allow tampering. Anyone
|
||||
|
/// that needs more fine control over the downloading process should probably be using the WebRequest
|
||||
|
/// class on its own.
|
||||
|
/// </para>
|
||||
|
/// </returns>
|
||||
|
private WebRequest GetWebRequest() |
||||
|
{ |
||||
|
// Check downloads are allowed.
|
||||
|
this.CheckCanDownload(); |
||||
|
|
||||
|
// Check the url is from a whitelisted location.
|
||||
|
this.CheckSafeUrlLocation(); |
||||
|
|
||||
|
if (this.webRequest == null) |
||||
|
{ |
||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.Uri); |
||||
|
request.Headers["Accept-Encoding"] = "gzip"; |
||||
|
request.Headers["Accept-Language"] = "en-us"; |
||||
|
request.Credentials = CredentialCache.DefaultNetworkCredentials; |
||||
|
request.AutomaticDecompression = DecompressionMethods.GZip; |
||||
|
|
||||
|
if (this.TimeoutLength > 0) |
||||
|
{ |
||||
|
request.Timeout = this.TimeoutLength; |
||||
|
} |
||||
|
|
||||
|
this.webRequest = request; |
||||
|
} |
||||
|
|
||||
|
return this.webRequest; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a value indicating whether the current url is in a list of safe download locations.
|
||||
|
/// </summary>
|
||||
|
private void CheckSafeUrlLocation() |
||||
|
{ |
||||
|
bool validUrl = RemoteFileWhiteList.Any(item => item.Host.ToUpperInvariant().Equals(this.url.Host.ToUpperInvariant())); |
||||
|
|
||||
|
if (!validUrl) |
||||
|
{ |
||||
|
throw new SecurityException("application is not configured to allow remote file downloads from this domain."); |
||||
|
} |
||||
|
} |
||||
|
#endregion
|
||||
|
#endregion
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
@ -0,0 +1,342 @@ |
|||||
|
// -----------------------------------------------------------------------
|
||||
|
// <copyright file="ImageProcessingModule.cs" company="James South">
|
||||
|
// Copyright (c) James South.
|
||||
|
// Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
|
// </copyright>
|
||||
|
// -----------------------------------------------------------------------
|
||||
|
|
||||
|
namespace ImageProcessor.Web.HttpModules |
||||
|
{ |
||||
|
#region Using
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Net; |
||||
|
using System.Reflection; |
||||
|
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
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Processes any image requests within the web application.
|
||||
|
/// </summary>
|
||||
|
public class ImageProcessingModule : IHttpModule |
||||
|
{ |
||||
|
#region Fields
|
||||
|
/// <summary>
|
||||
|
/// The key for storing the response type of the current image.
|
||||
|
/// </summary>
|
||||
|
private const string CachedResponseTypeKey = "CACHED_IMAGE_RESPONSE_TYPE"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The value to prefix any remote image requests with to ensure they get captured.
|
||||
|
/// </summary>
|
||||
|
private static readonly string RemotePrefix = ImageProcessorConfig.Instance.RemotePrefix; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The object to lock against.
|
||||
|
/// </summary>
|
||||
|
private static readonly object SyncRoot = new object(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The assembly version.
|
||||
|
/// </summary>
|
||||
|
private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A value indicating whether the application has started.
|
||||
|
/// </summary>
|
||||
|
private static bool hasModuleInitialized; |
||||
|
#endregion
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The delegate void representing the ProcessImage method.
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">
|
||||
|
/// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that provides
|
||||
|
/// references to the intrinsic server objects
|
||||
|
/// </param>
|
||||
|
private delegate void ProcessImageDelegate(HttpContext context); |
||||
|
|
||||
|
#region IHttpModule Members
|
||||
|
/// <summary>
|
||||
|
/// Initializes a module and prepares it to handle requests.
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">
|
||||
|
/// An <see cref="T:System.Web.HttpApplication"/> that provides
|
||||
|
/// access to the methods, properties, and events common to all
|
||||
|
/// application objects within an ASP.NET application
|
||||
|
/// </param>
|
||||
|
public void Init(HttpApplication context) |
||||
|
{ |
||||
|
if (!hasModuleInitialized) |
||||
|
{ |
||||
|
lock (SyncRoot) |
||||
|
{ |
||||
|
if (!hasModuleInitialized) |
||||
|
{ |
||||
|
DiskCache.CreateCacheDirectories(); |
||||
|
hasModuleInitialized = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
context.AddOnBeginRequestAsync(this.OnBeginAsync, this.OnEndAsync); |
||||
|
context.PreSendRequestHeaders += this.ContextPreSendRequestHeaders; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
// Nothing to dispose.
|
||||
|
} |
||||
|
#endregion
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The <see cref="T:System.Web.BeginEventHandler"/> that starts asynchronous processing
|
||||
|
/// of the <see cref="System.Web.HttpApplication.BeginRequest"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sender">The source of the event.</param>
|
||||
|
/// <param name="e">
|
||||
|
/// An <see cref="T:System.EventArgs">EventArgs</see> that contains
|
||||
|
/// the event data.
|
||||
|
/// </param>
|
||||
|
/// <param name="callBack">
|
||||
|
/// The delegate to call when the asynchronous method call is complete.
|
||||
|
/// If the callback is null, the delegate is not called.
|
||||
|
/// </param>
|
||||
|
/// <param name="state">
|
||||
|
/// Any additional data needed to process the request.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The status of the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
private IAsyncResult OnBeginAsync(object sender, EventArgs e, AsyncCallback callBack, object state) |
||||
|
{ |
||||
|
HttpContext context = ((HttpApplication)sender).Context; |
||||
|
|
||||
|
ProcessImageDelegate processImage = this.ProcessImage; |
||||
|
|
||||
|
return processImage.BeginInvoke(context, callBack, state); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The method that handles asynchronous events such as application events.
|
||||
|
/// </summary>
|
||||
|
/// <param name="result">
|
||||
|
/// The <see cref="T:System.IAsyncResult"/> that is the result of the
|
||||
|
/// <see cref="T:System.Web.BeginEventHandler"/> operation.
|
||||
|
/// </param>
|
||||
|
private void OnEndAsync(IAsyncResult result) |
||||
|
{ |
||||
|
// Ensure our ProcessImage has completed in the background.
|
||||
|
while (!result.IsCompleted) |
||||
|
{ |
||||
|
System.Threading.Thread.Sleep(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs just before ASP.NET send HttpHeaders to the client.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sender">The source of the event.</param>
|
||||
|
/// <param name="e">An <see cref="T:System.EventArgs">EventArgs</see> that contains the event data.</param>
|
||||
|
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
|
||||
|
/// <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>
|
||||
|
private void ProcessImage(HttpContext context) |
||||
|
{ |
||||
|
// Is this a remote file.
|
||||
|
bool isRemote = context.Request.Path.Equals(RemotePrefix, StringComparison.OrdinalIgnoreCase); |
||||
|
string path = 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(context.Request.QueryString.ToString()); |
||||
|
|
||||
|
if (urlDecode != null) |
||||
|
{ |
||||
|
string[] paths = urlDecode.Split('?'); |
||||
|
|
||||
|
path = paths[0]; |
||||
|
|
||||
|
if (paths.Length > 1) |
||||
|
{ |
||||
|
queryString = paths[1]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
path = HostingEnvironment.MapPath(context.Request.Path); |
||||
|
queryString = HttpUtility.UrlDecode(context.Request.QueryString.ToString()); |
||||
|
} |
||||
|
|
||||
|
// Only process requests that pass our sanitizing filter.
|
||||
|
if (ImageUtils.IsValidImageExtension(path) && !string.IsNullOrWhiteSpace(queryString)) |
||||
|
{ |
||||
|
if (this.FileExists(path, isRemote)) |
||||
|
{ |
||||
|
string fullPath = string.Format("{0}?{1}", path, queryString); |
||||
|
string imageName = Path.GetFileName(path); |
||||
|
string cachedPath = DiskCache.GetCachePath(fullPath, imageName); |
||||
|
bool isUpdated = DiskCache.IsUpdatedFile(path, cachedPath, isRemote); |
||||
|
|
||||
|
// Only process if the file has been updated.
|
||||
|
if (isUpdated) |
||||
|
{ |
||||
|
// Process the image.
|
||||
|
using (ImageFactory imageFactory = new ImageFactory()) |
||||
|
{ |
||||
|
if (isRemote) |
||||
|
{ |
||||
|
Uri uri = new Uri(path); |
||||
|
RemoteFile remoteFile = new RemoteFile(uri, false); |
||||
|
|
||||
|
using (MemoryStream memoryStream = new MemoryStream()) |
||||
|
{ |
||||
|
using (Stream responseStream = remoteFile.GetWebResponse().GetResponseStream()) |
||||
|
{ |
||||
|
if (responseStream != null) |
||||
|
{ |
||||
|
//lock (SyncRoot)
|
||||
|
//{
|
||||
|
// Trim the cache.
|
||||
|
DiskCache.TrimCachedFolders(); |
||||
|
|
||||
|
responseStream.CopyTo(memoryStream); |
||||
|
|
||||
|
imageFactory.Load(memoryStream) |
||||
|
.AddQueryString(queryString) |
||||
|
.Format(ImageUtils.GetImageFormat(imageName)) |
||||
|
.AutoProcess().Save(cachedPath); |
||||
|
|
||||
|
// Ensure that the LastWriteTime property of the source and cached file match.
|
||||
|
DateTime dateTime = DiskCache.SetCachedLastWriteTime(path, cachedPath, true); |
||||
|
|
||||
|
// Add to the cache.
|
||||
|
DiskCache.AddImageToCache(cachedPath, dateTime); |
||||
|
//}
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
//lock (SyncRoot)
|
||||
|
//{
|
||||
|
// Trim the cache.
|
||||
|
DiskCache.TrimCachedFolders(); |
||||
|
|
||||
|
imageFactory.Load(fullPath).AutoProcess().Save(cachedPath); |
||||
|
|
||||
|
// Ensure that the LastWriteTime property of the source and cached file match.
|
||||
|
DateTime dateTime = DiskCache.SetCachedLastWriteTime(path, cachedPath, false); |
||||
|
|
||||
|
// Add to the cache.
|
||||
|
DiskCache.AddImageToCache(cachedPath, dateTime); |
||||
|
//}
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
context.Items[CachedResponseTypeKey] = ImageUtils.GetResponseType(imageName).ToDescription(); |
||||
|
|
||||
|
// The cached file is valid so just rewrite the path.
|
||||
|
context.RewritePath(DiskCache.GetVirtualPath(cachedPath, context.Request), false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// returns a value indicating whether a file exists.
|
||||
|
/// </summary>
|
||||
|
/// <param name="path">The path to the file to check.</param>
|
||||
|
/// <param name="isRemote">Whether the file is remote.</param>
|
||||
|
/// <returns>True if the file exists, otherwise false.</returns>
|
||||
|
/// <remarks>If the file is remote the method will always return true.</remarks>
|
||||
|
private bool FileExists(string path, bool isRemote) |
||||
|
{ |
||||
|
if (isRemote) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
FileInfo fileInfo = new FileInfo(path); |
||||
|
return fileInfo.Exists; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 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
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">
|
||||
|
/// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that provides
|
||||
|
/// references to the intrinsic server objects
|
||||
|
/// </param>
|
||||
|
/// <param name="responseType">The HTTP MIME type to to send.</param>
|
||||
|
private void SetHeaders(HttpContext context, string responseType) |
||||
|
{ |
||||
|
HttpResponse response = context.Response; |
||||
|
|
||||
|
response.ContentType = responseType; |
||||
|
|
||||
|
response.AddHeader("Image-Served-By", "ImageProcessor/" + 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
|
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue