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 TXData
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'
));
Related types
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 TSparkleLoggingMiddleware.
To use the middleware, create it, set the desired properties if needed,
then add it to the TXData
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 TSparkleLoggingMiddleware 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 TXData
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;