Table of Contents

Middleware System

TMS Sparkle provides classes that allow you to create middleware interfaces and add them to the request processing pipeline. In other words, you can add custom functionality that pre-process the request before it's effectively processed by the main server module, and post-process the response provided by it.

Using middleware interfaces allows you to easily extend existing server modules without changing them. It also makes the request processing more modular and reusable. For example, the compression middleware is a generic one that compress the server response if the client accepts it. It can be added to any server module (XData, RemoteDB, Echo, or any other module you might create) to add such functionality.

A middleware is represented by the interface IHttpServerMiddleware, declared in unit Sparkle.HttpServer.Module. To add a middleware interface to a server module, just use the AddMiddleware function (in this example, we're using a TMS XData module, but can be any Sparkle server module):

var
  MyMiddleware: IHttpServerMiddleware;
  Module: TXDataServerModule;
{...}
  // After retrieving the MyMiddleware interface, add it to the dispatcher
  Module.AddMiddleware(MyMiddleware);
  Dispatcher.AddModule(Module);

The following topics explain in deeper details how to use middlewares with TMS Sparkle.

Compress Middleware

Use the Compress middleware to allow the server to return the response body in compressed format (gzip or deflate). The compression will happen only if the client sends the request with the header "accept-encoding" including either gzip or deflate encoding. If it does, the server will provide the response body compressed with the requested encoding. If both are accepted, gzip is preferred over deflate.

To use the middleware, just create an instance of TCompressMiddleware (declared in Sparkle.Middleware.Compress unit) and add it to the server module:

uses {...}, Sparkle.Middleware.Compress;
{...}
  Module.AddMiddleware(TCompressMiddleware.Create);

Setting a threshold

TCompressMiddleware provides the Threshold property which allows you to define the minimum size for the response body to be compressed. If it's smaller than that size, no compression will happen, regardless of the 'accept-encoding' request header value. The default value is 1024, but you can change it:

var
  Compress: ICompressMiddleware;
begin
  Compress := TCompressMiddleware.Create;
  Compress.Threshold := 2048; // set the threshold as 2k
  Module.AddMiddleware(Compress);

CORS Middleware

Use the CORS middleware to add CORS support to the Sparkle module. That will allow web browsers to access your server even if it is in a different domain than the web page.

To use the middleware, just create an instance of TCorsMiddleware (declared in Sparkle.Middleware.Cors unit) and add it to the server module:

uses {...}, Sparkle.Middleware.Cors;
{...}
  Module.AddMiddleware(TCorsMiddleware.Create);

Basically the above code will add the Access-Control-Allow-Origin header to the server response allowing any origin:

Access-Control-Allow-Origin: *

Additional settings

You can use overloaded Create constructors with additional parameters. First, you can set a different origin for the Access-Control-Allow-Origin:

Module.AddMiddleware(TCorsMiddeware.Create('somedomain.com'));

You can also configure the HTTP methods allowed by the server, which will be replied in the Access-Control-Allow-Methods header:

Module.AddMiddleware(TCorsMiddeware.Create('somedomain.com', 'GET,POST,DELETE,PUT'));

And finally the third parameter will specify the value of Access-Control-Max-Age header:

Module.AddMiddleware(TCorsMiddeware.Create('somedomain.com', 'GET,POST,DELETE,PUT', 1728000));

To illustrate, the above line will add the following headers in the response:

Access-Control-Allow-Origin: somedomain.com
Access-Control-Allow-Methods: GET,POST,DELETE,PUT
Access-Control-Max-Age: 1728000

Forward Middleware

Use the Forward middleware to improve behavior of Sparkle server when it's located behind reverse/edge proxies, like Nginx, Traefik, Haproxy, among others.

When using such proxies, several information of the server request object, like the requested URL, or the IP of the client, are provided as if the proxy is the client - which it is, actually. Thus the "IP of the client" will always be the IP of the proxy server, not of the original client. The same way, the requested URL is the one requested by the proxy itself, which might be different from the original URL requested by the client. For example, the client might have requested the URL https://yourserver.com, but the proxy will then request your internal server IP like http://192.168.0.105.

When you add the Forward middleware, Sparkle will process the x-forward-* headers sent by the proxy server, which holds information about the original client. Sparkle will then modify the request based on that information, so the request will contain the original requested URL, remote IP. This way your module (like XData) or any further middleware in the process chain will see the request as if it was sent directly by the client.

To use the middleware, just create an instance of TForwardMiddleware (declared in Sparkle.Middleware.Forward unit) and add it to the server module.

uses {...}, Sparkle.Middleware.Forward;
{...}
  Module.AddMiddleware(TForwardMiddleware.Create);

The above code will process forward headers coming from any client. It's also recommended, for security reasons, that the server only process headers sent by the known proxy. To do that, just create the Forward middleware passing a callback that will only accept processing if the proxy name (or IP) matches the one you know. It's up to you, of course, to know the name of your proxy:

uses {...}, Sparkle.Middleware.Forward;
{...}
  Module.AddMiddleware(TForwardMiddleware.Create(
    procedure(const Proxy: string; var Accept: Boolean)
    begin
      Accept := Proxy = '192.168.0.132';
    end;
  ));

JWT Authentication Middleware

Add the JWT Authentication Middleware to implement authentication using JSON Web Token. This middleware will process the authorization header, check if there is a JSON Web Token in it, and if it is, create the user identity and claims based on the content of JWT.

The middleware class is TJwtMiddleware, declared in unit Sparkle.Middleware.Jwt. The design-time class is TSparkleJwtMiddleware. It's only available for Delphi XE6 and up.

To use the middleware, create it passing the secret to sign the JWT, and then add it to the TXDataServerModule object:

uses {...}, Sparkle.Middleware.Jwt;
 
  Module.AddMiddleware(TJwtMiddleware.Create('my jwt secret'));

By default, the middleware rejects expired tokens and allows anonymous access. You can change this behavior by using the Create constructor as explained below in the Methods table.

Properties

Name Description
Secret: TBytes The secret used to verify the JWT signature. Usually you won't use this property as the secret is passed in the Create constructor.

Methods

Name Description
constructor Create(const ASecret: string; AForbidAnonymousAccess: Boolean = False; AAllowExpiredToken: Boolean = False); Creates the middleware using the secret specified in the ASecret parameter, as string. The string will be converted to bytes using UTF-8 encoding.
The second boolean parameter is AForbidAnonymousAccess, which is False by default. That means the middleware will allow requests without a token. Pass True to the second parameter to not allow anonymous access.
The third boolean parameter is AAllowExpiredToken which is False by default. That means expired tokens will be rejected by the server. Pass True to the third parameter to allow rejected tokens.
constructor Create(const ASecret: TBytes; AForbidAnonymousAccess: Boolean = False; AAllowExpiredToken: Boolean = False); Same as above, but creates the middleware using the secret specified in raw bytes instead of string.

Basic Authentication Middleware

Add the Basic Authentication middleware to implement authentication using Basic authentication. This middleware will check the authorization header of the request for user name and password provided using Basic authentication.

The user name and password will then be passed to a callback method that you need to implement to return an user identity and its claims. Different from the JWT Authentication Middleware which automatically creates the identity based on JWT content, in the basic authentication it's up to you to do that, based on user credentials.

If your module implementation returns a status code 401, the middleware will automatically add a www-authenticate header to the response informing the client that the request needs to be authenticated, using the specified realm.

Example:

uses {...}, Sparkle.Middleware.BasicAuth, Sparkle.Security;

  Module.AddMiddleware(TBasicAuthMiddleware.Create(
    procedure(const UserName, Password: string; var User: IUserIdentity)
    begin
      // Implement custom logic to authenticate user based on UserName and Password
      // For example, go to the database, check credentials and then create an user
      // identity with all permissions (claims) for this user
      User := TUserIdentity.Create;
      User.Claims.AddOrSet('roles').AsString := SomeUserPermission;
      User.Claims.AddOrSet('sub').AsString := UserName;
    end,
    'My Server Realm'
  ));

TAuthenticateBasicProc = reference to procedure(const UserName, Password: string; var User: IUserIdentity);

The procedure that will be called for authentication. UserName and Password are the user credentials sent by the client, and you must then create and return the IUserIdentity interface in the User parameter.

Properties

Name Description
OnAuthenticate: TAuthenticateBasicProc The authentication callback that will be called when the middleware retrieves the user credentials.
Realm: string The realm that will be informed in the www-authenticate response header. Default realm is "TMS Sparkle Server".

Methods

Name Description
constructor Create(AAuthenticateProc: TAuthenticateBasicProc; const ARealm: string) Creates the middleware using the specified callback and realm value.
constructor Create(AAuthenticateProc: TAuthenticateBasicProc) Creates the middleware using the specified callback procedure.

Logging Middleware

Add the Logging Middleware to implement log information about requests and responses processed by the Sparkle server. This middleware uses the built-in Logging mechanism to log information about the requests and responses. You must then properly configure the output handlers to specify where data should be logged to.

The middleware class is TLoggingMiddleware, declared in unit Sparkle.Middleware.Logging. The design-time class is TSparkleLogging​Middleware.

To use the middleware, create it, set the desired properties if needed, then add it to the TXDataServerModule object:

uses {...}, Sparkle.Middleware.Logging;

var
  LoggingMiddleware: TLoggingMiddleware;

begin
  LoggingMiddleware := TLoggingMiddleware.Create;
  // Set LoggingMiddleware properties.
  Module.AddMiddleware(LoggingMiddleware);
end;

You don't need to set any property for the middleware to work. It will output information using the default format.

Properties

You can refer to TSparkleLogging​Middleware for a full list of properties.

Format string options

The format string is a string that represents a single log line and utilize a token syntax. Tokens are references by :token-name. If tokens accept arguments, they can be passed using [], for example: :token-name[param] would pass the string 'param' as an argument to the token token-name.

Each token in the format string will be replaced by the actual content. As an example, the default format string for the logging middleware is

:method :url :statuscode - :responsetime ms

which means it will output the HTTP method of the request, the request URL, the status code, an hifen, then the response time followed by the letters "ms". This is an example of such output:

GET /request/path 200 - 4.12 ms

Here is the list of available tokens:

:method
The HTTP method of the request, e.g. "GET" or "POST".

:protocol The HTTP protocol of the request, e.g. "HTTP1/1".

:req[header]
The given header of the request. If the header is not present, the value will be displayed as an empty string in the log. Example: :req[content-type] might output "text/plain".

:reqheaders
All the request headers in raw format.

:remoteaddr
The remote address of the request.

:res[header]
The given header of the response. If the header is not present, the value will be displayed as an empty string in the log. Example: :res[content-type] might output "text/plain".

:resheaders
All the response headers in raw format.

:responsetime[digits]
The time between the request coming into the logging middleware and when the response headers are written, in milliseconds. The digits argument is a number that specifies the number of digits to include on the number, defaulting to 2.

:statuscode
The status code of the response.

:url
The URL of the request.

Encryption Middleware

Add the Encryption Middleware to allow for custom encryption of request and response body message. With this middleware you can add custom functions that process the body content, receiving the input bytes and returning processed output bytes.

The middleware class is TEncryptionMiddleware, declared in unit Sparkle.Encryption.Logging.

To use the middleware, create it, set the desired properties if needed, then add it to the TXDataServerModule object:

uses {...}, Sparkle.Middleware.Encryption;

var
  EncryptionMiddleware: TEncryptionMiddleware;

begin
  EncryptionMiddleware := TEncryptionMiddleware.Create;
  // Set EncryptionMiddleware properties.
  Module.AddMiddleware(EncryptionMiddleware);
end;

These are the properties you can set. You need to set at the very minimum either EncryptBytes and DecryptBytes properties.

Properties

Name Description
CustomHeader: string If different from empty string, then the request and response will not be performed if the request from the client includes HTTP header with the same name as CustomHeader property and same value as CustomHeaderValue property.
CustomHeaderValue: string See CustomHeader property for more information.
EncryptBytes: TEncryptDecryptBytesFunc Set EncryptBytes function to define the encryption processing of the message body. This function is called for every message body that needs to be encrypted. The bytes to be encrypted are passed in the ASource parameter, and the processed bytes should be provided in the ADest parameter.
If the function returns True, then the encrypted message (content of ADest) will be used. If the function returns False, then the content of ASource will be used (thus the original message will be used).
DecryptBytes: TEncryptDecryptBytesFunc Set the DecryptBytes function to define the decryption processing of the message body. See property EncryptBytes for more details.

TEncryptDecryptBytesFunc

TEncryptDecryptBytesFunc = reference to function(const ASource: TBytes; out ADest: TBytes): Boolean;

Creating Custom Middleware

To create a new middleware, create a new class descending from THttpServerMiddleware (declared in Sparkle.HttpServer.Module) and override the method ProcessRequest:

uses {...}, Sparkle.HttpServer.Module;  
 
type
  TMyMiddleware = class(THttpServerMiddleware, IHttpServerMiddleware)
  protected
    procedure ProcessRequest(Context: THttpServerContext; Next: THttpServerProc); override;
  end;

In the ProcessRequest method you do any processing of the request you want, and then you should call the Next function to pass the request to the next process requestor in the pipeline (until it reaches the main server module processor):

procedure TMyMiddleware.ProcessRequest(Context: THttpServerContext; Next: THttpServerProc);
begin
  if Context.Request.Headers.Exists('custom-header') then
    Next(Context)
  else
    Context.Response.StatusCode := 500;
end;

In the example above, the middleware checks if the header "custom-header" is present in the request. If it does, then it calls the next processor which will continue to process the request normally. If it does not, a 500 status code is returned and processing is done. You can of course modify the request object before forwarding it to the next processor. Then you can use this middleware just by adding it to any server module:

Module.AddMiddleware(TMyMiddleware.Create);

Alternatively, you can use the TAnonymousMiddleware (unit Sparkle.HttpServer.Module) to quickly create a simple middleware without needing to create a new class. The following example does the same as above:

Module.AddMiddleware(TAnonymousMiddleware.Create(
procedure(Context: THttpServerContext; Next: THttpServerProc)
begin
  if Context.Request.Headers.Exists('custom-header') then
    Next(Context)
  else
    Context.Response.StatusCode := 500;
end
));

Processing the response

Processing the response requires a different approach because the request must reach the final processor until the response can be post-processed by the middleware. To do that, you need to use the OnHeaders method of the response object. This method is called right after the response headers are build by the final processor, but right before it's sent to the client. So the middleware has the opportunity to get the final response but still modify it before it's sent:

procedure TMyMiddleware2.ProcessRequest(C: THttpServerContext; Next: THttpServerProc);
begin
  C.Response.OnHeaders(
    procedure(Resp: THttpServerResponse)
    begin
      if Resp.Headers.Exists('some-header') then
        Resp.StatusCode := 500;
    end
  );
  Next(C);
end;

The above middleware code means: when the response is about to be sent to the client, check if the response has the header "some-header". If it does, then return with status code of 500. Otherwise, continue normally.

Generic Middleware

Generic middleware provides you an opportunity to add code that does any custom processing for the middleware that is not covered by the existing pre-made middleware classes.

The middleware class is TSparkleGenericMiddleware, declared in unit Sparkle.Comp.GenericMiddleware. It's actually just a wrapper around the techniques described to create a custom middleware, but available at design-time.

This middleware publishes two events:

Events

Name Description
OnRequest: TMiddlewareRequestEvent This event is fired to execute the custom code for the middleware. Please refer to Creating Custom Middleware to know how to implement such code. If you don't call at least Next(Context) in the code, your server will not work as the request chain will not be forwarded to your module. Below is an example.
OnMiddlewareCreate: TMiddlewareCreateEvent Fired when the IHttpServerMiddleware interface is created. You can use this event to actually create your own IHttpServerMiddleware interface and pass it in the Middleware parameter to be used by the Sparkle module.

TMiddlewareRequestEvent

TMiddlewareRequestEvent = procedure(Sender: TObject; Context: THttpServerContext; Next: THttpServerProc) of object;

procedure TForm3.XDataServer1GenericRequest(Sender: TObject;
  Context: THttpServerContext; Next: THttpServerProc);
begin
  if Context.Request.Headers.Exists('custom-header') then
    Next(Context)
  else
    Context.Response.StatusCode := 500;
end;

TMiddlewareCreateEvent

TMiddlewareCreateEvent = procedure(Sender: TObject; var Middleware: IHttpServerMiddleware) of object;

Passing data through context

In many situations you would want do add additional meta informatino to the request context, to be used by further middleware and module.

For example, you might create a database connection and let it be used by other middleware, or you want to set a specific id, etc. You can do that by using Data and Item properties of the context:

procedure TForm3.XDataServerFirstMiddlewareRequest(Sender: TObject; Context: THttpServerContext; Next: THttpServerProc);
var
  Conn: IDBConnection;
  Obj: TMyObject;
begin
  // Set an ID, object and specific interface to the context
  Obj := TMyObject.Create;
  try
    Conn := GetSomeConnection;

    Context.Data['TenantId'] := 5;
    Context.SetItem(Obj);
    Context.SetItem(Conn);
    Next(Context);
  finally
    Obj.Free;
  end;
end;

Then you can use it in further middleware and/or module this way:

procedure TForm3.XDataServerSecondMiddlewareRequest(Sender: TObject; Context: THttpServerContext; Next: THttpServerProc);
var
  Conn: IDBConnection;
  Obj: TMyObject;
  TenantId: Integer;
begin
  Conn := Context.Item<IDBConnection>;
  if Conn <> nil then ; // do something with connection

  Obj := Context.Item<TMyObject>;
  if Obj <> nil then ; // do something with Obj

  if not Context.Data['TenantId'].IsEmpty then
    // use TenantId
    TenantId := Context.Data['TenantId'].AsInteger;

  Next(Context);
end;

Finally, you can use such context data from anywhere in your application, using the global reference to the request. The global reference uses a thread-var, so you must access it from the same thread which is processing the request:

procedure TMyForm.SomeMethod;
var
  Context: THttpServerContext;
  Conn: IDBConnection;
  Obj: TMyObject;
  TenantId: Integer;
begin
  Context := THttpServerContext.Current;

  // now get custom data from context
  Conn := Context.Item<IDBConnection>;
  if Conn <> nil then ; // do something with connection

  Obj := Context.Item<TMyObject>;
  if Obj <> nil then ; // do something with Obj

  if not Context.Data['TenantId'].IsEmpty then
    // use TenantId
    TenantId := Context.Data['TenantId'].AsInteger;
end;