Browse Source

Adding configuration

TODO: Fix extensionless urls.

Former-commit-id: e7e9147599a53c8d00052af566e496ad26f19bc1
af/merge-core
James South 12 years ago
parent
commit
1fc0142351
  1. 16
      src/ImageProcessor.Web/Caching/DiskCache.cs
  2. 307
      src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs
  3. 103
      src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs
  4. 15
      src/ImageProcessor.Web/Configuration/Resources/security.config
  5. 3
      src/ImageProcessor.Web/Helpers/ImageHelpers.cs
  6. 162
      src/ImageProcessor.Web/Helpers/RemoteFile.cs
  7. 146
      src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs
  8. 6
      src/ImageProcessor.Web/Services/IImageService.cs
  9. 8
      src/ImageProcessor.Web/Services/LocalFileImageService.cs
  10. 61
      src/ImageProcessor.Web/Services/RemoteImageService.cs
  11. 6
      src/ImageProcessor/ImageFactory.cs
  12. 35
      src/TestWebsites/MVC/config/imageprocessor/security.config

16
src/ImageProcessor.Web/Caching/DiskCache.cs

@ -191,11 +191,11 @@ namespace ImageProcessor.Web.Caching
{
string key = Path.GetFileNameWithoutExtension(cachedPath);
CachedImage cachedImage = new CachedImage
{
Key = key,
Path = cachedPath,
CreationTimeUtc = DateTime.UtcNow
};
{
Key = key,
Path = cachedPath,
CreationTimeUtc = DateTime.UtcNow
};
CacheIndexer.Add(cachedImage);
}
@ -289,7 +289,7 @@ namespace ImageProcessor.Web.Caching
// That name can also be used as a key for the cached image and we should be able to use
// The characters of that hash as sub-folders.
string parsedExtension = ImageHelpers.GetExtension(this.fullPath);
string fallbackExtension = this.imageName.Substring(this.imageName.LastIndexOf(".", StringComparison.Ordinal) + 1);
//string fallbackExtension = this.imageName.Substring(this.imageName.LastIndexOf(".", StringComparison.Ordinal) + 1);
string encryptedName = (streamHash + this.fullPath).ToSHA1Fingerprint();
// Collision rate of about 1 in 10000 for the folder structure.
@ -297,9 +297,9 @@ namespace ImageProcessor.Web.Caching
string virtualPathFromKey = pathFromKey.Replace(@"\", "/");
string cachedFileName = string.Format(
"{0}.{1}",
"{0}{1}",
encryptedName,
!string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension.Replace(".", string.Empty) : fallbackExtension);
!string.IsNullOrWhiteSpace(parsedExtension) ? "." + parsedExtension.Replace(".", string.Empty) : string.Empty);
this.physicalCachedPath = Path.Combine(AbsoluteCachePath, pathFromKey, cachedFileName);
this.virtualCachedPath = Path.Combine(VirtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/");

307
src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs

@ -36,13 +36,6 @@ namespace ImageProcessor.Web.Configuration
private static readonly Lazy<ImageProcessorConfiguration> Lazy =
new Lazy<ImageProcessorConfiguration>(() => new ImageProcessorConfiguration());
/// <summary>
/// A collection of the <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElementCollection"/> elements
/// for available plugins.
/// </summary>
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> PluginSettings =
new ConcurrentDictionary<string, Dictionary<string, string>>();
/// <summary>
/// A collection of the processing presets defined in the configuration.
/// for available plugins.
@ -134,74 +127,6 @@ namespace ImageProcessor.Web.Configuration
}
}
#endregion
#region Security
/// <summary>
/// Gets a list of white listed url[s] that images can be downloaded from.
/// </summary>
public Uri[] RemoteFileWhiteList
{
get
{
return GetImageSecuritySection().ImageServices.Cast<ImageSecuritySection.SafeUrl>().Select(x => x.Url).ToArray();
}
}
/// <summary>
/// Gets a list of image extensions for url[s] with no extension.
/// </summary>
public ImageSecuritySection.SafeUrl[] RemoteFileWhiteListExtensions
{
get
{
return GetImageSecuritySection().ImageServices.Cast<ImageSecuritySection.SafeUrl>().ToArray();
}
}
/// <summary>
/// Gets a value indicating whether the current application is allowed to download remote files.
/// </summary>
public bool AllowRemoteDownloads
{
get
{
return GetImageSecuritySection().AllowRemoteDownloads;
}
}
/// <summary>
/// Gets the maximum length to wait in milliseconds before throwing an error requesting a remote file.
/// </summary>
public int Timeout
{
get
{
return GetImageSecuritySection().Timeout;
}
}
/// <summary>
/// Gets the maximum allowable size in bytes of e remote file to process.
/// </summary>
public int MaxBytes
{
get
{
return GetImageSecuritySection().MaxBytes;
}
}
/// <summary>
/// Gets the remote prefix for external files for the application.
/// </summary>
public string RemotePrefix
{
get
{
return GetImageSecuritySection().RemotePrefix;
}
}
#endregion
#endregion
#region Methods
@ -228,80 +153,6 @@ namespace ImageProcessor.Web.Configuration
});
}
/// <summary>
/// Returns the <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElementCollection"/> for the given plugin.
/// </summary>
/// <param name="name">
/// The name of the plugin to get the settings for.
/// </param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElementCollection"/> for the given plugin.
/// </returns>
public Dictionary<string, string> GetPluginSettings(string name)
{
return PluginSettings.GetOrAdd(
name,
n =>
{
ImageProcessingSection.PluginElement pluginElement = GetImageProcessingSection()
.Plugins
.Cast<ImageProcessingSection.PluginElement>()
.FirstOrDefault(x => x.Name == n);
Dictionary<string, string> settings;
if (pluginElement != null)
{
settings = pluginElement.Settings
.Cast<ImageProcessingSection.SettingElement>()
.ToDictionary(setting => setting.Key, setting => setting.Value);
}
else
{
settings = new Dictionary<string, string>();
}
return settings;
});
}
/// <summary>
/// Returns the <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElementCollection"/> for the given plugin.
/// </summary>
/// <param name="name">
/// The name of the plugin to get the settings for.
/// </param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElementCollection"/> for the given plugin.
/// </returns>
public Dictionary<string, string> GetServiceSettings(string name)
{
return PluginSettings.GetOrAdd(
name,
n =>
{
ImageSecuritySection.ServiceElement pluginElement = GetImageSecuritySection()
.ImageServices
.Cast<ImageSecuritySection.ServiceElement>()
.FirstOrDefault(x => x.Name == n);
Dictionary<string, string> settings;
if (pluginElement != null)
{
settings = pluginElement.Settings
.Cast<ImageProcessingSection.SettingElement>()
.ToDictionary(setting => setting.Key, setting => setting.Value);
}
else
{
settings = new Dictionary<string, string>();
}
return settings;
});
}
/// <summary>
/// Retrieves the processing configuration section from the current application configuration.
/// </summary>
@ -329,6 +180,7 @@ namespace ImageProcessor.Web.Configuration
return imageSecuritySection ?? (imageSecuritySection = ImageSecuritySection.GetConfiguration());
}
#region GraphicesProcessors
/// <summary>
/// Gets the list of available GraphicsProcessors.
/// </summary>
@ -369,6 +221,69 @@ namespace ImageProcessor.Web.Configuration
}
}
/// <summary>
/// Loads graphics processors from configuration.
/// </summary>
/// <exception cref="TypeLoadException">
/// Thrown when an <see cref="IWebGraphicsProcessor"/> cannot be loaded.
/// </exception>
private void LoadGraphicsProcessorsFromConfiguration()
{
ImageProcessingSection.PluginElementCollection pluginConfigs = imageProcessingSection.Plugins;
this.GraphicsProcessors = new List<IWebGraphicsProcessor>();
foreach (ImageProcessingSection.PluginElement pluginConfig in pluginConfigs)
{
Type type = Type.GetType(pluginConfig.Type);
if (type == null)
{
throw new TypeLoadException("Couldn't load IWebGraphicsProcessor: " + pluginConfig.Type);
}
this.GraphicsProcessors.Add(Activator.CreateInstance(type) as IWebGraphicsProcessor);
}
// Add the available settings.
foreach (IWebGraphicsProcessor webProcessor in this.GraphicsProcessors)
{
webProcessor.Processor.Settings = this.GetPluginSettings(webProcessor.GetType().Name);
}
}
/// <summary>
/// Returns the <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElementCollection"/> for the given plugin.
/// </summary>
/// <param name="name">
/// The name of the plugin to get the settings for.
/// </param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElementCollection"/> for the given plugin.
/// </returns>
private Dictionary<string, string> GetPluginSettings(string name)
{
ImageProcessingSection.PluginElement pluginElement = GetImageProcessingSection()
.Plugins
.Cast<ImageProcessingSection.PluginElement>()
.FirstOrDefault(x => x.Name == name);
Dictionary<string, string> settings;
if (pluginElement != null)
{
settings = pluginElement.Settings
.Cast<ImageProcessingSection.SettingElement>()
.ToDictionary(setting => setting.Key, setting => setting.Value);
}
else
{
settings = new Dictionary<string, string>();
}
return settings;
}
#endregion
#region ImageServices
/// <summary>
/// Gets the list of available ImageServices.
/// </summary>
@ -392,9 +307,11 @@ namespace ImageProcessor.Web.Configuration
this.ImageServices = availableTypes.Select(x => (Activator.CreateInstance(x) as IImageService)).ToList();
// Add the available settings.
foreach (IImageService imageService in this.ImageServices)
foreach (IImageService service in this.ImageServices)
{
imageService.Settings = this.GetServiceSettings(imageService.GetType().Name);
string name = service.GetType().Name;
service.Settings = this.GetServiceSettings(name);
service.WhiteList = this.GetServiceWhitelist(name);
}
}
catch (ReflectionTypeLoadException)
@ -410,62 +327,94 @@ namespace ImageProcessor.Web.Configuration
}
/// <summary>
/// Loads graphics processors from configuration.
/// Loads image services from configuration.
/// </summary>
/// <exception cref="TypeLoadException">
/// Thrown when an <see cref="IWebGraphicsProcessor"/> cannot be loaded.
/// Thrown when an <see cref="IGraphicsProcessor"/> cannot be loaded.
/// </exception>
private void LoadGraphicsProcessorsFromConfiguration()
private void LoadImageServicesFromConfiguration()
{
ImageProcessingSection.PluginElementCollection pluginConfigs = imageProcessingSection.Plugins;
this.GraphicsProcessors = new List<IWebGraphicsProcessor>();
foreach (ImageProcessingSection.PluginElement pluginConfig in pluginConfigs)
ImageSecuritySection.ServiceElementCollection services = imageSecuritySection.ImageServices;
this.ImageServices = new List<IImageService>();
foreach (ImageSecuritySection.ServiceElement config in services)
{
Type type = Type.GetType(pluginConfig.Type);
Type type = Type.GetType(config.Type);
if (type == null)
{
throw new TypeLoadException("Couldn't load IWebGraphicsProcessor: " + pluginConfig.Type);
throw new TypeLoadException("Couldn't load IImageService: " + config.Type);
}
this.GraphicsProcessors.Add(Activator.CreateInstance(type) as IWebGraphicsProcessor);
}
// Add the available settings.
foreach (IWebGraphicsProcessor webProcessor in this.GraphicsProcessors)
foreach (IImageService service in this.ImageServices)
{
webProcessor.Processor.Settings = this.GetPluginSettings(webProcessor.GetType().Name);
string name = service.GetType().Name;
service.Settings = this.GetServiceSettings(name);
service.WhiteList = this.GetServiceWhitelist(name);
}
}
/// <summary>
/// Loads image services from configuration.
/// Returns the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElementCollection"/> for the given plugin.
/// </summary>
/// <exception cref="TypeLoadException">
/// Thrown when an <see cref="IGraphicsProcessor"/> cannot be loaded.
/// </exception>
private void LoadImageServicesFromConfiguration()
/// <param name="name">
/// The name of the plugin to get the settings for.
/// </param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElementCollection"/> for the given plugin.
/// </returns>
private Dictionary<string, string> GetServiceSettings(string name)
{
ImageSecuritySection.ServiceElementCollection services = imageSecuritySection.ImageServices;
this.ImageServices = new List<IImageService>();
foreach (ImageSecuritySection.ServiceElement config in services)
{
Type type = Type.GetType(config.Type);
ImageSecuritySection.ServiceElement serviceElement = GetImageSecuritySection()
.ImageServices
.Cast<ImageSecuritySection.ServiceElement>()
.FirstOrDefault(x => x.Name == name);
if (type == null)
{
throw new TypeLoadException("Couldn't load IImageService: " + config.Type);
}
Dictionary<string, string> settings;
this.GraphicsProcessors.Add(Activator.CreateInstance(type) as IWebGraphicsProcessor);
if (serviceElement != null)
{
settings = serviceElement.Settings
.Cast<ImageSecuritySection.SettingElement>()
.ToDictionary(setting => setting.Key, setting => setting.Value);
}
else
{
settings = new Dictionary<string, string>();
}
// Add the available settings.
foreach (IImageService service in this.ImageServices)
return settings;
}
/// <summary>
/// Gets the whitelist of <see cref="System.Uri"/> for the given service.
/// </summary>
/// <param name="name">
/// The name of the service to return the whitelist for.
/// </param>
/// <returns>
/// The <see cref="System.Uri"/> array containing the whitelist.
/// </returns>
private Uri[] GetServiceWhitelist(string name)
{
ImageSecuritySection.ServiceElement serviceElement = GetImageSecuritySection()
.ImageServices
.Cast<ImageSecuritySection.ServiceElement>()
.FirstOrDefault(x => x.Name == name);
Uri[] whitelist = { };
if (serviceElement != null)
{
service.Settings = this.GetServiceSettings(service.GetType().Name);
whitelist = serviceElement.WhiteList.Cast<ImageSecuritySection.SafeUrl>()
.Select(s => s.Url).ToArray();
}
return whitelist;
}
#endregion
#endregion
}
}

103
src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs

@ -25,70 +25,9 @@ namespace ImageProcessor.Web.Configuration
{
#region Properties
/// <summary>
/// Gets or sets a value indicating whether the current application is allowed download remote files.
/// Gets the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.ServiceElementCollection"/>
/// </summary>
/// <value><see langword="true"/> if the current application is allowed download remote files; otherwise, <see langword="false"/>.</value>
[ConfigurationProperty("allowRemoteDownloads", DefaultValue = false, IsRequired = true)]
public bool AllowRemoteDownloads
{
get { return (bool)this["allowRemoteDownloads"]; }
set { this["allowRemoteDownloads"] = value; }
}
/// <summary>
/// Gets or sets the maximum allowed remote file timeout in milliseconds for the application.
/// </summary>
/// <value>The maximum number of days to store an image in the cache.</value>
/// <remarks>Defaults to 30000 (30 seconds) if not set.</remarks>
[ConfigurationProperty("timeout", DefaultValue = "300000", IsRequired = true)]
public int Timeout
{
get
{
return (int)this["timeout"];
}
set
{
this["timeout"] = value;
}
}
/// <summary>
/// Gets or sets the maximum allowed remote file size in bytes for the application.
/// </summary>
/// <value>The maximum number of days to store an image in the cache.</value>
/// <remarks>Defaults to 4194304 (4Mb) if not set.</remarks>
[ConfigurationProperty("maxBytes", DefaultValue = "4194304", IsRequired = true)]
public int MaxBytes
{
get
{
return (int)this["maxBytes"];
}
set
{
this["maxBytes"] = value;
}
}
/// <summary>
/// Gets or sets the prefix for remote files for the application.
/// </summary>
/// <value>The prefix for remote files for the application.</value>
[ConfigurationProperty("remotePrefix", DefaultValue = "", IsRequired = true)]
public string RemotePrefix
{
get { return (string)this["remotePrefix"]; }
set { this["remotePrefix"] = value; }
}
/// <summary>
/// Gets the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.WhiteListElementCollection"/>
/// </summary>
/// <value>The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.WhiteListElementCollection"/></value>
/// <value>The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.ServiceElementCollection"/></value>
[ConfigurationProperty("services", IsRequired = true)]
public ServiceElementCollection ImageServices
{
@ -344,19 +283,19 @@ namespace ImageProcessor.Web.Configuration
}
/// <summary>
/// Gets or sets the <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElement"/>
/// Gets or sets the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElement"/>
/// at the specified index within the collection.
/// </summary>
/// <param name="index">The index at which to get the specified object.</param>
/// <returns>
/// The the <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElement"/>
/// The the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElement"/>
/// at the specified index within the collection.
/// </returns>
public ImageProcessingSection.SettingElement this[int index]
public ImageSecuritySection.SettingElement this[int index]
{
get
{
return (ImageProcessingSection.SettingElement)BaseGet(index);
return (ImageSecuritySection.SettingElement)BaseGet(index);
}
set
@ -375,9 +314,9 @@ namespace ImageProcessor.Web.Configuration
/// </summary>
/// <param name="key">the key representing the element</param>
/// <returns>the setting element</returns>
public new ImageProcessingSection.SettingElement this[string key]
public new ImageSecuritySection.SettingElement this[string key]
{
get { return (ImageProcessingSection.SettingElement)BaseGet(key); }
get { return (ImageSecuritySection.SettingElement)BaseGet(key); }
}
/// <summary>
@ -403,7 +342,7 @@ namespace ImageProcessor.Web.Configuration
/// <returns>The element key for a specified PluginElement configuration element.</returns>
protected override object GetElementKey(ConfigurationElement element)
{
return ((ImageProcessingSection.SettingElement)element).Key;
return ((ImageSecuritySection.SettingElement)element).Key;
}
/// <summary>
@ -414,7 +353,7 @@ namespace ImageProcessor.Web.Configuration
/// </returns>
protected override ConfigurationElement CreateNewElement()
{
return new ImageProcessingSection.SettingElement();
return new ImageSecuritySection.SettingElement();
}
}
@ -484,28 +423,6 @@ namespace ImageProcessor.Web.Configuration
set { this["url"] = value; }
}
/// <summary>
/// Gets or sets a value indicating whether the white listed url is extension-less.
/// </summary>
[ConfigurationProperty("extensionLess", DefaultValue = false, IsRequired = false)]
public bool ExtensionLess
{
get { return (bool)this["extensionLess"]; }
set { this["extensionLess"] = value; }
}
/// <summary>
/// Gets or sets the image format for the extension-less url.
/// </summary>
[ConfigurationProperty("imageFormat", DefaultValue = "", IsRequired = false)]
public string ImageFormat
{
get { return (string)this["imageFormat"]; }
set { this["imageFormat"] = value; }
}
}
}
}

15
src/ImageProcessor.Web/Configuration/Resources/security.config

@ -1,4 +1,13 @@
<security allowRemoteDownloads="true" timeout="300000" maxBytes="4194304" remotePrefix="/remote.axd">
<whiteList>
</whiteList>
<security>
<services autoLoadPlugins="true">
<service name="LocalFileImageService" type="ImageProcessor.Web.Services.LocalFileImageService, ImageProcessor.Web"/>
<service name="RemoteImageService" type="ImageProcessor.Web.Services.RemoteImageService, ImageProcessor.Web">
<settings>
<setting key="MaxBytes" value="4194304"/>
<setting key="Timeout" value="3000"/>
</settings>
<whitelist>
</whitelist>
</service>
</services>
</security>

3
src/ImageProcessor.Web/Helpers/ImageHelpers.cs

@ -12,6 +12,7 @@ namespace ImageProcessor.Web.Helpers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@ -45,7 +46,7 @@ namespace ImageProcessor.Web.Helpers
/// <returns>True the value contains a valid image extension, otherwise false.</returns>
public static bool IsValidImageExtension(string fileName)
{
return EndFormatRegex.IsMatch(fileName);
return EndFormatRegex.IsMatch(fileName) || string.IsNullOrWhiteSpace(Path.GetExtension(fileName));
}
/// <summary>

162
src/ImageProcessor.Web/Helpers/RemoteFile.cs

@ -13,16 +13,11 @@ namespace ImageProcessor.Web.Helpers
#region Using
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Web.Configuration;
#endregion
/// <summary>
@ -48,37 +43,6 @@ namespace ImageProcessor.Web.Helpers
internal sealed class RemoteFile
{
#region Fields
/// <summary>
/// The white-list of url[s] from which to download remote files.
/// </summary>
public static readonly ImageSecuritySection.SafeUrl[] RemoteFileWhiteListExtensions = ImageProcessorConfiguration.Instance.RemoteFileWhiteListExtensions;
/// <summary>
/// The white-list of url[s] from which to download remote files.
/// </summary>
private static readonly Uri[] RemoteFileWhiteList = ImageProcessorConfiguration.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 = ImageProcessorConfiguration.Instance.Timeout;
/// <summary>
/// The maximum size, in bytes, that a remote file download attempt can download.
/// </summary>
private static readonly int MaxBytes = ImageProcessorConfiguration.Instance.MaxBytes;
/// <summary>
/// Whether to allow remote downloads.
/// </summary>
private static readonly bool AllowRemoteDownloads = ImageProcessorConfiguration.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>
@ -87,7 +51,7 @@ namespace ImageProcessor.Web.Helpers
/// <summary>
/// The maximum allowable download size in bytes.
/// </summary>
private readonly int maxDownloadSize;
private int maxDownloadSize;
/// <summary>
/// The length of time, in milliseconds, that a remote file download attempt can last before timing out.
@ -105,10 +69,7 @@ namespace ImageProcessor.Web.Helpers
/// 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)
internal RemoteFile(Uri filePath)
{
if (filePath == null)
{
@ -116,32 +77,10 @@ namespace ImageProcessor.Web.Helpers
}
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>
@ -170,16 +109,11 @@ namespace ImageProcessor.Web.Helpers
{
get
{
return this.IgnoreRemoteDownloadSettings ? this.timeoutLength : TimeoutMilliseconds;
return this.timeoutLength;
}
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)
{
// ReSharper disable once NotResolvedInText
@ -198,7 +132,7 @@ namespace ImageProcessor.Web.Helpers
/// in the current application instance.
/// </para>
/// <para>
/// Set this value to 0 if there should be no timeout.
/// Set this value to 0 if there should be no max bytes.
/// </para>
/// </remarks>
/// </summary>
@ -206,23 +140,18 @@ namespace ImageProcessor.Web.Helpers
{
get
{
return this.IgnoreRemoteDownloadSettings ? this.maxDownloadSize : MaxBytes;
return this.maxDownloadSize;
}
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)
{
// ReSharper disable once NotResolvedInText
throw new ArgumentOutOfRangeException("MaxDownloadSize");
}
this.timeoutLength = value;
this.maxDownloadSize = value;
}
}
#endregion
@ -244,7 +173,7 @@ namespace ImageProcessor.Web.Helpers
/// </returns>
internal async Task<WebResponse> GetWebResponseAsync()
{
WebResponse response = null;
WebResponse response;
try
{
response = await this.GetWebRequest().GetResponseAsync();
@ -285,47 +214,9 @@ namespace ImageProcessor.Web.Helpers
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>
internal string GetFileAsString()
{
Task<WebResponse> responseTask = this.GetWebResponseAsync();
using (WebResponse response = responseTask.Result)
{
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.
@ -339,12 +230,6 @@ namespace ImageProcessor.Web.Helpers
/// </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);
@ -363,39 +248,6 @@ namespace ImageProcessor.Web.Helpers
return this.webRequest;
}
/// <summary>
/// Returns a value indicating whether the current url is in a list of safe download locations.
/// </summary>
private void CheckSafeUrlLocation()
{
string upper = this.url.Host.ToUpperInvariant();
// Check for root or subdomain.
bool validUrl = false;
foreach (Uri uri in RemoteFileWhiteList)
{
if (!uri.IsAbsoluteUri)
{
Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart(new[] { '.', '/' }));
validUrl = upper.StartsWith(rebaseUri.Host.ToUpperInvariant()) || upper.EndsWith(rebaseUri.Host.ToUpperInvariant());
}
else
{
validUrl = upper.StartsWith(uri.Host.ToUpperInvariant()) || upper.EndsWith(uri.Host.ToUpperInvariant());
}
if (validUrl)
{
break;
}
}
if (!validUrl)
{
throw new SecurityException("Application is not configured to allow remote file downloads from this domain.");
}
}
#endregion
#endregion
}

146
src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs

@ -29,8 +29,9 @@ namespace ImageProcessor.Web.HttpModules
using ImageProcessor.Web.Caching;
using ImageProcessor.Web.Configuration;
using ImageProcessor.Web.Extensions;
using ImageProcessor.Web.Helpers;
using ImageProcessor.Web.Services;
#endregion
/// <summary>
@ -69,11 +70,6 @@ namespace ImageProcessor.Web.HttpModules
/// </summary>
private static readonly AsyncDuplicateLock Locker = new AsyncDuplicateLock();
/// <summary>
/// The value to prefix any remote image requests with to ensure they get captured.
/// </summary>
private static string remotePrefix;
/// <summary>
/// Whether to preserve exif meta data.
/// </summary>
@ -129,11 +125,6 @@ namespace ImageProcessor.Web.HttpModules
/// </param>
public void Init(HttpApplication context)
{
if (remotePrefix == null)
{
remotePrefix = ImageProcessorConfiguration.Instance.RemotePrefix;
}
if (preserveExifMetaData == null)
{
preserveExifMetaData = ImageProcessorConfiguration.Instance.PreserveExifMetaData;
@ -280,14 +271,17 @@ namespace ImageProcessor.Web.HttpModules
private async Task ProcessImageAsync(HttpContext context)
{
HttpRequest request = context.Request;
IImageService currentService = this.GetImageServiceForRequest(request);
// Fixes issue 10.
bool isRemote = request.Path.EndsWith(remotePrefix, StringComparison.OrdinalIgnoreCase);
if (currentService == null)
{
throw new HttpException(500, "No ImageService found for current request.");
}
bool isRemote = !currentService.IsFileLocalService;
string requestPath = string.Empty;
string queryString = string.Empty;
bool validExtensionLessUrl = false;
string urlParameters = "";
string extensionLessExtension = "";
if (isRemote)
{
@ -307,7 +301,7 @@ namespace ImageProcessor.Web.HttpModules
requestPath = paths[0];
// Handle extension-less urls.
if (paths.Count() > 2)
if (paths.Length > 2)
{
queryString = paths[2];
urlParameters = paths[1];
@ -316,15 +310,6 @@ namespace ImageProcessor.Web.HttpModules
{
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
@ -334,7 +319,7 @@ namespace ImageProcessor.Web.HttpModules
}
// Only process requests that pass our sanitizing filter.
if ((ImageHelpers.IsValidImageExtension(requestPath) || validExtensionLessUrl) && !string.IsNullOrWhiteSpace(queryString))
if (ImageHelpers.IsValidImageExtension(requestPath) && !string.IsNullOrWhiteSpace(queryString))
{
// Replace any presets in the querystring with the actual value.
queryString = this.ReplacePresetsInQueryString(queryString);
@ -342,23 +327,6 @@ namespace ImageProcessor.Web.HttpModules
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(requestPath, fullPath, imageName);
string cachedPath = cache.CachedPath;
@ -397,72 +365,37 @@ namespace ImageProcessor.Web.HttpModules
// Process the image.
using (ImageFactory imageFactory = new ImageFactory(preserveExifMetaData != null && preserveExifMetaData.Value))
{
if (isRemote)
using (await Locker.LockAsync(cachedPath))
{
using (await Locker.LockAsync(cachedPath))
byte[] imageBuffer;
if (isRemote)
{
Uri uri = new Uri(requestPath + "?" + urlParameters);
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)
{
responseStream.CopyTo(memoryStream);
// 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 };
}
}
}
}
imageBuffer = await currentService.GetImage(uri);
}
}
else
{
using (Locker.Lock(cachedPath))
else
{
// Check to see if the file exists.
// ReSharper disable once AssignNullToNotNullAttribute
FileInfo fileInfo = new FileInfo(requestPath);
imageBuffer = await currentService.GetImage(requestPath);
}
if (!fileInfo.Exists)
{
throw new HttpException(404, "No image exists at " + fullPath);
}
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(requestPath)
imageFactory.Load(memoryStream)
.AutoProcess(queryString)
.Save(cachedPath);
// Add to the cache.
cache.AddImageToCache(cachedPath);
// Store the cached path, response type, and cache dependencies in the context for later retrieval.
// 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> { requestPath, cachedPath };
context.Items[CachedResponseFileDependency] = new List<string> { cachedPath };
}
}
}
@ -579,6 +512,33 @@ namespace ImageProcessor.Web.HttpModules
return queryString;
}
/// <summary>
/// Gets the correct <see cref="IImageService"/> for the given request.
/// </summary>
/// <param name="request">
/// The current image request.
/// </param>
/// <returns>
/// The <see cref="IImageService"/>.
/// </returns>
private IImageService GetImageServiceForRequest(HttpRequest request)
{
IImageService imageService = null;
IList<IImageService> services = ImageProcessorConfiguration.Instance.ImageServices;
string path = request.Path;
foreach (IImageService service in services)
{
string key = service.Key;
if (!string.IsNullOrWhiteSpace(key) && path.EndsWith(key, StringComparison.InvariantCultureIgnoreCase))
{
imageService = service;
}
}
return imageService ?? services.FirstOrDefault(s => string.IsNullOrWhiteSpace(s.Key));
}
#endregion
}
}

6
src/ImageProcessor.Web/Services/IImageService.cs

@ -10,6 +10,7 @@
namespace ImageProcessor.Web.Services
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -37,6 +38,11 @@ namespace ImageProcessor.Web.Services
/// </summary>
Dictionary<string, string> Settings { get; set; }
/// <summary>
/// Gets or sets the white list of <see cref="System.Uri"/>.
/// </summary>
Uri[] WhiteList { get; set; }
/// <summary>
/// Gets the image using the given identifier.
/// </summary>

8
src/ImageProcessor.Web/Services/LocalFileImageService.cs

@ -10,6 +10,7 @@
namespace ImageProcessor.Web.Services
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@ -51,6 +52,11 @@ namespace ImageProcessor.Web.Services
/// </summary>
public Dictionary<string, string> Settings { get; set; }
/// <summary>
/// Gets or sets the white list of <see cref="System.Uri"/>.
/// </summary>
public Uri[] WhiteList { get; set; }
/// <summary>
/// Gets the image using the given identifier.
/// </summary>
@ -76,7 +82,7 @@ namespace ImageProcessor.Web.Services
using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
{
buffer = new byte[file.Length];
buffer = new byte[file.Length];
await file.ReadAsync(buffer, 0, (int)file.Length);
}

61
src/ImageProcessor.Web/Services/RemoteImageService.cs

@ -14,6 +14,7 @@ namespace ImageProcessor.Web.Services
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security;
using System.Threading.Tasks;
using ImageProcessor.Web.Helpers;
@ -28,7 +29,13 @@ namespace ImageProcessor.Web.Services
/// </summary>
public RemoteImageService()
{
this.Settings = new Dictionary<string, string>();
this.Settings = new Dictionary<string, string>
{
{ "MaxBytes", "4194304" },
{ "Timeout", "30000" }
};
this.WhiteList = new Uri[] { };
}
/// <summary>
@ -62,6 +69,11 @@ namespace ImageProcessor.Web.Services
/// </summary>
public Dictionary<string, string> Settings { get; set; }
/// <summary>
/// Gets or sets the white list of <see cref="System.Uri"/>.
/// </summary>
public Uri[] WhiteList { get; set; }
/// <summary>
/// Gets the image using the given identifier.
/// </summary>
@ -74,7 +86,16 @@ namespace ImageProcessor.Web.Services
public async Task<byte[]> GetImage(object id)
{
Uri uri = new Uri(id.ToString());
RemoteFile remoteFile = new RemoteFile(uri, false);
// Check the url is from a whitelisted location.
this.CheckSafeUrlLocation(uri);
RemoteFile remoteFile = new RemoteFile(uri)
{
MaxDownloadSize = int.Parse(this.Settings["MaxBytes"]),
TimeoutLength = int.Parse(this.Settings["Timeout"])
};
byte[] buffer = { };
// Prevent response blocking.
@ -101,5 +122,41 @@ namespace ImageProcessor.Web.Services
return buffer;
}
/// <summary>
/// Returns a value indicating whether the current url is in a list of safe download locations.
/// </summary>
/// <param name="url">
/// The <see cref="System.Uri"/> to check against.
/// </param>
private void CheckSafeUrlLocation(Uri url)
{
string upper = url.Host.ToUpperInvariant();
// Check for root or sub domain.
bool validUrl = false;
foreach (Uri uri in this.WhiteList)
{
if (!uri.IsAbsoluteUri)
{
Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart(new[] { '.', '/' }));
validUrl = upper.StartsWith(rebaseUri.Host.ToUpperInvariant()) || upper.EndsWith(rebaseUri.Host.ToUpperInvariant());
}
else
{
validUrl = upper.StartsWith(uri.Host.ToUpperInvariant()) || upper.EndsWith(uri.Host.ToUpperInvariant());
}
if (validUrl)
{
break;
}
}
if (!validUrl)
{
throw new SecurityException("Application is not configured to allow remote file downloads from this domain.");
}
}
}
}

6
src/ImageProcessor/ImageFactory.cs

@ -739,7 +739,11 @@ namespace ImageProcessor
{
if (this.ShouldProcess)
{
Dictionary<string, string> resizeSettings = new Dictionary<string, string> { { "MaxWidth", resizeLayer.Size.Width.ToString("G") }, { "MaxHeight", resizeLayer.Size.Height.ToString("G") } };
Dictionary<string, string> resizeSettings = new Dictionary<string, string>
{
{ "MaxWidth", resizeLayer.Size.Width.ToString("G") },
{ "MaxHeight", resizeLayer.Size.Height.ToString("G") }
};
Resize resize = new Resize { DynamicParameter = resizeLayer, Settings = resizeSettings };
this.CurrentImageFormat.ApplyProcessor(resize.ProcessImage, this);

35
src/TestWebsites/MVC/config/imageprocessor/security.config

@ -1,15 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<security allowRemoteDownloads="true" timeout="300000" maxBytes="524288" remotePrefix="/remote.axd">
<whiteList>
<add url="images.mymovies.net"/>
<add url="http://maps.googleapis.com" extensionLess="true" imageFormat=".png"/>
<add url="fbcdn"/>
<add url="http://fbcdn-profile-"/>
<add url="https://profile."/>
<add url="http://profile."/>
<add url="https://pbs.twimg.com"/>
<add url="http://pbs.twimg.com"/>
<add url="http://placekitten.com"/>
<add url="http://hanselminutes.com/"/>
</whiteList>
<security>
<services autoLoadPlugins="true">
<service name="LocalFileImageService" type="ImageProcessor.Web.Services.LocalFileImageService, ImageProcessor.Web"/>
<service name="RemoteImageService" type="ImageProcessor.Web.Services.RemoteImageService, ImageProcessor.Web">
<settings>
<setting key="MaxBytes" value="4194304"/>
<setting key="Timeout" value="30000"/>
</settings>
<whitelist>
<add url="images.mymovies.net"/>
<add url="http://maps.googleapis.com"/>
<add url="fbcdn"/>
<add url="http://fbcdn-profile-"/>
<add url="https://profile."/>
<add url="http://profile."/>
<add url="https://pbs.twimg.com"/>
<add url="http://pbs.twimg.com"/>
<add url="http://placekitten.com"/>
<add url="http://hanselminutes.com/"/>
</whitelist>
</service>
</services>
</security>

Loading…
Cancel
Save