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
Best of all no code to trouble shoot and this works 100% of the time