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.
|