Low level client implementation
For technical information about how interfaces are called in
Delphi, see this
nice article and the FakeCall
method implementation.
Here is the core of this client-side implementation of the "call stubs":
for i := 0 to fMethodsCount-1 do begin fFakeVTable[i+RESERVED_VTABLE_SLOTS] := P; P^ := $68ec8b55; inc(P); // push ebp; mov ebp,esp P^ := i; inc(P); // push {MethodIndex} P^ := $e2895251; inc(P); // push ecx; push edx; mov edx,esp PByte(P)^ := $e8; inc(PByte(P)); // call FakeCall P^ := PtrUInt(@TInterfacedObjectFake.FakeCall)-PtrUInt(P)-4; inc(P); P^ := $c25dec89; inc(P); // mov esp,ebp; pop ebp P^ := fMethods[i].ArgsSizeInStack or $900000; // ret {StackSize}; nop inc(PByte(P),3); end;
Just for fun... :)
I could not resist posting this code here; if you are curious, take a look at
the "official" RTTI.pas
or RIO.pas
units as provided
by Embarcadero, and you will probably find out that the mORMot
implementation is much easier to follow, and also faster (it does not recreate
all the stubs or virtual tables at each call, for instance).
Security
As stated above, in the features grid, a complete security pattern is available when using client-server services. In a 17, securing messages between clients and services is essential to protecting data.
Security is implemented at several levels:
- For communication stream - e.g. when using HTTPS protocol, or a custom cypher
within HTTP content-encoding;
- At RESTful / URI authentication level - see 18; introducing Group
and User notions;
- At interface
or method (service/operation) level - we'll discuss
this part now.
By default, all services and operations (i.e. all interfaces and methods) are allowed to execution.
Then, on the server side (it's an implementation detail), the
TServiceFactoryServer
instance (available from
TSQLRestServer.Services
property) provides the following methods
to change the security policy for each interface
:
- AllowAll()
and Allow()
to enable methods execution
globally;
- DenyAll()
and Deny()
to disable methods execution
globally;
- AllowAllByID()
and AllowByID()
to enable methods
execution by Group IDs;
- DenyAllByID()
and DenyByID()
to disable methods
execution by Group IDs;
- AllowAllByName()
and AllowByName()
to enable
methods execution by Group names;
- DenyAllByName()
and DenyByName()
to disable methods
execution by Group names.
The first four methods will affect everybody. The next *ByID()
four methods accept a list of authentication Group IDs (i.e.
TSQLAuthGroup.ID
values), where as the *ByName()
methods will handle TSQLAuthGroup.Ident
property values.
In fact, the execution can be authorized for a particular group of
authenticated users.
Your service can therefore provide some basic features, and then enables
advanced features for administrators or supervisors only.
Since the User / Group policy is fully customizable in our RESTful
authentication scheme, mORMot provides a versatile and
inter-operable security pattern.
Here is some extract of the supplied regression tests:
(...) S := fClient.Server.Services['Calculator'] as TServiceFactoryServer; Test([1,2,3,4,5],'by default, all methods are allowed'); S.AllowAll; Test([1,2,3,4,5],'AllowAll should change nothing'); S.DenyAll; Test([],'DenyAll will reset all settings'); S.AllowAll; Test([1,2,3,4,5],'back to full acccess for everybody'); S.DenyAllByID([GroupID]); Test([],'our current user shall be denied'); S.AllowAll; Test([1,2,3,4,5],'restore allowed for everybody'); S.DenyAllByID([GroupID+1]); Test([1,2,3,4,5],'this group ID won''t affect the current user'); S.DenyByID(['Add'],GroupID); Test([2,3,4,5],'exclude a specific method for the current user'); S.DenyByID(['totext'],GroupID); Test([2,3,5],'exclude another method for the current user'); (...)
The Test()
procedure is used to validate the corresponding
methods of ICalculator
(1=Add, 2=Multiply,
3=Subtract, 4=ToText...).
In this above code, the GroupID
value is retrieved as such:
GroupID := fClient.MainFieldID(TSQLAuthGroup,'User');
And the current authenticated user is member of the 'User'
group:
fClient.SetUser('User','synopse'); // default user for Security tests
Since TSQLRestServer.ServiceRegister
method returns the first
created TServiceFactoryServer
instance, and since all Allow*
/ AllowAll* / Deny* / DenyAll*
methods return also a
TServiceFactoryServer
instance, you can use some kind of "fluent
interface" in your code to set the security policy, as such:
Server.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared). DenyAll.AllowAllByName(['Supervisor']);
This will allow access to the ICalculator
methods only for the
Supervisor group of users.
Transmission content
All data is transmitted as JSON arrays or objects, according to the requested URI.
We'll discuss how data is expected to be transmitted, at the application level.
Request format
As stated above, there are two mode of routing, defined by
TServiceRoutingMode
. The routing to be used is defined globally in
the TSQLRest.Routing
property.
rmREST |
rmJSON_RPC |
|
Description | URI-based layout | JSON-RPC mode |
Default | Yes | No |
URI scheme | /Model/Interface.Method[/ClientDrivenID] | /Model/Interface |
Body content | JSON array of parameters |
"method":"MethodName","params":[...][,"id":ClientDrivenID] |
Security | RESTful authentication for each method | RESTful authentication for the whole service (interface) |
Speed | 10% faster | 10% slower |
In the default rmREST
mode, both service and operation (i.e.
interface and method) are identified within the URI. And the message body is a
standard JSON array of the supplied parameters (i.e. all const
and
var
parameters).
Here is a typical request for ICalculator.Add
:
POST /root/Calculator.Add (...) [1,2]
For a sicClientDriven
mode service, the needed instance ID is
appended to the URI:
POST /root/ComplexNumber.Add/1234 (...) [20,30]
Here, 1234
is the identifier of the server-side instance ID,
which is used to track the instance life-time, in sicClientDriven
mode.
One benefit of using URI is that it will be more secure in our RESTful authentication scheme - see 18: each method (and even any client driven session ID) will be signed properly.
In this rmREST
mode, the server is also able to retrieve the
parameters from the URI, if the message body is left void. This is not used
from a Delphi client (since it will be more complex and therefore slower), but
it can be used for a client, if needed:
POST root/Calculator.Add?+%5B+1%2C2+%5D
In the above line, +%5B+1%2C2+%5D
will be decoded as
[1,2]
on the server side.
If rmJSON_RPC
mode is used, the URI will define the interface,
and then the method name will be inlined with parameters, e.g.
POST /root/Calculator (...) {"method":"Add","params":[1,2],"id":0}
Here, the "id"
field can be not set (and even not existing),
since it has no purpose in sicShared
mode.
For a sicClientDriven
mode service:
POST /root/ComplexNumber (...) {"method":"Add","params":[20,30],"id":1234}
This mode will be a little bit slower, but will probably be more AJAX ready. It's up to you to select the right routing scheme to be used.
Response format
The framework will always return the data in the same format, whatever the routing mode used.
Basically, this is a JSON object, with one nested "result":
property, and the client driven "id":
value (e.g. always 0 in
sicShared
mode):
POST /root/Calculator.Add (...) [1,2]
will be answered as such:
{"result":[3],"id":0}
The result array contains all var
and out
parameters values (in their declaration order), and then the method main
result.
For instance, here is a transmission stream for a
ICalculator.ComplexCall
request in rmREST
mode:
POST root/Calculator.ComplexCall (...) [[288722014,1231886296], ["one","two","three"], ["ABC","DEF","GHIJK"], "BgAAAAAAAAAAAAAAAAAAACNEOlxEZXZcbGliXFNRTGl0ZTNcZXhlXFRlc3RTUUwzLmV4ZQ==", "Xow1EdkXbUkDYWJj"]
will be answered as such:
'{"result":[ ["ABC","DEF","GHIJK","one,two,three"], "X4w1EdgXbUkUMjg4NzIyMDE0LDEyMzE4ODYyOTY=", "Xow1EdkXbUkjRDpcRGV2XGxpYlxTUUxpdGUzXGV4ZVxUZXN0U1FMMy5leGU="], "id":0}'
It matches the var / const / out
parameters declaration of the
method:
function ComplexCall(const Ints: TIntegerDynArray; Strs1: TRawUTF8DynArray; var Str2: TWideStringDynArray; const Rec1: TVirtualTableModuleProperties; var Rec2: TSQLRestCacheEntryValue): TSQLRestCacheEntryValue;
And its implementation:
function TServiceCalculator.ComplexCall(const Ints: TIntegerDynArray; Strs1: TRawUTF8DynArray; var Str2: TWideStringDynArray; const Rec1: TVirtualTableModuleProperties; var Rec2: TSQLRestCacheEntryValue): TSQLRestCacheEntryValue; var i: integer; begin result := Rec2; result.JSON := StringToUTF8(Rec1.FileExtension); i := length(Str2); SetLength(Str2,i+1); Str2[i] := UTF8ToWideString(RawUTF8ArrayToCSV(Strs1)); inc(Rec2.ID); dec(Rec2.TimeStamp); Rec2.JSON := IntegerDynArrayToCSV(Ints,length(Ints)); end;
Note that TIntegerDynArray
, TRawUTF8DynArray
and
TWideStringDynArray
values were marshaled as JSON arrays, whereas
complex records (like TSQLRestCacheEntryValue
) have been Base-64
encoded.
The framework is able to handle class instances as parameters, for instance
with the following interface, using a TPersistent
child class with
published properties (it would be the same for TSQLRecord
ORM
instances):
type TComplexNumber = class(TPersistent) private fReal: Double; fImaginary: Double; public constructor Create(aReal, aImaginary: double); reintroduce; published property Real: Double read fReal write fReal; property Imaginary: Double read fImaginary write fImaginary; end;
IComplexCalculator = interface(ICalculator) ['{8D0F3839-056B-4488-A616-986CF8D4DEB7}'] /// purpose of this unique method is to substract two complex numbers // - using class instances as parameters procedure Substract(n1,n2: TComplexNumber; out Result: TComplexNumber); end; As stated above, it is not possible to return a class as a result of afunction
(who will be responsible of handling its life-time?). So in this method declaration, the result is declared asout
parameter.
During the transmission, published properties of TComplexNumber
parameters will be serialized as standard JSON objects:
POST root/ComplexCalculator.Substract (...) [{"Real":2,"Imaginary":3},{"Real":20,"Imaginary":30}]
will be answered as such:
{"result":[{"Real":-18,"Imaginary":-27}],"id":0}
Those content have perfectly standard JSON declarations, so can be generated and consumed directly in any AJAX client.
In case of an error, the standard message object will be returned:
{ "ErrorCode":400, "ErrorText":"Error description" }
The following error descriptions may be returned by the service implementation from the server side:
ErrorText |
Description |
Method name required | rmJSON_RPC call without "method": field |
Unknown method | rmJSON_RPC call with invalid method name (in
rmRest mode, there is no specific message, since it may be a valid
request) |
Parameters required | The server expect at least a void JSON array (aka [] ) as
parameters |
Unauthorized method | This method is not allowed with the current authenticated user group - see Security above |
... instance id:? not found or deprecated | The supplied "id": parameter points to a wrong instance (in
sicPerSession / sicPerUser / sicPerGroup mode) |
ExceptionClass: Exception Message (with 500 Internal Server
Error ) |
An exception was raised during method execution |
Continue reading...
This article is part of a list of other blog articles, extracted from the official Synopse mORMot framework documentation:- Interface based services;
- Defining a data contract;
- Service side implementation;
- Using services on the Client or Server sides;
- Interface based services implementation details;
- WCF, mORMot and Event Sourcing.
Feedback and questions are welcome on our forum, just as usual.