// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // Encapsulates methods used to download files from a website address. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Web.Helpers { #region Using using System; using System.Collections.Generic; using System.Globalization; using System.Net; using System.Security; using System.Threading.Tasks; using System.Web; #endregion /// /// Encapsulates methods used to download files from a website address. /// /// /// /// 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. /// /// 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. /// /// 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. /// /// Adapted from BlogEngine.Net /// internal sealed class RemoteFile { #region Fields /// /// The Uri of the remote file being downloaded. /// private readonly Uri url; /// /// The maximum allowable download size in bytes. /// private int maxDownloadSize; /// /// The length of time, in milliseconds, that a remote file download attempt can last before timing out. /// private int timeoutLength; /// /// The WebResponse object used internally for this RemoteFile instance. /// private WebRequest webRequest; #endregion #region Constructors /// /// Initializes a new instance of the RemoteFile class. /// /// The url of the file to be downloaded. internal RemoteFile(Uri filePath) { if (filePath == null) { throw new ArgumentNullException("filePath"); } this.url = filePath; } #endregion #region Properties /// /// Gets the Uri of the remote file being downloaded. /// public Uri Uri { get { return this.url; } } /// /// Gets or sets the length of time, in milliseconds, that a remote file download attempt can /// last before timing out. /// /// /// This value can only be set if the instance is supposed to ignore the remote download settings set /// in the current application instance. /// /// /// Set this value to 0 if there should be no timeout. /// /// /// public int TimeoutLength { get { return this.timeoutLength; } set { if (value < 0) { // ReSharper disable once NotResolvedInText throw new ArgumentOutOfRangeException("TimeoutLength"); } this.timeoutLength = value; } } /// /// Gets or sets the maximum download size, in bytes, that a remote file download attempt can be. /// /// /// This value can only be set if the instance is supposed to ignore the remote download settings set /// in the current application instance. /// /// /// Set this value to 0 if there should be no max bytes. /// /// /// public int MaxDownloadSize { get { return this.maxDownloadSize; } set { if (value < 0) { // ReSharper disable once NotResolvedInText throw new ArgumentOutOfRangeException("MaxDownloadSize"); } this.maxDownloadSize = value; } } #endregion #region Methods #region Internal /// /// Returns the WebResponse used to download this file. /// /// /// This method is meant for outside users who need specific access to the WebResponse this class /// generates. They're responsible for disposing of it. /// /// /// /// The WebResponse used to download this file. /// /// The . /// internal async Task GetWebResponseAsync() { WebResponse response; try { response = await this.GetWebRequest().GetResponseAsync(); } catch (WebException ex) { if (ex.Status == WebExceptionStatus.NameResolutionFailure) { throw new HttpException(404, "No image exists at " + Uri); } throw; } if (response != null) { 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; } #endregion #region Private /// /// Creates the WebRequest object used internally for this RemoteFile instance. /// /// /// /// 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. /// /// private WebRequest GetWebRequest() { 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; } #endregion #endregion } }