Introduction
Often we have to provide our customers with downloading links which must allow them to download specific files for each customer depending for example on the previously entered account or any registration information. The web page has following interface: on the first web page user must enter user name and on the next page we providing this user with the link to download a copy of an application. Once user downloaded application and started it he should see "Welcome" window with his name which was specified on the first web page.
There are lots of methods for implementing such functionality. One of them is to modify or recompile downloadable application / package with the user name information right before transmitting it from the server to client side.
This task can be performed in three simple steps:
- Load the downloadable file into the memory.
- Replace the specified number of bytes at the specified position in the file with new values.
- Combine and send the web response with modified file data to the client.
Lets go ahead and explore each of these steps.
Customizing the downloading process
In order to implement custom actions with downloadable resource we can use the Button or the LinkButton web controls which allow to implement the server code-behind for the click action of the control.
The whole process consists of two steps: combining of the web Response steam and providing this response with correct HTTP headers. The server response stream represents the file data to be sent to the web client. In order to provide this web client with information about the file name being transmitted and the MIME content type of the file data we must insert this information into the response HTTP header fields.
The code below demonstrates how to load the file located on the server into the stream and store it to the HTTP response stream.
private void lnkDownload_Click(object sender, System.EventArgs e) {
FileStream stream = new FileStream(Server.MapPath("TestDownload.exe"),
FileMode.Open,
FileAccess.Read, FileShare.Read);
try {
int bufSize = (int)stream.Length;
byte[] buf = new byte[bufSize];
int bytesRead = stream.Read(buf, 0, bufSize);
Response.OutputStream.Write(buf, 0, bytesRead);
Response.End();
}
finally {
stream.Close();
}
}
According to RFC 2616 and RFC 1806 we need to specify both the Content-Type and Content-Disposition header fields with the following information for transmitting of the binary data:
Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition", "attachment;filename=" + "TestDownload.exe");
Please put this code just before writing the data into the HTTP Response stream.
Modifying a file
There are some difficulties in determining of the place within the binary data where modifications need to be done. If you have an ordinal application executable file this place can be within the executable resources or at the random position within the code. This is mostly depending on the task you are trying to accomplish and can be changed for different downloadable files. Other solution is to prepare the batch file with parameters and use it to recompile your application or package with custom parameters.
Suppose we found right position within the file and need to replace original content with the new data entered by the customer:
private void PatchData(byte[] buf, string userName, int position) {
byte[] patch = Encoding.Unicode.GetBytes(userName);
System.Array.Copy(patch, 0, buf, position, patch.Length);
}
We are also assuming that file is not very large and can be loaded into one single memory buffer.
Since the downloadable executable files are often may be recompiled and replaced, the positions for patching also changes. So it would be a good idea to do not hard-code these parameters within the ASP.NET code-behind dll and put them e.g into the Web.config file.
Web.config file
...
<appSettings>
<add key="fileName" value="TestDownload.exe" />
<add key="position" value="3422" />
</appSettings>
...
private void lnkDownload_Click(object sender, System.EventArgs e) {
string fileName = ConfigurationSettings.AppSettings["fileName"];
int position = Convert.ToInt32(ConfigurationSettings.AppSettings["position"]);
FileStream stream = new FileStream(Server.MapPath(fileName), FileMode.Open,
FileAccess.Read, FileShare.Read);
try {
Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition", "attachment;filename=" + fileName);
int bufSize = (int)stream.Length;
byte[] buf = new byte[bufSize];
int bytesRead = stream.Read(buf, 0, bufSize);
PatchData(buf, edtUserName.Text, position);
Response.OutputStream.Write(buf, 0, bytesRead);
Response.End();
}
finally {
stream.Close();
}
}
Source Code and working sample
The current implementation of this algorithm has one restriction - in order to simplify this demo we did not implement the functionality for resuming of the downloading process. In case if your downloadable files are large, you might want to change this behavior and add resuming support. In order to implement the randon resource access functionality you will need to analyze the RANGE request header field. Within this request the web clients specify the range of bytes within the downloadable resource they need to download. The value of the RANGE field may consist of one or two digits, e.g. 1024-23544. This means that the web client is about to receive the amount of bytes between 1024 and 23544 bytes. Please see the Hypertext Transfer Protocol RFC documents to learn more about web range requests.
This code is constantly being refined and improved and your comments and suggestions are always welcome.