Saturday, December 29, 2012

Force a file download from an ASP.Net site

Hack the Gibson

When you use the Response.Buffer to download files, the user will be prompted to either Open | Save | Cancel.  There are instances where you would prefer to just have the user download the file without any interaction.  The obvious problem with this is the security flaw.  Not giving the user the option to “opt-out” can ethically be disturbing, however there are those times that there is no way around it and must be done.

When you download a file from the internet, the browser takes the Response and tries to determine the content-type.  This is so that it will know whether to render the contents or save the file to the HDD.  For instance, when you browse to a .html or .txt, the browser reads the header information and will render the object.  When you browse a .zip file the browser decides that it can not render the contents (without additional help) and will prompt you to save the file.
This is further complicated by content-types that can be both rendered or saved, for instance .pdf or .avi.

In our case, we’ll be telling the browser what to do with the content we are going to serve to it.  We do this by using the System.Web.UI.Page inheritance that is inherited in every .Net page.  We invoke the Response member of the inherited System.Web.UI.Page.

So now we get to the coding part.  Let’s jam up VS2010.  Create a new empty website.  Make sure you are set to VB and the .Net 4.0 Framework or <.

It really doesn’t matter what you have in the front end, we are concerned with sending a response before the page is rendered.   In which case we will use the Page_Load event.  This event is triggered after the page has been processed BUT BEFORE it has been rendered to the client. 
Of course we could have put our code in the button_click event, but for simplicity I am adding it to the page_load.

Before we go any further, we need to add an image file to the site (the one you want the user to download).  Right click your solution name in the Solution Explorer and then click Add Existing Item.  Now pick an image that has a relatively small name (for this example).  As you can see, my image is named gen_img.jpg:
image

 

Now we get to the code.  We will build the code as needed and then I will show the code in its entirety:
Protected Sub Page_Load(sender As Object, e As System.EventArgs) _
Handles Me.Load
End Sub



Here you can see that we have our Page_Load method declaration.  The first thing we need to do is define what file we want to render -



Protected Sub Page_Load(sender As Object, e As System.EventArgs) _
Handles Me.Load
Dim filename As String = "gen_img.jpg"
End Sub

Of course, we are setting this at runtime, but we could easily provide it programmatically upon a file selection (in a real world scenario).  Next, we need to clear the response.buffer.  The response.clear does this for us, to ensure it is empty.  This clears the body but keeps the headers in place.  This will suit our purpose:




Protected Sub Page_Load(sender As Object, e As System.EventArgs) _
Handles Me.Load
Dim filename As String = "gen_img.jpg"
Response.Clear()
End Sub



Now we need to let the client browser know what type of content to expect.  We do this with the Response.ContentType property:



Protected Sub Page_Load(sender As Object, e As System.EventArgs) _
Handles Me.Load
Dim filename As String = "gen_img.jpg"
Response.Clear()
Response.ContentType = "image/jpeg"
End Sub



These are called MIME-Types.  MIMES allow computers to determine how to handle content.  For example, the system will read the extension (in our case, it is .jpg) which it will then use a table that exists in the system to match it to its MIME-Type (in this case, it is image/jpeg).  THEN, it will look at another table and decide what application is the default application to handle the file.

Typically I employ my class library to match extension to mime-type.  Here, for simplicity (and because it is beyond the context of this example) I simply wrote it out.


The next code line is the most important in accomplishing the automagical download.  We are going to add a header:




Protected Sub Page_Load(sender As Object, e As System.EventArgs) _
Handles Me.Load
Dim filename As String = "gen_img.jpg"
Response.Clear()
Response.ContentType = "image/jpeg"
Response.AddHeader("Content-Disposition", _
String.Format("attachment; filename={0}", filename))
End Sub



We’ll discuss the Content-Disposition Header and what it takes to make it work correctly a little later, lets finish the code.  Finally, we need to write the local file to the Response stream:



    Protected Sub Page_Load(sender As Object, e As System.EventArgs) _
Handles Me.Load
Dim filename As String = "gen_img.jpg"
Response.Clear()
Response.ContentType = "image/jpeg"
Response.AddHeader("Content-Disposition", _
String.Format("attachment; filename={0}", filename))
Response.Write(Context.Server.MapPath(filename))
End Sub



Notice the Response.Write method takes a  parameter.  The parameter is the COMPLETE path to the local file you want to write to the stream (In our case, the path to the image file).  In the above code, I am using the base website directory as the path with the filename.  This is because it exists in my home directory of the website.  Suppose I had it in a subdirectory, then the code would look like, in code splice:




Response.Write(Context.Server.MapPath("~/images/" & filename))



Finally, we need to close the Response stream:



Protected Sub Page_Load(sender As Object, e As System.EventArgs) _
Handles Me.Load
Dim filename As String = "gen_img.jpg"
Response.Clear()
Response.ContentType = "image/jpeg"
Response.AddHeader("Content-Disposition", _
String.Format("attachment; filename={0}", filename))
Response.Write(Context.Server.MapPath(filename))
Response.End()
End Sub



This being our entire Page_Load method.  Now if you run this, in IE you will presented with the download manager (if it’s a small file) that has the downloaded file listed.  Now, adding Content-Disposition as a header to the Response stream for the client browser comes with some rules.




  • ContentType must be applied PRIOR to the Content-Disposition


  • The filename must use an ASCII character set


  • Internet Explorer 8.0 is the first stable release that can be used with Content-Disposition, prior releases may work but are not guarenteed.


  • The AddHeader tag should NOT contain any directory/path information



You can see how easy it is to manipulate the browser into automagically (forcing) a download.  So there is some ethical responsibility to be applied here. 
You can also see how easy it would be to apply this to a file in a database such as SQL.  Simply change the filename, Content-Type as fields to be read in and change the Response.Write to use the Byte array instead of a physical file.



And… That’s how easy it is…

Håþþ¥ .ñꆆïñg…

No comments:

Post a Comment