Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
![]() |
![]() |
![]() |
|
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; secureBoth 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 GMTFor example:
Mon, 01-Feb-1999 07:11:42 GMTNote 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=VALUEIn 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).