WebTracker - Session and Security Management for Delphi ISAPI web sites
          Copyright (c) 1997-2000  GreenLight Software
                     All rights reserved.


WebTracker brings together Session management and site security to the
stateless world of HTTP.  No Cookies or Hidden fields required.
WebTracker uses a "thin" URL to uniquely identify each user and a small
encrytion string to identify parameters.

WebTracker works behind the scenes. You are free to return sensitive data
back to the user with the knowledge that anything that could be maliciously
reused will be encrypted.  You don't even have to worry about setting or
getting the session details.  Each Session is totally restored by the time
your "Action" and "Tag Substitution" handlers are called.

WebTracker is not a component.  It is a Unit that you include in your ISAPI
application.  There is nothing to install.  The Unit contains the definition
of a TWebSession object that you simply add to your TWebModule.  This makes
it very easy to convert existing ISAPI applications to use Session Management.

===============================================================================
This is Shareware
===============================================================================

You are free to use the compiled unit in any ISAPI project you want (including
commercial applications).  A unit for D3, D4 and D5 has been included.  A small
registration fee of $49 Cdn (~$32 US) will give you a copy of the source code.

Registration:
-------------
  Registration is $49Cdn

  Contact:
  Dave Hackett
  GreenLight Software
  6 Crystal Drive
  Bedford, NS    B4A 3R4
  dave.hackett@ns.sympatico.ca

  Visa and MasterCard accepted.
  Receipt and credit card slip will be snail mailed back to you.


===============================================================================
General Usage
===============================================================================

You start by including the WebTracker unit in your project.  Make sure
you copy the correct unit into your project directory for the version of Delphi
that you are running.

  uses
    Windows, Messages, SysUtils, Classes, HTTPApp, WebTracker;

Now declare an instance of the WebSession object in your TWebModule object.

  type
    TWebModule1 = class(TWebModule)
    private
      { Private declarations }
    public
      WebSession : TWebSession;
    end;

Remember, by default, Delphi creates 32 instances of your TWebModule.  Having the
WebSession object declared local to the WebModule allows you to work with a user's
request without blocking the other 31 modules.  However, if a second request from
the SAME user comes in before the first request is fulfilled, the second request is
blocked until the first request is handled.  This prevents the same TWebSession object
from being updated by 2 or more processes at the same time.

Next, create an OnCreate event handler for your TWebModule and initialize the
the WebSession variable.

  procedure TWebModule1.WebModule1Create(Sender: TObject);
  begin
    WebSession := nil;
  end;

Now create an Action Handler event to start tracking the session.

I usually start my secure web sites with a static Login page.  This page
contains a POST request with the Action set to "/Login".  Only after the user
has been authenticated do I create an instance of the TWebSession object.

  procedure TWebModule1.WebModule1waLoginAction(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  var
    UserId, Password : string;
  begin
    UserId := Request.ContentFields.Values['UserId'];
    Password := Request.ContentFields.Values['Password'];

    { ... Validate the User ... }

    {create a web session object to track this user}
    WebSession := TWebSession.Create;

    Response.Content := ppMainMenu.Content;
    Handled := true;
  end;

Next, create an "AfterDispatch" event handler for your TWebModule to save
the session before you pass your results back to the user.

  procedure TWebModule1.WebModule1AfterDispatch(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  begin
    if Assigned(WebSession) then
      WebTracker.SaveWebSession(WebSession, Response);
  end;

I usually check for an "assigned" WebSession just in case not all of my web 
pages track a session.  Feel free to drop the If statement if you know
that you will always have a session.

Finally, create a "BeforeDispatch" event handler to restore the session when the 
next web request comes in.

  procedure TWebModule1.WebModule1BeforeDispatch(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  begin
    if Request.PathInfo <> '/Login' then
      WebSession := WebTracker.FindWebSession(Request);
  end;

Since my static login web page has no Session associated with it, I make sure that
I don't try to restore one for that web request.

That's it.


===============================================================================
Storing and retrieving data from the Session
===============================================================================
The TWebSession object has an "AddData" and a "GetData" method to track objects
specific to this session.  Lets say you want to track the results of a TQuery
for this user.  Create the Query, run it and add it to the Web Session's data.

  Query := TQuery.Create(nil);
  Query.DatabaseName := 'DBDemos';
  Query.SQL.Add('select * from Clients where State = "CA";');
  Query.Open;
  WebSession.AddData('California Clients', Query);

Now, the next time the user makes a web request, the Query object will still be
open and sitting on the first record retrieved.

  Query := TQuery(WebSession.GetData('California Clients'));
  Response.Content := Query.FieldByName('LAST_NAME').AsString;

Note: you have to typecast the result from the GetData call back to the original
data type.  If you haven't figured it out already, the string parameter is the
"Key" used to store and retrieve the data.

A word of warning.  Leaving Query and Table results open will use up a lot of
database licenses very quickly.  For myself, I find it better to copy the data
that I need into a new object that I define and then close the Query.  As a
matter of fact, all of my web projects have a unit called DataObjs where I
define all of the information that will be tracked (see Demo2).

  type
    TUserProfile = class(TObject)
      FirstName : string;
      LastName : string;
      Email : string;
      end;

  ...

  Query := TQuery.Create(nil);
  Query.DatabaseName := 'DBDemos';
  Query.SQL.Add('select * from Clients where LAST_NAME = "Wilson";');
  Query.Open;

  UserProfile := TUserProfile.Create;
  UserProfile.FirstName := Query.FieldByName('FIRST_NAME').AsString;
  UserProfile.LastName := Query.FieldByName('LAST_NAME').AsString;
  UserProfile.Email := Query.FieldByName('EMAIL').AsString;
  Query.Free;

  WebSession.AddData('Profile', UserProfile);

FYI: The TWebSession.Destroy method will automatically call the Free
method of all added objects.


===============================================================================
Security Usage
===============================================================================

I've seen many "secure" ISAPI web sites that fail because they leave the front
door wide open.  By this I mean that the web pages returned back to the user
contain too much information that could be modified for malicious purposes.

For example:  A user has logged in and I've returned a list of their accounts
as a series of hyperlinks.  When they click on the hyperlink, I will retrieve
the details for that account.  The URL for the hyperlink looks something like
this...

  <a href="www.mysite.com/scripts/MyISAPI.dll?Id=Dave&Account=123456>

Everything is plainly visible.  The user could simply view the source of the
HTML, copy the hyperlink URL back into the browser, change the account number
or User Id and gain access to information that does not belong to them.  Most
programmers don't reauthorize a user after they have gained entry to the
secure site because it slows things down and makes the database server work
that much harder.

To solve this problem, WebTracker will encrypt and decrypt all neccessary
parameters in the HTML code just before you return the WebResponse to the user.

To encrypt your HTML response, call the WebSession's EncryptParameters method.  
The best place to do this is in the "AfterDispatch" event handler for your 
TWebModule, right after you save your session.

  procedure TWebModule1.WebModule1AfterDispatch(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  begin
    if Assigned(WebSession) then
      begin
      WebTracker.SaveWebSession(WebSession, Response);
      WebSession.EncryptParameters(Response);
      end;
  end;

To decrypt the parameters so that your Action and Tag Substitution event
handlers can work as normal, call the WebSession's DecryptParameters method.
Again, the best place for this is in the "BeforeDispatch" event handler.

  procedure TWebModule1.WebModule1BeforeDispatch(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  begin
    if Request.PathInfo <> '/Login' then
      begin
      WebSession := WebTracker.FindWebSession(Request);
      WebSession.DecryptParameters(Request);
      end;
  end;


===============================================================================
Optional Global Settings
===============================================================================

There are 2 WebTracker global variables that you can change.  They should only
be set once within your ISPAPI application.  To insure that they are set only
once, place them in the Initialization section of your TWebModule unit.

  initialization
    WebTracker.ClientTimeOut := (1.00/24/60) * 10; //10 minutes
    WebTracker.CleanUpTimeInterval := (1.00/12);   //2 hours

The ClientTimeOut variable is the maximum amount of inactivity time that a user
can have between web requests.  If a request comes in and it's been more then
this amount of time since their last request, an EExpiredUserError exception is
raised.  The default is 20 minutes.

The CleanUpTimeInterval is how often expired Web User's are removed from memory.
The default is 4 hours.  If your site has a lot of traffic, you may want to set
this to a lower number to conserve memory.


===============================================================================
Error Handling
===============================================================================

There are only 3 error conditions that WebTracker will raise.

  EUnknownWebSessionError
  EExpiredWebSessionError
  EInvalidWebSessionError

EUnknownWebSessionError means that the session id is missing or the session id
was not found in the list of id's being tracked (i.e. the user changed it!).

EExpiredWebSessionError means that the user has been away too long and that
their session is now expired.  Typically, you would send them back to the
login web page.

EInvalidWebSessionError means that the parameters that should be part of
this web request were corrupted (i.e. the user changed something).

To capture the exceptions so you can present your own custom error page to the
user, place a try...except block in the "BeforeDispatch" event handler.

  procedure TWebModule1.WebModule1BeforeDispatch(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  begin
    try
      if Request.PathInfo <> '/Login' then
        begin
        WebSession := WebTracker.FindWebSession(Request);
        WebSession.DecryptParameters(Request);
        end;
    except
      on EUnknownWebSessionError do
        begin
        Response.Content := 'Error:  Unknown Session.';
        Handled := true;
        end;
      on E.EExpiredWebSessionError do
        begin
        Response.Content := ppErrorHeader.Content + E.Message + ppErrorFooter.Content;
        Handled := true;
        end;
      end;
  end;


===============================================================================
Internals
===============================================================================


Exceptions:
-----------

  EUnauthorizedUserError : means a user id was passed in that is not valid (the user probably changed it)
  EExpiredUserError      : means that the user has been away for > ClientTimeOut value
  EInvalidDataError      : means that one of the encrypted parameters could not be decrypted (user probably changed it)


TWebSession object
---------------------
  Properties:
    GuidStr : string;       >> guaranteed unique identifier (read only)
    Expired : boolean;
    LastAccess : TDateTime  >> the DateTime stamp of the session's last request (read only)

  Methods:
    constructor Create;
    destructor  Destroy;
    procedure   DecryptParameters(Request: TWebRequest);
    procedure   EncryptParameters(Response: TWebResponse);
    procedure   AddData(Key : string; DataObject : TObject);
    function    GetData(Key : string) : TObject;

Global WebTracker Routines (thread safe)
--------------------------------------------
  procedure SaveWebSession(WebSession : TWebSession; Response: TWebResponse);
  function FindWebSession(Request: TWebRequest): TWebSession;

Global variables (not thread safe - please set only in the Initialization sections}
--------------------------------------------------------------------------------------
  ClientTimeOut : single;              //inactivity timer - default = 20 minutes
  CleanUpTimeInterval : single;        //time between cleaning up old expired TWebSessions - default = 2 hours


===============================================================================
Demo #1
===============================================================================

Flash up your personal web server and compile the Demo1 web application.  Please
make sure that your output directory in the compiler options is pointing to your
Scripts directory.  If your using Delphi v5, you will need to change the HTTPApp
reference in the DPR Uses clause to WebBroker.

Copy the included HTML pages (there are 2) to your WWW Root directory.

Start up your web browser and set the URL to...
  http://localhost/Demo1Login.htm

The HTML Login page should load.  If you viewed the HTML source, you would see that
pressing the Login button would send a POST command to the Demo1.DLL located in
the Scripts directory with the web action of "/Login".

The "/Login" action handler creates a "UserProfile" object and adds it to the
session for later retrieval.  It then returns the CheckProfile HTML.  Note that
there is no tag substitution needed here.  When the call to "SaveSession" is made,
the HTML going back to the user is scanned and all references to this ISAPI DLL
are changed such that the first query parameter is the session id.

View the HTML source after pressing the Login button.  Note the Session Id that
is now part of the hyperlink URL.

Now click on the hyperlink.  The "/ViewProfile" action handler can already assume
that the Web Session has been restored (or an exception would have happened and you
would never get this far) and such can ask it to retrieve the UserProfile stored
by the "/Login" action handler.  The action handler then returns the details of
the UserProfile which is just the contents of the UserId and Password field from
the Login web page.


===============================================================================
Demo #2
===============================================================================

Demo 2 builds upon some of the stuff from Demo 1.  First, you should notice that
I reset the global timer variables in the Initialization section at the bottom
of the unit.

Next, notice that I've added the Encrypt and Decrypt calls to the Before and
After Dispatch event handlers.

This time, when the user logs in, I query the Clients.dbf table from DBDemos to
retrieve more UserProfile information.  I pretend that this customer has more
then 1 account to show that the HTML "<Select" values will be encrypted.

Run the demo and login.  View the source and you will see that the Values for
the "<Select>" option tags are 4 digit hex numbers even though they were set
to perfectly readable values within the ISAPI application.

Press the button to view the account details.  Note:  I set the Value parameter
for the account list to the index position - not the account number.  This way
I can jump directly to that account once I've restored the UserProfile object
(my preferred method).

Now you should see a page with a list of all of the stocks belonging to that
account number.  Again, view the source and you will notice that the hyperlink
for each stock symbol is encrypted and the session id has been appended.  The
Action for the Form POST also has the Session Id appended and the Values for
the radio buttons and the hidden field have been encyrpted.

Click on a hyperlink or select one of the symbols and press the button to see
the details of that database record.  In the ISAPI code that you write, all of
the parameters have been restored to their original values.

Note:  in this example, I create and open a query when I build the list of
stock holdings and I leave it open.  The next request coming in is positioned
at the same place in the query as the previous request left off.

===============================================================================

I hope you enjoy this code.  I've been using these techniques since Delphi 3
and I'm pleased that I've finally taken the time to share it with everyone.

DaveH


