Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #18
See Also: other Dr.Bob Examines columns or Delphi articles

DrBobCGI 3.2
In The Delphi Magazine issue #29 (january 1998), I first started the development of a unit called DrBobCGI to assist in web server application development (with support for obtaining GET or POST field values). Back in those days, WebBroker was part of Delphi 3 Client/Server, so DrBobCGI was a welcome addition for non-Delphi 3 C/S users.
In later issues of The Delphi Magazine, I extended DrBobCGI with support for the HttpUserAgent (the type and version of browser) and RemoteAddress (the IP-address of the browser), Cookies (in issue #44 and #45), Authorization (base64-encoded), ScriptName (the name of the web server application itself) and finally cross-platform support for both Delphi 4+ and all versions of Kylix. The latest (2002/06/23) version 3.2 of DrBobCGI is now available for download (note that it no longer works with Delphi 3 or lower).

Cookies
CGI and ISAPI server-side applications communicate using HTTP, which is a state-less protocol. This means that in order to save "state-information", we must do something special. In fact, there are three common ways to safe state information: FAT URLs, hidden fields and cookies. DrBobCGI supports all three, but I want to focus on passing (and reading) cookies now.

WebApp42
Let's assume we have a CGI application or ISAPI.DLL, called WebApp42, which starts by asking our name, and which needs to "maintain" the "value" of our name for the remainder of the session. If no name can be maintained, then WebServ needs to ask for the name everytime a user re-connects (hence one of the reasons for the need to maintain state). In fact, cookies can remain persistent even over sessions (days later).

Cookies
Cookies are sent by the server to the browser. When using cookies, the initiative is with the web server, but the client has the ability to deny or disable a cookie. Sometimes, servers even send cookies when you don't ask for them, which can be a reason why some people dislike cookies (like I did for years, for example). There comes a time, however, when cookies are really useful, for example when maintaining state information beyond a single session. In these cases, when information must be retained for a period of time, cookies are just about the only possible solution.

Set-Cookie
Assuming we use a regular "standard" 32-bit version of Delphi (not the WebBroker components), then we need to use a low-level way to set the value of a new cookie. Fortunately, cookies are set in the HTTP header that a CGI application (or ISAPI DLL) needs to return. The syntax is as follows (the uppercase fields denote values that can be specified by the user):

  Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure
Both the NAME and the VALUE can be anything set by the user. So, we can have Name=Bob or Answer=42 or 1=2. Note that the "NAME=VALUE" pair is the only required attribute of a Set-Cookie command. The DATE in expires=DATE defined the date after which the cookie is invalid (i.e. after which the cookie will no longer be available). DATE must be formatted as follows:
  Day, DD-MMM-YYYY HH:MM:SS GMT
For example:
  Mon, 01-Feb-1999 07:11:42 GMT
Note that GMT is the only legal time zone, enforcing consistency of many (international) visitors and web servers. This means that we may need to convert our time to the GMT timezone, and format the Date according to the above specifications, but that's just about the biggest problem (if any) we'll face when using cookies. The DOMAIN in domain=DOMAIN specifies the internet domain name of the host from which the current URL is fetched. If the domain of the URL is the same as the DOMAIN for a specific cookie in the cookie-list (on disk), then the PATH (in path=PATH) of that cookie is checked as well to see if the cookie indeed should be sent along with the URL fetched from the domain and path as specified. The default value of DOMAIN is the host name of the server that generated the cookie, and the default value of PATH is a single "/" character (meaning everything matches). If you leave both of them empty, then a cookie set for any page in your website will be valid (i.e. sent along with) any other page of your website as well. This may or may not be what you intended, but at least the option is open to you. One warning: the PATH is case-sensitive (I guess the DOMAIN is as well), so be sure to remember that a cookie generated for HOME.HTM will not be sent back to home.htm (if you specified HOME.HTM as specific path, that is). Finally, the "secure" attribute specifies whether or not to use the cookie using a secure channel (i.e. using HTTPS = HTTP over SSL). If secure is not specified, the cookie is considered (mostly) harmless, and will be sent in the clear over unsecured channels.

Personally, when I use cookies, I want them to be available for every page of my website, so I usually don't bother with the DOMAIN or PATH attributes, nor do I use the secure attribute, which leaves the NAME=VALUE and expires=DATE attributes only. If you don't specify a value for expires=DATE, then the cookie will be valid during the lifetime of the session only (i.e. once you close down the browser, the cookie is gone). So, in order to get a persistent cookie, you must set this expires DATE to a valid (future) value.

  program WebApp42;
  uses
    DrBobCGI, SysUtils;
  begin
    writeln('content-type: text/html');
    writeln('Set-Cookie: Name=Bob; path=/'); // non-persistent cookie
    writeln;
    writeln('<html>');
    writeln('<body>');
    writeln('<h1>Cookies</h1>');
    writeln('<hr>');
    writeln(CookieValue(''));
    writeln('</body>');
    writeln('</html>');
  end.
Note that when you execute the above program, you need to refresh the browser to see the cookie result from CookieValue (the first time, only the cookie will be set, but no value will be send back - this will be done the second time around, when you actually pick up the cookie). That's not a bug but a feature of cookies, of course.

HTTP_COOKIE
Now that we know how to set the value (and other attributes) of a cookie, it's time to find out how to get (or read) the values from the cookies back from the cookie-file on disk. Fortunately, the hard work (getting the cookies from the cookie-file on disk) is done for us by the web browser, which passes the result in the header of the resulting HTML document (again) using the following syntax:

  Cookie: NAME=VALUE; NAME=VALUE
In our specific case, we would get "Cookie: Name=Bob" only, but all matching cookie NAME=VALUES will be sent along with the URL we requested (note that "matching" here refers to matching DOMAIN and PATH, and only for cookies that are not expired yet). Reading cookie values is nothing more than parsing the single "Cookie:" line and obtaining the NAME=VALUE pairs, just like regular CGI GET or POST variables. And in fact, I've used my existing code (unit DrBobCGI) that could process GET and POST variables, and extended it with Cookie support.

DrBobCGI 3.2
Now that we know how to set cookies (using a Set-Cookie statement) and get cookie values back (the values assigned to the HTTP_COOKIE environment string), it's time to pick up the DrBobCGI unit and enhance it with cookie support - the CookieValue function. I've also made sure that when you use a combination of GET and POST, they'll both end up in the Value function. What remains to be done is support for multi-values input fields (i.e. two mentions of the same name with a different value), but that's something for version 4.

  unit DrBobCGI;
  {===================================================================}
  { DrBobCGI (c) 1999-2002 by Bob Swart (aka Dr.Bob - www.drbob42.com }
  { version 1.0 - obtain standard CGI variable values by "value()".   }
  { version 2.0 - obtain CGI values, cookies and IP/UserAgent values. }
  { version 2.1 - obtain Authorisation values (base64-encoded string) }
  { version 3.0 - ported to Kylix 1+ while still works with Delphi 4+ }
  {               Note: DrBobCGI does not work with Delphi 3 or lower }
  { version 3.1 - combining GET and POST fields inside one Data field }
  { version 3.2 - fix _Value to allow Sep to be a '&' or '; ' string! }
  {===================================================================}
  {$IFDEF WIN32}
    {$IFNDEF MSWINDOWS}
      {$DEFINE MSWINDOWS}
    {$ENDIF}
  {$ENDIF}
  interface
  type
    TRequestMethod = (Unknown,Get,Post);
  var
    RequestMethod: TRequestMethod = Unknown;

  var
    ContentLength: Integer = 0;
    RemoteAddress: String[16] = ''; { IP }
    HttpUserAgent: String[128] = ''; { Browser, OS }
    Authorization: String[255] = ''; { Authorization }
    ScriptName: String[128] = ''; { scriptname URL }

    function Value(const Field: ShortString; Convert: Boolean = True): ShortString;
    function CookieValue(const Field: ShortString): ShortString;

  implementation
  uses
    {$IFDEF MSWINDOWS}
      Windows,
    {$ENDIF}
    {$IFDEF LINUX}
      Libc,
    {$ENDIF}
      SysUtils;

    function _Value(const Field: ShortString;
                    const Data: AnsiString; Sep: String = '&';
                    Convert: Boolean = True): ShortString;
    { 1998/01/02: check for complete match of Field name }
    { 1999/03/01: do conversion *after* searching fields }
    { 2002/06/23: separator can be a '&' or '; ' string! }
    var
      i: Integer;
      Str: String[3];
      len: Byte absolute Result;
    begin
      len := 0; { Result := '' }
      i := Pos(Sep+Field+'=',Data);
      if i = 0 then
      begin
        i := Pos(Field+'=',Data);
        if i > Length(Sep) then i := 0
      end
      else Inc(i,Length(Sep)); { skip Sep }
      if i > 0 then
      begin
        Inc(i,Length(Field)+1);
        while Data[i] <> Sep[1] do
        begin
          Inc(len);
          if (Data[i] = '%') and Convert then // special code
          begin
            Str := '$00';
            Str[2] := Data[i+1];
            Str[3] := Data[i+2];
            Inc(i,2);
            Result[len] := Chr(StrToInt(Str))
          end
          else
            if (Data[i] = ' ') and not Convert then Result[len] := '+'
            else
              Result[len] := Data[i];
          Inc(i)
        end
      end
      else Result := '$' { no javascript }
    end {_Value};

  var
    Data: AnsiString = '';

    function Value(const Field: ShortString; Convert: Boolean = True): ShortString;
    begin
      Result := _Value(Field, Data, '&', Convert)
    end;

  var
    Cookie: ShortString;

    function CookieValue(const Field: ShortString): ShortString;
    begin
      Result := _Value(Field, Cookie, '; ');
      if Result = '$' then Result := Cookie { debug }
    end;

  var
    P: PChar;
    StartData,i: Integer;
  {$IFDEF MSWINDOWS}
    Str: ShortString;
  {$ENDIF}

  initialization
  {$IFDEF MSWINDOWS}
  // Tested on IIS and PWS
    P := GetEnvironmentStrings;
    while P^ <> #0 do
    begin
      Str := StrPas(P);
      if Pos('REQUEST_METHOD=',Str) > 0 then
      begin
        Delete(Str,1,Pos('=',Str));
        if Str = 'POST' then RequestMethod := Post
        else
          if Str = 'GET' then RequestMethod := Get
      end;
      if Pos('CONTENT_LENGTH=',Str) = 1 then
      begin
        Delete(Str,1,Pos('=',Str));
        ContentLength := StrToInt(Str)
      end;
      if Pos({HTTP_}'QUERY_STRING=',Str) > 0 then
      begin
        Delete(Str,1,Pos('=',Str));
        Data := Str + '&'
      end;
      if Pos({HTTP_}'COOKIE=',Str) > 0 then // TDM #45
      begin
        Delete(Str,1,Pos('=',Str));
        Cookie := Str + ';'
      end
      else
      if Pos({HTTP_}'REMOTE_ADDR',Str) > 0 then // TDM #39
      begin
        Delete(Str,1,Pos('=',Str));
        RemoteAddress := Str
      end
      else
      if Pos({HTTP_}'USER_AGENT',Str) > 0 then // TDM #39
      begin
        Delete(Str,1,Pos('=',Str));
        if Pos(')',Str) > 0 then
          Delete(Str,Pos(')',Str)+1,Length(Str)); {!!}
        HttpUserAgent := Str
      end
      else
      if (Pos({HTTP_}'AUTHORIZATION',Str) > 0) then // TDM #55
      begin
        Delete(Str,1,Pos('=',Str));
        Authorization := Str;
      end
      else
      if Pos({HTTP_}'SCRIPT_NAME',Str) > 0 then // TDM #71
      begin
        Delete(Str,1,Pos('=',Str));
        ScriptName := Str
      end;
      Inc(P, StrLen(P)+1)
    end;
  {$ENDIF}
  {$IFDEF LINUX}
  // Tested on Apache for Linux
    P := getenv('REQUEST_METHOD');
    if P = 'POST' then RequestMethod := Post
    else
      if P = 'GET' then RequestMethod := Get;
    ContentLength := StrToIntDef(getenv('CONTENT_LENGTH'),0);
    Data := getenv('HTTP_QUERY_STRING');
    if Data = '' then
      Data := getenv('QUERY_STRING');
    if Data <> '' then Data := Data + '&';
    Cookie := StrPas(getenv('HTTP_COOKIE'));
    if Cookie = '' then
      Cookie := StrPas(getenv('COOKIE'));
    RemoteAddress := StrPas(getenv('HTTP_REMOTE_ADDR'));
    if RemoteAddress = '' then
      RemoteAddress := StrPas(getenv('REMOTE_ADDR'));
    HttpUserAgent := StrPas(getenv('HTTP_USER_AGENT'));
    if HttpUserAgent = '' then
      HttpUserAgent := StrPas(getenv('USER_AGENT'));
    Authorization := StrPas(getenv('HTTP_AUTHORIZATION'));
    if Authorization = '' then
      Authorization := StrPas(getenv('AUTHORIZATION'));
    ScriptName := StrPas(getenv('HTTP_SCRIPT_NAME'));
    if ScriptName = '' then
      ScriptName := StrPas(getenv('SCRIPT_NAME'));
  {$ENDIF}
    if RequestMethod = Post then
    begin
      StartData := Length(Data);
      SetLength(Data,StartData+ContentLength+1);
      for i:=1 to ContentLength do read(Data[StartData+i]);
      Data[StartData+ContentLength+1] := '&';
    { if IOResult <> 0 then { skip }
    end;
    i := 0;
    while i < Length(Data) do
    begin
      Inc(i);
      if Data[i] = '+' then Data[i] := ' '
    end;
    if i > 0 then Data[i+1] := '&'
             else Data := '&';
  finalization
    Cookie := '';
    Data := ''
  end.
Note that this new version of DrBobCGI is cross-platform and works with all versions of Kylix as well as Delphi 4 or higher (or C++Builder 4 or higher for that matter).


This webpage © 2001-2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.