Wednesday, December 22, 2010

Dynamic web pages with DWScript and IdHTTPServer

I've finally found some time in my busy schedule to write a new post, this post is about generating dynamic web pages using DWScript(http://www.delphitools.info -- if you find DWScript useful please do not hesitate to donate to Eric, he is doing a wonderful job with DWScript) as script interpreter and IdHTTPServer as HTTP server.
But first let's understand the difference between static and dynamic web pages:
1. Static web pages:
- static web pages are just plain HTML files which will be manually updated by the developer or website owner whenever he wants;
Here's a drawing of the process that takes place in the case of static web pages



2. Dynamic web pages:
- dynamic web pages are similar to static HTML files, however this HTML files also contain script which is interpreted by a script interpreter which can be almost any script interpreter out there, i.e. perl, php, python, ruby, etc. for this example I've used DWScript;
Here's a drawing of the process that takes place in the case of dynamic web pages



as you can see the noticeable difference between static and dynamic web pages is the script interpreter which comes into play just before serving the HTML to the client.

In this post I won't cover the benefits of using dynamic web pages and the possible exploits.

For this post I've modified the HTTP server which I've created for a video tutorial, so here's the updated source of the uClientContext.pas file:
unit uClientContext;

interface

uses
  SysUtils,
  Classes,
  IdBaseComponent,
  IdComponent,
  IdCustomTCPServer,
  IdCustomHTTPServer,
  IdHTTPServer,
  IdContext,
  dwsComp,
  dwsCompiler,
  dwsExprs,
  dwsClassesLibModule,
  dwsMathFunctions,
  dwsStringFunctions,
  dwsStringResult,
  dwsTimeFunctions,
  dwsVariantFunctions,
  dwsHtmlFilter;

type
  TClientContext = class(TIdServerContext)
  private
    FLogStrings: TStrings;
    procedure Log(const s: string);
  public
    procedure HandleRequest(ARequestInfo: TIdHTTPRequestInfo;
      AResponseInfo: TIdHTTPResponseInfo);
    procedure ServeHTMLFile(const AFileName: string;
      ARequestInfo: TIdHTTPRequestInfo;
      AResponseInfo: TIdHTTPResponseInfo);
  public
    property LogStrings: TStrings read FLogStrings write FLogStrings;
  end;

implementation

var
  WebDir: string;

{ TClientContext }

procedure TClientContext.HandleRequest(ARequestInfo: TIdHTTPRequestInfo;
  AResponseInfo: TIdHTTPResponseInfo);
const
  SERROR_404 = 'Error 404 page not found "%s"';
var
  LLocation: string;
begin
  try
    LLocation := ARequestInfo.Document;
    if LLocation <> EmptyStr then begin
      if (LLocation = '/') or (LLocation = '/*') or SameText(LLocation, '/index.html') then
        ServeHTMLFile(WebDir + 'index.html', ARequestInfo, AResponseInfo)
      else begin
        LLocation := WebDir + Copy(LLocation, 2, MaxInt);
        if NOT SameText(ExtractFileExt(LLocation), '.html') then
          LLocation := LLocation + '.html';
        if FileExists(LLocation) then
          ServeHTMLFile(LLocation, ARequestInfo, AResponseInfo)
        else
          AResponseInfo.ContentText := Format(SERROR_404, [LLocation]);
      end;
    end else
      AResponseInfo.ContentText := Format(SERROR_404, [LLocation]);
  except
    on E: Exception do
      Log('Exception occured from IP ' + Connection.Socket.Binding.PeerIP +
        sLineBreak + E.Message);
  end; // trye
end;

procedure TClientContext.ServeHTMLFile(const AFileName: string;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
  LHTMLFile: TStringList;
  LScript: TDelphiWebScript;
  LHTMLFilter: TdwsHtmlFilter;
  LClasses: TdwsClassesLib;
  LProgram: TdwsProgram;
begin
  LScript := TDelphiWebScript.Create(NIL);
  LScript.Config.ScriptPaths.Add(WebDir);
  LClasses := TdwsClassesLib.Create(NIL);
  LHTMLFilter := TdwsHtmlFilter.Create(NIL);
  LScript.Config.Filter := LHTMLFilter;
  LScript.AddUnit(TdwsHtmlUnit.Create(LScript));
  LScript.AddUnit(Tdws2StringsUnit.Create(LScript));
  LHTMLFile := TStringList.Create;
  try
    LClasses.Script := LScript;
    LHTMLFile.LoadFromFile(AFileName);
    LProgram := LScript.Compile(LHTMLFile.Text);
    try
      if NOT LProgram.Msgs.HasErrors then begin
        LProgram.Execute;
        AResponseInfo.ContentText := (LProgram.Result as TdwsDefaultResult).Text;
      end else
        AResponseInfo.ContentText := LProgram.Msgs.AsInfo
    finally
      FreeAndNil(LProgram);
    end; // tryf
  finally
    FreeAndNil(LHTMLFile);
    FreeAndNil(LClasses);
    FreeAndNil(LScript);
    FreeAndNil(LHTMLFilter);
  end; // tryf
end;

procedure TClientContext.Log(const s: string);
begin
  if Assigned(FLogStrings) then
    FLogStrings.Add(s);
end;

initialization
  WebDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)) + 'www');

end.
as you can see the source code is pretty similar to the initial code, just that I've added a new method called ServeHTMLFile -- this method is called only if the requested HTML file is found in the www directory.
Technique: we don't create the interpreter instance unless the requested file is found in the www directory -- the reason is pretty obvious, we try to avoid memory allocation if it's not necessary, we could also improve the efficiency by caching the files in memory in order to serve them faster(RAM IO is faster than disk IO therefore this will give a significant speed improvement when server has thousands requests per second) however this will be covered in a future post hopefully.
In order to provide a proof of concept I've created a fairly simple "website" which has 3 buttons, each button redirects the client to a new web page:
index.html file
<HTML>
  <BODY>
    Hello world!!<BR>
    <BUTTON ONCLICK="window.location.href='/primes100.html'">show me primes up to 100</BUTTON> <BR>
    <BUTTON ONCLICK="window.location.href='/primes200.html'">show me primes up to 200</BUTTON> <BR>
    <BUTTON ONCLICK="window.location.href='/primes300.html'">show me primes up to 300</BUTTON> <BR>            
  </BODY>
</HTML>
very simple, right?
we also have a utils.inc file in which we have a method which checks if a number is prime, this file is also located in www directory
function IsPrime(Value: integer): boolean;
var
  Index: Integer;
begin
  Result := False;
  if Value <= 0 then
    Exit;
  for Index := 2 to Round(Sqrt(Value)) do
    if (Value mod Index) = 0 then
      Exit;
  Result := True;
end;
here are the other 3 HTML files primes100.html
<HTML>
  <BODY>
    <%
      {$I 'utils.inc'}
      var
        Index: Integer;
      for Index := 1 to 100 do
        if IsPrime(Index) then
          Send('<BR>' + IntToStr(Index)); 
    %>
  </BODY>  
</HTML>
primes200.html
<HTML>
  <BODY>
    <%
      {$I 'utils.inc'}
      var
        Index: Integer;
      for Index := 1 to 200 do
        if IsPrime(Index) then
          Send('<BR>' + IntToStr(Index)); 
    %>
  </BODY>  
</HTML>
primes300.html
<HTML>
  <BODY>
    <%
      {$I 'utils.inc'}
      var
        Index: Integer;
      for Index := 1 to 300 do
        if IsPrime(Index) then
          Send('<BR>' + IntToStr(Index)); 
    %>
  </BODY>  
</HTML>
Now, this is an extremely simple example, but as you can see it can be used as a template for a real hardcore web server. Unfortunately I don't have enough time these days for more in depth details, but you can download binary + source code or just the source code and enjoy the power and simplicity of DWScript.
The application is created in Delphi 2010.

Blogroll(General programming and Delphi feeds)