I have a windows server running IIS 10 with 100+ applications on it. The applications run the gambit from .net 2.0 to .net core 3.1 to blazer server side apps. I recently received a mandate to inject a static Banner that states some "US GOVERNMENT WARNING". The problem I am having is my managed handler is working with intermittence success. The managed HTTP handler I wrote is based on this Code Project article https://www.codeproject.com/Articles/11662/ASP-NET-Watermarker-Module.
When I say the module works intermittently I mean the HTML is injected and some request but not others, for example I can refresh my app 10 times and only 2 times does the banner get injected.
Also , these apps are all scheduled to be modified to so they can inject a banner on their own the problems with a 100+ apps we dont have the time to get them all modified and deployed to production before the Dec deadline.
here is the code I have , hoping someone can point out where I went wrong.
This is the base module class as was done in the code project article
public class InjectModuleBase : IHttpModule
{
private FilterReplacementDelegate _replacementDelegate = null;
private InjectFilterStream _filterStream = null;
EventLog eventLog = new EventLog("Application");
private void UpdateResponse(HttpContext context)
{
if (context != null)
{
// construct the delegate function, using the FilterString method;
// as this method is virtual, it would be overriden in subclasses
_replacementDelegate = new FilterReplacementDelegate(FilterString);
//if a request sets this header is present this request will not be altered
var ingnor = context.Request.Headers.Get("IGNORINJECT");
var ingorRequest = false;
if (!string.IsNullOrEmpty(ingnor))
{
if(!bool.TryParse(ingnor,out ingorRequest))
{
ingorRequest = false;
}
}
var enableLog = ConfigurationManager.AppSettings.Get("BannerInjectEnableLog") ?? "false";
var loggingEnabled = false;
bool.TryParse(enableLog, out loggingEnabled);
//This can be an app level or Machine level configuration , a comma delimted string of
//file extensions that are execluded from processing
var fileExt = ConfigurationManager.AppSettings.Get("BannerInjectExcludedFiles");
var excludedFileTypes = new string[]
{
".css", ".js", ".jpg", ".png",
".ico", ".map", ".eot", ".svg",
".ttf", ".woff", ".woff2", ".json"
};
var endPointsIgnore = new string[] { "jquery", "css" };
if(endPointsIgnore.Any( (c=> {
return context.Request.Url.PathAndQuery.ToLower().Contains(c);
})))
{
return;
}
if (!string.IsNullOrEmpty(fileExt))
excludedFileTypes = fileExt.Split(',');
if (loggingEnabled)
{
eventLog.WriteEntry($"Trying to process request {context.Request.CurrentExecutionFilePath}", EventLogEntryType.Information);
}
//styles.eecf6feb6435acaa19af.css
var ext = Path.GetExtension(context.Request.CurrentExecutionFilePath);
//Dont want any JS or CSS files
if (!excludedFileTypes.Contains(ext))
{
//If the IGNORINJECT banner present ignore the request
if (ingorRequest == false)
{
if (loggingEnabled)
{
eventLog.WriteEntry($"Processing Request {context.Request.CurrentExecutionFilePath}", EventLogEntryType.Information);
}
// construct the filtering stream, taking the existing
// HttpResponse.Filter to preserve the Filter chain;
// we'll also pass in a delegate for our string replacement
// function FilterString(), and the character encoding object
// used by the http response stream. These will then be used
// within the custom filter object to perform the string
// replacement.
_filterStream = new InjectFilterStream(
context.Response.Filter
, _replacementDelegate
, context.Response.ContentEncoding);
context.Response.Filter = _filterStream;
}
}
}
}
public InjectModuleBase()
{
eventLog.Source = "Application";
}
// required to support IHttpModule
public void Dispose()
{
}
public void Init(HttpApplication app)
{
// setup an application-level event handler for BeginRequest
app.BeginRequest += (new EventHandler(this.Application_BeginRequest));
app.PostMapRequestHandler += App_PostMapRequestHandler;
app.AcquireRequestState += App_AcquireRequestState;
app.PostAcquireRequestState += App_PostAcquireRequestState;
app.EndRequest += App_EndRequest;
}
private void App_EndRequest(object sender, EventArgs e)
{
}
private void App_PostAcquireRequestState(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)sender).Context;
UpdateResponse(context);
}
private void App_AcquireRequestState(object sender, EventArgs e)
{
}
private void App_PostMapRequestHandler(object sender, EventArgs e)
{
}
private void Application_BeginRequest(object source, EventArgs e)
{
}
// This is the function that will be called when it's time to perform
// string replacement on the web buffer. Subclasses should override this
// method to define their own string replacements
protected virtual string FilterString(string s)
{
// by default, perform no filtering; just return the given string
return s;
}
}
This is the stream implementation class same as the code project article
public delegate string FilterReplacementDelegate(string s);
public class InjectFilterStream : Stream
{
// the original stream we are filtering
private Stream _originalStream;
// the response encoding, passed in the constructor
private Encoding _encoding;
// our string replacement function for inserting text
private FilterReplacementDelegate _replacementFunction;
// for supporting length & position properties, required when inheriting from Stream
private long _length;
private long _position;
MemoryStream _cacheStream = new MemoryStream(5000);
int _cachePointer = 0;
// constructor must have the original stream for which this one is acting as
// a filter, the replacement function delegate, and the HttpResponse.ContentEncoding
public InjectFilterStream(Stream originalStream, FilterReplacementDelegate replacementFunction, Encoding encoding)
{
// remember all these objects for later
_originalStream = originalStream;
_replacementFunction = replacementFunction;
_encoding = encoding;
}
// properties/methods required when inheriting from Stream
public override bool CanRead { get { return false; } }
public override bool CanSeek { get { return true; } }
public override bool CanWrite { get { return true; } }
public override long Length { get { return _length; } }
public override long Position { get { return _position; } set { _position = value; } }
public override int Read(Byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin direction)
{
return _originalStream.Seek(offset, direction);
}
public override void SetLength(long length)
{
_length = length;
}
public override void Flush()
{
_originalStream.Flush();
}
// override the Write method to inspect the buffer characters and
// perform our text replacement
public override void Write(byte[] buffer, int offset, int count)
{
// we want to retrieve the bytes in the buffer array, which are already
// encoded (using, for example, utf-8 encoding). We'll use the
// HttpResponse.ContentEncoding object that was passed in the
// constructor to return a string, while accounting for the character
// encoding of the response stream
string sBuffer = _encoding.GetString(buffer, offset, count);
// having retrieved the encoded bytes as a normal string, we
// can execute the replacement function
string sReplacement = _replacementFunction(sBuffer);
// finally, we have to write back out to our original stream;
// it is our responsibility to convert the string back to an array of
// bytes, again using the proper encoding.
_originalStream.Write(_encoding.GetBytes(sReplacement)
, 0, _encoding.GetByteCount(sReplacement));
}
}
and this is the Module that ties it all together
public class HtmlInjectModule : InjectModuleBase
{
EventLog eventLog = new EventLog("Application");
string _banner = "<div class='hsftic_cui_banner' style=' padding: 10px;background-color: #ff9800;color: white;'>" +
"<span class='cui_closebtn' style='margin-left: 15px;color: white;font-weight: bold;float: right;font-size: 22px;line-height: 20px;cursor: pointer;transition: 0.3s;' onclick=\"this.parentElement.style.display='none';\">×</span> " +
"<strong>{0}</strong></div>";
private string FixPaths(string Output)
{
string path = HttpContext.Current.Request.Path;
if (path == "/")
{
path = "";
}
Output = Output.Replace("\"~/", "\"" + path + "/").Replace("'~/", "'" + path + "/");
return Output;
}
protected override string FilterString(string s)
{
eventLog.Source = "Application";
var Message = ConfigurationManager.AppSettings.Get("BannerInjectMessage") ?? "Admin Message";
var enableLog = ConfigurationManager.AppSettings.Get("BannerInjectEnableLog") ?? "false";
var copyRequest = ConfigurationManager.AppSettings.Get("BannerInjectCopyRequest") ?? "false";
var loggingEnabled = false;
var copyRequestEnable = false;
bool.TryParse(enableLog, out loggingEnabled);
bool.TryParse(copyRequest, out copyRequestEnable);
_banner = string.Format(_banner, Message);
if (loggingEnabled) {
eventLog.WriteEntry("Trying to add banner", EventLogEntryType.Information);
}
if (copyRequestEnable)
{
if (!Directory.Exists("C:\\temp"))
{
Directory.CreateDirectory("C:\\temp");
}
using(var str = File.Create($"C:\\temp\\{Path.GetFileNameWithoutExtension(Path.GetRandomFileName())}.txt")){
using (var sw = new StreamWriter(str))
{
sw.Write(s);
sw.Flush();
sw.Close();
}
}
}
var html = s;
html = FixPaths(html);
Regex rBodyBegin = new Regex("<body.*?>", RegexOptions.IgnoreCase);
Match m = rBodyBegin.Match(html);
if (m.Success)
{
string matched = m.Groups[0].Value;
html = html.Replace(matched, matched + _banner);
if (loggingEnabled)
{
eventLog.WriteEntry("added banner", EventLogEntryType.Information);
}
}
return html;
}
}
Now I am sure someone will say I need to use middleware for .net core apps , I know that , I just need to be able to use this with out modifying existing applications , my goal is to add something to the that is server wide and address each application on its own release schedule.
in summary , the above code works 100% of the time for ASP.NET apps , 30% of the time on Blazor Server APP and 10% of the time on ASP.NET Core apps.
Any insight would be greatly appreciated..
I found no solution to why this was not working 100% of the time. So I ended up using the URL Rewrite Module in IIS and creating a couple of rules. One for dealing with compression and the other for adding the banner.
Here is the XML that would go in the applicationHost.config or webConfig
<rewrite>
<globalRules>
<rule name="RemoveAcceptEncoding">
<match url="(.*)" />
<serverVariables>
<set name="HTTP_X_ORIGINAL_ACCEPT_ENCODING" value="{HTTP_ACCEPT_ENCODING}" />
<set name="HTTP_ACCEPT_ENCODING" value="" />
</serverVariables>
<action type="None" />
</rule>
</globalRules>
<allowedServerVariables>
<add name="HTTP_ACCEPT_ENCODING" />
<add name="HTTP_X_ORIGINAL_ACCEPT_ENCODING" />
</allowedServerVariables>
<outboundRules>
<rule name="RestoreAcceptEncoding" preCondition="NeedsRestoringAcceptEncoding">
<match serverVariable="HTTP_ACCEPT_ENCODING" pattern="^(.*)" />
<action type="Rewrite" value="{HTTP_X_ORIGINAL_ACCEPT_ENCODING}" />
</rule>
<preConditions>
<preCondition name="IsHtml">
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
</preCondition>
<preCondition name="NeedsRestoringAcceptEncoding">
<add input="{HTTP_X_ORIGINAL_ACCEPT_ENCODING}" pattern=".+" />
</preCondition>
</preConditions>
<rule name="BannerInject" preCondition="IsHtml" enabled="true">
<match filterByTags="None" pattern="<body(\s*[^>]*)>" negate="false" />
<action type="Rewrite" value="<body{R:1}><div class='hsftic_cui_banner' style='position:absolute;top:0px;left:0px;width:100%;z-index:2147483647; padding: 7px;background-color: #ff9800;color: white;'><span class='cui_closebtn' style='margin-left: 15px;color: white;font-weight: bold;float: right;font-size: 18px;line-height: 20px;cursor: pointer;transition: 0.3s;margin-right:5px' onclick="this.parentElement.style.display='none';">&times;</span><strong>BANNER TEXT HERE</strong></div>" />
</rule>
</outboundRules>
</rewrite>
Best of all no code to trouble shoot and this works 100% of the time