Generating Temporary Download URLs
Sometimes you need to generate temporary links - URLs that expire after a
certain timespan - so that a resource indicated by the link can be accessed only
for a specific period. The URL if accessed after the stipulated time window
refuses to serve the intended resource. Consider, for example, a user
registration system that requires an email verification step. When the system
sends an automated email to the user you may want that the user should confirm
the email within two business days, otherwise the verification is suspended. The
user is then required to get a fresh verification email or contact the
administrator for further action. The same situation arises while downloading
files. You may want to giveaway a few copies of the software you are developing
and may want to generate temporary URLs to the downloads that expire after say
30 days.
Although there can be various approaches to generate such temporary URLs,
this article demonstrates a flexible and easy way to do so using ASP.NET
routing coupled with download token generation logic. To begin developing this
application, create an empty Web Forms application and add a SQL Server Database
to it. The database will have just one table - Downloads - and its ADO.NET
Entity Data Model is shown below:
As you can see the Downloads table contains eight columns - Id, DownloadTitle,
Url, DownloadToken, ExpireAfterDownload, ExpiryDate, Downloaded and Hits. The
DownloadTitle columns contains a user friendly title for the download. You can
use this title on some download page to point the user to the download file. The
Url column contains the actual URL of the file to be downloaded. This URL
will never be displayed to the end user; it used internally by your code to
access the download file. The DownloadToken column is very important because it
holds a unique, randomly generated token for each download link. This token is
visible to the user as a part of the download link you share with them. For
example, you may create a download like for the form: http://some_domain/downloads/ABCD1234.
In this URL ABCD1234 is a randomly generated token. You learn to generate such a
token later in this article. A download token can expire in two ways:
- It might have been marked to expire as soon as the user downloads a
file.
- It might have assigned a specific expiry date.
The former setting is controlled by ExpireAfterDownload column (bit data
type) and the later setting is controlled by ExpiryDate column (datetime data
type). Finally, the Hits column stores the number of times a URL was accessed.
Note that if you use ExpireAfterDownload setting you will also need to think
about situations where a user initiates the download but the download fails for
some reason.
Now, open Global.asax and add the following route mapping:
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapPageRoute("Downloads",
"downloads/{downloadtoken}",
"~/downloadfile.aspx");
}
This route mapping will enable you to generate URLs of this form:
http://localhost/downloads/<download_token>
The URLs of the above form will be mapped to downloadfile.aspx. You will
create downloadfile.aspx later in this article.
Now, add a Web Form (TemporaryUrlGenerator.aspx) to the project and design it
like this:
Clicking on the Create button generates a download token and thus a new
temporary URL. The click event handler of the Create button is shown below:
protected void Button1_Click(object sender, EventArgs e)
{
DownloadsDbEntities db = new DownloadsDbEntities();
Download d = new Download();
d.DownloadTitle = TextBox1.Text;
d.Url = TextBox2.Text;
d.ExpireAfterDownload = CheckBox1.Checked;
d.ExpiryDate = DateTime.Parse(TextBox3.Text);
d.DownloadToken = GetDownloadToken(10);
d.Hits = 0;
d.Downloaded = false;
db.Downloads.Add(d);
db.SaveChanges();
HyperLink1.NavigateUrl = string.Format("~/downloads/{0}", d.DownloadToken);
HyperLink1.Text = Page.ResolveClientUrl(HyperLink1.NavigateUrl);
}
The above code creates an instance of Download model class and sets its
various properties. Notice the code marked in bold letters. The DownloadToken
property is set to the return value of a helper function - GetDownloadToken().
The value of 10 supplied while calling GetDownloadToken() indicates the length
of the download token (10 characters in this case). Once various properties of
Download object are set, it is added to Downloads DbSet and SaveChanges() method
is called on the context to save the changes to the database.
The NavigateUrl and Text properties of a HyperLink control are set to
~/downloads/<download_token>.
The GetDownloadToken() helper function is discussed next:
private string GetDownloadToken(int length)
{
int intZero = '0';
int intNine = '9';
int intA = 'A';
int intZ = 'Z';
int intCount = 0;
int intRandomNumber = 0;
string strDownloadToken="";
Random objRandom = new Random(System.DateTime.Now.Millisecond);
while (intCount < length)
{
intRandomNumber = objRandom.Next(intZero, intZ);
if (((intRandomNumber >= intZero) &&
(intRandomNumber <= intNine) ||
(intRandomNumber >= intA) && (intRandomNumber <= intZ)))
{
strDownloadToken = strDownloadToken + (char)intRandomNumber;
intCount++;
}
}
return strDownloadToken;
}
The GetDownloadToken() method accepts an integer parameter - length - that
indicates the length of the download token. Inside, it stores the ASCII code for
0, 9, A and Z in four variables - intZero, intNine, intA and intZ. Then a Random
instance is created by passing the number of Milliseconds to its constructor.
This value will act as the seed value for the Random object. A while loop is
then used from 0 to the intended length value. With each iteration a new random
number is generated using Next() method of Random class. The parameters to the
Next() method indicate min value and max value. This random number is then
converted to char and appended to the downloadtoken variable. Finally,
GetDownloadToken() returns the generated download token. It must be remembered
that although this logic attempts to generate different download token every
time it is called, it is possible that duplicate download tokens get generated.
You should check the generated token against the database (not included in the
above code).
Next, add another Web Form to the project (DownloadList.aspx) that lists
available downloads in a GridView. This Web Form is shown below:
Various hyperlinks shown inside the GridView point to the downloads using the
URL format mentioned earlier: ~/downloads/<download_token>
Now, add one more Web Form to the project (DownloadFile.aspx). Recollect that
Global.asax maps incoming download requests to DownloadFile.aspx. Add the
following code in its Page_Load event handler:
protected void Page_Load(object sender, EventArgs e)
{
string downloadtoken=Page.RouteData.Values["downloadtoken"].ToString();
DownloadsDbEntities db = new DownloadsDbEntities();
var data = from d in db.Downloads
where d.DownloadToken == downloadtoken
select d;
Download obj = data.SingleOrDefault();
if(obj.ExpireAfterDownload.Value)
{
if(obj.Downloaded.Value)
{
Response.Redirect("~/DownloadError.aspx");
}
}
else
{
if(obj.ExpiryDate.Value<DateTime.Now)
{
Response.Redirect("~/DownloadError.aspx");
}
}
string path = Server.MapPath(obj.Url);
FileStream fs = File.OpenRead(path);
byte[] fileData = new byte[fs.Length];
fs.Read(fileData, 0, (int)fs.Length);
Response.Clear();
Response.AddHeader("Content-Type", "application/zip");
Response.AddHeader("Content-Disposition","inline;filename=" + Path.GetFileName(path));
Response.BinaryWrite(fileData);
Response.Flush();
Response.Close();
if(obj.ExpireAfterDownload.Value)
{
obj.Downloaded = true;
obj.Hits = obj.Hits + 1;
db.SaveChanges();
}
}
The DownloadFile.aspx doesn't have any UI as such. Its only purpose is to
serve the requested download file if the link has not yet expired. If a link has
expired it redirects the user to an error page. Inside the Page_Load event
handler, the code retrieves the downloadtoken route parameter using RouteData.
This will return the download token part from the URL. For example, if a URL is
/downloads/ABCD, it will return ABCD. Based on this download token the
corresponding record from the table is retrieved using the LINQ to Entities
query.
An "if" block then checks whether ExpireAfterDownload property of the
Download object is true. If so, it further checks Downloaded property. If the
Downloaded property is already true it means that the user has already
downloaded the file and hence cannot download it again. An error page is then
shown to the user. If ExpireAfterDownload is false, the code checks the
ExpiryDate property and determines whether the link has expired or not. If a
link has expired, an error page is displayed to the user. The following figure
shows the error page:
If the code so far concludes that the link is still active, the requested
file is read from its location. This is done using the Url property and
FileStream class. The response stream is then cleared and the entire file
content is written onto the response stream using BinaryWrite() method. The
response stream is then flushed and closed. Notice how the response headers -
Content-Type and Content-Disposition are set. These headers will cause the
browser to display download prompt to the user. The actual URL of the file is
not revealed to the user. Note that the code assumes that ZIP files are being
downloaded. You should change the MIME type as required.
If the link was marked for expiration after downloading it once, its
Downloaded property is set to true. Additionally, its Hits count is incremented.
The changes are saved to the database using SaveChanges() method.
That's it! Generate a few temporary URLs and try downloading the files using
the generated URLs. Test whether the expiration settings (ExpireAfterDownload
and ExpiryDate) work as expected.