用于横幅注入的 HttpModule

HttpModule For Banner Injection

我有一个 windows 服务器 运行ning IIS 10,上面有 100 多个应用程序。应用程序 运行 从 .net 2.0 到 .net core 3.1 再到 Blazer 服务器端应用程序的策略。我最近收到了一项任务,要求我注入一个静态横幅,上面写着一些“美国政府警告”。我遇到的问题是我的托管处理程序正在间歇性地成功工作。我编写的托管 HTTP 处理程序基于此代码项目文章 https://www.codeproject.com/Articles/11662/ASP-NET-Watermarker-Module

当我说模块间歇性工作时,我的意思是 HTML 被注入,一些请求而不是其他请求,例如,我可以刷新我的应用程序 10 次,只有 2 次横幅被注入。

此外,这些应用程序都计划进行修改,以便它们可以自己插入横幅 100 多个应用程序的问题我们没有时间在 12 月截止日期之前将它们全部修改并部署到生产环境.

这是我的代码,希望有人能指出我哪里错了。

这是代码项目文章

中完成的基本模块class
 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;
    }
}

这是流实现class与代码项目文章相同

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));

    }
}

这是将它们联系在一起的模块

 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;
    }
}

现在我确定有人会说我需要为 .net 核心应用程序使用中间件,我知道,我只需要能够在不修改现有应用程序的情况下使用它,我的目标是添加一些东西到这是服务器范围内的,并根据自己的发布时间表处理每个应用程序。

总而言之,上述代码 100% 的时间用于 ASP.NET 应用程序,30% 的时间用于 Blazor 服务器应用程序,10% 的时间用于 ASP.NET 核心应用程序。

任何见解将不胜感激..

我没有找到解决为什么这不是 100% 的时间工作的方法。所以我最终在 IIS 中使用 URL 重写模块并创建了一些规​​则。一个用于处理压缩,另一个用于添加横幅。

这是 XML 将进入 applicationHost.config 或 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>

最棒的是没有代码可以解决问题,这在 100% 的时间都有效