January 2018 : Instructor-led Online Course in ASP.NET Core 2.0. Conducted by Bipin Joshi. Read more...
Registration for January 2018 batch of ASP.NET Core 2.0 instructor-led online course has already started. Conducted by Bipin Joshi. Register today ! Click here for more details.

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.

 


Bipin Joshi is a software consultant, an author and a yoga mentor having 22+ years of experience in software development. He also conducts online courses in ASP.NET MVC / Core and Design Patterns. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Meditation and Mindfulness to interested individuals. To know more about him click here.

Get connected : Twitter  Facebook  Google+  LinkedIn

Posted On : 18 November 2013


Tags : ASP.NET Web Forms