2011. március 18., péntek

Writing a simple ISAPI Filter for IIS


Problem/Question/Abstract:

This article shows you the basics of creating a simple ISAPI filter and how to use one to map sub-domains into specific folders.

http://daniel.yourdomain.com
will map the same way as
http://www.yourdomain.com/members/daniel

Answer:

Borland has gone a long way helping us to create web applications. As of today, however, there is no real support for writing ISAPI Filters.

Note: ISAPI Filters are quite different from ISAPI/NSAPI Extensions!

INTRODUCTION

I have had no chance, so far, to look into Delphi 6 and figure how it supports ISAPI Filters, a first look revealed no new facts, anyway. ISAPI Filters are not called by referencing them in the URI of the request like
http://www.yourserver.com/cgi/isapi.dll?params=54,4568,54.8
rather they are installed with the IIS-Console and invoked on EVERY call made to your web site. Therefore, ISAPI Filters have to be very fast in processing requests.

THE LIBRARY BASICS

Every ISAPI Dll has to export two (or three) functions. These functions are called by thi IIS to initialize the filter, to process every single request and to destroy the filter.

Attn.: These names are case-sensitive!

GetFilterVersion is called once, while the filter is running. All preliminary work is to be done here. The filter has to return on what kind of events it is supposed to process data.

HttpFilterProc is called for every object requested by the client, regardless of whether it is an image, a html file or an ASP script.

TerminateFilter is called when the web server is shuting down. Use this function to free resources reservered, etc. This function must return the value 1 - as off IIS 4 (NT) and IIS 5 (W2K).

THE EVENTS PASSED TO HTTPFILTERPROC

The following basic events are defined:

SF_NOTIFY_READ_RAW_DATA - The filter wants to process all incoming data (like form data)
SF_NOTIFY_PREPROC_HEADERS - The filter wants to process all header information sent by the client (browser type, uri, cookies, ...)
SF_NOTIFY_AUTHENTICATION - The filter is used to check the user authentication sent by the client
SF_NOTIFY_URL_MAP - The filter is used to map incoming requests to specific folders and files on the hard drive
SF_NOTIFY_ACCESS_DENIED - The filter wants to process responses when authentication has failed (for logging, ...)
SF_NOTIFY_SEND_RAW_DATA - The filter wants to process all outgoing data (like link checker, cookie munchers, ...)
SF_NOTIFY_LOG - The filter is used to change the data sent to the IIS log files
SF_NOTIFY_END_OF_NET_SESSION - The filter has to free resources reserved for specific users

Note: The samples named are just a short list of possible uses for ISAPI filters.

Because a filter is invoked on every request, you have to register the filter for every event you want to handle. Events, the filter does not register for, are not sent to it, thus increasing the speed of the filter.

THE SUBDOMAIN <--> URL MAPPER

In this article I show you one simple, but useful application of such an ISAPI filter. This filter does not respect some aspects of good programming, like loading setup information, but rather has them written directly into the code - this one is left for you. I am just showing you the basics of ISAPI Filter programming.

The filter to be installed on the "Properties" dialog with your IIS-Console for the specific "Web Site."
Attn: You can install the filter on multiple "Web Sites," it is, however, loaded once only for all web sites using it. Therefore, you have to take care of this fact within your code.

Further: I have not made this sample aware of the multi-threading part an ISAPI Filter has to take care of.

WHAT DOES THIS SAMPLE DO

Incoming request like
http://username.yourdomain.com/guestbook.asp
are mapped as they were entered like
http://www.yourdomain.com/members/username/guestbook.asp
"Members" can be defined by you. This filter maps the subdomain only, if the subfolder corresponding with the subdomain exists.

MORE ON ISAPI FILTERS

In a future article I will publish a complete unit with all definitions for ISAPI Filters. At present I am still working on them. You can read more on ISAPI filters on the msdn.microsoft.com home page.

Here are the ISAPI header conversions: ISAPI Filter Header (D3K)

THE SOURCE CODE FOR THE ISAPI FILTER LIBRARY

library DomainMapper;

uses
  Windows, SysUtils, Classes, FileCtrl;

{$R *.RES}

// these constants must be changed to match your environment
const
  // as found on the hard drive
  FAbsBaseFolder = 'C:\INetPub\WWWRoot\Members\';
  // as entered in the URL
  FRelBaseFolder = '/members/';

const
  SF_MAX_FILTER_DESC_LEN = (256 + 1);

  SF_NOTIFY_SECURE_PORT = $00000001;
  SF_NOTIFY_NONSECURE_PORT = $00000002;
  SF_NOTIFY_READ_RAW_DATA = $00008000;
  SF_NOTIFY_PREPROC_HEADERS = $00004000;
  SF_NOTIFY_AUTHENTICATION = $00002000;
  SF_NOTIFY_URL_MAP = $00001000;
  SF_NOTIFY_ACCESS_DENIED = $00000800;
  SF_NOTIFY_SEND_RAW_DATA = $00000400;
  SF_NOTIFY_LOG = $00000200;
  SF_NOTIFY_END_OF_NET_SESSION = $00000100;

  SF_NOTIFY_ORDER_HIGH = $00080000;
  SF_NOTIFY_ORDER_MEDIUM = $00040000;
  SF_NOTIFY_ORDER_LOW = $00020000;
  SF_NOTIFY_ORDER_DEFAULT = SF_NOTIFY_ORDER_LOW;
  SF_NOTIFY_ORDER_MASK = SF_NOTIFY_ORDER_HIGH or SF_NOTIFY_ORDER_MEDIUM or
    SF_NOTIFY_ORDER_LOW;

  SF_STATUS_REQ_FINISHED = $8000000;
  SF_STATUS_REQ_FINISHED_KEEP_CONN = $8000001;
  SF_STATUS_REQ_NEXT_NOTIFICATION = $8000002;
  SF_STATUS_REQ_HANDLED_NOTIFICATION = $8000003;
  SF_STATUS_REQ_ERROR = $8000004;
  SF_STATUS_REQ_READ_NEXT = $8000005;

type
  PHTTP_FILTER_VERSION = ^HTTP_FILTER_VERSION;
  HTTP_FILTER_VERSION = record
    dwServerFilterVersion: DWORD;
    dwFilterVersion: DWORD;
    lpszFilterDesc: array[0..SF_MAX_FILTER_DESC_LEN - 1] of Char;
    dwFlags: DWORD;
  end;
  THTTP_FILTER_VERSION = HTTP_FILTER_VERSION;
  LPHTTP_FILTER_VERSION = PHTTP_FILTER_VERSION;

  LPVOID = POINTER;

  TFilterGetServerVariableProc = function(
    var pfc {: THTTP_FILTER_CONTEXT}; VariableName: PChar; Buffer: Pointer;
    var Size: DWORD
    ): BOOL; stdcall;

  TFilterAddResponseHeadersProc = function(
    var pfc {: THTTP_FILTER_CONTEXT}; lpszHeaders: PChar; dwReserved: DWORD
    ): BOOL; stdcall;

  TFilterWriteClientProc = function(
    var pfc {: THTTP_FILTER_CONTEXT}; Buffer: Pointer; var Bytes: DWORD;
    dwReserved: DWORD
    ): BOOL; stdcall;

  TFilterAllocMemProc = function(
    var pfc {: THTTP_FILTER_CONTEXT}; cbSize: DWORD; dwReserved: DWORD
    ): Pointer; stdcall;

  TFilterServerSupportFunctionProc = function(
    var pfc {: THTTP_FILTER_CONTEXT}; sfReq: DWORD; pData: Pointer;
    ul1, ul2: DWORD
    ): BOOL; stdcall;

  PHTTP_FILTER_CONTEXT = ^THTTP_FILTER_CONTEXT;
  THTTP_FILTER_CONTEXT = record
    cbSize: DWORD;
    Revision: DWORD;
    ServerContext: Pointer;
    ulReserved: DWORD;
    fIsSecurePort: BOOL;
    pFilterContext: Pointer;
    GetServerVariable: TFilterGetServerVariableProc;
    AddResponseHeaders: TFilterAddResponseHeadersProc;
    WriteClient: TFilterWriteClientProc;
    AllocMem: TFilterAllocMemProc;
    ServerSupportFunction: TFilterServerSupportFunctionProc;
  end;
  HTTP_FILTER_CONTEXT = THTTP_FILTER_CONTEXT;

  PCardinal = ^Cardinal;
  TGetServerVariable = function(
    var pfc: THTTP_FILTER_CONTEXT;
    VariableName: PChar;
    Buffer: LPVOID;
    BuffSize: PCardinal
    ): BOOL; StdCall;

  TGetHeaderProc = function(var pfc: THTTP_FILTER_CONTEXT; lpszName: PChar;
    var lpvBuffer; var lpdwSize: DWORD): BOOL stdcall;
  TSetHeaderProc = function(var pfc: THTTP_FILTER_CONTEXT; lpszName,
    lpszValue: PChar): BOOL stdcall;
  TAddHeaderProc = function(var pfc: THTTP_FILTER_CONTEXT; lpszName,
    lpszValue: PChar): BOOL stdcall;

  PHTTP_FILTER_PREPROC_HEADERS = ^THTTP_FILTER_PREPROC_HEADERS;
  THTTP_FILTER_PREPROC_HEADERS = record
    GetHeader: TGetHeaderProc;
    SetHeader: TSetHeaderProc;
    AddHeader: TAddHeaderProc;
    dwReserved: DWORD;
  end;

  { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    Exported Functions
  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * }

function GetFilterVersion(var pVer: HTTP_FILTER_VERSION): BOOL; stdcall; export;
begin
  with pVer do
  begin
    // version
    dwFilterVersion :=
      MakeLong(0 {minor}, 1 {major});
    // description
    StrPCopy(lpszFilterDesc, 'A Simple Domain Mapper');
    // notification flags
    dwFlags :=
      SF_NOTIFY_ORDER_DEFAULT or
      SF_NOTIFY_PREPROC_HEADERS;
  end;
  Result := True;
end;

function HttpFilterProc(var FilterContext: HTTP_FILTER_CONTEXT;
  NotificationType: DWORD; pvNotification: LPVOID): DWORD; stdcall; export;
var
  Buffer: array[0..4096] of Char;
  Size: Cardinal;
  P: Integer;
  DestFolder, Domain, SubDomain, URL: string;
  Data: THTTP_FILTER_PREPROC_HEADERS;
begin
  try
    Result := SF_STATUS_REQ_NEXT_NOTIFICATION;
    if NotificationType = SF_NOTIFY_PREPROC_HEADERS then
    begin
      // we can check whether to map the current sub-domain into a directory
      Data := THTTP_FILTER_PREPROC_HEADERS(pvNotification^);
      // get the domain requested by the user
      Size := SizeOf(Buffer);
      FillChar(Buffer, Size, 0);
      TGetServerVariable(FilterContext.GetServerVariable)
        (FilterContext, 'SERVER_NAME', @Buffer[0], @Size);
      Domain := StrPas(@Buffer[0]);
      // get the sub-domain requested
      P := Pos('.', Domain);
      if P = 0 then
        Exit;
      SubDomain := Copy(Domain, 1, P - 1);
      // create the destination folder
      DestFolder := FAbsBaseFolder + SubDomain;
      if DirectoryExists(ExtractFilePath(DestFolder)) then
      begin
        // the sub-domain my be routed - a corresponding folder does exist
        // get the uri of the document requested
        Size := SizeOf(Buffer);
        FillChar(Buffer, Size, 0);
        TGetHeaderProc(Data.GetHeader)(FilterContext, 'url', Buffer[0], Size);
        URL := StrPas(Buffer);
        // add the virtual base folder to the uri
        if URL = '' then
          URL := FRelBaseFolder + SubDomain
        else
          URL := FRelBaseFolder + SubDomain + URL;
        // send new uri to IIS
        StrPCopy(@Buffer[0], URL);
        TSetHeaderProc(Data.SetHeader)(FilterContext, 'url', @Buffer[0]);
      end;
    end;
  except
    // on error return request to process next ISAPI message
    Result := SF_STATUS_REQ_NEXT_NOTIFICATION;
  end;
end;

function TerminateFilter(Flags: DWORD): DWORD; stdcall; export;
begin
  // must return 1 (as of IIS 4 and IIS 5)
  Result := 1;
end;

exports
  GetFilterVersion,
  HttpFilterProc,
  TerminateFilter;

begin
end.

There is a Delphi Component that makes it easy to write ISAPI Filters with Delphi. It is distributed as source code. It is an excellent starting point for beginners as well as a time saving tool for advanced users.

TbcISAPIFilter component is available at: http://www.bestcode.com/html/isapi_filter.html

Nincsenek megjegyzések:

Megjegyzés küldése