Scott Hanselman

An IP Address Blocking HttpModule for ASP.NET in 9 minutes

December 15, '04 Comments [11] Posted in ASP.NET | HttpModule | Bugs
Sponsored By

I'm sure this has been done before, but it was faster to write it than to google for it. There's some IP Addresses that have been bothering me and I don't have access to a firewall or IIS at my ISP, so...

I can upload a text file called blockedips.txt to my site and the changes happen immediately.

    9 namespace YourModuleNameHere
   10 {
   11     public class IPBlackList : IHttpModule
   12     {
   13         private EventHandler onBeginRequest;
   14 
   15         public IPBlackList()
   16         {
   17             onBeginRequest = new EventHandler(this.HandleBeginRequest);
   18         }
   19 
   20         void IHttpModule.Dispose()
   21         {
   22         }
   23 
   24         void IHttpModule.Init(HttpApplication context)
   25         {
   26             context.BeginRequest += onBeginRequest;
   27         }
   28 
   29         const string BLOCKEDIPSKEY = "blockedips";
   30         const string BLOCKEDIPSFILE = "SiteConfig/blockedips.config";
   31 
   32         public static StringDictionary GetBlockedIPs(HttpContext context)
   33         {
   34             StringDictionary ips = (StringDictionary)context.Cache[BLOCKEDIPSKEY ];
   35             if (ips == null)
   36             {
   37                 ips = GetBlockedIPs(GetBlockedIPsFilePathFromCurrentContext(context));
   38                 context.Cache.Insert(BLOCKEDIPSKEY , ips, new CacheDependency(GetBlockedIPsFilePathFromCurrentContext(context)));
   39             }
   40             return ips;
   41         }
   42 
   43         private static string BlockedIPFileName = null;
   44         private static object blockedIPFileNameObject = new object();
   45         public static string GetBlockedIPsFilePathFromCurrentContext(HttpContext context)
   46         {
   47             if (BlockedIPFileName != null)
   48                 return BlockedIPFileName;
   49             lock(blockedIPFileNameObject)
   50             {
   51                 if (BlockedIPFileName == null)
   52                 {
   53                     BlockedIPFileName = context.Server.MapPath(BLOCKEDIPSFILE);
   54                 }
   55             }
   56             return BlockedIPFileName;
   57         }
   58 
   59         public static StringDictionary GetBlockedIPs(string configPath)
   60         {
   61             StringDictionary retval = new StringDictionary();
   62             using (StreamReader sr = new StreamReader(configPath))
   63             {
   64                 String line;
   65                 while ((line = sr.ReadLine()) != null)
   66                 {
   67                     line = line.Trim();
   68                     if (line.Length != 0)
   69                     {
   70                         retval.Add(line, null);
   71                     }
   72                 }
   73             }
   74             return retval;
   75         }
   76 
   77         private void HandleBeginRequest( object sender, EventArgs evargs )
   78         {
   79             HttpApplication app = sender as HttpApplication;
   80 
   81             if ( app != null )
   82             {
   83                 string IPAddr = app.Context.Request.ServerVariables["REMOTE_ADDR"];
   84                 if (IPAddr == null || IPAddr.Length == 0)
   85                 {
   86                     return;
   87                 }
   88 
   89                 StringDictionary badIPs = GetBlockedIPs(app.Context);
   90                 if (badIPs != null && badIPs.ContainsKey(IPAddr))
   91                 {
   92                     app.Context.Response.StatusCode = 404;
   93                     app.Context.Response.SuppressContent = true;
   94                     app.Context.Response.End();
   95                     return;
   96                 }
   97             }
   98         }
   99     }
  100 }

And in your web.config:

   42 <system.web>
   43    <httpModules>
   44         <add type="YourModuleNameHere.IPBlackList, YourAssemblyNameHere"
   45             name="IPBlackList" />
   46    </httpModules>
   47 </system.web>

No warrenty, express or implied. If it sucks or has bugs/security holes, let me know as it's 9 minutes work.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Wednesday, December 15, 2004 6:03:05 AM UTC
Thats cool Scott! We have something similar running under IIS as an ISAPI filter that we use to allow/disallow access to certain urls.

As someone who keeps meaning to get more into ASP.NET and doesn't seem to ever find the time, it's interesting to see this kind of solution, along with the cache use.

Nice use of 9 minutes!
Wednesday, December 15, 2004 3:10:48 PM UTC
FYI: Might not matter since you may be using the dotted-decimal IP representation, but the StringDictionary will force lowercase on all your keys. This may cause the ContainsKey method to return false unless you also lowercase the argument to ContainsKey.
jond
Wednesday, December 15, 2004 3:12:46 PM UTC
Doh! My bad...ContainsKey will lowercase the argument for you...sorry about that.
jond
Wednesday, December 15, 2004 6:53:57 PM UTC
Thanks for the code Scott. This will come in handy.
Friday, December 17, 2004 6:07:03 PM UTC
I read some guidance once that suggested using HttpApplication.CompleteRequest() instead of HttpResponse.End() as you do in line 94.

However, I don't think I fully understood the reason, or when it was appropriate to use one over the other, so I'm not saying your code is "wrong".

The only difference I can tell is that Response.End raises a ThreadAbortException, while CompleteRequest() just ends the request. And raising an exception is more expensive than not, so maybe the guidance was performance related?

Anyone have any input on this?
Friday, December 17, 2004 9:26:41 PM UTC
Good info....interesting....CompleteRequest is on the Application, and sets an internal bool:

public void CompleteRequest()
{
this._requestCompleted = true;
}


and HttpResponse.End does this (notice that it calls CompleteRequest() also...


public void End()
{
if (this._context.IsInCancellablePeriod)
{
InternalSecurityPermissions.ControlThread.Assert();
Thread.CurrentThread.Abort(new HttpApplication.CancelModuleException(false));
}
else if (!this._flushing)
{
this.Flush();
this._ended = true;
this._context.ApplicationInstance.CompleteRequest();
}
}


I'll do some testing and look at the differences...thanks!

Scott
Scott Hanselman
Sunday, December 19, 2004 7:25:20 PM UTC
What would it take to turn this into a module people can drop into any IIS website and turn on?

What would it take to make it reference some DNS based RBL?

Would that DNS RBL end comment spam for it's users?
Sunday, December 19, 2004 8:01:06 PM UTC
1. It already is a module you can drop into any (ASP.NET) IIS website. You're looking at it.
2. Show me a blacklist to point to.
3. Probably not.

Scott Hanselman
Monday, December 20, 2004 12:13:44 AM UTC
Sorbs has lists for open http proxies and open socks proxies: http://www.dnsbl.nl.sorbs.net/using.shtml
Sunday, January 16, 2005 6:55:49 PM UTC
Great code Scott. I've already had it in use for a couple weeks now. I was going through the code changing a couple things for a new implementation when I noticed tha common oversight at line 67:

line.Trim();
if (line.Length != 0)
{
retval.Add(line, null);
}

Just an explanation for anyone who does not know, since I brought it up. Trim() only returns a new trimmed string, it does not operate on the variable itself. You need to assign the Trim() method's return value to a string in order to get the trimmed value. If the 'line' variable only contained four spaces, it's tested length would still be 4. To trim the string the code should be:

line = line.Trim();
Sunday, January 16, 2005 8:42:30 PM UTC
You're totally right...brain fart. Thanks!
Scott Hanselman
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.