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
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
. 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'
));
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
.
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
Name | Description |
---|---|
FormatString: string | Specifies the format string for the message to be logged for each request/response, according to the format string options, described at the end of this topic. The default format string is::method :url :statuscode - :responsetime ms |
ExceptionFormatString: string | Specifies the format string for the message to be logged when an exception happens during the request processing. In other words, if any exception occurs in the server during request processing, and LogExceptions property is True, the exception will be logged using this format. Different from FormatString property which has a specific format, the content of this property is just a simple string used by Delphi Format function. Three parameters are passed to the Format function, in this order: The exception message, the exception class name and the log message itself. Default value is: (%1:s) %0:s - %2:s |
LogExceptions: Boolean | Indicates if the middleware should catch unhandled exceptions raised by the server and log them. Default is True. Note if LogExceptions is True logged exceptions will not be propagated up in the chain. If LogExceptions is False, the exception will be raised again to be handled by some other middleware or the low level exception handling system in Sparkle. |
LogLevel: TLogLevel | The level of log messages generated by the middleware. It could be Trace, Debug, Info, Warning, Error. Default value is Trace. |
ExceptionLogLevel: TLogLevel | The level of exception log messages generated by the middleware. It could be Trace, Debug, Info, Warning, Error. Default value is Error. |
property OnFilterLog: TLoggingFilterProc | OnFilterLog event is fired for every incoming request that should be logged. This is an opportunity for you to filter which requests should be logged. For example, you might want to only log messages for requests that return an HTTP status code indicating an error. Below is an example of use. |
property OnFilterLogEx: TLoggingFilterExProc | OnFilterExLog event is fired for every incoming request that should be logged. This is an opportunity for you to filter which requests should be logged. For example, you might want to only log messages for requests that return an HTTP status code indicating an error. Below is an example of use. |
TLoggingFilterProc
TLoggingFilterProc = reference to procedure(Context: THttpServerContext; var Accept: Boolean);
LoggingMiddleware.OnFilterLog := procedure(Context: THttpServerContext; var Accept: Boolean)
begin
Accept := Context.Response.StatusCode >= 400;
end;
TLoggingFilterExProc
TLoggingFilterExProc = reference to procedure(Context: THttpServerContext; Info: TLoggingInfo; var Accept: Boolean);
LoggingMiddleware.OnFilterLog := procedure(Context: THttpServerContext;
Info: TLoggingInfo; var Accept: Boolean)
begin
if Context.Response.StatusCode >= 400 then
begin
// log the messages ourselves, with different level
Accept := False;
Info.Logger.Warning(Info.LogMessage);
end;
end;
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;