From 1fc0142351377ba4c02ce94a602aaf0315c39837 Mon Sep 17 00:00:00 2001 From: James South Date: Tue, 23 Sep 2014 00:04:51 +0100 Subject: [PATCH] Adding configuration TODO: Fix extensionless urls. Former-commit-id: e7e9147599a53c8d00052af566e496ad26f19bc1 --- src/ImageProcessor.Web/Caching/DiskCache.cs | 16 +- .../ImageProcessorConfiguration.cs | 307 ++++++++---------- .../Configuration/ImageSecuritySection.cs | 103 +----- .../Configuration/Resources/security.config | 15 +- .../Helpers/ImageHelpers.cs | 3 +- src/ImageProcessor.Web/Helpers/RemoteFile.cs | 162 +-------- .../HttpModules/ImageProcessingModule.cs | 146 +++------ .../Services/IImageService.cs | 6 + .../Services/LocalFileImageService.cs | 8 +- .../Services/RemoteImageService.cs | 61 +++- src/ImageProcessor/ImageFactory.cs | 6 +- .../MVC/config/imageprocessor/security.config | 35 +- 12 files changed, 319 insertions(+), 549 deletions(-) diff --git a/src/ImageProcessor.Web/Caching/DiskCache.cs b/src/ImageProcessor.Web/Caching/DiskCache.cs index c21d52b6aa..b5e94ea064 100644 --- a/src/ImageProcessor.Web/Caching/DiskCache.cs +++ b/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(@"\", "/"); diff --git a/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs b/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs index fde8deeb27..20e4cbc8db 100644 --- a/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs +++ b/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs @@ -36,13 +36,6 @@ namespace ImageProcessor.Web.Configuration private static readonly Lazy Lazy = new Lazy(() => new ImageProcessorConfiguration()); - /// - /// A collection of the elements - /// for available plugins. - /// - private static readonly ConcurrentDictionary> PluginSettings = - new ConcurrentDictionary>(); - /// /// A collection of the processing presets defined in the configuration. /// for available plugins. @@ -134,74 +127,6 @@ namespace ImageProcessor.Web.Configuration } } #endregion - - #region Security - /// - /// Gets a list of white listed url[s] that images can be downloaded from. - /// - public Uri[] RemoteFileWhiteList - { - get - { - return GetImageSecuritySection().ImageServices.Cast().Select(x => x.Url).ToArray(); - } - } - - /// - /// Gets a list of image extensions for url[s] with no extension. - /// - public ImageSecuritySection.SafeUrl[] RemoteFileWhiteListExtensions - { - get - { - return GetImageSecuritySection().ImageServices.Cast().ToArray(); - } - } - - /// - /// Gets a value indicating whether the current application is allowed to download remote files. - /// - public bool AllowRemoteDownloads - { - get - { - return GetImageSecuritySection().AllowRemoteDownloads; - } - } - - /// - /// Gets the maximum length to wait in milliseconds before throwing an error requesting a remote file. - /// - public int Timeout - { - get - { - return GetImageSecuritySection().Timeout; - } - } - - /// - /// Gets the maximum allowable size in bytes of e remote file to process. - /// - public int MaxBytes - { - get - { - return GetImageSecuritySection().MaxBytes; - } - } - - /// - /// Gets the remote prefix for external files for the application. - /// - public string RemotePrefix - { - get - { - return GetImageSecuritySection().RemotePrefix; - } - } - #endregion #endregion #region Methods @@ -228,80 +153,6 @@ namespace ImageProcessor.Web.Configuration }); } - /// - /// Returns the for the given plugin. - /// - /// - /// The name of the plugin to get the settings for. - /// - /// - /// The for the given plugin. - /// - public Dictionary GetPluginSettings(string name) - { - return PluginSettings.GetOrAdd( - name, - n => - { - ImageProcessingSection.PluginElement pluginElement = GetImageProcessingSection() - .Plugins - .Cast() - .FirstOrDefault(x => x.Name == n); - - Dictionary settings; - - if (pluginElement != null) - { - settings = pluginElement.Settings - .Cast() - .ToDictionary(setting => setting.Key, setting => setting.Value); - } - else - { - settings = new Dictionary(); - } - - return settings; - }); - } - - /// - /// Returns the for the given plugin. - /// - /// - /// The name of the plugin to get the settings for. - /// - /// - /// The for the given plugin. - /// - public Dictionary GetServiceSettings(string name) - { - return PluginSettings.GetOrAdd( - name, - n => - { - ImageSecuritySection.ServiceElement pluginElement = GetImageSecuritySection() - .ImageServices - .Cast() - .FirstOrDefault(x => x.Name == n); - - Dictionary settings; - - if (pluginElement != null) - { - settings = pluginElement.Settings - .Cast() - .ToDictionary(setting => setting.Key, setting => setting.Value); - } - else - { - settings = new Dictionary(); - } - - return settings; - }); - } - /// /// Retrieves the processing configuration section from the current application configuration. /// @@ -329,6 +180,7 @@ namespace ImageProcessor.Web.Configuration return imageSecuritySection ?? (imageSecuritySection = ImageSecuritySection.GetConfiguration()); } + #region GraphicesProcessors /// /// Gets the list of available GraphicsProcessors. /// @@ -369,6 +221,69 @@ namespace ImageProcessor.Web.Configuration } } + /// + /// Loads graphics processors from configuration. + /// + /// + /// Thrown when an cannot be loaded. + /// + private void LoadGraphicsProcessorsFromConfiguration() + { + ImageProcessingSection.PluginElementCollection pluginConfigs = imageProcessingSection.Plugins; + this.GraphicsProcessors = new List(); + 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); + } + } + + /// + /// Returns the for the given plugin. + /// + /// + /// The name of the plugin to get the settings for. + /// + /// + /// The for the given plugin. + /// + private Dictionary GetPluginSettings(string name) + { + ImageProcessingSection.PluginElement pluginElement = GetImageProcessingSection() + .Plugins + .Cast() + .FirstOrDefault(x => x.Name == name); + + Dictionary settings; + + if (pluginElement != null) + { + settings = pluginElement.Settings + .Cast() + .ToDictionary(setting => setting.Key, setting => setting.Value); + } + else + { + settings = new Dictionary(); + } + + return settings; + } +#endregion + + #region ImageServices /// /// Gets the list of available ImageServices. /// @@ -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 } /// - /// Loads graphics processors from configuration. + /// Loads image services from configuration. /// /// - /// Thrown when an cannot be loaded. + /// Thrown when an cannot be loaded. /// - private void LoadGraphicsProcessorsFromConfiguration() + private void LoadImageServicesFromConfiguration() { - ImageProcessingSection.PluginElementCollection pluginConfigs = imageProcessingSection.Plugins; - this.GraphicsProcessors = new List(); - foreach (ImageProcessingSection.PluginElement pluginConfig in pluginConfigs) + ImageSecuritySection.ServiceElementCollection services = imageSecuritySection.ImageServices; + this.ImageServices = new List(); + 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); } } /// - /// Loads image services from configuration. + /// Returns the for the given plugin. /// - /// - /// Thrown when an cannot be loaded. - /// - private void LoadImageServicesFromConfiguration() + /// + /// The name of the plugin to get the settings for. + /// + /// + /// The for the given plugin. + /// + private Dictionary GetServiceSettings(string name) { - ImageSecuritySection.ServiceElementCollection services = imageSecuritySection.ImageServices; - this.ImageServices = new List(); - foreach (ImageSecuritySection.ServiceElement config in services) - { - Type type = Type.GetType(config.Type); + ImageSecuritySection.ServiceElement serviceElement = GetImageSecuritySection() + .ImageServices + .Cast() + .FirstOrDefault(x => x.Name == name); - if (type == null) - { - throw new TypeLoadException("Couldn't load IImageService: " + config.Type); - } + Dictionary settings; - this.GraphicsProcessors.Add(Activator.CreateInstance(type) as IWebGraphicsProcessor); + if (serviceElement != null) + { + settings = serviceElement.Settings + .Cast() + .ToDictionary(setting => setting.Key, setting => setting.Value); + } + else + { + settings = new Dictionary(); } - // Add the available settings. - foreach (IImageService service in this.ImageServices) + return settings; + } + + /// + /// Gets the whitelist of for the given service. + /// + /// + /// The name of the service to return the whitelist for. + /// + /// + /// The array containing the whitelist. + /// + private Uri[] GetServiceWhitelist(string name) + { + ImageSecuritySection.ServiceElement serviceElement = GetImageSecuritySection() + .ImageServices + .Cast() + .FirstOrDefault(x => x.Name == name); + + Uri[] whitelist = { }; + if (serviceElement != null) { - service.Settings = this.GetServiceSettings(service.GetType().Name); + whitelist = serviceElement.WhiteList.Cast() + .Select(s => s.Url).ToArray(); } + + return whitelist; } #endregion + #endregion } } \ No newline at end of file diff --git a/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs b/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs index 814fee0b0c..f57f64a6bf 100644 --- a/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs +++ b/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs @@ -25,70 +25,9 @@ namespace ImageProcessor.Web.Configuration { #region Properties /// - /// Gets or sets a value indicating whether the current application is allowed download remote files. + /// Gets the /// - /// if the current application is allowed download remote files; otherwise, . - [ConfigurationProperty("allowRemoteDownloads", DefaultValue = false, IsRequired = true)] - public bool AllowRemoteDownloads - { - get { return (bool)this["allowRemoteDownloads"]; } - set { this["allowRemoteDownloads"] = value; } - } - - /// - /// Gets or sets the maximum allowed remote file timeout in milliseconds for the application. - /// - /// The maximum number of days to store an image in the cache. - /// Defaults to 30000 (30 seconds) if not set. - [ConfigurationProperty("timeout", DefaultValue = "300000", IsRequired = true)] - public int Timeout - { - get - { - return (int)this["timeout"]; - } - - set - { - this["timeout"] = value; - } - } - - /// - /// Gets or sets the maximum allowed remote file size in bytes for the application. - /// - /// The maximum number of days to store an image in the cache. - /// Defaults to 4194304 (4Mb) if not set. - [ConfigurationProperty("maxBytes", DefaultValue = "4194304", IsRequired = true)] - public int MaxBytes - { - get - { - return (int)this["maxBytes"]; - } - - set - { - this["maxBytes"] = value; - } - } - - /// - /// Gets or sets the prefix for remote files for the application. - /// - /// The prefix for remote files for the application. - [ConfigurationProperty("remotePrefix", DefaultValue = "", IsRequired = true)] - public string RemotePrefix - { - get { return (string)this["remotePrefix"]; } - - set { this["remotePrefix"] = value; } - } - - /// - /// Gets the - /// - /// The + /// The [ConfigurationProperty("services", IsRequired = true)] public ServiceElementCollection ImageServices { @@ -344,19 +283,19 @@ namespace ImageProcessor.Web.Configuration } /// - /// Gets or sets the + /// Gets or sets the /// at the specified index within the collection. /// /// The index at which to get the specified object. /// - /// The the + /// The the /// at the specified index within the collection. /// - 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 /// /// the key representing the element /// the setting element - 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); } } /// @@ -403,7 +342,7 @@ namespace ImageProcessor.Web.Configuration /// The element key for a specified PluginElement configuration element. protected override object GetElementKey(ConfigurationElement element) { - return ((ImageProcessingSection.SettingElement)element).Key; + return ((ImageSecuritySection.SettingElement)element).Key; } /// @@ -414,7 +353,7 @@ namespace ImageProcessor.Web.Configuration /// protected override ConfigurationElement CreateNewElement() { - return new ImageProcessingSection.SettingElement(); + return new ImageSecuritySection.SettingElement(); } } @@ -484,28 +423,6 @@ namespace ImageProcessor.Web.Configuration set { this["url"] = value; } } - - /// - /// Gets or sets a value indicating whether the white listed url is extension-less. - /// - [ConfigurationProperty("extensionLess", DefaultValue = false, IsRequired = false)] - public bool ExtensionLess - { - get { return (bool)this["extensionLess"]; } - - set { this["extensionLess"] = value; } - } - - /// - /// Gets or sets the image format for the extension-less url. - /// - [ConfigurationProperty("imageFormat", DefaultValue = "", IsRequired = false)] - public string ImageFormat - { - get { return (string)this["imageFormat"]; } - - set { this["imageFormat"] = value; } - } } } } diff --git a/src/ImageProcessor.Web/Configuration/Resources/security.config b/src/ImageProcessor.Web/Configuration/Resources/security.config index 99cd3d3ff5..aaa2fbf9be 100644 --- a/src/ImageProcessor.Web/Configuration/Resources/security.config +++ b/src/ImageProcessor.Web/Configuration/Resources/security.config @@ -1,4 +1,13 @@ - - - + + + + + + + + + + + + diff --git a/src/ImageProcessor.Web/Helpers/ImageHelpers.cs b/src/ImageProcessor.Web/Helpers/ImageHelpers.cs index e52f5aa3b0..21445c1d88 100644 --- a/src/ImageProcessor.Web/Helpers/ImageHelpers.cs +++ b/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 /// True the value contains a valid image extension, otherwise false. public static bool IsValidImageExtension(string fileName) { - return EndFormatRegex.IsMatch(fileName); + return EndFormatRegex.IsMatch(fileName) || string.IsNullOrWhiteSpace(Path.GetExtension(fileName)); } /// diff --git a/src/ImageProcessor.Web/Helpers/RemoteFile.cs b/src/ImageProcessor.Web/Helpers/RemoteFile.cs index 679157dc18..caff6afe38 100644 --- a/src/ImageProcessor.Web/Helpers/RemoteFile.cs +++ b/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 /// @@ -48,37 +43,6 @@ namespace ImageProcessor.Web.Helpers internal sealed class RemoteFile { #region Fields - /// - /// The white-list of url[s] from which to download remote files. - /// - public static readonly ImageSecuritySection.SafeUrl[] RemoteFileWhiteListExtensions = ImageProcessorConfiguration.Instance.RemoteFileWhiteListExtensions; - - /// - /// The white-list of url[s] from which to download remote files. - /// - private static readonly Uri[] RemoteFileWhiteList = ImageProcessorConfiguration.Instance.RemoteFileWhiteList; - - /// - /// The length of time, in milliseconds, that a remote file download attempt can last before timing out. - /// - private static readonly int TimeoutMilliseconds = ImageProcessorConfiguration.Instance.Timeout; - - /// - /// The maximum size, in bytes, that a remote file download attempt can download. - /// - private static readonly int MaxBytes = ImageProcessorConfiguration.Instance.MaxBytes; - - /// - /// Whether to allow remote downloads. - /// - private static readonly bool AllowRemoteDownloads = ImageProcessorConfiguration.Instance.AllowRemoteDownloads; - - /// - /// Whether this RemoteFile instance is ignoring remote download rules set in the current application - /// instance. - /// - private readonly bool ignoreRemoteDownloadSettings; - /// /// The Uri of the remote file being downloaded. /// @@ -87,7 +51,7 @@ namespace ImageProcessor.Web.Helpers /// /// The maximum allowable download size in bytes. /// - private readonly int maxDownloadSize; + private int maxDownloadSize; /// /// 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 RemoteFile class. /// /// The url of the file to be downloaded. - /// - /// If set to , then RemoteFile should ignore the current the applications instance's remote download settings; otherwise,. - /// - 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 - /// - /// Gets a value indicating whether this RemoteFile instance is ignoring remote download rules set in the - /// current application instance. - /// - /// This should only be set to true if the supplied url is a verified resource. Use at your own risk. - /// - /// - /// - /// if this RemoteFile instance is ignoring remote download rules set in the current - /// application instance; otherwise, . - /// - public bool IgnoreRemoteDownloadSettings - { - get - { - return this.ignoreRemoteDownloadSettings; - } - } - /// /// Gets the Uri of the remote file being downloaded. /// @@ -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. /// /// - /// Set this value to 0 if there should be no timeout. + /// Set this value to 0 if there should be no max bytes. /// /// /// @@ -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 /// internal async Task GetWebResponseAsync() { - WebResponse response = null; + WebResponse response; try { response = await this.GetWebRequest().GetResponseAsync(); @@ -285,47 +214,9 @@ namespace ImageProcessor.Web.Helpers return response; } - - /// - /// Returns the remote file as a String. - /// - /// This returns the resulting stream as a string as passed through a StreamReader. - /// - /// - /// The remote file as a String. - internal string GetFileAsString() - { - Task 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 - /// - /// Performs a check to see whether the application is able to download remote files. - /// - private void CheckCanDownload() - { - if (!this.IgnoreRemoteDownloadSettings && !AllowRemoteDownloads) - { - throw new SecurityException("Application is not configured to allow remote file downloads."); - } - } /// /// Creates the WebRequest object used internally for this RemoteFile instance. @@ -339,12 +230,6 @@ namespace ImageProcessor.Web.Helpers /// 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; } - - /// - /// Returns a value indicating whether the current url is in a list of safe download locations. - /// - 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 } diff --git a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs index c5e4c24a8d..e1fd2b583c 100644 --- a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs +++ b/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 /// @@ -69,11 +70,6 @@ namespace ImageProcessor.Web.HttpModules /// private static readonly AsyncDuplicateLock Locker = new AsyncDuplicateLock(); - /// - /// The value to prefix any remote image requests with to ensure they get captured. - /// - private static string remotePrefix; - /// /// Whether to preserve exif meta data. /// @@ -129,11 +125,6 @@ namespace ImageProcessor.Web.HttpModules /// 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 { 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 { requestPath, cachedPath }; + context.Items[CachedResponseFileDependency] = new List { cachedPath }; } } } @@ -579,6 +512,33 @@ namespace ImageProcessor.Web.HttpModules 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; + 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 } } diff --git a/src/ImageProcessor.Web/Services/IImageService.cs b/src/ImageProcessor.Web/Services/IImageService.cs index 07b5d43fcb..7d0c2a5fcb 100644 --- a/src/ImageProcessor.Web/Services/IImageService.cs +++ b/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 /// Dictionary Settings { get; set; } + /// + /// Gets or sets the white list of . + /// + Uri[] WhiteList { get; set; } + /// /// Gets the image using the given identifier. /// diff --git a/src/ImageProcessor.Web/Services/LocalFileImageService.cs b/src/ImageProcessor.Web/Services/LocalFileImageService.cs index b961f6455e..3a95d558a4 100644 --- a/src/ImageProcessor.Web/Services/LocalFileImageService.cs +++ b/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 /// public Dictionary Settings { get; set; } + /// + /// Gets or sets the white list of . + /// + public Uri[] WhiteList { get; set; } + /// /// Gets the image using the given identifier. /// @@ -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); } diff --git a/src/ImageProcessor.Web/Services/RemoteImageService.cs b/src/ImageProcessor.Web/Services/RemoteImageService.cs index 55bb14d2d9..fadf68c236 100644 --- a/src/ImageProcessor.Web/Services/RemoteImageService.cs +++ b/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 /// public RemoteImageService() { - this.Settings = new Dictionary(); + this.Settings = new Dictionary + { + { "MaxBytes", "4194304" }, + { "Timeout", "30000" } + }; + + this.WhiteList = new Uri[] { }; } /// @@ -62,6 +69,11 @@ namespace ImageProcessor.Web.Services /// public Dictionary Settings { get; set; } + /// + /// Gets or sets the white list of . + /// + public Uri[] WhiteList { get; set; } + /// /// Gets the image using the given identifier. /// @@ -74,7 +86,16 @@ namespace ImageProcessor.Web.Services public async Task 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; } + + /// + /// Returns a value indicating whether the current url is in a list of safe download locations. + /// + /// + /// The to check against. + /// + 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."); + } + } } } diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index 3de01f167a..9e88c09448 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -739,7 +739,11 @@ namespace ImageProcessor { if (this.ShouldProcess) { - Dictionary resizeSettings = new Dictionary { { "MaxWidth", resizeLayer.Size.Width.ToString("G") }, { "MaxHeight", resizeLayer.Size.Height.ToString("G") } }; + Dictionary resizeSettings = new Dictionary + { + { "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); diff --git a/src/TestWebsites/MVC/config/imageprocessor/security.config b/src/TestWebsites/MVC/config/imageprocessor/security.config index d4781f99cd..8671f06df6 100644 --- a/src/TestWebsites/MVC/config/imageprocessor/security.config +++ b/src/TestWebsites/MVC/config/imageprocessor/security.config @@ -1,15 +1,24 @@  - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + +