PageRenderTime 47ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/BlogEngine/DotNetSlave.BusinessLogic/Web/HttpModules/CompressionModule.cs

#
C# | 420 lines | 187 code | 50 blank | 183 comment | 20 complexity | 7d6ed6bddfaddc744ad6a0cfed3e9f3f MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0, BSD-3-Clause
  1. namespace BlogEngine.Core.Web.HttpModules
  2. {
  3. using System;
  4. using System.IO;
  5. using System.IO.Compression;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. using System.Web;
  9. using System.Web.UI;
  10. /// <summary>
  11. /// Compresses the output using standard gzip/deflate.
  12. /// </summary>
  13. public sealed class CompressionModule : IHttpModule
  14. {
  15. #region Constants and Fields
  16. /// <summary>
  17. /// The deflate string.
  18. /// </summary>
  19. private const string Deflate = "deflate";
  20. /// <summary>
  21. /// The gzip string.
  22. /// </summary>
  23. private const string Gzip = "gzip";
  24. #endregion
  25. #region Public Methods
  26. /// <summary>
  27. /// Compresses the response stream using either deflate or gzip depending on the client.
  28. /// </summary>
  29. /// <param name="context">
  30. /// The HTTP context to compress.
  31. /// </param>
  32. public static void CompressResponse(HttpContext context)
  33. {
  34. if (IsEncodingAccepted(Deflate))
  35. {
  36. context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress);
  37. WillCompressResponse = true;
  38. SetEncoding(Deflate);
  39. }
  40. else if (IsEncodingAccepted(Gzip))
  41. {
  42. context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
  43. WillCompressResponse = true;
  44. SetEncoding(Gzip);
  45. }
  46. }
  47. #endregion
  48. #region Private Methods
  49. private static bool WillCompressResponse
  50. {
  51. get
  52. {
  53. HttpContext context = HttpContext.Current;
  54. if (context == null) { return false; }
  55. return context.Items["will-compress-resource"] != null && (bool)context.Items["will-compress-resource"];
  56. }
  57. set
  58. {
  59. HttpContext context = HttpContext.Current;
  60. if (context == null) { return; }
  61. context.Items["will-compress-resource"] = value;
  62. }
  63. }
  64. #endregion
  65. #region Implemented Interfaces
  66. #region IHttpModule
  67. /// <summary>
  68. /// Disposes of the resources (other than memory) used by the module
  69. /// that implements <see cref="T:System.Web.IHttpModule"></see>.
  70. /// </summary>
  71. void IHttpModule.Dispose()
  72. {
  73. // Nothing to dispose;
  74. }
  75. /// <summary>
  76. /// Initializes a module and prepares it to handle requests.
  77. /// </summary>
  78. /// <param name="context">
  79. /// An <see cref="T:System.Web.HttpApplication"></see>
  80. /// that provides access to the methods, properties, and events common to
  81. /// all application objects within an ASP.NET application.
  82. /// </param>
  83. void IHttpModule.Init(HttpApplication context)
  84. {
  85. context.PreRequestHandlerExecute += ContextPostReleaseRequestState;
  86. context.Error += new EventHandler(context_Error);
  87. }
  88. void context_Error(object sender, EventArgs e)
  89. {
  90. HttpContext context = ((HttpApplication)sender).Context;
  91. Exception ex = context.Server.GetLastError();
  92. // If this CompressionModule will be compressing the response and an unhandled exception
  93. // has occurred, remove the WebResourceFilter as that will cause garbage characters to
  94. // be sent to the browser instead of a yellow screen of death.
  95. if (WillCompressResponse)
  96. {
  97. context.Response.Filter = null;
  98. WillCompressResponse = false;
  99. }
  100. }
  101. #endregion
  102. #endregion
  103. #region Methods
  104. /// <summary>
  105. /// Checks the request headers to see if the specified
  106. /// encoding is accepted by the client.
  107. /// </summary>
  108. /// <param name="encoding">
  109. /// The encoding.
  110. /// </param>
  111. /// <returns>
  112. /// The is encoding accepted.
  113. /// </returns>
  114. private static bool IsEncodingAccepted(string encoding)
  115. {
  116. var context = HttpContext.Current;
  117. return context.Request.Headers["Accept-encoding"] != null &&
  118. context.Request.Headers["Accept-encoding"].Contains(encoding);
  119. }
  120. /// <summary>
  121. /// Adds the specified encoding to the response headers.
  122. /// </summary>
  123. /// <param name="encoding">The encoding.</param>
  124. private static void SetEncoding(string encoding)
  125. {
  126. HttpContext.Current.Response.AppendHeader("Content-encoding", encoding);
  127. }
  128. /// <summary>
  129. /// Handles the BeginRequest event of the context control.
  130. /// </summary>
  131. /// <param name="sender">
  132. /// The source of the event.
  133. /// </param>
  134. /// <param name="e">
  135. /// The <see cref="System.EventArgs"/> instance containing the event data.
  136. /// </param>
  137. private static void ContextPostReleaseRequestState(object sender, EventArgs e)
  138. {
  139. var context = ((HttpApplication)sender).Context;
  140. if (!BlogSettings.Instance.EnableHttpCompression) { return; }
  141. if (context.CurrentHandler is Page && context.Request["HTTP_X_MICROSOFTAJAX"] == null &&
  142. context.Request.HttpMethod == "GET")
  143. {
  144. CompressResponse(context);
  145. if (BlogSettings.Instance.CompressWebResource)
  146. {
  147. context.Response.Filter = new WebResourceFilter(context.Response.Filter);
  148. WillCompressResponse = true;
  149. }
  150. }
  151. else if (!BlogSettings.Instance.CompressWebResource && context.Request.Path.Contains("WebResource.axd"))
  152. {
  153. context.Response.Cache.SetExpires(DateTime.Now.AddDays(30));
  154. }
  155. }
  156. #endregion
  157. /// <summary>
  158. /// The web resource filter.
  159. /// </summary>
  160. private class WebResourceFilter : Stream
  161. {
  162. #region Constants and Fields
  163. /// <summary>
  164. /// The _sink.
  165. /// </summary>
  166. private readonly Stream sink;
  167. /// <summary>
  168. /// Regex for parsing webresource.axd
  169. /// </summary>
  170. private static readonly Regex WebResourceRegex =
  171. new Regex(
  172. "<script\\s*src=\"((?=[^\"]*webresource.axd)[^\"]*)\"\\s*type=\"text/javascript\"[^>]*>[^<]*(?:</script>)?",
  173. RegexOptions.IgnoreCase | RegexOptions.Compiled);
  174. #endregion
  175. #region Constructors and Destructors
  176. /// <summary>
  177. /// Initializes a new instance of the <see cref="WebResourceFilter"/> class.
  178. /// </summary>
  179. /// <param name="sink">
  180. /// The sink stream.
  181. /// </param>
  182. public WebResourceFilter(Stream sink)
  183. {
  184. this.sink = sink;
  185. }
  186. #endregion
  187. #region Properties
  188. /// <summary>
  189. /// Gets a value indicating whether CanRead.
  190. /// </summary>
  191. public override bool CanRead
  192. {
  193. get
  194. {
  195. return true;
  196. }
  197. }
  198. /// <summary>
  199. /// Gets a value indicating whether CanSeek.
  200. /// </summary>
  201. public override bool CanSeek
  202. {
  203. get
  204. {
  205. return true;
  206. }
  207. }
  208. /// <summary>
  209. /// Gets a value indicating whether CanWrite.
  210. /// </summary>
  211. public override bool CanWrite
  212. {
  213. get
  214. {
  215. return true;
  216. }
  217. }
  218. /// <summary>
  219. /// Gets Length.
  220. /// </summary>
  221. public override long Length
  222. {
  223. get
  224. {
  225. return 0;
  226. }
  227. }
  228. /// <summary>
  229. /// Gets or sets Position.
  230. /// </summary>
  231. public override long Position { get; set; }
  232. #endregion
  233. #region Public Methods
  234. /// <summary>
  235. /// The close.
  236. /// </summary>
  237. public override void Close()
  238. {
  239. this.sink.Close();
  240. }
  241. /// <summary>
  242. /// Evaluates the replacement for each link match.
  243. /// </summary>
  244. /// <param name="match">
  245. /// The match.
  246. /// </param>
  247. /// <returns>
  248. /// The evaluator.
  249. /// </returns>
  250. public string Evaluator(Match match)
  251. {
  252. var relative = match.Groups[1].Value;
  253. var absolute = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority);
  254. return match.Value.Replace(
  255. relative, string.Format("{0}js.axd?path={1}", Utils.ApplicationRelativeWebRoot, HttpUtility.UrlEncode(absolute + relative)));
  256. }
  257. /// <summary>
  258. /// When overridden in a derived class, clears all buffers for this stream and causes any buffered data to be written to the underlying device.
  259. /// </summary>
  260. /// <exception cref="T:System.IO.IOException">
  261. /// An I/O error occurs.
  262. /// </exception>
  263. public override void Flush()
  264. {
  265. this.sink.Flush();
  266. }
  267. /// <summary>
  268. /// When overridden in a derived class, reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
  269. /// </summary>
  270. /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and (<paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
  271. /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
  272. /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
  273. /// <returns>
  274. /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
  275. /// </returns>
  276. /// <exception cref="T:System.ArgumentException">
  277. /// The sum of <paramref name="offset"/> and <paramref name="count"/> is larger than the buffer length.
  278. /// </exception>
  279. /// <exception cref="T:System.ArgumentNullException">
  280. /// <paramref name="buffer"/> is null.
  281. /// </exception>
  282. /// <exception cref="T:System.ArgumentOutOfRangeException">
  283. /// <paramref name="offset"/> or <paramref name="count"/> is negative.
  284. /// </exception>
  285. /// <exception cref="T:System.IO.IOException">
  286. /// An I/O error occurs.
  287. /// </exception>
  288. /// <exception cref="T:System.NotSupportedException">
  289. /// The stream does not support reading.
  290. /// </exception>
  291. /// <exception cref="T:System.ObjectDisposedException">
  292. /// Methods were called after the stream was closed.
  293. /// </exception>
  294. public override int Read(byte[] buffer, int offset, int count)
  295. {
  296. return this.sink.Read(buffer, offset, count);
  297. }
  298. /// <summary>
  299. /// When overridden in a derived class, sets the position within the current stream.
  300. /// </summary>
  301. /// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
  302. /// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
  303. /// <returns>
  304. /// The new position within the current stream.
  305. /// </returns>
  306. /// <exception cref="T:System.IO.IOException">
  307. /// An I/O error occurs.
  308. /// </exception>
  309. /// <exception cref="T:System.NotSupportedException">
  310. /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output.
  311. /// </exception>
  312. /// <exception cref="T:System.ObjectDisposedException">
  313. /// Methods were called after the stream was closed.
  314. /// </exception>
  315. public override long Seek(long offset, SeekOrigin origin)
  316. {
  317. return this.sink.Seek(offset, origin);
  318. }
  319. /// <summary>
  320. /// When overridden in a derived class, sets the length of the current stream.
  321. /// </summary>
  322. /// <param name="value">The desired length of the current stream in bytes.</param>
  323. /// <exception cref="T:System.IO.IOException">
  324. /// An I/O error occurs.
  325. /// </exception>
  326. /// <exception cref="T:System.NotSupportedException">
  327. /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output.
  328. /// </exception>
  329. /// <exception cref="T:System.ObjectDisposedException">
  330. /// Methods were called after the stream was closed.
  331. /// </exception>
  332. public override void SetLength(long value)
  333. {
  334. this.sink.SetLength(value);
  335. }
  336. /// <summary>
  337. /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
  338. /// </summary>
  339. /// <param name="buffer">An array of bytes. This method copies <paramref name="count"/> bytes from <paramref name="buffer"/> to the current stream.</param>
  340. /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.</param>
  341. /// <param name="count">The number of bytes to be written to the current stream.</param>
  342. /// <exception cref="T:System.ArgumentException">
  343. /// The sum of <paramref name="offset"/> and <paramref name="count"/> is greater than the buffer length.
  344. /// </exception>
  345. /// <exception cref="T:System.ArgumentNullException">
  346. /// <paramref name="buffer"/> is null.
  347. /// </exception>
  348. /// <exception cref="T:System.ArgumentOutOfRangeException">
  349. /// <paramref name="offset"/> or <paramref name="count"/> is negative.
  350. /// </exception>
  351. /// <exception cref="T:System.IO.IOException">
  352. /// An I/O error occurs.
  353. /// </exception>
  354. /// <exception cref="T:System.NotSupportedException">
  355. /// The stream does not support writing.
  356. /// </exception>
  357. /// <exception cref="T:System.ObjectDisposedException">
  358. /// Methods were called after the stream was closed.
  359. /// </exception>
  360. public override void Write(byte[] buffer, int offset, int count)
  361. {
  362. var html = Encoding.UTF8.GetString(buffer, offset, count);
  363. html = WebResourceRegex.Replace(html, new MatchEvaluator(this.Evaluator));
  364. var outdata = Encoding.UTF8.GetBytes(html);
  365. this.sink.Write(outdata, 0, outdata.GetLength(0));
  366. }
  367. #endregion
  368. }
  369. }
  370. }