Pete Brown's irritatedVowel.com
   home    wallpaper    railroad    .net, c#/vb    photography    birds    psp tubes    home/wood projects    games    recipes  
about   |   privacy   |   guestbook   |   pete's blog          
AddThis Social Bookmark Button

Section Contents
Naming Conventions and Standards
Best Practices
Links
Demos
HTTPModule Example
ASP.Net Cache API
AddThis Social Bookmark Button

ASP.Net HTTP Module to Prevent Deep Linking

[ Note : This article assumes that you are running ASP .NET on an IIS box with Windows 2000 or above ]

A common problem on IIS servers is how to prevent "deep linking".  In my specific case, I wanted to prevent people from using my images as the background for their web site, or posting them out of context in email messages, bulletin boards etc.  Of course, deep-linking is only one way to handle that.  Anyone on the internet could very easily copy and paste the image into their email message or their web site directory.  Nothing (not even those obnoxious right-click prevention scripts, that you can overcome in two seconds) can prevent that.  However, by preventing deep-linking, I at least save my bandwidth. 

I evaluated writing both HTTP Modules (IHttpModule) and HTTP Handlers (IHttpHandler) for this little project, and found that HTTP Modules are appropriate for this task.  For the purposes of this brief article, I will assume you have read about the ASP.NET processing pipeline via other sources.  I will not repeat all of that information here.  All the code here is in C#, but it translates quite easily to Visual Basic .Net

How it Works

When a request is made to my site, the HTTP Module I wrote is part of the pipeline for processing the request. My HTTP Module checks the configuration file to see if the request matches criteria that is specified in the file, and if so, will either allow the request to proceed normally, or will redirect the request to this small 8k image

Considering many of the larger images on my wallpaper site are 500k+, and the majority of the other images are in the 100 to 250k range, this saves me a ton of bandwidth.

Pretty simple stuff, especially if you're used to servers like Apache which allow you to handle this via configuration files with no code.  Before ASP .NET, you were likely writing an ISAPI filter to handle this redirect.

 

Disclaimer

Forcing all image requests to go through the ASP.NET pipeline exacts a noticeable performance penalty.  I have not checked to see if it is any better in Windows Server 2003, but it appears to be marginally faster on that OS.  Use this code at your own risk.

 

Code

The code may not be beautiful, but I'm not planning on entering it in any beauty contests ;-)

Class Definition

Any class that is to be an HTTP Module must implement the IHttpModule interface.

public class DeepLinkingModule : IHttpModule

Interface Member Functions

You are required to implement both methods of the IHttpModule interface. The Init method is where you should set up your event handlers. The Dispose method is where you should release any unmanaged resources.  In my case, I only need to hook into the BeginRequest event, as I want to control what happens when a request is made for a picture on my site.

public void Init(HttpApplication app)
{
       app.BeginRequest += new EventHandler(BeginRequest);
}
 
public void Dispose()
{
       // do nothing
}

BeginRequest Event Handler

The BeginRequest Event Handler simply handles the event and calls the AuthorizedReferrer function.  The AuthorizedReferre function is described in the next block.

void BeginRequest(Object source, EventArgs e)
{
       HttpApplication app = (HttpApplication) source;
       Uri requestURL = app.Request.Url ;
       Uri referrerURL = app.Request.UrlReferrer ;
 
       string redirectURL = string.Empty;
      
       if (!AuthorizedReferrer(requestURL, referrerURL, app, out redirectURL))
       {
              app.Response.Redirect(redirectURL) ;
       }
}

AuthorizedReferrer Work Function

This is where the meat of the processing occurs.  Watch out for line wrap.  All I'm doing here is checking to see if the referrer is one of the referrers listed in my configuration XML file.

bool AuthorizedReferrer(Uri requestURL, Uri referrerURL, HttpApplication app, out string redirectURL)
{
       bool authorized = true ;
 
       System.Xml.XmlDocument doc = new XmlDocument() ;
 
       redirectURL = String.Empty ;
 
       try
       {
              string requestFolder = GetFolderPathFromRequestURL(requestURL, app) ;
              string fileName = GetFileNameFromRequestURL(requestURL, app) ;
              string referrerName ;
 
              if (referrerURL == null)
                     referrerName = "" ;
              else
                     referrerName = referrerURL.ToString() ;
 
              doc.Load(requestFolder + "YourSpecialConfigFile.xml") ;
 
              try
              {
                     System.Xml.XmlNode root = doc.DocumentElement ;
 
                     foreach (System.Xml.XmlNode fileNode in root.ChildNodes)
                     {
                           string fileMask = fileNode.Attributes["mask"].InnerText.ToLower().Trim() ;
                           string redirectOnFailure = fileNode.Attributes["failure"].InnerText.ToLower().Trim() ;
 
                           if (FileNameMatchesMask(fileName, fileMask))
                           {
                                  authorized = false ;       // assume failure
                                  redirectURL = redirectOnFailure ;
 
                                  foreach (System.Xml.XmlNode referrerNode in fileNode.ChildNodes)
                                  {
                                         string referrerMask = referrerNode.Attributes["referrer"].InnerText ;
 
                                         if (referrerNode.Name.ToLower() == "allow")
                                         {
                                                if (ReferrerMatchesMask(referrerName, referrerMask))
                                                {                                                                                        authorized = true ;
                                                }
                                         }
                                         else if (referrerNode.Name.ToLower() == "deny")
                                         {
                                                if (FileNameMatchesMask(fileName, fileMask) && ReferrerMatchesMask(referrerName, referrerMask))
                                                {
                                                       authorized = false ;
                                                }
                                         }
                                  }
                           }
                     }
              }
              catch (Exception)
              {
                     authorized = true ;
              }
 
       }
       catch (FileNotFoundException)
       {
              // swallow it. There is no deep link config file in the folder.
              authorized = true ;
 
       }
 

return authorized ;
}

Support Functions

Yes, I cheated and used the Visual Basic "Like" operator from C#.  Why didn't I use regex?  Simply because I wanted to use "normal" wildcards as are commonly used when dealing with filenames.  Note the use of the Path object to save on LOC in the GetFolderPathFromRequestURL and GetFileNameFromRequestURL functions.

bool FileNameMatchesMask(string fileName, string mask)
{
       return Microsoft.VisualBasic.CompilerServices.StringType.StrLike(fileName.ToLower(), mask.ToLower(), Microsoft.VisualBasic.CompareMethod.Text) ;
}
 
bool ReferrerMatchesMask(string referrerName, string mask)
{
       return Microsoft.VisualBasic.CompilerServices.StringType.StrLike(referrerName.ToLower(), mask.ToLower(), Microsoft.VisualBasic.CompareMethod.Text) ;
}
 
             
string GetFolderPathFromRequestURL(Uri requestURL, HttpApplication app)
{
       string fullPath = requestURL.AbsolutePath.Replace(@"/", @"\") ;
       fullPath = app.Server.MapPath(fullPath) ;
 
       return Path.GetDirectoryName(fullPath) + @"\" ;

}
string GetFileNameFromRequestURL(Uri requestURL, HttpApplication app)
{
       string fullPath = requestURL.AbsolutePath.Replace(@"/", @"\") ;
       fullPath = app.Server.MapPath(fullPath) ;
       return Path.GetFileName(fullPath) ;

}

Configuration Files

This is what the configuration file looks like.  I put one of these in each directory where I wish to prevent deep-linking.  Since my wife posts bird photos to the Birds and Blooms bulletin board, I have specifically set that up as an allowed referrer.  Notice I also had to set my own site as an allowed referrer.    I originally named the files with a .XML extension, but later changed it to .config..

I also removed the "deny referrer=" bit later as well.  As it turns out, many folks have either deliberately or accidentally broken http referrer logic in their browsers so that the referrer returns nothing.  Some firewall software also blocks http referrer as well.  Finally, note that more often than not, the first request for the image suceeds and returns back the image the user wants, but a refresh or subsequent requests will do what I want it to do - return back my lightweight "unauthorized" image.  I have yet to figure out exactly what causes this behavior.

<?xml version="1.0" encoding="utf-8" ?>
<authorization>
      <file mask="*.jpg" failure="/Images/my_invalid_deep_link_jpg.jpg">
            <allow referrer="http://www.irritatedvowel.*" />
            <allow referrer="http://www.irritablevowel.*" />
            <allow referrer="http://bbs.reimanpub.com/*" />
            <deny referrer="" />
      </file>

</authorization>

Web.Config

You need to register your HTTP Module with ASP .NET. You do this via either machine.config, or as I did in this case, web.config. The first part of the "type=" attribute is the class name, the second is the assembly name.

<system.web>
       <httpModules>
              <add name="DeepLinkingModule"
       type="IrritatedVowel.DeepLinkingHandler.DeepLinkingModule, IrritatedVowel.DeepLinkingHandler"/>
</httpModules>
...

IIS Configuration

ASP .NET normally does not process JPG files or other static files.  If ASP .NET does not process the request for the file, your HTTP Module will not be called.  Therefore, you have to tell IIS to send all requests for, in this case JPG files, through ASP .NET.  Please note, I have not investigated the performance implications of doing this on a busy site beyond simply understanding that there is a performance hit, you will need to do any further evaluation yourself.

In IIS, click on the web site you wish to configure, and select properties.  Select the "Home Directory" tab, and then click the configuration button.  You will see the dialog to the left

Note that in this screenshot, JPG files have already been set to be processed by ASP.Net

Select the .aspx file and click "edit" to view the mapping.  Write down the location of the ASP.Net Isapi extension.  Close the edit dialog.

Click the "Add" button to add a new extension

 

 

 

 

 

In my version, on Windows XP remotely configuring a Windows 2000 box, I could not copy and paste the executable name, that is why I mentioned writing it down.  Enter the full path to the aspnet_isapi.dll in the executable text box.

Type the file extension in the extension box.  Do not use wildcards to the left of the period.  If you do, OK will be grayed-out.

GET and HEAD are sufficient for the JPG files I am mapping.  If you are mapping a different type of file, you will need to evaluate what types of requests you want to pass through to ASP .NET

Click OK to accept.

Restart IIS (may not always be necessary, but a good idea anyway)

 

Optimizations

I later added an optimization which uses the Cache API. You can see the article on the cache API here .

One additional optimization you can make is to hard-code acceptance of your home domain into the check routines, and therefore bypass reading the XML file for requests that come from your own site.  I'll likely do that myself soon. 

Conclusion

Once you set up IIS to route requests through ASP.Net, you will be off and running.  There are many other uses for HttpModules including translating files from one type to another, security, and any other "filtering"-type tasks.  They're easy to write, much easier than ISAPI for normal folks, and you have access to all the same .Net tools you use to build any other .Net application.  There's no reason not to write one :-)

If you found this article to be helpful, be sure to drop me a note in my guestbook.

who's online