Our Staff Experience and Expertise

Secure File Downloads Using ColdFusion 7

It is occasionally necessary to control who can download a file. The typical method of doing this in a web application is to store the file somewhere outside of the document root and use an application server to process the request and output the file. Under ColdFusion, this is normally done with a combination of the <cfcontent> tag to reset page output and programmatically send the file, and the <cfheader> tag to set HTTP headers like Cache-Control and Content-Disposition. You will usually use something like this to send an Excel spreadsheet and ask the browser to open it: 

<cfcontent type="application" reset="true" file="c:\myfile.xls" type="application/vnd.ms-excel">
<cfheader name="Content-Disposition" value="inline; filename="myfile.xls">

For the most part, this solution works pretty well. However, there are two issues that crop up with this approach: Memory usage, and threads.

Memory Overload

Prior to CF8, using &lt;cfcontent&gt; to send a file, actually attemptted to load the entire file into memory before sending it out. So if you had a 10MB file and 100 people were attempting to download it, you could be using ~1GB of RAM for all these downloads. If you had a 2GB or larger file, your download would probably not work because the JVM that underlies CF is only able to address a maximum of 2GB of RAM.

While CF8 64-bit removes this 2GB memory limitation on 64-bit systems, it also uses buffered I/O to send the file, only keeping a small portion of it in memory at time.

Thread Starved

If you have larger files you want to serve up along with a decent number of users you will likely run into a lack of threads. While the user is downloading the file through <cfheader>, CF is actually pumping out the bits, keeping the Java thread in use.

This can quickly lead to thread starvation, meaning other users on the system start experiencing extremely sluggish speeds and even errors, waiting for someone else's download to finish. This problem is present regardless of the CF version you're on.

Solutions

Re-Evaluate Your Security Requirements

In some instances absolute security is not required. When that's the case, you can work around these problems by using a solution such as Ben Nadel's Semi-Secure File Downloads.

mod_xsendfile

If you're an Apache user, there's a solution that may well fix your problem - mod_xsendfile. This Apache module allows the application server — be it PHP, CF, perl, etc. — to set an HTTP header that causes the web server to send the file on behalf of the application server.

This uses a minimal amount of memory, and it releases the application server thread allowing Apache to do what it does best — pump out the bits.

<cfcontent reset="true" type="application/vnd.ms-excel">
<cfheader name="x-sendfile" value="c:\myfile.xls">
<cfheader name="Content-Disposition" value="inline; filename="myfile.xls">

If you're not using Apache (I'm really surprised there isn't an IIS filter for this) or can't use mod_xsendfile for whatever reason, things get more complicated.

Custom Code

You can tune the thread count, but that can lead to other performance problems. You can grow your cluster (increasing the total number of available threads) or hand the downloads off to a dedicated download server — but these solutions all require additional hardware costs. Without a mod_xsendfile solution, or a bigger budget, you're pretty much stuck.

You could try implementing a download queue — thereby only allowing only a few downloads at any one time — but this is fraught with issues, like how to determine if a user has cancelled a download, or if a download is stalled. It's very easy to quickly run out of download slots, making it impossible for anyone to download.

We can however, help out with the memory situation. While we can't fix the 2GB limitation, we can implement a buffered I/O solution for CF7 using Java, keeping downloads to ~80k of memory use, regardless of the size of the files being downloaded or how long it takes to download them.

Let's get started with some code.

Buffered File Sender

We know we'll need the file name and mime type, just as &lt;cfcontent&gt; does. But how will we take control of the server output? Fortunately CF gives us access to the underlying java servlet PageContext (javax.servlet.jsp.PageContext) through the getPageContext() method, and the HTTPServeletResponse (javax.servlet.http.HttpServletResponse) via getPageContext().getResponse().getContext().

The instances of these classes will allow us to reset the output buffer, set the mime type, set other headers like Content-Length, and Content-Disposition, as well as flush the data in the buffer out over the network. Looping over a BufferedInputStream (java.io.BufferedInputStream) will allow us to read small chunks of data from the file, and write to the context's output (java.io.OutputStream).

package com.seguetech.coldfusion.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.BufferedInputStream;
// These imports require the jrun.jar to be on your classpath.
import javax.servlet.jsp.PageContext;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletResponse;
/**
* When a file download is passed via the CFCONTENT tag in CF7 and earlier,
* CF reads the entire file into memory before sending it out.
* This class can at least help with resolving the memory issue by using
* buffered i/o to read the file in, and explicitly flushing the output buffer.
*/
public class BufferedFileSender {
  // the default mime-type, if one is not specified.
  protected static final String mimeType = "application/octet-stream";
  // since this is a secure file download, we want to disable caching.
  protected static final boolean disableCaching = true;
  // by default, send as an "attachment", or download file, rather
  // than attempting to load the file in the browser window.
  protected static final boolean sendInline = false;
  public BufferedFileSender() {}
  public static void send(String fileName, PageContext context) throws Exception {
  BufferedFileSender.send(fileName,context,mimeType,sendInline,disableCaching);
  }
  public static void send(
   String fileName, PageContext context, String mimeType
  ) throws Exception {
   BufferedFileSender.send(fileName,context,mimeType,sendInline,disableCaching);
  }
  public static void send(
   String fileName, PageContext context, String mimeType, boolean sendInline
  ) throws Exception {
   BufferedFileSender.send(fileName,context,mimeType,sendInline,disableCaching);
  }
  public static void send(
   String fileName, PageContext context, String mimeType, boolean sendInline,
   boolean disableCaching
  ) throws Exception {
   // input file
   File inFile = new File(fileName);
   // clear the output buffer, if we've tried to send anything.
   context.getOut().clear();
   // use reflection to get the HttpServletResponse.
   // This method isn't part of the standard object, it's CF specific.
   HttpServletResponse response = (HttpServletResponse)context.getResponse().getClass().getMethod("getResponse", new Class[] {}).invoke(context.getResponse(), new Object[] {});
   // double set the content type, just in case.
   context.getResponse().setContentType(mimeType);
   response.setContentType(mimeType);
   /**
   * since setContentLength() only take an int, and file.length()
   * returns a long, we use setHeader instead and pass it manually
   * as a string, since that’s what setContentLength() would output
   * anyway.
   */
   response.setHeader(”Content-Length”, Long.toString(inFile.length()));
   // If caching is disabled, then output the headers that disable caching.
   if (disableCaching) {
    response.setHeader(
     “Expires”,
     “Sat, 6 May 1995 12:00:00 GMT”
  );
  response.setHeader(
   “Cache-Control”,
   “no-store, no-cache, must-revalidate”
  );
  // bend IE to our will.
  response.addHeader(
   “Cache-Control”,
   “post-check=0, pre-check=0″
  );
  response.setHeader(
   “Pragma”,
   “no-cache”
  );
  }
  // initialize the disposition.
  String disposition = null;
  // if inline is enabled, send it inline.
  if (sendInline) {
   disposition = “inline; filename=” + inFile.getName();
  }
  // otherwise send it as an attachment (download).
  else {
   disposition = “attachement; filename=” + inFile.getName();
  }
  // set the content-disposition, which will cause the file to be downloaded,
  // rather than open in the browser.
  response.setHeader(
   “Content-Disposition”,
   Disposition
  );
  // get a buffered input stream, read up to 64kb into memory at a time.
  BufferedInputStream bis = new BufferedInputStream(
   new FileInputStream(inFile),
   65536
  );
  // get the output stream
  OutputStream os = response.getOutputStream();
  // this is our binary data.
  int data;
  // this is a counter.
  int count = 0;
  // loop while we have data.
  while ((data = bis.read()) != -1) {
   // increment the count.
   count = count++;
   // write the data to the output stream.
   os.write(data);
   // perform an explicit flush to the client every 16k
   if (count == 16384) {
    count = 0;
    context.getOut().flush();
   }
   }
   // close the input stream.
   bis.close();
  /**
  * This method call releases the page context. This prevents any further
  * output to the browser, but unlike <cfabort> it does not prevent code from
  * running after this, which has some potentially interesting uses
  */
  context.release();
  }
}

Compile this class (requires jrun.jar) and drop it in your [JRUN_HOME]\servers\cfusion\cfusion-ear\cfusion-war\WEB-INF\lib folder (or the equivalent for your platform and installation), and restart your ColdFusion server to activate it. Once installed, you can run it like this:

<!---
method signature: send(fileName, pageContext, mimeTipe, displayInline, disableCaching)
fileName and pageContext are required, the others are customizable.
--->
<!--- plain download, uses application/octect stream as mime-type, sends with Content-Disposition as inline, and sets anti-caching headers --->
<cfset createObject("java","com.seguetech.coldfusion.util.BufferedFileSender").send
 (”c:\myfile.txt”,getPageContext(),true) />
<!— html download, uses text/html stream as mime-type, sends with Content-Disposition as attachment, and doesn’t send anti-caching headers —>
<cfset createObject(”java”,”com.seguetech.coldfusion.util.BufferedFileSender”).send
 (”c:\myfile.txt”,getPageContext(),”text/html”,false,true) />
<!— our original example, sends and excel spreadsheet inline —>
<cfset createObject(”java”,”com.seguetech.coldfusion.util.BufferedFileSender”).send
 (”c:\myfile.xls”,getPageContext(),”application/vnd.ms-excel”,true) />

 

0 Comments

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.