ISAPI Versus ASP File Uploads in Web Applications

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.



Click here for a larger image.

Graphs of uploading time needed in ASP/ISAPI.

Environment: ASP, ISAPI

Description

This article describes two ways to to upload images and files to your Web server, and the advantages and disadvantages of both.

Uploading files from a browser is often a Web site requirement. For instance, let’s say that you have created a Web site for a real estate company. The site contains listings of properties along with a picture of each property. Realtors add new listings through an administration section of the site. Adding text for the new listing is easy using an HTML form. Then one day a realtor tells you that she wants to upload a photo of a property for sale. How would you do that with ASP and ISAPI?

This article is based on articles from MSDN.

How to Use

Both methods use the same functional structure.

First, the user must complete the fields in the PostFile.asp form. The JavaScript ValidateForm function is used to validate the content of some needed fields. In the Upload.asp file, we call the Upload function to make the upload, followed by the SaveFile function to write the file to disk. That ASP uploading function is hosted in the _incUpload.asp file.

The uploading form.

Second, the user must receive the results of Upload.asp (in case of ASP) or UploadExt.dll (in case of ISAPI) in the ReceiveRedirect.asp file. It is possible to finish the execution in Upload.asp or UploadExt.dll, but in most cases a redirect is needed to catch all the parameters of the form to write the file on another server.

The ISAPI form uses two optional hidden paramaters to tell to DLL the directory to store the upload (if isn’t specified, the script uses c:\temp\) and the name of the ASP redirection page (if the user wants this).

The default value of the upload directory (the default is relative to the root site directory \Data\) are put in the form from sPathData in an ASP variable (defined in _include.asp).

The receiving asp page.

Your IIS anonymous user account, usually IUSR_machinename, must have write access to the directory in which you want to save the files. In order to execute the ISAPI extension, the UploadExt.dll file must have the "Scripts and Executables" Execute Permission from the IIS console manager.

ASP Upload Details

One of the most glaring omissions in older versions of Active Server Pages (ASP) is the ability to handle a file that’s posted from an HTML form. While the ASP Request object has always allowed easy access to every other type of form field, posted files have been strictly off limits to pure ASP. The usual solution has been to implement support for file uploads by using the Posting Acceptor or a third-party component. In this article, we’ll show you how easy it is to get around this limitation and retrieve a file (or files!) from the data posted to an ASP file.

Upload the file

The first requirement for this process is to have a HTML form that actually uploads (or “posts”) a file to your ASP page.

The ASP script that saves the file starts out by allowing the server a generous amount of time to process whatever file is uploaded to it. This is a very important consideration because it can take a long time to process a large file. If your script times out before the process finishes, the uploaded file will be lost! Next, the script sets up some needed constants and defines several functions. We’ll return to these functions in a moment, but for now, let’s move on to the main part of the script and see how this process really works.

Catch the POST

The first thing the script does is retrieve all of the data that was posted from the HTML file and place it into a variable called biData. This is done by using the BinaryRead method of the Request object. As the name implies, this method reads a specified number of bytes from the post request in their raw, binary form. The actual number of bytes to read is provided by the TotalBytes property of the Request object.

You have to transform all of that binary data into something a bit friendlier. This is done with a simple loop that marches through the extracted binary data and, using a series of calls to some binary data manipulation functions, converts the data into an easily readable format and places it into the variable PostData.

Breaking up the raw data

Once we have the posted data in ASCII form, it’s a fairly simple process to work through it and extract each form field from it. The first step is to determine whether our data is properly encoded. After that, we can determine what the boundary is between each form element.

Amazingly, both of these pieces of information can be found in one convenient place: the HTTP_CONTENT_TYPE header. (Yes, you can still get at headers after calling BinaryRead. You just can’t get at the Form or QueryString collections.) As you can see here, the structure of this information is very simple:

-----------------------------7d22151d40264

Given this simple structure, it’s easy to extract the information we need. First, we use the Split function to break up the information around the semicolon. This allows us to check the encoding type easily by checking the first element of the array created by calling Split. Assuming that the encoding type is what we expect (“multipart/form-data”), we then extract the boundary, using another call to Split.

Each chunk of form data is itself in two chunks. The first is an informational chunk that tells all about the form field (in other words, its name and any associated information). The second chunk is the actual form data that was transmitted.

Content-Disposition: form-data; name="RedirectPage"

ReceiveRedirect.asp
-----------------------------7d22151d40264
Content-Disposition: form-data; name="Name"

Adrian Bacaianu
-----------------------------7d22151d40264
Content-Disposition: form-data; name="Upload"

Submit Query
-----------------------------7d22151d40264--

As you can see in this listing, the boundary information is used to separate each form field and its data. Unfortunately, the informational chunk of a form field isn’t separated from the data chunk by anything obvious. Well, actually, it is obvious, but we’ve all grown so used to ignoring this particular separator that it’s hard to notice. Basically, two pairs of carriage return and line feed characters separate the information chunk and the data chunk. That’s a total of four bytes between the chunks.

Splitting apart the form data

Now, it would be great if we could extract all of this form data into a nice collection, just like the Request.Form collection. So, with that in mind, will go to define a collection named myRequest and an array named myRequestFiles. While it would be nice to use a collection for any files that were uploaded, collections can’t easily track all the information required to properly handle a file. For example, for even a simple file, it’s handy to track the posted field name, the actual file contents that were transmitted, the file/path name of the file (for example, where it was on the client’s disk), and the MIME type of the file. A simple two-dimensional array is the easiest solution to this problem.

At this point, it’s a simple matter of looping through all of the form fields that were extracted when postData was Split along the boundary. As each field is examined, the informational and data chunks are extracted by using simple calls to the Mid function. Note that we don’t use Split here. This is because there could be dual carriage return and line feed pairs in the data itself.

After these chunks are extracted, the informational chunk is examined (via the InStr function) to see whether it contains the string “filename=”. If it does, that marks the field as an uploaded file. In that case, we call several functions, to extract the field name, filename, and MIME type of the file. This information is then added, along with the contents of the file itself, to the array of uploaded files.

If the field isn’t a file, its name is extracted using the GetFieldName function. And then it’s added to the myRequest collection.

That’s it! Once the loop finishes, the myRequest collection will hold all of our “normal” form fields and the myRequestFiles array will hold up to 10 uploaded files. Yep, this will indeed work for more than one uploaded file! If you need more than 10, just change the appropriate dimension when the myRequestFiles array is created.

Saving the file

At this point, all that’s left is to actually save the uploaded file (or files) on the server. Note that the contents of the other form fields are employed (via the myRequest collection) to determine what filename should be used to save the file. It’s important to note that for the save operation to work, your IIS anonymous user account, usually IUSR_machinename, must have write access to the directory you want to save the files in!

Conclusion

Well, that wasn’t really too hard now, was it? Of course, there are some drawbacks to this approach: Large files take a long time to process and binary files can be difficult to handle properly. But, once you’ve got the basics down (and now you do!), it’s a fairly straightforward process to adapt this technique to handle more troublesome files. And, best of all, you don’t have to buy and install a third-party object or use the much maligned Posting Acceptor!

ISAPI Upload Details

The MIME-compliant content type, called multipart/form-data, makes writing HTML that uploads files almost trivial. On the server side, though, ASP does not have a way to access data in the multipart/form-data format. The most flexible way to access the uploaded file is through a C++ ISAPI Extension DLL. This article describes a reusable ISAPI extension DLL that allows you to upload images and files without writing C++ code.

On the server, side though, ASP does not have a way to access data in the multipart/form-data format. There is a posting acceptor component, but its programmability is limited and it always stores the uploaded files into the file system. For instance, if you know in advance that users must always load small files and you process these files in memory, you might want a more flexible solution. The most flexible way to access the uploaded file is through a C++ ISAPI Extension DLL.

The ACTION attribute of the form element should point to a target component (for example, a CGI or ISAPI Extension DLL) that knows how to parse the multipart encoding and process the data. The user presses the Browse button, selects a file from the file system, and then presses the UPLOAD button to send the file. Of course, you might want to hire a designer to spice up the page with graphics and other cool JavaScript tricks, but as far as the HTML is concerned, it couldn’t be any simpler.

When a browser sends a request to a server, it always sends an HTTP packet with the data describing the request. The packet always contains the virtual path of the URL. For instance, if you call http://myserver/default.asp, the packet will contain the /default.asp path. In addition, if the request is the result of submitting an HTML form, the request contains the contents of the INPUT tags in the form.

The next question, of course, concerns how the data is encoded. It turns out that it’s done using MIME encoding. The default (and simplest) encoding is application/x-www-form-urlencoded, which is described in the HTML 4.01 specification at the W3C. The application/xwww-form-urlencode type is simple and perfect for submitting a few text fields.

For large text files or images, the specification defines another encoding: multipart/form-data. The following snippet shows what the HTTP packet of a form submitted using the multipart/form-data encoding looks like.

Raw data posted to the Web server is in the following format:

-----------------------------7d22151d40264
Content-Disposition: form-data; name="Filename"

UploadFileName.txt
-----------------------------7d22151d40264
Content-Disposition: form-data; name="Filedata";
        filename="C:\boot.ini"
Content-Type: application/octet-stream

[boot loader]
timeout=2
default=multi(0)disk(0)rdisk(0)partition(1)\WINNT

-----------------------------7d22151d40264
Content-Disposition: form-data; name="PathData"

C:\Projects\articles\Discover WEB. ISAPI versus ASP web
                                   upload\Work\UploadISAPI\Data\
-----------------------------7d22151d40264
Content-Disposition: form-data; name="RedirectPage"

ReceiveRedirect.asp
-----------------------------7d22151d40264
Content-Disposition: form-data; name="Name"

Adrian Bacaianu
-----------------------------7d22151d40264
Content-Disposition: form-data; name="Upload"

Submit Query
-----------------------------7d22151d40264--

The ISAPI extension DLL parses the input stream and collects all the name/value pairs. Then it creates a new dictionary COM object and stores the name/value pairs in the dictionary. Finally, it generates a new ID and stores the new dictionary into the dictionary of dictionaries using that ID as the key.

Accessing a parameter from that collection is very simple:

//here, create the "PathData" param
MultipartEntry *pEntry = cParser["PathData"];

if(pEntry != NULL)
   sPath = pEntry->Data();     //here, get data of the
                               //"PathData" param
sName = cParser[2]->Name();    //here, get the name
                               //("PathData") of the 2 param

If the response is printed to the Web browser directly from the ISAPI extension, it used the SendToBrowser method:

void SendToBrowser(LPEXTENSION_CONTROL_BLOCK pECB,
                   const String & sMsg)
{
    DWORD        dwLen = sMsg.size();
    if(dwLen > 0)              //use HTTP WriteClient function
        pECB->WriteClient(pECB->ConnID,
            (LPVOID) sMsg.c_str(), &dwLen, 0);
}

If the response is redirected from the ISAPI extension to another ASP page, it used the RedirectBrowser method:

void RedirectBrowser(LPEXTENSION_CONTROL_BLOCK pECB,
                     const String & sMsg)
{
    DWORD dwLen = sMsg.size();
    if(dwLen > 0)    // use HTTP ServerSupportFunction function
        pECB->ServerSupportFunction(pECB->ConnID,
              HSE_REQ_SEND_URL, (LPVOID)sMsg.c_str(), &dwLen, 0);
}

Conclusion

This article presented the design and implementation of an ISAPI extension that solves the problem for many of the common cases. If you are an ASP developer, downloading and building the code that accompanies this article will give you a simple but effective way to upload small files to your ASP-based Web site, without needing to deal with the complexity of ISAPI and C++. If you are a C++ developer, this article demonstrates how to use ISAPI to communicate between an ISAPI extension and ASP.

Downloads


Download ASP source files – 6 Kb


Download ISAPI source files – 69 Kb

Article History

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read