MS BackOffice Unleashed

Previous Page TOC Next Page



— 43


Interfacing Applications to IIS


This chapter explains how to interface with the Microsoft Internet Information Server. There are two different perspectives on interfacing:

The first deals with how various browsers handle the information provided by IIS. Most of this is general and works with other Web servers as well. The second is used to provide dynamic Web pages by executing a program on the server. Both perspectives are described in this chapter. The main focus is on extensions and interfaces to the actual server software. The chapter also provides general overviews of alternatives, mainly options for executing code on the client.

ISAPI, CGI, and Visual Basic Scripting Interface


If you want to create dynamic Web pages, you have to execute code somewhere. This could be on the server or on the client machine.

Client-Side Code


To execute code on the client machine, you can embed some form of a script in the HTML document. This could be JavaScript or VBScript. Currently, only Microsoft Internet Explorer 3 supports both (actually, IE 3 supports JScript, a 100% JavaScript compatible scripting language). Netscape Navigator 2 and 3 support JavaScript. Both scripting languages are embedded as plain text, so anyone can see, copy, modify, and use the code. Another drawback is that you are quite limited in what you can do with these scripting languages. To remedy this, Java and ActiveX controls can be used. Java applets and ActiveX controls are not included in a HTML document, but rather referenced from it. Net browser sees the reference and opens a second connection to the server to fetch the code. The code is then run on the client machine.

Although running code on the client machine is very tempting, it has its drawbacks:


Server-Side Code


The alternative is to run the code on the server. This supports all browsers, even the ones not created yet. With careful planning, even people running Lynx will be able to get exciting output from your site! What you lose is interactivity. Nothing happens in the users' browsers until they submit or request additional information from the server.

To provide input to server-side code, you'll usually use HTML forms, although this is not strictly necessary. The following are the three main jobs for server-side code:

To perform a calculation, the user enters the needed information into a form and sends it to the server. The server executes the needed code and then returns the result to the user, possibly with graphics, links, suggestions, and so on. You will typically develop custom code for this type of task.

If you want users to search a database, you might not need any initial input, but you are producing an HTML document to send back. Applications like this are ideal jobs for IDC (Internet Database Connector).

To sell merchandise on the Internet, you typically bring up a fairly large form where the user inputs name, address, card number, and so on. When the order is processed, a page containing a "Thank you for your order" usually is sufficient. This page does not need to be dynamic, but can often be created once and for all. There are several commercially available solutions for providing a "shopping basket" type of ordering. Alternatively, you can build your own using IDC, custom-code or both.

The following sections describe what interfaces IIS supports to enable you to create exciting, dynamic Web pages of the three types. The sample program provided later in this chapter, searches and displays posts I've saved from various newsgroups. The database here is simply a directory with text files.

CGI


If you want to perform a nontrivial calculation, and do not want to give away the code, you have to use some form of server-executable code. There are several ways to execute code on the server. Common to them all is that some HTML document references the executable, either directly or via a <form> tag. By locating the executable in a special directory (typically named Scripts or Bin), the server executes it rather than sending it to the client.

The most used server-side interface to Web servers is the Common Gateway Interface (CGI). To comply with the interface, the Web server software first fills environment variables with various information such as the following:

Then the server executes the referenced executable. This executable is called a script for historical reasons. When the web was young, CGI apps were usually written in one of UNIX's many scripting languages such as Perl. It is therefore common to call the application a CGI script, even if it's written in C or Pascal. The script reads the environment variables, as well as additional information from standard input, does its thing, and writes the resulting HTML document to standard output. The Web server sends this back to the client.

The main advantage of CGI is that most Web servers support it. Because of this, a script written in Perl for a UNIX server will likely run without modification on a Window NT based server. Another advantage is that since a CGI script is a separate application, it can be written in any language. A third advantage is that each instance of the CGI application is running in its own address space, so if one of them goes wild, none of the others are affected, and the server continues to work normally. Finally, all resources used are freed by the OS when the script terminates.

The various ups and downs of creating CGI scripts is a huge topic which will not be covered here. SAMS has several books on this topic, among others, CGI Developer's Guide by Eugene Eric Kim.

IIS supports running CGI scripts directly by adding elements to the registry. You can either associate an application with an extension the usual way. For instance, you tell Windows to always open files with extension TXT with Notepad. Similarly, you can tell Windows that all files with extension PL should be executed with Perl.EXE. Another possibility is to add a REG_SZ entry to the following registry key:

HKEY_LOCAL_MACHINE\SYSTEM

 \CurrentControlSet

 \Services

 \W3SVC

 \Parameters

 \ScriptMap

The main drawback of CGI is that each user needs a new process to be executing on the server. This can eat a lot of resources. If you have a heavy-traffic server (such as Alta Vista or Microsoft's home page), it is likely that the server will kneel and die. Because of this, Microsoft created an alternative, ISAPI.

ISAPI Extension DLL


To overcome the problems of CGI, IIS supports extensions in a DLL. The server needs to load the DLL only once, but can start many threads within that DLL. This way the startup time is minimized, and the resource use is lowered.



Because the DLL is loaded only once, the Web server must be stopped to be able to overwrite the DLL. This can be quite a hassle when developing and testing. To avoid this, set the following registry key to 0:
HKEY_LOCAL_MACHINE\SYSTEM
\CurrentControlSet
\Services
\W3SVC
\Parameters
\CacheExtensions


There are two types of ISAPI DLL's. The first is called an extension DLL. The other is called a filter DLL. Extension DLLs are called by IIS whenever a HTML page references it. They therefore sit between IIS and the resources on the server. Filter DLLs, on the other hand, are called at every client connection. They are located between the client and IIS, and can filter out and add information.

For an extension DLL to adhere to the ISAPI specification, it must export two functions, GetExtensionVersion() and HttpExtensionProc(). The first provides version control, and the last does all the actual work. The two functions are prototyped as follows in Borland Delphi 2:

function GetExtensionVersion(var Ver: THSE_VERSION_INFO) : BOOL; stdcall;

function HttpExtensionProc(var ECB: TEXTENSION_CONTROL_BLOCK) : DWORD; stdcall;

When IIS calls GetExtensionVersion(), it expects the DLL to set the two fields of the THSE_VERSION_INFO structure. A typical GetExtensionVersion(), looks like this:

function GetExtensionVersion(

 var Ver : THSE_VERSION_INFO

 ) : BOOL; stdcall

begin

 Ver.dwExtensionVersion := MAKELONG(HSE_VERSION_MINOR,

 HSE_VERSION_MAJOR);

 StrLCopy(Ver.lpszExtensionDesc, 'My Extension DLL',

 HSE_MAX_EXT_DLL_NAME_LEN);

 Result := True;

end; { GetExtensionVersion }

This tells IIS what version of the ISAPI specification the DLL was compiled for and gives the DLL a descriptive name. The name is not currently used for anything but could probably be used for logging purposes.

HttpExtensionProc() is a bit more interesting. It takes only one argument, but that is a pointer to a large structure, the TEXTENSION_CONTROL_BLOCK. This is defined as follows:

TEXTENSION_CONTROL_BLOCK = packed record

 cbSize: DWORD; // size of this struct.

 dwVersion: DWORD; // version info of this spec

 ConnID: HCONN; // Context number not to be modified!

 dwHttpStatusCode: DWORD; // HTTP Status code

 // null terminated log info specific to this Extension DLL

 lpszLogData: array [0..HSE_LOG_BUFFER_LEN-1] of Char;

 lpszMethod: PChar; // REQUEST_METHOD

 lpszQueryString: PChar; // QUERY_STRING

 lpszPathInfo: PChar; // PATH_INFO

 lpszPathTranslated: PChar; // PATH_TRANSLATED

 cbTotalBytes: DWORD; // Total bytes indicated from client

 cbAvailable: DWORD; // Available number of bytes

 lpbData: Pointer; // pointer to cbAvailable bytes

 lpszContentType: PChar; // Content type of client data

 GetServerVariable: TGetServerVariableProc;

 WriteClient: TWriteClientProc;

 ReadClient: TReadClientProc;

 ServerSupportFunction: TServerSupportFunctionProc;

 end;

The most important fields are lpszMethod, lpszQueryString, cbTotalBytes, cbAvailable, and lpbData. lpszMethod can be either POST or GET. POST can send more data, but GET is easier to handle. If the lpszMethod is POST, cbTotalBytes bytes has been sent from the client. Of these, cbAvailable are currently stored in an area pointed to by lpbData. To get more data, ReadClient must be used. If the lpszMethod is GET, all available data is contained in the 0 terminated lpszQueryString. (When I use the term "query string," it will refer to either lpszQueryString or ldbData, depending on the method.)

The query string contains all input from the form on the Web page. The entries are separated by an ampersand. Each entry contains the name of the variable, an equals sign, and the value. Because the parameters were originally passed on the command line to CGI scripts, all spaces are replaced with a plus sign. The query string is also limited to 7-bit ASCII, so all other characters as well as the special ones (=, &, +, and %) are translated into a hexadecimal representation. This starts with a % and is followed by two characters denoting the hexadecimal value of the actual character. To use the query string in any useful manner, it must be decoded.

An example will clarify. Consider a form with two input variables, Var1 and Var2. Suppose the user writes I am with you in Var1 and 100% in Var2. The query string will look like this:

Var1=I+am+with+you&Var2=100%25


Here are some notes on decoding the query string:
If a variable does not contain any data, the browser may or may not include the variable in the query string. When it is included, the equals sign is followed directly with an ampersand or the end of the string.
The last variable does not have an ampersand at the end.
Some browsers attach an extra CRLF to the query string. This is nonstandard.


The four last fields in the TEXTENSION_CONTROL_BLOCK are functions used to communicate with IIS and the client:


ISAPI Filter DLLs


A filter DLL is located between the Web server and the client. All data flowing between the two can be routed through the filter. It can then translate, modify, add, and remove information as it sees fit.

The ISAPI filter specification is very similar to the extension specification. A filter DLL exports two functions: GetFilterVersion() and HttpFilterProc(). Contrary to the GetExtensionVersio() function, the GetFilterVersion() does more important things than just return the supported version and a description. The prototype looks like this in C/C++:

BOOL WINAPI GetFilterVersion(HTTP_FILTER_VERSION * pVer);

HTTP_FILTER_VERSION looks like this:

typedef struct _HTTP_FILTER_VERSION

{

 DWORD dwServerFilterVersion; /* Version of the spec the *

 * server is using */

 DWORD dwFilterVersion; /* Fields specified by the *

 * client */

 CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN];

 DWORD dwFlags;

} HTTP_FILTER_VERSION;

The dwFilterVersion and lpszFilterDesc is set as in the extension DLL. dwFlags is a combination of flags indicating what the filter wants to see and at what priority the filter should run. Usually the default priority by specifying SF_NOTIFY_ORDER_DEFAULT is sufficient. Table 43.1 shows the different notifications a filter can hook.

Table 43.1. ISAPI Filter notifications

Notification Flag Description
SF_NOTIFY_SECURE_PORT Notify application only for sessions over a secure port.
SF_NOTIFY_NONSECURE_PORT Notify application only for sessions over a nonsecure port.
SF_NOTIFY_READ_RAW_DATA Allow the DLL to see the raw data.

The data returned will contain both headers and data.
SF_NOTIFY_PREPROC_HEADERS The server has preprocessed the headers.
SF_NOTIFY_AUTHENTICATION The server is authenticating the client.
SF_NOTIFY_URL_MAP The server is mapping a logical URL to a physical path.
SF_NOTIFY_SEND_RAW_DATA The server is sending raw data back to the client.
SF_NOTIFY_LOG The server is writing information to the server log.
SF_NOTIFY_END_OF_NET_SESSION The session with the client is ending.

When a filter hooks any of these, it will be called each time the server gets handles an event that matches. It is therefore often useful to hook SF_NOTIFY_URL_MAP. This will let you tag connections to files you will modify. If you write a filter for macro substitution, for instance, you have to make sure you're not modifying files the users are downloading.

When one of the hooked event occurs, HttpFilterProc() is called. It function has the following prototype:

DWORD WINAPI HttpFilterProc(

 HTTP_FILTER_CONTEXT * pfc,

 DWORD NotificationType,

 VOID * pvNotification

 );

The filter context uniquely identifies this communication with the client. It has the following attributes:

typedef struct _HTTP_FILTER_CONTEXT

{

 DWORD cbSize;

 DWORD Revision; /* Structure revision level. */

 PVOID ServerContext;

 DWORD ulReserved;

 BOOL fIsSecurePort; /* TRUE if this request is *

 * coming over a secure port */

 PVOID pFilterContext;/* A context that can be used *

 * by the filter */

 BOOL (WINAPI * GetServerVariable) (

 struct _HTTP_FILTER_CONTEXT * pfc,

 LPSTR lpszVariableName,

 LPVOID lpvBuffer,

 LPDWORD lpdwSize

 );

 BOOL (WINAPI * AddResponseHeaders) (

 struct _HTTP_FILTER_CONTEXT * pfc,

 LPSTR lpszHeaders,

 DWORD dwReserved

 );

 BOOL (WINAPI * WriteClient) (

 struct _HTTP_FILTER_CONTEXT * pfc,

 LPVOID Buffer,

 LPDWORD lpdwBytes,

 DWORD dwReserved

 );

 VOID * (WINAPI * AllocMem) (

 struct _HTTP_FILTER_CONTEXT * pfc,

 DWORD cbSize,

 DWORD dwReserved

 );

 BOOL (WINAPI * ServerSupportFunction) (

 struct _HTTP_FILTER_CONTEXT * pfc,

 enum SF_REQ_TYPE sfReq,

 PVOID pData,

 DWORD ul1,

 DWORD ul2

 );

} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;

As you can see, the filter can use many of the functions the extension DLL can. ReadClient() is not supported, but you can get all data from the client by hooking SF_NOTIFY_READ_RAW_DATA. AllocMem() can be used in conjunction with the pFilterContext field. Together they provide for connection-local memory. Any memory allocated by AllocMem() will automatically be released when the connection terminates. However, it is not very efficient. If you want high speed, you can allocate memory any way you want, and then free it during the SF_NOTIFY_END_OF_NET_SESSION event.

The NotificationType parameter is one of the flags in Table 43.1. If you have selected multiple flags, HttpFilterProc() will be called multiple times for each client connection.

pvNotification contains the data specific to the connection and the event. Table 43.2 summarizes the actual type of pvNotification for the different events.

Table 43.2. ISAPI Filter events

Event Type pvNotification Points To
SF_NOTIFY_READ_RAW_DATA HTTP_FILTER_RAW_DATA
SF_NOTIFY_SEND_RAW_DATA HTTP_FILTER_RAW_DATA
SF_NOTIFY_PREPROC_HEADERS HTTP_FILTER_PREPROC_HEADERS
SF_NOTIFY_AUTHENTICATION HTTP_FILTER_AUTHENT
SF_NOTIFY_URL_MAP HTTP_FILTER_URL_MAP
SF_NOTIFY_LOG HTTP_FILTER_LOG

Once you determine that you want to do something with an event, you are free to modify the data to be sent or read. You can also write additional information to the user. Because IIS only support the simple file-include part of Server Side Includes (SSI), you could write a filter that adds the missing functionality.

Problems with ISAPI DLLs


Unfortunately, using a DLL it isn't all sweet. Because the DLL is only loaded once, any resource leakage in it will accumulate and eventually bring down the server. When you create a DLL to provide dynamic Web pages, you usually use code written by others. This could be a graphics library, or the compiler's runtime library. Because you often don't have control over these, resource leaks can be impossible to fix. The only solution is to stop IIS regularly and then restart it. Windows NT frees all resources when a process is stopped.



Using the standard Windows NT scheduler, you can automatically start and stop the Web server service at regular intervals. This reduces the chances for violent crashes.

In addition to leakage problems, you get concurrency problems. This means, among other things, that you have to make sure global variables will not be read and written by different threads at the same time, the code has to be thread safe. Again, because you usually don't have control over all the source, maintaining thread safety can be very difficult.

Because Windows NT will not allow 32-bit code to execute 16-bit code, all ISAPI DLLs have to be 32-bit. Sometimes this is not an option. You might be depending on some old code that won't easily recompile, or maybe a third-party component that's not currently available in a 32-bit version. One solution to this is to run the 16-bit application as a CGI script. Another solution is to run the 16-bit code as a separate application and use the WM_COPYDATA message to transfer data.



WM_COPYDATA transfers large amounts of data from one process to another. 16-bit applications can receive up to 16M of data in a single message. 32-bit applications receive 4G. However, to support this, Win32 actually creates a copy of the sent data in the receiving application's address space. This copying takes time, slowing down the system considerably if you are copying large amounts of data. The data is not copied back into the caller's address space on return, so you have to send a new WM_COPYDATA to modify the data.



To be able to send any kind of message to another application running on the server, the checkbox named "Allow Service to Interact with Desktop" must be checked for the IIS WWW service. The checkbox is located in the dialog where startup values for the service are set.


Visual Basic


There are many things you cannot do in Visual Basic. One of them is to create general-purpose DLLs. However, it is possible to create an OLE Automation Server in a DLL, an in-process Automation Server. Microsoft has provided a general-purpose DLL called OLEISAPI.DLL that can be used to attach to any Automation Server to the Web. It is actually possible to export all of Microsoft Word through the support for OLE Automation in IIS. The possibilities with that are mind-boggling. However, I would advise against it because there would be nothing stopping an attacker from formatting your hard drive, deleting all the files in the Windows NT system directory or doing other not-so-pleasant things.

To reference an OLE Automation Server, you reference OLEISAPI.DLL/Appname.Method. Method() should accept two BSTR parameters; the first contains the query string, the second should be filled with the response. The Automation Server and all necessary support DLLs must be installed and properly configured.

Database Interface via ODBC


Quite often, you want users to search databases or add ordering information to databases. One way of doing this is with an ISAPI extension DLL. This has the advantage of being able to do anything it wants with the data. In many cases, however, writing a DLL is too time-consuming for just a simple database update. The solution then is the Internet Database Connector, IDC. Using IDC involves writing two files, one for the query and one for the result template. The query has an IDC extension. It contains the name of the ODBC data source, the name of the template file, and the SQL statement(s) to execute. The result template has a HTX extension and contains a document in an extended HTML format The extension enables the Web server (actually an ISAPI extension DLL called Httpodbc.dll) to build a Web page with the result of the SQL statement.

Setting Up the Data Source


Setting up a data source for a IDC file is very similar to setting up ordinary ODBC data sources:

  1. Start up the ODBC controller.

  2. Click on the System DSN button. This is important, because IIS is running as a service and is therefore not logged on as a user. As a result, it only has access to System Data Sources. From here on, the process is identical to normal ODBC setup.

  3. Click on the Add button to add a new data source.

  4. Select an ODBC driver from the list and click OK.

  5. Enter the location of the actual database and a name for the data source. Depending on the ODBC driver, various other information must also be entered.

  6. Click on OK and Close as required to exit the ODBC controller.


A Sample IDC File


The IDC file mainly contains the SQL statement(s) required to perform the database query. The following IDC file extracts references to documents from a document archive database. The entry screen has three input fields where you can input the search criteria. The input field for the customer name is called _customer, the input field for the issue is called _issue, and the input name for the project is called _project. These are referenced by enclosing them in percent symbols:

Datasource: doc_archive

Username: guest

Password: secret

Template: doc_arch.htx

SQLStatement:

+ select ID,

+ customer,

+ issue,

+ file,

+ project,

+ db_createdate

+ from doc_base

+ where customer = '%_customer%'

+ and issue like '%_issue%'

+ and project like '%_project%'

+ order by ID, db_createdate desc

Httpodbc.dll reads this file, hooks up to the doc_archive database using guest as the login name and secret as the password. The SQL statement is then executed, and a result table is returned. Using the returned table and the referenced HTX file, a Web page is created.

A Sample HTX File


The following is the HTX file that matches the IDC file. It looks very much like a normal HTML file except for some special <%xxx%> tags. These tags either expand to the content of a column in the result table, or are special macro markers for Httpodbc.dll. For instance, in the following code, the <%begindetail%> and <%enddetail%> tags signify that the text between them should be output once for each row in the table. <%file%>, on the other hand, expands to the content of the column named file in the current row:

<html>

<head></head>

<body>

<h1>Found documents</h1>

<table border="1">

 <tr>

 <th>ID</th>

 <th>Issue</A></th>

 <th>Creation data</th>

 <th>Project</th>

 </tr>

<%begindetail%>

 <tr>

 <td><%ID%></td>

 <td><a href="/documents/<%file%>"><%issue%></a></td>

 <td><%db_createdate%></td>

 <td><%Prosjekt%></td>

 </tr>

<%enddetail%>

</table>

</body>

</html>

The HTX files supports nested <%if%>, <%else%>, and <%endif%> tags. IIS 1.0 didn't support nested <%if%> constructs, but IIS 2.0 does. Armed with this, you can do different things, depending on various results in the table.

Problems with ODBC


Although using IDC is very nice, it has some serious drawbacks. One of the problems is that many ODBC drivers are not threadsafe. Currently, only the driver for MS SQL Server works reliably on a Web server. This should be fixed quickly, but make sure your ODBC driver works. Testing it is very simple. Just bring up two instances of your favorite browser and request the IDC document from the server in both windows simultaneously. If the server crashes, you need to change your ODBC driver.

Another problem with IDC is that error messages from the database turn up on the users' monitors. The only way to avoid that is to write an ISAPI filter DLL that modifies the output stream. Doing so is fairly simple, but it requires knowledge of either C/C++ or Borland Delphi. One of the sample DLLs in this chapter looks for errors from IIS and displays a nicer version. It should be fairly straightforward to modify it to look for database errors instead.

Active Web Concepts and Extensions


You have already explored in detail some possibilities for creating HTML documents on the fly. To create truly interactive Web pages, however, requires code to run on the client machine. There are a lot of ways to do this. This section touches lightly on the various concepts.

The simplest way to create a Web page with some movement on it is to use animated GIFs. An animated GIF is simply a series of pictures shown in quick succession. Running over the Internet is very slow, but once all the images are in the browser's cache, speed is acceptable. Because the pictures are shown in a constant loop, they quickly become boring and soon after that annoying. If you change them often enough, they might be worth it.

Another option is to use scripting languages such as JavaScript and VBScript. Although they are limited in functionality, they still can verify input. Combined with code on the server it could be a winning combination. The main advantage with a script is that it is embedded in the HTML document and is therefore very fast to download.

A third option is to use plug-ins. A plug-in is an extension of the user's browser. Usually, it knows how to decode one or several special file formats (or MIME formats to be correct). For instance, a plug-in can understand the Virtual Reality Markup Language (VRML). With it, the browser can render 3D worlds on the user's screen. Other plug-ins support real-time audio transmissions, such as radio, over the Internet. Plug-ins can usually be used to view many different sites, so although it might take a user quite some time to download a specific plug-in, once it is there it is instantly useful many places.

A fourth option is to execute code that is visually separate from the browser, but still controlled by it. Examples of this are Java applets and ActiveX controls. This category of extensions create and use their own windows. They receive user input, deal with it, and/or communicate with their originating server to create a really active Web presence. At this point, there's not much difference from downloading and running programs the way you are used to through anonymous FTP. The extensions are normally only usable for the given site, and they do take quite some time to download.

The last option is to abandon the limitations of the browser entirely and run everything over the Internet, much like you run programs on a LAN today. This has actually been suggested as the future of computing by Oracle, IBM, and others. Current bandwidth prohibits this, however.

Adaptive Web Programming for Various Browsers


One of the big problems with creating a Web presence today is the enormous number of different browsers available. Although there actually exists standards for HTML documents (three versions), few browsers implement the entire standard and most add some features of their own. This creates a nightmare for Web designers and programmers. One possibility is to use the least common denominator. This means no graphics, no tables, and no fun. An alternative is to target only the newest browsers. Unfortunately, this is rarely an option because most users don't want to spend the time and money to download 8M executables over a 14400 modem—especially not in countries where local calls are not free.

The solution, then, is to provide both. That creates the extra headache of keeping two separate versions of the documents updated, but it often is the only solution. The good news is that tags not supported in a browser are supposed to be ignored. This means that you can create an entrance that lets the people with the fancy browsers automatically get the fancy layout, while the rest get something decent.

One common way to separate the pages is to make one version with tables and simple graphics, and the other version support frames, JavaScript/VBScript, Java, ActiveX, animated GIFs, and so on. The reason for separating at frames versus no-frames is that most browsers support tables and graphics. The ones who don't can still display a useable result by using some simple HTML tricks. However, frame-enabled pages look really silly with browsers that don't support them.

Using HTML


Text-mode browsers generally support only the HTML 1.0 specification. Liberal use of the alt parameter on <img> tags enables these browsers to display something meaningful. Browsers for seeing-impaired people also have something to say if the alt parameter is used.

To create a table that displays acceptably on most browsers, including Lynx, add a <br> tag as the last tag in each <td> element. Add a <p> tag after the table, also. A typical table then looks like this:

<table>

 <tr>

 <td>Row 1, Column 1<br></td>

 <td>Row 1, Column 2<br></td>

 </tr>

 <tr>

 <td>Row 2, Column 1<br></td>

 <td>Row 2, Column 2<br></td>

 </tr>

 <!-- And so on -->

</table>

<p>

This will not disturb browsers that support tables, because they try to limit the size of each cell. A <br> won't matter in that case.

To differentiate between frames and no-frames browsers, use the following HTML document:

<html>

<head>

<title>A Frame Selector</title>

</head>

<frameset rows="100%,*">

<noframes>

<body>

This will only be read by browsers that have no idea about frames.

You can put an entire page here, or just a note stating that the

users should upgrade their browsers. Just telling the users to

upgrade will probably not gain you many friends, but at least you

don't have to maintain two separate versions of the same

information.

</body>

</noframes>

<frame src="framed.htm">

<frame src="dummy.htm" scrolling="NO" noresize>

</frameset>

</html>

The reason this works is that browsers that do not support frames will ignore the <frameset> and <noframes> tags. This brings them to the <body> tag, which they handle as usual. When they are finished with the </body> tag, they will ignore the rest of the file, except the very last tag.

Frame-enabled browsers, on the other hand, will understand all tags and create two frames—one that uses the entire screen, and one that uses the rest. The last will, of course, be invisible. All the fancy formatting, such as animated GIFs, JavaScript, and real frames, is then placed in the framed.htm file.

Using ISAPI


One of the functions available to ISAPI extension and filter DLLs is GetServerVariable(). If you pass ALL_HTTP to this function, it will return a newline-separated list of additional variables. One of these is HTTP_USERAGENT, an identifier of the user's browser. You can use this to target each different version of browser specifically. Doing so can be necessary, because each browser has its own set of features and bugs. It is, however, a large undertaking, because there are an incredible number of different browsers. Each time a new one comes along, you have to update the code. Using plain HTML as shown previously, should get you by in most cases.

JAVA and J++ Alternatives


While the incredible explosion of the usage of the Internet totally surprised most of the existing software houses, the popularity of Java was more expected. But Java is still one of the most hyped-up and confusing part of the Internet hoopla. There's Java applications, Java applets, and JavaScript. In addition, Microsoft has introduced ActiveX controls. This has been looked upon as evidence that Microsoft doesn't support Java. While describing all these technologies in detail is way beyond the scope of this chapter (and this book as well), a basic knowledge is very useful when considering the options to make attractive web pages. I'll therefore provide an overview of the involved technologies here.

Java


Java is a general-purpose multiplatform programming language based on C++. The language is developed and owned by Sun Microsystems, Inc. The main purpose of Java is to be able to create efficient programs with an attractive GUI that can be downloaded over a network (such as the Internet) and executed. All potentially difficult issues of C++ programming has been left out. Multiple inheritance and pointers are among the things that were discarded. Other useful features, such as garbage collection and the capability of supporting multiple interfaces in a single object, were added. This creates a compact language that is easy to write code in and easy to compile. Microsoft claims its Visual J++ compiles at speeds up to one million lines per minute.

A Java compiler does not create a binary executable. Instead, the source code is compiled to a byte code that is interpreted on the target platform. This means that a Java application can be compiled on a NeXT machine running NeXTStep and executed under Windows NT on a PowerPC machine. This makes Java applications very attractive for distributed computing in a heterogeneous network environment.

Of course, executing through an interpreter is slow, because each instruction has to be decoded each time. The answer is JIT, a Just In Time compiler. JIT simply compiles the code to native machine code before the execution of the Java application starts. After that, the Java application runs at full speed.

A reduced version of Java applications, known as Java applets, can be run with both MS IE 3 and Netscape Navigator 2 and 3. The Java applets can create a separate GUI for your Web pages, with buttons, windows, animation and the like. Someone has already created a multiplayer version of Tetris using Java at

 http://ariel.cobite.com/ultram/tetris/tetris.html

One of the biggest problems when defining what Java applets could do, was to ensure that downloaded code was safe to execute—no matter where it came from or what evil intentions the original author had. To make sure violent code is not allowed to run, Java applets are, among other things, prohibited from writing to disk and communicating with other servers than the one they were downloaded from. They are also verified thoroughly before being allowed to run. Unfortunately, if there are bugs in the verifier, the system breaks down. The latest version of JavaScript can control Java applets, so they work in conjunction.

JavaScript is defined by Netscape. It is based on Java, but much of the original language has been removed to make JavaScript a lightweight scripting language. JavaScript supports most of the operators of Java, but has only a very limited number of datatypes. It is not possible to define new datatypes as is possible in Java.

J++


Visual J++ is Microsoft's development platform for Java applets. It is based on their Visual C++ Developer Studio. J++ is Java with some added class libraries to make it possible to integrate Java, ActiveX controls, and VBScript. Among the extensions is that Microsoft's Virtual Java Machine, the interpreter that runs Java code, automatically creates COM objects of all Java objects. The result is that all available COM objects is used in the Java program looking like native Java classes. This makes it possible to integrate Java and ActiveX without any additional coding.

Visual J++ is currently in beta. Its main competitors are Symantec Café and Borland Latte. Café 1.0 was released in March 1996. Café 1.5 is currently in beta. All three are supposed to be released during the fall of 1996.

ActiveX


ActiveX controls, the concept formerly known as OLE Controls, is a specification for COM-based controls that can be transferred and executed over the Internet. Currently, the only implementation is on Windows, but there are supposedly UNIX versions and a Mac version in the works. ActiveX controls are allowed to do anything they want, including deleting files and formatting your hard drive. Instead of limiting the capabilities of the controls, and therefore their usefulness, Microsoft decided to include a verification system with the ActiveX specification. When you hit a Web page that references an ActiveX control, the author of the control is identified. If you trust the author, the control is downloaded and checked to make sure it has not been tampered with. Finally, it is executed. This is very similar to how shrinkwrapped applications work today. You don't worry about Microsoft Office destroying your hard disk, because you know who made it and you trust them.

Microsoft claims that ActiveX controls are not direct competitors to Java applets, but rather that they supplement each other. The evidence of this is most apparent in the different constraints they live under. Java applets are prohibited from doing anything that could potentially damage the client machine. ActiveX controls, on the other hand, does not have any limits of operation. This makes them more dangerous to use, but not worse than the shareware and freeware utilities we use every day.

I suspect Java applets will be used for smaller tasks that enhance the Web experience. ActiveX controls will be used for small distributed and client/server applications. Finally Java applications will be used as a multi-platform alternative to C++.

Sample Programs


The first of the two sample programs in this chapter is an extension DLL. I read several newsgroups regularly. Because my current work involves Borland Delphi, I spend most of my Internet-time with the Delphi groups. Often, someone writes an article I find valuable enough to save. Usually it's about something I know I will be using in the near future. The number of saved articles quickly grew, and it became hard to find what I was looking for. Because we are running Microsoft IIS at work, I wrote a little DLL using Borland Delphi 2 to help me organize my articles.

Figure 43.1 shows what the input form looks like. The user can type words here that should be included and/or excluded from the search. Words separated by space are OR-ed. Clicking on the Search Now! button in the example brings up all articles containing the word dynamic or the word array. The example is available from my Web page at http://www.delfidata.no/users/~martin by following the Delphi link.

FIGURE 43.1. Input form for the sample ISAPI DLL.

The second sample is a simple filter DLL that shows a nice message if a user requests a nonexisting URL. ISS normally shows a rather ugly HTTP/1.0 404 Object Not Found message. The DLL adds a nicer message and enables the user to link back to the referring page. Figure 43.2 shows what the user will see when they follow an outdated link.

FIGURE 43.2. A user follows an outdated link.

The Extension DLL


The sample extension DLL is very simple. It searches for all files with a TXT extension in a given directory. Each file is then loaded into memory and checked for the Include and Exclude parameters from the Web page. If the file is accepted, the subject and from fields of the posting are extracted and listed on the page. Figure 43.3 shows the result of the query in Figure 43.1.

FIGURE 43.3. Example of search results.

The DLL is composed of the following files:

All the code for the sample is included at the end of the chapter. In the following paragraphs, you look at the most important areas of the DLL and explore possible enhancements and possibilities.

The Main Code

The GetExtensionVersion() function isn't very interesting. It doesn't do much other than telling IIS what version of the ISAPI specification the DLL supports. HttpExtensionProc(), on the other hand, is where all the action occurs. It looks like this:

function HttpExtensionProc(

 var ECB : TEXTENSION_CONTROL_BLOCK

 ) : DWORD; stdcall;

var

hMutex : THandle;

begin

 hMutex := CreateMutex(nil, False, 'NewsListMutex');

 WaitForSingleObject(hMutex, INFINITE);

 with TNLExt.Create(ECB) do begin

 DoIt;

 Result := ReturnValue;

 Free;

 end;

 ReleaseMutex(hMutex);

 CloseHandle(hMutex);

end; { HttpExtensionProc }

As you can see, it is very simple, but it does nonetheless perform some crucial operations. First of all, it threadsafes the DLL. This is done in a rude manner, namely by limiting the number of threads that can execute at any time to one. If two users try to access this DLL at the same time, one has to wait for the other to finish. This is not as bad as it sounds, because the DLL only takes a couple of seconds to run.

Using a mutex is a bit of an overkill here; a critical section object should be sufficient because you only need to control threads in a process and not different processes. However, critical section objects require initialization and finalization calls to be performed once. Usually, this happens in the DllEntryPoint() function when it receives DLL_PROCESS_ATTACH and DLL_PROCESS_DETACH. I put all the synchronization code here to keep the code simpler.

Between the WaitForSingleObject() and ReleaseMutex() calls, I created an object of type TNLExt. This object encapsulates the process of listing the news postings to the user.

TNLExt.DoIt is also a simple function, which looks like this:

procedure TNLExt.DoIt;

begin

 try

 with httpConnection do begin

 StdHeader('Delphi NewsList');

 StdBody;

 Paragraph;

 Header(1, 'The Delphi News List');

 end;

 ListFiles;

 except

 on E : Exception do

 httpConnection.Error('Exception ' + E.ClassName + #13 +

 E.Message);

 end;

end; { TNLExt.DoIt }

httpConnection is an object of class THTTPExt. This class encapsulates the TEXTENSION_CONTROL_BLOCK and adds methods to make it easier to send text to the user's browser. WriteClient clutters the code, but procedures such as OutLine(), StdHeader(), Header(), and so on make the code easier to read and write. The five functions of the THTTPExt class used here do the following:

  1. StdHeader() sends a note to the browser that it should expect text to follow. It also adds <html>, <head>, and <title> tags. The title is the string passed as a parameter.

  2. StdBody() just outputs a <body> tag. It could be extended to include background picture and color information.

  3. Paragraph() outputs a <p> tag.

  4. Header() creates a header tag at the given level and outputs the text. In the previous code, the output would be <h1>The Delphi News List</h1>.

ListFiles does all the actual work of searching through the directory, checking the files, and adding the ones that matches the search criteria to a table. The code for that is not very Web specific, so I'll just refer to the complete listing at the end. A commented version of all the code is available on my home page.

The exception handling ensures that errors in the extension DLL don't affect the Web server. Here, I'm just outputting the exception class and the message to the Web page to aid my debugging efforts. In a production environment, Error() would probably write the string parameter to a log file and display a general error message to the user.



When creating HTML documents dynamically, there is nothing stopping you from outputting the entire document as one line. However, this is extremely hard to read when you need to debug your DLL. Using indentation and line breaks liberally does wonders for readability and debugging.


Managing the Connection

There are two main objects dealing with the Web page in the NewsList.DLL: TNLExt and THTTPExt. They divide the work of getting a result to the user evenly. TNLExt decides the content and the layout of the page, and THTTPExt controls the actual sending of data and the physical layout to make the document readable in a text editor as well as a word processor.

The constructor of TNLExt looks like this:

constructor TNLExt.Create(var ECB : TEXTENSION_CONTROL_BLOCK);

var

 sIniFile : string;

 tini : TIniFile;

begin

 inherited Create;

 SetLength(sIniFile, 250);

 GetModuleFilename(hInstance, PChar(sIniFile), Length(sIniFile));

 sIniFile := Copy(sIniFile, 1, Pos('.', sIniFile)) + 'ini';

 tini := TIniFile.Create(sIniFile);

 sInsideSearchDir := tini.ReadString('Inside',

 'SearchDir',

 '.');

 sOutsideSearchDir := tini.ReadString('Outside',

 'SearchDir',

 '.');

 if sInsideSearchDir[Length(sInsideSearchDir)] <> '\' then begin

 sInsideSearchDir := sInsideSearchDir + '\';

 end;

 if sOutsideSearchDir[Length(sOutsideSearchDir)] <> '/' then begin

 sOutsideSearchDir := sOutsideSearchDir + '/';

 end;

 tini.Free;

 httpConnection := THTTPExt.Create(ECB);

end; { TNLExt.Create }

destructor TNLExt.Destroy;

begin

 httpConnection.Free;

 inherited Destroy;

end; { TNLExt.Destroy }

Even though it is rather long, it performs only two tasks. The first is to read an INI file to determine where it should look for news postings, and how to link to them. The second is to create the THTTPExt object used for this connection. A typical INI file looks like this:

 [Inside]

SearchDir=L:\~martin\delphi\

[Outside]

SearchDir=/users/~martin/delphi

This tells NewsList.DLL that it should search for postings in L:\~martin\delphi\. If a file called Posting.TXT should be included in the Web page, it should be referred to as /users/~martin/delphi/Posting.TXT. This is a relative path that will expand to http://www.delfidata.no/users/~martin/delphi/Posting.TXT in my case. Putting this information in an INI file is only one option, but it is very convenient because it's easy to modify if the DLL needs to be moved.

The constructor of THTTPExt looks like this:

constructor THTTPExt.Create(var ECB : TEXTENSION_CONTROL_BLOCK);

begin

 inherited Create;

 fECB := ECB;

 fECB.dwHttpStatusCode := 200; // Success!

 fECB.lpszLogData[0] := #0; // No log data by default.

 fdwReturn := HSE_STATUS_SUCCESS;

 fsQueryString := GetQueryString;

end; { THTTPExt.Create }

This initializes the connection to assume it will succeed. I believe it is better to send some nice information to the users explaining that something went wrong rather than return a failure and let their browsers handle it. Usually, error messages from browsers are big and ugly and not very helpful.

GetQueryString() simply creates a copy of lpszQueryString or lpbData, depending on whether lpszMethod is POST or GET. This frees the remanding code from checking lpszMethod all the time; it can simply use fsQueryString or (preferably) the Query property.

When the DLL is done, the TNLExt object is freed. The destructor of TNLExt frees the THTTPExt object. During its constructor, the closing </body> and </html> tags are transmitted. You could also add a standard footer here, if you want.

Getting the Value of the Variables

As described earlier, the query string contains all variables from the Web page. To be compatible with the largest number of servers, the string is encoded to exclude spaces and characters with the high-bit set. A typical query string for the NewsList.DLL is Include=dynamic+array&Exclude=. This lists all articles containing either the word dynamic or the word array or both. THTTPExt contains a function called GetQueryVariable(). This function takes two parameters: the name of the variable, and a default value if the variable isn't found. Typical defaults are an empty string and a string containing 0. Here is the code for GetQueryVariable():

function THTTPExt.GetQueryVariable(

 const sVarName : string;

 const sDefault : string

 ) : string;

var

 ns : integer;

 nStart : integer;

 bDone : boolean;

begin

 {

 Find sVarName in Query.

 }

 nStart := Pos(LowerCase(sVarName), LowerCase(Query));

 if nStart = 0 then begin

 Result := sDefault;

 end

 else begin

 {

 Convert to 'normal' format.

 }

 Result := '';

 {

 Skip the varname and the '=' sign.

 }

 ns := nStart + Length(sVarName) + 1;

 bDone := False;

 while not bDone and (ns <= Length(Query)) do begin

 case Query[ns] of

 '+' :

 Result := Result + ' ';

 '%' : begin

 Inc(ns);

 Result := Result +

 Char(StrToInt('$' + Copy(Query, ns, 2)));

 Inc(ns);

 end;

 '&' :

 bDone := True;

 else begin

 Result := Result + Query[ns];

 end;

 end;

 Inc(ns);

 end;

 end;

end; { THTTPExt.GetQueryVariable }

The algorithm for this function is very simple. First, I search the query string for the variable name. If it is not found, the default string is returned. Otherwise, the value is copied, character-by-character, from the query string to the returned string. Of special note here is the conversion of a character from a %XX form to a normal character, and the termination at the end of the query string or when an ampersand is found.

This DLL manages a very simple database, but there's nothing preventing you from using a similar approach to dynamically publish Microsoft Word documents, for instance. One possibility is to add a real database that contains keywords, an abstract, the author name, and the title. Depending on your documentation guidelines, this information might already be available, so transferring it to the database could be as simple as moving the document to the right directory. A conversion utility can wait for new documents and use Word's Automation Server capabilities to extract information to put in the database.

The DLL also does a very simple search. When the number of articles increase, the search time increases. An option is to generate a database of all words longer than five characters, as well as capitalized words because they are usually acronyms. This can also be done automatically.

The Filter DLL


The filter DLL is very simple. It hooks SF_NOTIFY_SEND_RAW_DATA and looks for the ugly error message from IIS. If it is found, the DLL replaces it with a more understandable one.

The complete listing is as follows:

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

#include <httpfilt.h>

#include <alloc.h>

char *szErrorString = "HTTP/1.0 404 Object Not Found";

char *szMsgStart =

 "<html>\

 <head>\

 <title>Unknown URL</title>\

 </head>\

 <body>\

 <h1>Ouch!</h1>\

 <p>\

 That URL is unknown to me! \

 Please notify the administrator \

 of the "; /* referring page */

char *szMsgEnd =

 "</body>\

 </html>";

BOOL WINAPI __stdcall __export GetFilterVersion(

 HTTP_FILTER_VERSION *pVer)

{

 pVer->dwFlags = SF_NOTIFY_SEND_RAW_DATA |

 SF_NOTIFY_ORDER_DEFAULT;

 pVer->dwFilterVersion = MAKELONG(0, 1);

 strcpy(pVer->lpszFilterDesc, "Invalid URL Catcher");

 return TRUE;

}

DWORD WINAPI __stdcall __export HttpFilterProc(

 HTTP_FILTER_CONTEXT *pfc,

 DWORD NotificationType,

 VOID *pvData)

{

 PHTTP_FILTER_RAW_DATA pRawData;

 LPSTR lpszData;

 DWORD dwLength;

 char szBuffer[1024];

 char *sz;

 switch (NotificationType)

 {

 case SF_NOTIFY_SEND_RAW_DATA:

 pRawData = pvData;

 lpszData = (LPSTR) calloc(pRawData->cbInBuffer + 1,

 sizeof(char));

 memcpy(lpszData, pRawData->pvInData,

 pRawData->cbInBuffer);

 if (strstr(lpszData, szErrorString))

 {

 *((char *) pRawData->pvInData) = '\0';

 pRawData->cbInBuffer = 0;

 dwLength = strlen(szMsgStart);

 pfc->WriteClient(pfc, szMsgStart, &dwLength, 0);

 dwLength = sizeof(szBuffer);

 pfc->GetServerVariable(pfc, "ALL_HTTP", szBuffer,

 &dwLength);

 sz = strstr(szBuffer, "HTTP_REFERER");

 if (sz)

 {

 sz += strlen("HTTP_REFERER") + 1;

 strtok(sz, "\r\n");

 sz = strdup(sz);

 wsprintf(szBuffer, "<a href=%s>\

 referring page.</a>",

 sz);

 free(sz);

 }

 else

 {

 strcpy(szBuffer, "referring page.");

 }

 dwLength = strlen(szBuffer);

 pfc->WriteClient(pfc, szBuffer, &dwLength, 0);

 dwLength = strlen(szMsgEnd);

 pfc->WriteClient(pfc, szMsgEnd, &dwLength, 0);

 return SF_STATUS_REQ_FINISHED_KEEP_CONN;

 }

 free(lpszData);

 break;

 default:

 break;

 }

 return SF_STATUS_REQ_NEXT_NOTIFICATION;

}

That's all! However simple it seems, there are some things to look out for. First, if you don't set the pvInData and cbInBuffer fields to 0, IIS shows its ugly error message anyway. Second, the search string should be unique. The previous code will terminate any request that contains the original error message. One optimization would be to check either more of the message, or to verify that the entire message is very short.

Third, the referring page will be set only if there is a referring page. If the user types the URL directly, there will be nowhere to link. Fourth, you should normally return SF_STATUS_REQ_NEXT_NOTIFICATION to inform IIS that the other filters that are installed can get a hand on the data. However, this is not what we want in this case. Therefore, the filter returns SF_STATUS_REQ_FINISHED_KEEP_CONN when it has shown the alternative error message.



Here are a couple of tips for developing filters:
Make sure the compiler is set to align record fields at 4-byte boundaries (double word). Without this, IIS will simply never call your HttpFilterProc().
If you are using any DLLs, make sure that they are present on the Web server. Most C/C++ compilers put the RTL in a DLL by default. If a DLL is missing, IIS will not load your filter.



Complete Source Code Listing


The file HTTPExt.PAS can be found at http://www.borland.com/techsupport/delphi/devcorner/internet/internet.html as httpext.zip. The other files needed for compiling and running the project are listed here. Tester.PAS is needed only for testing. It contains a stub that sets up the TEXTENSION_CONTROL_BLOCK structure and calls HttpExtensionProc(). Currently, it supports only WriteClient().

NewsList.DPR

{ $DEFINE EXEFILE}

{$IFDEF EXEFILE}

program NewsList;

(*

 This code is based on code donated to the public domain by

 Martin Larsson, http://www.delfidata.no/users/~martin.

*)

uses

 Main in 'Main.pas',

 HTTPExt in 'Httpext.pas',

 ISAPILib in ISAPILib.pas

 Tester in 'Tester.PAS',

 NLExt in 'NLExt.pas';

begin

 Test;

{$ELSE}

library NewsList;

uses

 Main in 'Main.pas',

 HTTPExt in 'Httpext.pas',

 NLExt in 'NLExt.pas',

 ISAPILib in ISAPILib.pas;

exports

 GetExtensionVersion,

 HttpExtensionProc;

begin

{$ENDIF}

end.

Main.PAS

unit Main;

(*

 This code is based on code donated to the public domain by

 Martin Larsson, http://www.delfidata.no/users/~martin.

*)

interface

uses

 HTTPExt, Windows;

function GetExtensionVersion(

 var Ver : THSE_VERSION_INFO

 ) : BOOL; stdcall;

function HttpExtensionProc(

 var ECB : TEXTENSION_CONTROL_BLOCK

 ) : DWORD; stdcall;

implementation

uses

 NLExt, SysUtils, Dialogs;

function GetExtensionVersion(

 var Ver : THSE_VERSION_INFO

 ) : BOOL; stdcall

begin

 Ver.dwExtensionVersion := MAKELONG(HSE_VERSION_MINOR,

 HSE_VERSION_MAJOR);

 StrLCopy(Ver.lpszExtensionDesc, 'Delphi NewsList',

 HSE_MAX_EXT_DLL_NAME_LEN);

 Result := True;

end; { GetExtensionVersion }

function HttpExtensionProc(

 var ECB : TEXTENSION_CONTROL_BLOCK

 ) : DWORD; stdcall;

var

hMutex : THandle;

begin

 hMutex := CreateMutex(nil, False, 'NewsListMutex');

 WaitForSingleObject(hMutex, INFINITE);

 with TNLExt.Create(ECB) do begin

 DoIt;

 Result := ReturnValue;

 Free;

end;

 ReleaseMutex(hMutex);

 CloseHandle(hMutex);

end; { HttpExtensionProc }

end.

NLExt.PAS

unit NLExt;

(*

 This code is based on code donated to the public domain by

 Martin Larsson, http://www.delfidata.no/users/~martin.

*)

interface

uses

 ISAPILib, HTTPExt, Windows;

type

 TNLExt = class(TObject)

 private

 httpConnection : THTTPExt;

 sInsideSearchDir : string;

 sOutsideSearchDir : string;

 procedure ListFiles;

 function Line(s : string; nStart : integer) : string;

 procedure GetSubjectAndAuthor(

 const sEntireFile : string;

 var sSubject, sAuthor : string);

 function GetReturnValue : DWORD;

 function GetEntireFile(sName : string) : string;

 function IncludeFile(sEntireFile : string) : boolean;

 public

 constructor Create(var ECB : TEXTENSION_CONTROL_BLOCK);

 destructor Destroy; override;

 procedure DoIt;

 property ReturnValue : DWORD read GetReturnValue;

 end;

implementation

uses

 IniFiles, SysUtils, Classes;

constructor TNLExt.Create(var ECB : TEXTENSION_CONTROL_BLOCK);

var

 sIniFile : string;

 tini : TIniFile;

begin

 inherited Create;

 SetLength(sIniFile, 250);

 GetModuleFilename(hInstance, PChar(sIniFile), Length(sIniFile));

 sIniFile := Copy(sIniFile, 1, Pos('.', sIniFile)) + 'ini';

 tini := TIniFile.Create(sIniFile);

 sInsideSearchDir := tini.ReadString('Inside',

 'SearchDir',

 '.');

 sOutsideSearchDir := tini.ReadString('Outside',

 'SearchDir',

 '.');

 if sInsideSearchDir[Length(sInsideSearchDir)] <> '\' then begin

 sInsideSearchDir := sInsideSearchDir + '\';

 end;

 if sOutsideSearchDir[Length(sOutsideSearchDir)] <> '/' then begin

 sOutsideSearchDir := sOutsideSearchDir + '/';

 end;

 tini.Free;

 httpConnection := THTTPExt.Create(ECB);

end; { TNLExt.Create }

destructor TNLExt.Destroy;

begin

 httpConnection.Free;

 inherited Destroy;

end; { TNLExt.Destroy }

function TNLExt.GetReturnValue : DWORD;

begin

 Result := httpConnection.ReturnValue;

end; { TNLExt.GetReturnValue }

procedure TNLExt.DoIt;

begin

 try

 with httpConnection do begin

 StdHeader('Delphi NewsList');

 StdBody;

 Paragraph;

 Header(1, 'The Delphi News List');

 end;

 ListFiles;

 except

 on E : Exception do

 httpConnection.Error('Exception ' + E.ClassName + #13 +

 E.Message);

 end;

end; { TNLExt.DoIt }

function TNLExt.IncludeFile(sEntireFile : string) : boolean;

var

 sInclude : string;

 sExclude : string;

 n, nLast : integer;

begin

 Result := False;

 sInclude := httpConnection.GetQueryVariable('Include', '');

 sInclude := Trim(sInclude);

 n := Pos(' ', sInclude);

 while n <> 0 do begin

 Delete(sInclude, n, 1);

 n := Pos(' ', sInclude);

 end;

 if Length(sInclude) > 0 then begin

 sInclude := AnsiLowerCase(sInclude);

 nLast := 1;

 n := Pos(' ', sInclude);

 while not Result and (n <> 0) do begin

 Result := Pos(Copy(sInclude, nLast, n - 1),

 sEntireFile) <> 0;

 nLast := n + 1;

 n := Pos(' ', Copy(sInclude, nLast, Length(sInclude)));

 end;

 if not Result then begin

 sInclude := Copy(sInclude, nLast, Length(sInclude));

 Result := Pos(sInclude, sEntireFile) <> 0;

 end;

 end

 else begin

 Result := True;

 end;

 sExclude := httpConnection.GetQueryVariable('Exclude', '');

 sExclude := AnsiLowerCase(sExclude);

 sExclude := Trim(sExclude);

 n := Pos(' ', sExclude);

 while n <> 0 do begin

 Delete(sExclude, n, 1);

 n := Pos(' ', sExclude);

 end;

 nLast := 1;

 n := Pos(' ', sExclude);

 while Result and (n <> 0) do begin

 Result := Pos(Copy(sExclude, nLast, n - 1), sEntireFile) = 0;

 nLast := n + 1;

 n := Pos(' ', Copy(sExclude, nLast, Length(sExclude)));

 end;

 if Result then begin

 sExclude := Copy(sExclude, nLast, Length(sExclude));

 Result := Pos(sExclude, sEntireFile) = 0;

 end;

end; { TNLExt.IncludeFile }

procedure TNLExt.ListFiles;

var

 sSubject : string;

 sAuthor : string;

 tsr : TSearchRec;

 nFinding : integer;

 sEntireFile : string;

begin

 with httpConnection do begin

 TableStart;

 nFinding := FindFirst(sInsideSearchDir + '*.txt',

 faAnyFile, tsr);

 try

 RowStart;

 TableHeading('Subject');

 TableHeading('Author');

 RowEnd;

 while nFinding = 0 do begin

 sEntireFile := GetEntireFile(sInsideSearchDir + tsr.Name);

 sSubject := tsr.Name;

 sAuthor := 'Unknown';

 GetSubjectAndAuthor(sEntireFile, sSubject, sAuthor);

 if IncludeFile(AnsiLowerCase(sEntireFile)) then begin

 RowStart;

 Column(MakeLink(sSubject, sOutsideSearchDir + tsr.Name));

 Column(sAuthor);

 RowEnd;

 end;

 nFinding := FindNext(tsr);

 end;

 finally

 FindClose(tsr);

 end;

 TableEnd;

 end;

end; { TNLExt.ListFiles }

function TNLExt.Line(s : string; nStart : integer) : string;

begin

 Result := '';

 while not (s[nStart] in [#10, #13]) do begin

 case s[nStart] of

 '>' :

 Result := Result + '&gt;';

 '<' :

 Result := Result + '&lt;';

 else

 Result := Result + s[nStart];

 end;

 Inc(nStart);

 end;

end; { TNLExt.Line }

procedure TNLExt.GetSubjectAndAuthor(

 const sEntireFile : string;

 var sSubject, sAuthor : string);

var

 nPos : integer;

begin

 nPos := Pos('From:', sEntireFile);

 if nPos <> 0 then begin

 sAuthor := Line(sEntireFile, nPos + 6);

 end;

 nPos := Pos('Subject:', sEntireFile);

 if nPos <> 0 then begin

 sSubject := Line(sEntireFile, nPos + 9);

 end;

end; { TNLExt.GetSubjectAndAuthor }

function TNLExt.GetEntireFile(sName : string) : string;

var

 tfs : TFileStream;

begin

 tfs := TFileStream.Create(sName, fmShareDenyWrite);

 try

 SetLength(Result, tfs.Size + 1);

 Result[tfs.Size + 1] := #0;

 tfs.ReadBuffer(PChar(Result)^, tfs.Size);

 finally

 tfs.Free;

 end;

end;

end.

ISAPILib.PAS

unit ISAPILib;

(*

 This code is based on code donated to the public domain by

 Martin Larsson, http://www.delfidata.no/users/~martin.

*)

interface

uses

 HTTPExt, Windows;

type

 THTTPExt = class(TObject)

 private

 fECB : TEXTENSION_CONTROL_BLOCK;

 fdwReturn : DWORD;

 fsQueryString : string;

 function GetQueryString : string;

 public

 constructor Create(var ECB : TEXTENSION_CONTROL_BLOCK);

 destructor Destroy; override;

 procedure OutStr(s : string);

 procedure Error(s : string);

 procedure OutLine(s : string);

 procedure StdHeader(sTitle : string);

 procedure StdBody;

 procedure Paragraph;

 procedure Header(nLevel : integer; s : String);

 procedure TableStart;

 procedure TableEnd;

 procedure RowStart;

 procedure RowEnd;

 procedure Column(sText : string);

 procedure TableHeading(sText : string);

 procedure Link(sText, sLocation : string);

 function MakeLink(sText, sLocation : string) : string;

 function GetQueryVariable(

 const sVarName : string;

 const sDefault : string

 ) : string;

 property ReturnValue : DWORD read fdwReturn;

 property Query : string read fsQueryString;

 end;

const

 sNewLine = #13 + #10;

implementation

uses

 SysUtils;

constructor THTTPExt.Create(var ECB : TEXTENSION_CONTROL_BLOCK);

begin

 inherited Create;

 fECB := ECB;

 fECB.dwHttpStatusCode := 200; // Success!

 fECB.lpszLogData[0] := #0; // No log data by default.

 fdwReturn := HSE_STATUS_SUCCESS;

 fsQueryString := GetQueryString;

end; { THTTPExt.Create }

destructor THTTPExt.Destroy;

begin

 OutLine('</body>');

 OutLine('</html>');

 inherited Destroy;

end; { THTTPExt.Destroy }

procedure THTTPExt.OutStr(s : string);

var

 dw : DWORD;

begin

 dw := Length(s);

 fECB.WriteClient(fECB.ConnID, PChar(s), dw, 0);

end; { THTTPExt.OutStr }

procedure THTTPExt.Error(s : string);

var

 dw : DWORD;

begin

 dw := Length(s);

 fECB.WriteClient(fECB.ConnID, PChar(s), dw, 0);

end; { THTTPExt.Error }

procedure THTTPExt.OutLine(s : string);

begin

 OutStr(s + sNewLine);

end; { THTTPExt.OutLine }

procedure THTTPExt.StdHeader(sTitle : string);

var

 s : string;

 dwSize : DWORD;

 dwType : DWORD;

begin

 s := '';

 dwSize := 0;

 dwType := 0;

 fECB.ServerSupportFunction(

 fECB.ConnID,

 HSE_REQ_SEND_RESPONSE_HEADER,

 PChar(s), dwSize, dwType);

 OutLine('<html>');

 OutLine('<head>');

 OutLine(' <title>');

 OutLine(' ' + sTitle);

 OutLine(' </title>');

 OutLine('</head>');

end; { THTTPExt.StdHeader }

procedure THTTPExt.StdBody;

begin

 OutLine('<body>');

end; { THTTPExt.StdBody }

procedure THTTPExt.Paragraph;

begin

 OutLine('<p>');

end; { THTTPExt.Paragraph }

procedure THTTPExt.Header(nLevel : integer; s : String);

var

 sHeader : string;

begin

 sHeader := 'h' + IntToStr(nLevel);

 OutLine('<' + sHeader + '>' + s + '</' + sHeader + '>');

end; { THTTPExt.Header }

procedure THTTPExt.TableStart;

begin

 OutLine('<table>');

end; { THTTPExt.TableStart }

procedure THTTPExt.TableEnd;

begin

 OutLine('</table>');

end; { THTTPExt.TableEnd }

procedure THTTPExt.RowStart;

begin

 OutLine(' <tr>');

end; { THTTPExt.TableStart }

procedure THTTPExt.RowEnd;

begin

 OutLine(' </tr>');

end; { THTTPExt.TableEnd }

procedure THTTPExt.Column(sText : string);

begin

 OutLine(' <td>');

 OutLine(' ' + sText);

 OutLine(' </td>');

end; { THTTPExt.Column }

procedure THTTPExt.TableHeading(sText : string);

begin

 OutLine(' <th>');

 OutLine(' ' + sText);

 OutLine(' </th>');

end; { THTTPExt.Column }

procedure THTTPExt.Link(sText, sLocation : string);

begin

 OutLine(MakeLink(sText, sLocation));

end; { THTTPExt.Link }

function THTTPExt.MakeLink(sText, sLocation : string) : string;

begin

 Result := '<a href="' + sLocation + '">' + sText + '</a>';

end; { THTTPExt.MakeLine }

function THTTPExt.GetQueryVariable(

 const sVarName : string;

 const sDefault : string

 ) : string;

var

 ns : integer;

 nStart : integer;

 bDone : boolean;

begin

 {

 Find sVarName in Query.

 }

 nStart := Pos(LowerCase(sVarName), LowerCase(Query));

 if nStart = 0 then begin

 Result := sDefault;

 end

 else begin

 {

 Convert to 'normal' format.

 }

 Result := '';

 {

 Skip the varname and the '=' sign.

 }

 ns := nStart + Length(sVarName) + 1;

 bDone := False;

 while not bDone and (ns <= Length(Query)) do begin

 case Query[ns] of

 '+' :

 Result := Result + ' ';

 '%' : begin

 Inc(ns);

 Result := Result +

 Char(StrToInt('$' + Copy(Query, ns, 2)));

 Inc(ns);

 end;

 '&' :

 bDone := True;

 else begin

 Result := Result + Query[ns];

 end;

 end;

 Inc(ns);

 end;

 end;

end; { THTTPExt.GetQueryVariable }

function THTTPExt.GetQueryString : string;

var

 pc : PChar;

begin

 if CompareText(string(fECB.lpszMethod), 'POST') = 0 then begin

 SetLength(Result, fECB.cbAvailable);

 StrLCopy(PChar(Result), fECB.lpbData, fECB.cbAvailable);

end

 else begin

 pc := AllocMem(StrLen(fECB.lpszQueryString) + 1);

 StrCopy(pc, fECB.lpszQueryString);

 Result := StrPas(fECB.lpszQueryString);

 FreeMem(pc, StrLen(fECB.lpszQueryString) + 1);

 end;

 if Result[Length(Result) - 1] = #13 then begin

 Delete(Result, Length(Result) - 1, 2);

 end;

end; { THTTPExt.GetQueryString }

end.

Tester.PAS

unit Tester;

(*

 This code is based on code donated to the public domain by

 Martin Larsson, http://www.delfidata.no/users/~martin.

*)

interface

uses

 HTTPExt, Windows;

procedure Test;

function GetServerVariable(

 hConn : HCONN;

 VariableName : PChar;

 Buffer : Pointer;

 var Size : DWORD

 ) : BOOL; stdcall;

function WriteClient(

 ConnID : HCONN;

 Buffer : Pointer;

 var Bytes : DWORD;

 dwReserved : DWORD

 ) : BOOL; stdcall;

function ReadClient(

 ConnID : HCONN;

 Buffer : Pointer;

 var Size : DWORD

 ) : BOOL; stdcall;

function ServerSupportFunction(

 hConn : HCONN;

 HSERRequest : DWORD;

 Buffer : Pointer;

 var Size : DWORD;

 var DataType : DWORD

 ) : BOOL; stdcall;

implementation

uses

 Main, Classes, SysUtils;

const

 sFilename = 'e:\arbeid\test.htm';

 sQuery = 'Include=%25&Exclude=';

procedure Test;

var

 tecb : TEXTENSION_CONTROL_BLOCK;

begin

 FillChar(tecb, sizeof(TEXTENSION_CONTROL_BLOCK), 0);

 tecb.cbSize := sizeof(TEXTENSION_CONTROL_BLOCK);

 tecb.WriteClient := WriteClient;

 tecb.ReadClient := ReadClient;

 tecb.GetServerVariable := GetServerVariable;

 tecb.ServerSupportFunction := ServerSupportFunction;

 tecb.lpszMethod := 'GET';

 if CompareText(tecb.lpszMethod, 'POST') = 0 then begin

 GetMem(tecb.lpbData, Length(sQuery) + 1);

 StrPCopy(tecb.lpbData, sQuery);

 tecb.cbAvailable := Length(sQuery);

 end

 else begin

 GetMem(tecb.lpszQueryString, Length(sQuery) + 1);

 StrPCopy(tecb.lpszQueryString, sQuery);

 end;

 HttpExtensionProc(tecb);

end; { Test }

function GetServerVariable(

 hConn : HCONN;

 VariableName : PChar;

 Buffer : Pointer;

 var Size : DWORD

 ) : BOOL; stdcall;

begin

 Result := True;

end; { GetServerVariableProc }

function WriteClient(

 ConnID : HCONN;

 Buffer : Pointer;

 var Bytes : DWORD;

 dwReserved : DWORD

 ) : BOOL; stdcall;

var

 tfs : TFileStream;

begin

 tfs := nil;

 try

 tfs := TFileStream.Create(sFilename, fmOpenWrite);

 tfs.Seek(0, soFromEnd);

 except

 on EFOpenError do begin

 tfs.Free;

 tfs := TFileStream.Create(sFilename,

 fmOpenWrite or fmCreate);

 end;

 end;

 tfs.WriteBuffer(Buffer^, Bytes);

 tfs.Free;

 Result := True;

end; { WriteClientProc }

function ReadClient(

 ConnID : HCONN;

 Buffer : Pointer;

 var Size : DWORD

 ) : BOOL; stdcall;

begin

 Result := True;

end; { ReadClientProc }

function ServerSupportFunction(

 hConn : HCONN;

 HSERRequest : DWORD;

 Buffer : Pointer;

 var Size : DWORD;

 var DataType : DWORD

 ) : BOOL; stdcall;

begin

 Result := True;

end; { ServerSupportFunctionProc }

initialization

 DeleteFile(sFilename);

end.

Summary


In this chapter, we looked at the various interfaces to Microsoft's Internet Information Server. There are several ways to couple your code up with IIS. The simplest is to use one of the various scripting languages such as JavaScript and VBScript. To increase interactivity and versatility, Java applets and ActiveX controls can be used. These techniques are not specific to IIS, but rather demands support on the client side.

If you don't want to limit your audience by requiring specific capabilities in the browser, you must run code on the server. IIS supports the standard CGI interface, so most existing CGI script will run unmodified. To overcome the limitations of CGI, IIS offers the ability to write special DLLs that run within the IIS memory space. These DLLs follow the ISAPI specification for extensions or filters. ISAPI extension DLLs are the direct alternative to CGI scripts, and are usually used for forms processing and calculations. Filters, on the other hand, operate between the server and the client. This means they have the ability to perform macro-expansion, on-the-fly document conversion among other things.

When all you want to do is to interact directly with a database, you can use the high-level database interface, the Internet Database Connector (IDC). This is actually supported through a standard IIS extension DLL. Through IDC, you can connect to any database through ODBC and SQL.

IDC and ISAPI filters and extension DLLs were covered in detail, but we also touched upon Java, JavaScript, ActiveX controls and CGI.

Hopefully, the information contained in this chapter will enable you to add the dynamics to your web pages that makes them interesting to visit over and over again.

Previous Page Page Top TOC Next Page