PageRenderTime 42ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/BlogEngine/DotNetSlave.BusinessLogic/RemoteFile.cs

#
C# | 278 lines | 146 code | 44 blank | 88 comment | 17 complexity | 0323357a056679e17d989273c78ccab5 MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0, BSD-3-Clause
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Net;
  6. using System.Security;
  7. using System.IO;
  8. namespace BlogEngine.Core
  9. {
  10. /// <summary>
  11. /// Class used to download files from a website address.
  12. /// </summary>
  13. /// <remarks>
  14. ///
  15. /// The purpose of this class is so there's one core way of downloading remote files with urls that are from
  16. /// outside users. There's various areas in BlogEngine where an attacker could supply an external url to the server
  17. /// and tie up resources.
  18. ///
  19. /// For example, the JavascriptHandler accepts off-server addresses as a path. An attacker could, for instance, pass the url
  20. /// 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
  21. /// could also use this same method to use one BlogEngine instance to hammer another site by, again, passing an off-server
  22. /// address of the victims site to the JavascriptHandler.
  23. ///
  24. /// RemoteFile makes use of two BlogSettings properties: AllowServerToDownloadRemoteFiles and RemoteFileDownloadTimeout.
  25. ///
  26. /// This class will not throw an exception if the Uri supplied points to a resource local to the running BlogEngine instance.
  27. ///
  28. /// There shouldn't be any security issues there, as the internal WebRequest instance is still calling it remotely.
  29. /// Any local files that shouldn't be accessed by this won't be allowed by the remote call.
  30. ///
  31. /// </remarks>
  32. internal sealed class RemoteFile
  33. {
  34. /// <summary>
  35. /// Creates a new RemoteFile instance that can be used to download files from another server.
  36. /// </summary>
  37. /// <param name="filePath">The url of the file to be downloaded.</param>
  38. /// <param name="ignoreRemoteDownloadSettings">Set to true if RemoteFile should ignore the current BlogEngine instance's remote download settings.</param>
  39. internal RemoteFile(Uri filePath, bool ignoreRemoteDownloadSettings)
  40. {
  41. if (filePath == null)
  42. {
  43. throw new ArgumentNullException("filePath");
  44. }
  45. this.url = filePath;
  46. this._ignoreRemoteDownloadSettings = ignoreRemoteDownloadSettings;
  47. this._timeoutLength = BlogSettings.Instance.RemoteFileDownloadTimeout;
  48. }
  49. #region "Public Methods"
  50. /// <summary>
  51. /// Returns the WebResponse used to download this file.
  52. /// </summary>
  53. /// <returns></returns>
  54. /// <remarks>
  55. ///
  56. /// This method is meant for outside users who need specific access to the WebResponse this class
  57. /// generates. They're responsible for disposing of it.
  58. ///
  59. /// </remarks>
  60. public WebResponse GetWebResponse()
  61. {
  62. var response = this.GetWebRequest().GetResponse();
  63. long contentLength = response.ContentLength;
  64. // WebResponse.ContentLength doesn't always know the value, it returns -1 in this case.
  65. if (contentLength == -1) {
  66. // Response headers may still have the Content-Length inside of it.
  67. string headerContentLength = response.Headers["Content-Length"];
  68. if (!String.IsNullOrEmpty(headerContentLength))
  69. {
  70. contentLength = long.Parse(headerContentLength);
  71. }
  72. }
  73. // -1 means an unknown ContentLength was found
  74. // Numbers any lower than that will always indicate that someone tampered with
  75. // the Content-Length header.
  76. if (contentLength <= -1)
  77. {
  78. response.Close();
  79. throw new SecurityException("An attempt to download a remote file has been halted due to unknown content length.");
  80. }
  81. else if ((BlogSettings.Instance.RemoteMaxFileSize > 0) && (contentLength > BlogSettings.Instance.RemoteMaxFileSize))
  82. {
  83. response.Close();
  84. throw new SecurityException("An attempt to download a remote file has been halted because the file is larger than allowed.");
  85. }
  86. return response;
  87. }
  88. private WebRequest _webRequest;
  89. /// <summary>
  90. /// Returns the remote file as a string.
  91. /// </summary>
  92. /// <returns></returns>
  93. /// <remarks>
  94. /// This returns the resulting stream as a string as passed through a StreamReader.
  95. /// </remarks>
  96. public string GetFileAsString()
  97. {
  98. using (var response = this.GetWebResponse())
  99. {
  100. using (var reader = new StreamReader(response.GetResponseStream()))
  101. {
  102. return reader.ReadToEnd();
  103. }
  104. }
  105. }
  106. #endregion
  107. #region "Private Methods"
  108. private void CheckCanDownload()
  109. {
  110. if (!this.IgnoreRemoteDownloadSettings && !BlogSettings.Instance.AllowServerToDownloadRemoteFiles)
  111. {
  112. if (!this.UriPointsToLocalResource)
  113. {
  114. throw new SecurityException("BlogEngine is not configured to allow remote file downloads.");
  115. }
  116. }
  117. }
  118. /// <summary>
  119. /// Creates the WebRequest object used internally for this RemoteFile instance.
  120. /// </summary>
  121. /// <returns>
  122. ///
  123. /// The WebRequest should not be passed outside of this instance, as it will allow tampering. Anyone
  124. /// that needs more fine control over the downloading process should probably be using the WebRequest
  125. /// class on its own.
  126. ///
  127. /// </returns>
  128. private WebRequest GetWebRequest()
  129. {
  130. this.CheckCanDownload();
  131. if (this._webRequest == null)
  132. {
  133. var request = (HttpWebRequest)WebRequest.Create(this.Uri);
  134. request.Headers["Accept-Encoding"] = "gzip";
  135. request.Headers["Accept-Language"] = "en-us";
  136. request.Credentials = CredentialCache.DefaultNetworkCredentials;
  137. request.AutomaticDecompression = DecompressionMethods.GZip;
  138. if (this.TimeoutLength > 0)
  139. {
  140. request.Timeout = this.TimeoutLength;
  141. }
  142. this._webRequest = request;
  143. }
  144. return this._webRequest;
  145. }
  146. #endregion
  147. #region "Properties"
  148. #region "IgnoreRemoteDownloadSettings"
  149. private readonly bool _ignoreRemoteDownloadSettings;
  150. /// <summary>
  151. /// Gets whether this RemoteFile instance is ignoring remote download rules set in the current BlogSettings instance.
  152. /// </summary>
  153. /// <remarks>
  154. ///
  155. /// This should only be set to true if the supplied url is a verified resource. Use at your own risk.
  156. ///
  157. /// </remarks>
  158. public bool IgnoreRemoteDownloadSettings
  159. {
  160. get
  161. {
  162. return this._ignoreRemoteDownloadSettings;
  163. }
  164. }
  165. #endregion
  166. /// <summary>
  167. /// Gets whether the Uri property is pointed at a resource local to the running BlogEngine instance.
  168. /// </summary>
  169. /// <remarks>
  170. /// This property is to determine whether the remote path supplied is pointing to a local file instance.
  171. /// This check is required because when a user has CompressWebResource set to true, it sends js.axd
  172. /// the full site path as its query parameter.
  173. /// </remarks>
  174. public bool UriPointsToLocalResource
  175. {
  176. get
  177. {
  178. return (this.Uri.AbsoluteUri.StartsWith(Utils.AbsoluteWebRoot.AbsoluteUri));
  179. }
  180. }
  181. #region "Uri"
  182. private readonly Uri url;
  183. /// <summary>
  184. /// Gets the Uri of the remote file being downloaded.
  185. /// </summary>
  186. public Uri Uri
  187. {
  188. get
  189. {
  190. return this.url;
  191. }
  192. }
  193. #endregion
  194. #region "TimeoutLength"
  195. private int _timeoutLength;
  196. /// <summary>
  197. /// Gets or sets the length of time, in milliseconds, that a remote file download attempt can last before timing out.
  198. /// </summary>
  199. /// <remarks>
  200. /// This value can only be set if the instance is supposed to ignore the remote download settings set
  201. /// in the current BlogSettings instance.
  202. ///
  203. /// Set this value to 0 if there should be no timeout.
  204. ///
  205. /// </remarks>
  206. public int TimeoutLength
  207. {
  208. get
  209. {
  210. return (this.IgnoreRemoteDownloadSettings ? this._timeoutLength : BlogSettings.Instance.RemoteFileDownloadTimeout);
  211. }
  212. set
  213. {
  214. if (!this.IgnoreRemoteDownloadSettings)
  215. {
  216. throw new SecurityException("TimeoutLength can not be adjusted on RemoteFiles that are abiding by remote download rules");
  217. }
  218. else
  219. {
  220. if (value < 0)
  221. {
  222. throw new ArgumentOutOfRangeException("TimeoutLength must be a value greater than or equal to 0 milliseconds");
  223. }
  224. else
  225. {
  226. this._timeoutLength = value;
  227. }
  228. }
  229. }
  230. }
  231. #endregion
  232. #endregion
  233. }
  234. }