Search code examples
c#asp.netasp.net-coreblazor-server-sidehttpmodule

HttpModule For Banner Injection


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';\">&times;</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..


Solution

  • 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="&lt;body(\s*[^>]*)>" negate="false" />
    
                    <action type="Rewrite" value="&lt;body{R:1}>&lt;div class='hsftic_cui_banner' style='position:absolute;top:0px;left:0px;width:100%;z-index:2147483647; padding: 7px;background-color: #ff9800;color: white;'>&lt;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=&quot;this.parentElement.style.display='none';&quot;>&amp;times;&lt;/span>&lt;strong>BANNER TEXT HERE&lt;/strong>&lt;/div>" />
    
                </rule>
    
             </outboundRules>
    
        </rewrite>
    

    Best of all no code to trouble shoot and this works 100% of the time