// --------------------------------------------------------------------------------------------------------------------
//
// 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
}
}