PageRenderTime 17ms CodeModel.GetById 2ms app.highlight 10ms RepoModel.GetById 2ms app.codeStats 0ms

/BlogEngine/DotNetSlave.BusinessLogic/RemoteFile.cs

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