Drew asked the following question regarding "one-time" URLs:
Is there a way to accomplish generating One-Time URLs with Coldfusion? I have found an article on how to do this with PHP. But this would be a handy feature for selling GIS maps for my work place. Any insight on how to do such a thing with CF?
Of course! Anything PHP can do ColdFusion can do quicker and easier! (Ok, before the hate mail comes in from the PHP crowd, please note that I'm kidding. Kinda.)
In the article, the author describes creating a text file to store a set of valid unique tokens. If you pass in a valid token, you get access to the secret file. Let's talk about how this could be done in ColdFusion. Also, let's not limit our code to just one file but to any file you want protected.
First off - in general, I don't like storing information in text files when the information needs to be updated frequently. It is always a pain to deal with the locking when reading and writing, so in general, I tend to avoid it. Let's use a database table for our solution. I have to be a bit generic since obviously this is a made up solution, but hopefully it will give you an idea of how to proceed.
My table will have two fields. The first field is the unique token. I'm going to use ColdFusion's built in UUID support, so I need one 35 character string field. I will call this the token field. The other column will need to either be a filename, or pointer to the resource you want to serve up. To make things simple, I will call this the filename column. Last but not least, the table itself will be called onetimeurls. (That's really a bad name, but hey, it's Sunday.)
So, I have table. How would I generate the UUID and store the record? Here is one way it could be done:
<cfset token = createUUID()>
<cfset filename = "somefile">
<cfquery datasource="somedsn">
insert into onetimeurls(token,filename)
values(
<cfqueryparam cfsqltype="cf_sql_varchar" value="#token#" maxlength="35">,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#filename#" maxlength="255">)
</cfquery>
<cfset onetimeurl = "http://wwww.yourhost.com/get.cfm?token=#token#">
<cfoutput>
You may download your file here:<br />
<a href="#onetimeurl#">#onetimeurl#</a>
</cfoutput>
(Quick note: I typed this by hand so please forgive any typos.) Nothing too crazy here. I created the token with createUUID(). The filename value was hard coded, but obviously in a real application it would not be. The token/filename pair are then inserted into the database. Lastly, I created a URL pointing at a file named get.cfm. This could be emailed as well.
So, what would get.cfm do?
<cfif not structKeyExists(url, "token") or not isValid("uuid", url.token)>
<cflocation url="/" addToken="false">
</cfif>
<cfquery name="validToken" datasource="somedsn">
select filename
from onetimeurls
where token = <cfqueryparam cfsqltype="cf_sql_varchar" value="#url.token#" maxlength="35">
</cfquery>
<cfif validToken.recordCount is 1>
<cfquery datasource="somedsn">
delete from onetimeurls
where token = <cfqueryparam cfsqltype="cf_sql_varchar" value="#url.token#" maxlength="35">
</cfquery>
<cfheader name="Content-disposition" value="attachment;filename=#getFileFromPath(validtoken.filename)#">
<cfcontent file="#validtoken.filename#" type="application/unknown">
<cfelse>
Sorry, but your URL did not work. Please be sure you
entered it correctly.
</cfif>
My file starts by doing some simple validation. URL.token must exist, and it must be a valid UUID. I then check to see if a corresponding record exists in the database. I retrieve the filename value since I will need it. If it does exist, I immidiately delete it (remember, this is a one time url feature) and then serve up the file using cfheader/cfcontent. If their token did not exist, I display a message to make sure they used the URL correctly.
Obviously there are different ways of doing this. It need not be a "one time url", but could allow for 5 downloads. In that case, you would just add a new column to store the number of times the file was downloaded. You could also add a "validTo" column with a datetime stamp. This would allow downloads until a certain date. Another option - add a password column. This way the file could be downloaded with the addition of a password.
Anyone doing something like this? If so, please share.
Archived Comments
Ray,
I would make one change - instead of sending the real filename as the attachment name in the header, I'd change it to a uuid filename with the real files extension.
<cfset tmpFileName = "#createuuid()#.#listlast(validtoken.filename, '.')#">
<cfheader name="Content-disposition" value="attachment;filename=#tmpFileName#">
<cfcontent file="#validtoken.filename#" type="application/unknown">
Why? if the file is outside of web root, you knowing just the file name will not let someone else get it, or let you get it again.
touché
Still a good point _if_ the file was under file root. People on ISPs sometimes would be in that situation.
@ Raymond
You'd probably want to add a named <cflock> to the get.cfm template as well so to avoid potentially having multiple hits to the same URL.
Dan, I thought of that, but, it occured to me these URLs are unique. So it would only occur if the user itself clicked multiple times.
Guys - I set up the new mail server here. Would you let me know i fyou get this please.
How many users will this technique support? It uses a CF thread for each download and it could easily choke on large files or multiple simultaneous downloads.
We've switched to a FTP server that uses an ODBC database for the user accounts (SurgeFTP or Serv-U), create a randomized user account on-the-fly with privileges set to download only with a upload/download ratio of 1:1... which means that the download link will only work once and all of the heavy lifting is removed from ColdFusion and the web server.
The database has a datestamp field and is used for a clean-up script that deletes old user accounts (1:1 ratio used up) and the download file is also deleted.
I really want to figure out how to protected downloads using Flash so that Flash can report back to the server after a successful download and automate the clean-up process. This will also remove the heavy lifting from CF (but not the webserver.) There is a java applet that will do this... but I don't want to force someone to install java just to download a file whereas most users already have Flash installed.
While I don't think it would choke on multiple downloads, I wouldn't use CF to download large files. For that you can consider other solutions like the one you provided.
What happens when you use the CFCONTENT tag? Isn't a ColdFusion thread used during the entire download process? If you have the server configured to handle 8 simulutaneous requests, won't your website be unresponsive if 8 large files (er, requests) are currently in use by multiple 56k modem users?
I'll only use CFCONTENT for extremely small files or dynamically generated content (ie excel spreadsheet). When it comes to large files (or very popular files), I'll use the hash technique and then CFLOCATE to either a password protected FTP account (after email address verification testing is successful) using my instructions (above) or anonymous FTP with a hashed filename.
It only becomes a real issues when the content being downloaded is either being personalized or is being paid for.
I checked and you are right. I'd probably suggest raising the number of threads and ensuring you aren't serving up large files.
I'm developing something similar. Serving paid for audio files via a UUID to an mp3 file above the root using a db for lookup.
I'm concerned about the comments on file size and threads. Being a comsumer site I can't rely on an ftp app being available.
Any other solutions?
dickbob
there is a way for Generating One-Time URLs with java?