Connections with DataSnap

By: John Kaster

Abstract: See how to access the current connection's web request and web response in your DataSnap/REST server methods unit

    Introduction

DataSnap is a distributed computing technology available in RAD Studio. The RAD Studio XE release greatly increases the cross-platform and cross-language reach of DataSnap, particularly for REST implementations.

The DataSnap dispatcher handles all the client-based communication and marshalling automatically, so developers can usually forget about the communication and marshalling mechanics, and focus directly on implementing custom logic in the DataSnap server's methods. Sometimes, however, the active user connection is needed in the server methods. Read on to find out how this can be easily accomplished with a DataSnap/REST server.

If you're not familiar with the process of building a DataSnap/REST server, read my previous article on building, debugging, and deploying a DataSnap/REST ISAPI dll which covers every step.

The examples in this article are extracted from the new QualityCentral (QC) middle-tier being written with RAD Studio XE using DataSnap/REST. To support the existing single sign-on system for EDN, we need to be able to retrieve the HTTP request for the user communicating with the QC server.

The units generated by the DataSnap/REST wizard are usually named Unit1, Unit2, and ServerMethods1. (These names can vary if you have a project group open and adding a new project to the group via the wizard.)

This is the run-down on the new unit names: The unit that handles client communications and marshalling of data and calls between the DataSnap server and the REST clients is called QCDispatcher, and the unit that contains my QualityCentral-specific logic is called QCMethods. The implementation of the actual QualityCentral logic is in a class called TQC.

Because I might want to support dataset provider/resolver operations on this DataSnap server, I selected the option in the DataSnap wizard that allows me to descend from TDSServerModule, which implements the IAppServer interface.

This is our starting point for the server methods:

  TQC = class(TDSServerModule)
  private
    { Private declarations }
  public
    { Public declarations }
    function EchoString(Value: string): string;
    function ReverseString(Value: string): string;
  end;

    Tracking HTTP request and response

To track the connection-specific information for the user, we need threadvars for the HTTP request and response objects. For more information on threadvars, see the Embarcadero online documentation site and the Delphi Basics web site. Because they need to be visible outside the unit in which they are defined, their declaration is in the interface section of QCDispatcher.

Here's an excerpt from the source file:

threadvar
  /// <summary>Web request thread variable for the current connection</summary>
  Request: TWebRequest;
  /// <summary>Web response thread variable for the current connection</summary>
  Response: TWebResponse;

var
  WebModuleClass: TComponentClass = TQCDispatch;

implementation

uses QCMethods, WebReq;

We can use the BeforeDispatch and AfterDispatch methods of QCDispatcher webmodule to manage the values assigned to these threadvars. The BeforeDispatch event is triggered right after the client connection is established. The AfterDispatch method is called right before the response is sent to back to the requestor.

You can read more about the web dispatcher component on the docwiki site.

The BeforeDispatch event was already generated by the wizard. The assignment of the threadvars can be added to it.

procedure TQCDispatch.WebModuleBeforeDispatch(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  // Assign connection-specific Request and Response variables to this thread
  QCDispatcher.Request := Request;
  QCDispatcher.Response := Response;

  if FServerFunctionInvokerAction <> nil then
    FServerFunctionInvokerAction.Enabled := AllowServerFunctionInvoker;
end;

The AfterDispatch event can be created by clicking on the design surface for web module, and double clicking on the AfterDispatch entry in the object inspector.

Hide image

This is the implementation for AfterDispatch:

procedure TQCDispatch.WebModuleAfterDispatch(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  QCDispatcher.Request := nil;
  QCDispatcher.Response := nil;
end;

Note: These are the WebModule dispatch events being modified, not the WebFileDispatcher events.

    Using connection info with DataSnap server methods

Now that we have thread-specific values visible to us, we can create a simple server method to verify the assignment logic of the BeforeDispatch event.

Adding the public declaration:

function UserIP: string;

to the TQC class and pressing Shift+Ctrl+C will invoke class completion and generate the stub of the implementation for the function.

function TQC.UserIP: string;
begin

end;

However, the request object we need to access in this method isn't visible yet. I can add the QCDispatcher unit to my QCMethods unit by pressing Alt+F11 to display the "Use Unit" dialog in the editor. By selecting the unit, indicating it should used in the implementation section (to avoid circular reference errors when compiling), and clicking OK, the unit reference is added to the appropriate uses clause.

Hide image

The "Use Unit" dialog is smart enough to only offer the units that are not already used in the active source file. If I invoke it again after using QCDispatcher, the only other unit in my project is displayed. (The radio button also defaults to the last option selected.)

Hide image

The form unit is not needed in QCMethods, so I cancelled out of the dialog. (I just wanted to describe the intelligence of "Use Unit".)

The implementation of UserIP is pretty simple so far:

function TQC.UserIP: string;
begin
  Result := QCDispatcher.Request.RemoteAddr;
end;

I'm using a VCL form application for DataSnap because it's by far the easiest way to iteratively develop, test, and debug a DataSnap server. The DataSnap ISAPI article I mentioned earlier shows the steps for converting from one type of DataSnap server to another.

    A Quick Test

I'll use the Server Function Invoker to perform a quick test.

Hide image

Click one of the Run buttons. (The first time running the app usually displays a Windows firewall prompt to allow or deny it access to the port.)

Hide image

Click Open Browser to display the DataSnap server home page in your browser.

Hide image
Click to see full-sized image

Click the Server Functions link, which will open another browser window.

Hide image
Click to see full-sized image

Expand TQC's methods and click the Execute button on UserIP, to see the local IP address returned. If you want to verify the results on another machine just to make sure you see a different IP address, you can use something like Firebug to quickly retrieve the REST URL for the request made by the Server Function Invoker.

Hide image
Click to see full-sized image

You can right mouse click on the desired request, and copy the location to your clipboard. In this case, it's http://localhost:8080/datasnap/rest/TQC/UserIP/. To test from a different machine, I just need to replace localhost with the name of the machine running the DataSnap server. In this case, that's jfkm6300.

Here's the result of the request from another machine on my home network.

Hide image

    DataSnap/REST calling pattern

As you can probably figure out from the URL pattern above, this is the default pattern for a DataSnap/REST call:

http://servername[:port]/datasnap/rest/classname/method/arg1/arg2/...argN
  • servername is the name of the server or domain – just a standard part of the HTTP request.
  • port is required if a non-standard port is assigned for either http or https.
  • /datasnap/rest is the pattern for special dispatch handling by the DataSnap server. (Both the "datasnap" and "rest" values can be overridden by identifiers of your choice.)
  • /classname is the name of the class for the RPC call
  • /method is the name of the class method (function or procedure)
  • /arg1 … /argn are the parameters passed to the method. If the arguments contain special characters, they must be URL-encoded

Further discussion of the ways you can use HTTP to invoke DataSnap methods will have to wait for another article. In the meantime, if you'd like to explore it on your own, you can look at function ServerFunctionExecutor(className, connectionInfo, owner) in the ServerFunctionExecutor.js that gets generated for your DataSnap/REST project, or at the various proxies you can generate for a DataSnap/REST client.

    Supporting load-balanced connections

The next step for the UserIP method is correctly handling requests coming through a load balancer, where the standard user address for the HTTP request is the IP of the load balancer, and a custom header has to be examined to get the end-user IP address. These are the relevant routines to conditionally extract the value of this header from the HTTP request if it exists:

function StrIsEmpty(const AInput: string) : boolean;
begin
  Result := Length(Trim(AInput)) = 0;
end;

function StrIsFull(const AInput: string): boolean;
begin
  Result := not StrIsEmpty(AInput);
end;

function UserHostAddress(const ARequest: TWebRequest): string;
const
  cnXForwardedFor = 'x-forwarded-for';
var
  lStr: string;
  lParts: TStringDynArray;
  lIndex: Integer;
begin
  lStr := String(ARequest.GetFieldByName(cnXForwardedFor));
  if StrIsFull(lStr) then
  begin
    lParts := SplitString(lStr, ',');
    lIndex := High(lParts);
    while ((lIndex >= Low(lParts)) and (StrIsEmpty(lParts[lIndex]))) do
      Dec(lIndex);
    Result := String(lParts[lIndex]);
  end
  else
    Result := String(ARequest.RemoteAddr);
end;

As you can see from the code above, the HTTP header we're looking "x-forwarded-for". Luckily, the X-Forwarded-For header is a de facto standard, so the above routine should work with most of the available load-balancing solutions.

Through the use of this routine, the UserIP function can be updated to understand HTTP requests in a load-balanced environment:

function TQC.UserIP: string;
begin
  Result := UserHostAddress(QCDispatcher.Request);
end;

    The door is open

Now that we can get specific connection information in our server methods, implementing single sign-on by retrieving HTTP packet information for the specific user, IP white listing, and other customizations are all readily achievable.

There is certainly plenty more to write about DataSnap.

Stay tuned!

Server Response from: ETNASC01