用于横幅注入的 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';\">×</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="<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>
最棒的是没有代码可以解决问题,这在 100% 的时间都有效
我有一个 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';\">×</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="<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>
最棒的是没有代码可以解决问题,这在 100% 的时间都有效