Was ist ISAPI und was macht man damit?
Eine ISAPI Extension DLL in Delphi
[Rev. 1]


!!! mit Delphi ab Version 4 !!!

May the source be with you, stranger ...

GoPyright © 2001 by -=Assarbad [GoP]=-


ISAPI?

ISAPI ist ganz ausgesprochen das "Internet Server Applications Programming Interface". Und tatsächlich, ISAPI taucht nicht nur beim IIS von Microsoft auf, sondern wird auch von anderen Servern benutzt. Im Windowsbereich wird privat der PWS (Personal Web Server) oft benutzt oder der ab Windows 2000 auch in der Professional (adäquat zu NT Workstation) integrierte IIS, der auf den NT Serversystemen natürlich unbeschränkt läuft. ISAPI fähig ist desweiteren der Sambar Server (Freeware) und einige andere kommerzielle Lösungen.

Sinn und Zweck ist es, die Interaktion zwischen Benutzer und Server dynamischer zu gestalten. Eine simple Möglichkeit ist zB die Begrüßung einer Person anhand der IP Adresse. Dies kann durch das dynamische Erstellen einer HTML-Seite bewerkstelligt werden, auch wenn dies wie das Schießen auf Spatzen mit Kanonen anmutet ;)

Zu diesem Behufe gibt es schon diverse Standards. Das bekannteste Beispiel stellt wohl das CGI (Common Gateway Interface) dar, welches grob gesagt Benutzerdaten aus dem Standardinput liest und in den Standardoutput schreibt. Meist wird in diesem Zusammenhang Perl benutzt. WinCGI ist in Anlehnung an CGI entstanden und benutzt ausschließlich ausführbare Binärdateien im Gegensatz zu den interpretierten CGI-Scripts!

Eine andere Variante der Interaktion sind wiederum die Skriptsprachen, wie VBS (in ASP) und JScript/Java (in ASP oder unabhängig).

ISAPI ähnelt zB der NSAPI (Netscape Server API) und wird oft in einem Atemzug mit dieser genannt. Da mir ausschließlich Varianten bekannt sind, in denen ISAPI DLLs existieren, gehe ich davon aus, daß es sich um eine reine Win32-Angelegenheit handelt, auch wenn die Implementation auf anderen Systemen Solaris / Linux / BSD etc denkbar ist!

Voraussetzungen

Ich setze hierfür natürlich eine intime Kenntnis von OPASCAL voraus. Man sollte auch Grundlagen der Win32-Programmierung beherrschen und sich mit Stringmanipulation auskennen.

Wozu gibts dann ISAPI, der Rest reicht doch?!

ISAPI hat einen entscheidenen Vorteil vor den anderen Varianten:

  1. Läuft als Anhängsel des Serverprozesses (DLL)
  2. Ist deshalb schneller und stabiler
  3. Ermöglicht das Erstellen von Filtern*

Hier wurde schon der erste Aspekt erwähnt: DLL. Unter Win32 ist dies die einzige Form, in der Prozeduren innerhalb verschiedener Kontexte ausgeführt werden können. Ein Prozess lädt eine DLL und kann danach deren Funktionen und Prozeduren benutzen.

* Filter erlauben es dem Webmaster Eingaben und Ausgaben abzufangen und zu manipulieren. Beispiele wären:

  1. Das Anhängen von Datum und Fußzeile an jede Webseite eines Servers.
  2. Das Umleiten von einer nicht-vorhandenen Webseite auf die Hauptseite oder eine Fehlerseite.
  3. Abbilden beliebiger Pfade auf andere Pfade.
  4. usw. ...

Grundlagen

"ISAPI-Anwendungen" bestehen meiner Kenntnis nach immer aus DLLs. Diese DLLs müssen mindesten folgende zwei mit der STDCALL Konvention deklarierte Funktionsexporte bereitstellen.

function GetExtensionVersion(var pVer: THSE_VERSION_INFO): BOOL; stdcall;
function HttpExtensionProc(var pECB: TEXTENSION_CONTROL_BLOCK): DWORD; stdcall;

Die benötigten Typen sind in folgenden Dateien oder späteren Versionen dieser Dateien deklariert: ISAPI.PAS und ISAPI2.PAS.

Weitere benutzte Win32 API Funktionen sind zB: lstrlen(), lstrcmpi(), wvsprintf(), LoadLibrary(), FormatMessage(), CreateFile(), CreateFileMapping(), MapViewOfFile(), UnMapViewOfFIle(), CloseHandle(), WriteFile(), GetLocalTime().

Da wir eine ISAPI Extension schreiben, brauchen wir ein paar HTTP-Grundlagen. Es gibt zwei grundlegende HTTP-Methoden die von einer Extension bearbeitet werden können: POST und GET. GET benutzt man (der Browser ;) um ein Dokument (sei es HTML oder ZIP) per HTTP anzufordern. POST wird meist benutzt um Daten eines Formulars an eine Web-Anwendung, wie zB CGI oder ISAPI zu "posten". Wir werden uns auf letzteres konzentrieren, wobei sich das Problem wie folgt definiert: Eine ISAPI Extension schreiben, die in der Lage ist Daten eines Formulars zu empfangen, abzuspeichern und eine weitere Seite anzuzeigen.

Grundgerüst

function GetExtensionVersion(var pVer: THSE_VERSION_INFO): BOOL; stdcall;
begin
  pVer.dwExtensionVersion := MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
  pVer.lpszExtensionDesc := description + #0;
  result := true;
end;

function HttpExtensionProc(var pECB: TEXTENSION_CONTROL_BLOCK): DWORD; stdcall;
begin
  SetString(queryString, PChar(pECB.lpbData), pECB.cbTotalBytes);

//Bearbeitung hier

  page := 'Content-Type: text/html' + #13#10#13#10 + page;
  dwLen := lStrLen(@page[1]);
  lstrcpy(Response, '200 OK');
  if pECB.ServerSupportFunction(pECB.ConnID, HSE_REQ_SEND_RESPONSE_HEADER, @Response, @dwlen, @page[1]) then
    result := HSE_STATUS_SUCCESS else result := HSE_STATUS_ERROR;
end;

Die Variablendeklarationen wurden hier explizit herausgelassen und sind zum Funktionieren sebstverständlich unabdingbar. Sie werden kurz erläutert.

Die Funktion GetExtensionVersion() gibt, wie ersichtlich, die unterstützte ISAPI-Version zurück und ebenfalls eine Kurzbeschreibung der Extension, die in der Variablen description versteckt ist. Das Ganze wird dann in die mit pVer übergebene Struktur geschrieben.

HttpExtensionProc() wiederum bearbeitet eine Anforderung. In diesem Falle, wird noch nicht nach GET oder POST unterschieden. Als erstes wird die Variable querystring mit dem (geposteten) Datenanteil der Anforderung initialisiert. Danach sollte man sinnvollerweise die Parameter einzeln aus dem String auslesen (Bearbeitung ;) und dann verarbeiten. Zum Schluß werden Daten zurückgegeben (in der Variablen page) und mit Hilfe der Callback-Funktion des Servers pECB.ServerSupportFunction() zurückgeschrieben. Das ist dann auch, was beim Clientbrowser ankommt. Es gibt dafür auch andere Möglichkeiten als diese (zB WriteClient()) aber hier werde ich nur diese besprechen, da es ein simples Beispiel bleiben soll.

Tip: Die obigen Funktionen werden in C so definiert, daß Pointer auf eine Struktur übergeben werden. In OPASCAL kann man sich das Gewurschtel mit dem Pointer aber sparen, indem Parameter als procedure procname(var name:type); übergeben werden. Dies ist equivalent zu procedure procname(name:^type); (syntaktisch nicht ganz korrekt, also Pointer auf einen Typen), aber einfacher.

Hilfsfunktionen

Parameterübergaben bei einem POST sehen wie folgt aus:
text=bla+bla+bla&zweiterparam=yes&dritter=no&vierter=1
Sonderzeichen, wie zum Bsp. deutsch Umlaute oder Backslash etc werden hexadezimal codiert und wie folgt notiert
%FF = Zeichen #0255.
Dabei sind die Zeichen immer nur ein Byte breit (= 2 hexadezimale Stellen).
Das "+" im obigen Beispiel codiert das Leerzeichen (space, blank space). Diese speziellen Zeichen müssen also im Laufe der Weiterverarbeitung dekodiert werden. Dies erreichen wir mit folgenden drei Funktionen

function EscapeToAscii(str: string): string;

  function HexToAscii(c: string): byte;
  begin
    c := '$' + c;
    result := _strtoint(c);
  end;

var
  j: integer;
  pc: pchar;
  c: byte;
begin
  pc := @str[1];
  j := 1;
  while (pc[0] <> #0) do begin
    case pc[0] of
      '%':
        begin
          c := HexToAscii(pc[1] + pc[2]);
          str[j] := char(c);
          Inc(j);
          Inc(pc, 3);
        end;
    else
      begin
        str[j] := pc[0];
        Inc(pc);
        Inc(j);
      end;
    end;
  end;
  SetLength(str, j - 1);
  result := str;
end;

function spaceout(s: string): string;
var i: integer;
begin
  i := 1;
  while i <= length(s) do
    case s[i] of
      '+':
        if (i + 1) <= length(s) then
          case s[i + 1] of
            '+':
              begin
                inc(i);
                s[i] := ' ';
                inc(i);
              end;
          else
            begin
              s[i] := ' ';
              inc(i);
            end;
          end else
        begin
          s[i] := ' ';
          inc(i);
        end;
    else inc(i);
    end;
  result := s;
end;

function GetAllparams(s: string): ATIStrings;
var po: integer;

  procedure fillarray(s1: string);
  var posi: integer;
  begin
    setlength(result, length(result) + 1);
    posi := pos('=', s1);
    if posi > 0 then begin
      result[length(result) - 1].name := EscapeToAscii(copy(s1, 1, posi - 1));
      delete(s1, 1, posi);
      if s1 <> '' then
        result[length(result) - 1].value := EscapeToAscii(s1);
    end else begin
      result[length(result) - 1].name := EscapeToAscii(s1);
      result[length(result) - 1].value := '';
    end;
  end;

begin
  setlength(result, 0);
  po := pos('&', s);
  case po > 0 of
    TRUE:
      while po > 0 do begin
        fillarray(copy(s, 1, po - 1));
        delete(s, 1, po);
        po := pos('&', s);
      end;
  end;
  Fillarray(s);
end;

Erstere konvertiert dabei die hexadezimal codierten Anteile zurück in ASCII/ANSI Zeichen. Letztere konvertiert "+" zu " ". (ein echtes "+" wird mit "++" codiert!)

Zum eigentlichen

Prinzip der Extension wird sein, daß sie einen POST annimmt, und entsprechend der Eingabe etwas zurückgibt. Die Eingabe, das sind in diesem Falle die Parameter, die an die DLL gereicht werden. Der Parameter mit dem Namen "file" und der Parameter "savetofile" (beide werden im übergebenen Formular als "hidden" deklariert) haben hierbei besondere Bedeutung, auf die später näher eingegangen wird.

lstrcmpi() gibt bei Übereinstimmung der übergebenen Strings (das "i" steht für case-insensitive, also Klein/Großschreibung nicht beachtend - Gegenpart ist lstrcmp) 0 (null) zurück, oder eben einen entsprechenden anderen Wert, je nach Relation zwischen den Strings (siehe PSDK oder Win32.HLP).

  case lstrcmpi(pECB.lpszMethod, 'POST') of
    0:
      begin
        filetoshow := GetParam('file', allparam);
        case filetoshow = '' of
          true: page := GetErrorPage(3);
          false: if fileexists(filetoshow) then begin
              page := GetFileInString(filetoshow);
              param := Getparam('savetofile', allparam);
              if param <> '' then begin
                GetLocalTime(st);
                datum := frmt('[%u-%u-%u]', [pointer(st.wDay), pointer(st.wMonth), pointer(st.wYear)]);
                temp := '';
                if fileexists(param) then temp := GetFileInString(param);
                temp := temp + #13#10#13#10 + '------------------------------------------' + #13#10 + 'Record from ' + datum;
                for i := 0 to length(allparam) - 1 do begin
                  temp := temp + #13#10 + spaceout(allparam[i].name) + ' = ' + spaceout(allparam[i].value);
                end;
                hFileWrite := CreateFile(@param[1], GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, CREATE_ALWAYS, FILE_ATTRIBUTE_ARCHIVE, 0);
                pc := @temp[1];
                WriteFile(hFileWrite, pc^, length(temp), SizeWritten, nil);
                CloseHandle(hFileWrite);
              end;
            end else page := GetErrorPage(2);
        end;
      end;
  else page := GetErrorPage(20);
  end;

In dieser CASE Schleife überprüfen wir also, ob es sich um einen POST handelt. Wenn ja, dann weitere Bearbeitung, wenn nein, dann Fehlerausgabe.

allparam ist als folgender Typ deklariert:

type
  ATIStrings = array of record
    name,
      value: string;
  end;

und wurde vorher aus dem übergebenen String extrahiert. Dazu benutzen wir die oben bereits genannte Funktion GetAllParams(), die EscapeToAscii() schon integriert.

Nachdem wir alle Parameter haben, lesen wir erstmal "file" aus. Wenn es die Variable gibt und sie einen sinnvollen Wert enthält (Datei existiert etc) dann wird die Seite schonmal in den Rückgabewert (hier page) eingelesen. Dazu bedienen wir uns FileMapping in der programminternen Routine GetFileInString(). Falls dies von Interesse ist, bitte die entsprechende Win32 API Dokumentation zu den einzelnen Funktionen befragen ;)

Die Variable "savetofile", insofern vorhanden, enthält jetzt ganz simpel den Namen und VOLLEN lokalen Pfad der Datei, in der die Formulardaten gesichert werden sollen. Dementsprechend lesen wir die Datei aus und hängen die neuen Daten an. Schema ist dabei Name_der_Variablen = Wert_der_Variablen. Das Datum wird mit integriert und fertig. In allen anderen Fällen wird ein Fehler zurückgegeben.

Fertig! ... aber wie benutzen?

Ganz einfach ;) Man benötigt ein wenig Kenntnis in HTML, oder einen guten HTML-Editor. Man bastelt sich ein Formular (Eingabefelder, Radiobuttons, Comboboxen etc) in HTML und definiert als Methode POST und als Aktion den relativen Pfad der DLL (zum Rootpfad des Servers)
Bsp: <FORM METHOD=POST ACTION="/bin/questionnaire.dll">

Hernach noch flux den Pfad der nächsten anzuzeigenden HTML-Datei über die Variable "file" deklarieren:
Bsp: <INPUT TYPE="hidden" NAME="file" VALUE="D:\usersd\xxxxxx\Webpage\thanks.html">

Und definieren, wo die Datei gespeichert werden soll:
Bsp: <INPUT TYPE="hidden" NAME="savetofile" VALUE="D:\usersd\xxxxxx\results.txt">

Nochmals: Die Pfade müssen vollständige absolute Pfade sein! Die Pfade sollten existieren, ansonsten gibt es eine Fehlermeldung (beim Anzeigen) oder es wird nix geschrieben (beim Abspeichern).

Um zum Formular / Fragebogen zu kommen empfiehlt sich folgender Ausgangspunkt. Dieses "Formular" präsentiert einen Button, der zB mit "Hier klicken um die Befragung zu starten" beschriftet werden könnte. (siehe test.html)

<FORM METHOD=POST ACTION="/bin/questionnaire.dll">
<INPUT TYPE="hidden" NAME="file" value="D:\usersd\xxxxxx\Webpage\Questionnaire1.html">
<INPUT TYPE=submit VALUE="Questionnaire 1">
</FORM>

Schlußworte

Die Extension wurde dazu geschrieben, einem afrikanischen Freund von mir eine Formular-Auswertung über das Internet zu ermöglichen. Er braucht(e) es dazu, seine Master-Thesis zu schreiben um den Master of Science Degree zu erreichen.

Euch allen, die daraus etwas gelernt haben, wünsche ich viel Spaß damit.

Nur mal zum Nachdenken: Die DLL benutzt nicht die SYSUTILS.PAS! Dies verringert die Größe nochmals um wenige kByte.

Die beiliegenden HTMLs (questionnaire1.html, test.html und Thanks.htm) sind Kopien der Fragebögen meines Freundes und sollen nur als Beispiele dienen.

 

Beispiel von oben als Download

Viel Spaß mit ISAPI Programmierung wünscht
-=Assarbad=-

Links:

  1. Downloadpfad für alle Tutorials
  2. Assarbad's Delphi Page
  3. Weitere ISAPI Ressourcen

GoPyright © 2001 by -=Assarbad [GoP]=-
Mail an den Autor

Die Weiterverbreitung in unveränderter Form ist ausdrücklich erlaubt. Eine modifizierte Version bedarf meiner Zustimmung. Bitte dazu Kontakt über obige Mail-Adresse aufnehmen.