Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Comment: |
|
---|---|
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA1: |
ace12b0996bf53914ff62c0757d4160f |
User & Date: | abouchez 2012-12-04 17:27:47 |
2012-12-05
| ||
10:35 | renamed aParams: TSQLRestServerCallBackParams into more explicit/neutral Ctxt: TSQLRestServerCallBackParams signature check-in: c1b6696521 user: abouchez tags: trunk | |
2012-12-04
| ||
17:27 |
| |
2012-12-03
| ||
13:53 | introducing TSQLHttpClientGeneric.Compression property to set the handled compression schemes at runtime check-in: 47acace079 user: abouchez tags: trunk | |
Changes to SQLite3/Documentation/Synopse SQLite3 Framework.pro.
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
....
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
|
- By some RESTful @**service@s, implemented in the Server as {\i published methods}, and consumed in the Client via native Delphi methods; - Defining some {\i service @*contract@s} as standard Delphi {\f1\fs20 interface}, and then run it seamlesly on both client and client sides. The first of the two last items can be compared to the {\i DataSnap} Client-Server features, also @*JSON@-based, introduced in Delphi 2010. See for instance the following example available on the Internet at @http://docwiki.embarcadero.com/RADStudio/en/Developing_DataSnap_Applications The second is purely interface-based, so matches the "designed by contract" principle - see @47@ - as implemented by Microsoft's @*WCF@ technology. {\i Windows Communication Foundation} is the unified programming model provided by Microsoft for building service-oriented applications - see @http://msdn.microsoft.com/en-us/library/dd456779. We included most of the nice features made available in WCF in {\i mORMot}, in a KISS manner. :49 Client-Server services via methods To implement a service in the {\i Synopse mORMot framework}, the first method is to define @**published method@ Server-side, then use easy functions about JSON or URL-parameters to get the request encoded and decoded as expected, on Client-side. We'll implement the same example as in the official Embarcadero docwiki page above. Add two numbers. Very useful service, isn't it? : My Server is rich On the server side, we need to customize the standard {\f1\fs20 TSQLRestServer} class definition (more precisely a {\f1\fs20 @*TSQLRestServerDB@} class which includes a {\i SQlite3} engine, or a lighter {\f1\fs20 @*TSQLRestServerFullMemory@} kind of server, which is enough for our purpose), by adding a new {\f1\fs20 published} method: !type ! TSQLRestServerTest = class(TSQLRestServerFullMemory) ! (...) !! published !! function Sum(var aParams: TSQLRestServerCallBackParams): Integer; ! end; The method name ("Sum") will be used for the URI encoding, and will be called remotely from {\i ModelRoot/Sum} URL. The {\i ModelRoot} is the one defined in the {\f1\fs20 Root} parameter of the {\i model} used by the application. This method, like all Server-side methods, MUST have the same exact parameter definition as in the {\f1\fs20 TSQLRestServerCallBack} prototype: !type ! TSQLRestServerCallBack = function(var aParams: TSQLRestServerCallBackParams): Integer of object; Then we implement this method: !function TSQLRestServerTest.Sum(var aParams: TSQLRestServerCallBackParams): Integer; !var a,b: Extended; !begin ! if not UrlDecodeNeedParameters(aParams.Parameters,'A,B') then ! begin ! result := 404; // invalid Request ! aParams.ErrorMsg^ := 'Missing Parameter'; ! exit; ! end; ! while aParameters<>nil do ! begin ! UrlDecodeExtended(aParams.Parameters,'A=',a); ! UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); ! end; ! aParams.Resp := JSONEncodeResult([a+b]); ! // same as : aResp := JSONEncode(['result',a+b],TempMemoryStream); ! result := 200; // success !end; The only not obvious part of this code is the parameters marshaling, i.e. how the values are retrieved from the incoming {\f1\fs20 aParams.Parameters} text buffer, then converted into native local variables. On the Server side, typical implementation steps are therefore: - Use the {\f1\fs20 UrlDecodeNeedParameters} function to check that all expected parameters were supplied by the caller in {\f1\fs20 aParams.Parameters}; - Call {\f1\fs20 UrlDecodeInteger / UrlDecodeInt64 / UrlDecodeExtended / UrlDecodeValue / UrlDecodeObject} functions (all defined in {\f1\fs20 SynCommons.pas}) to retrieve each individual parameter from standard JSON content; - Implement the service (here it is just the {\f1\fs20 a+b} expression); - Then return the result into {\f1\fs20 aParams.Resp} variable. The powerful {\f1\fs20 UrlDecodeObject} function (defined in {\f1\fs20 mORMot.pas}) can be used to un-serialize most class instance from its textual JSON representation ({\f1\fs20 @*TPersistent@, @*TSQLRecord@, TStringList}...). Note that due to this implementation pattern, the {\i mORMot} service implementation is very fast, and not sensitive to the "Hash collision attack" security issue, as reported with {\i Apache} - see @http://blog.synopse.info/post/2011/12/30/Hash-collision-attack for details. The implementation must return the HTTP error code (e.g. 200 on success) as an integer value, and any response in {\f1\fs20 aParams.Resp} as a serialized JSON object by default (using e.g. {\f1\fs20 TSQLRestServer.JSONEncodeResult}), since default mime-type is {\f1\fs20 JSON_CONTENT_TYPE}: $ {"Result":"OneValue"} or a JSON object containing an array: $ {"Result":["One","two"]} So you can consume these services, implemented Server-Side in fast Delphi code, with any @*AJAX@ application on the client side (if you use HTTP as communication protocol). The {\f1\fs20 aParams.Head^} parameter may be overridden on the server side to set a custom header which will be provided to the client - it may be useful for instance to specify another mime-type than the default constant {\f1\fs20 JSON_CONTENT_TYPE}, i.e. {\f1\fs20 'application/json; charset=UTF-8'}, and returns plain text, HTML or binary. In case of an error on the server side (only two valid status codes are {\f1\fs20 200} and {\f1\fs20 201}), the client will receive a corresponding serialized JSON error object, as such: ${ $ "ErrorCode":404, $ "ErrorText":"Missing Parameter" $} The {\f1\fs20 aParams.ErrorMsg^} parameter can be overridden on the server side to specify a custom error message in plain English, which will be returned to the client in case of an invalid status code. If no custom {\f1\fs20 ErrorMsg} is specified, the framework will return the corresponding generic HTTP status text. The {\f1\fs20 aParams.Context} parameter may contain at calling time the expected {\f1\fs20 TSQLRecord} ID (as decoded from {\i RESTful} URI), and the current session, user and group IDs. If @*authentication@ - see @18@ - is not used, this parameter is meaningless: in fact, {\f1\fs20 aParams.Context.Session} will contain either 0 if any @*session@ is not yet started, or 1 if authentication mode is not active. Server-side implementation can use the {\f1\fs20 TSQLRestServer.SessionGetUser} method to retrieve the corresponding user details (note that when using this method, the returned {\f1\fs20 TSQLAuthUser} instance is a local thread-safe copy which shall be freed when done). An {\i important point} is to remember that the implementation of the callback method {\b must be thread-safe} - as stated by @25@. In fact, the {\f1\fs20 TSQLRestServer.URI} method expects such callbacks to handle the thread-safety on their side. It's perhaps some more work to handle a critical section in the implementation, but, in practice, it's the best way to achieve performance and scalability: the resource locking can be made at the tiniest code level. : The Client is always right The client-side is implemented by calling some dedicated methods, and providing the service name ({\f1\fs20 'sum'}) and its associated parameters: !function Sum(aClient: TSQLRestClientURI; a, b: double): double; !var err: integer; !begin ! val(aClient.CallBackGetResult('sum',['a',a,'b',b]),Result,err); !end; You could even implement this method in a dedicated client method - which make sense: ................................................................................ You have to create the server instance, and the corresponding {\f1\fs20 TSQLRestClientURI} (or {\f1\fs20 TMyClient}), with the same database model, just as usual... On the Client side, you can use the {\f1\fs20 CallBackGetResult} method to call the service from its name and its expected parameters, or create your own caller using the {\f1\fs20 UrlEncode()} function. Note that you can specify most class instance into its JSON representation by using some {\f1\fs20 TObject} into the method arguments: !function TMyClient.SumMyObject(a, b: TMyObject): double; !var err: integer; !begin ! val(CallBackGetResult('summyobject',['a',a,'b',b]),Result,err); !end; This Client-Server protocol uses JSON here, but you can serve any kind of data, binary, HTML, whatever... just by overriding the content type on the server. The usual protocols of our framework can be used: @*HTTP@/1.1, Named Pipe, Windows GDI messages, direct in-memory/in-process access. Of course, these services can be related to any table/class of our @*ORM@ framework, so you would be able to create easily any RESTful compatible requests on URI like {\f1\fs20 ModelRoot/TableName/ID/MethodName}. The ID of the corresponding record is decoded from its {\i RESTful} scheme into {\f1\fs20 aParams.Context.ID}. For example, here we return a @*BLOB@ field content as hexadecimal: !function TSQLRestServerTest.DataAsHex(var aParams: TSQLRestServerCallBackParams): Integer; !var aData: TSQLRawBlob; !begin ! result := 404; // invalid Request ! if (self=nil) or (aParams.Table=nil) or ! not aParams.Table.InheritsFrom(TSQLRecord) or ! (aParams.Context.ID<0) then ! exit; // we need a valid record and its ID ! if not RetrieveBlob(TSQLRecordPeople,aParams.Context.ID,'Data',aData) then ! exit; // impossible to retrieve the Data BLOB field ! aParams.Resp := JSONEncodeResult([SynCommons.BinToHex(aData)]); ! // idem: aResp := JSONEncode(['result',BinToHex(aRecord.fData)],TempMemoryStream); ! result := 200; // success !end; :63 Interface based services The @49@ implementation gives full access to the lowest-level of the {\i mORMot}'s core, so it has some advantages: - It can be tuned to fit any purpose (such as retrieving or returning some HTML or binary data, or modifying the HTTP headers on the fly); - It is integrated into the @*REST@ful URI model, so it can be related to any table/class of our @*ORM@ framework (like {\f1\fs20 DataAsHex} service above), or it can handle any remote query (e.g. any @*AJAX@ or @*SOAP@ requests); - It has a very low performance overhead, so can be used to reduce server workload for some common tasks. But this implementation pattern has some drawbacks: - Most content marshaling is to be done by hand, so may introduce implementation issues; - Client and server side code does not have the same implementation pattern, so you will have to code explicitly data marshaling twice, for both client and server; - The services do not have any hierarchy, and are listed as a plain list, which is not very convenient; - It is difficult to synchronize several service calls within a single context, e.g. when a workflow is to be handled during the application process (you have to code some kind of state machine on both sides); - @*Security@ is handled globally for the user, or should be checked by hand in the implementation method (using the {\f1\fs20 aParams.Context} values). You can get rid of those limitations with the interface-based service implementation of {\i mORMot}. For a detailed introduction and best practice guide to @*SOA@, you can consult this classic article: @http://www.ibm.com/developerworks/webservices/library/ws-soa-design1 According to this document, all expected SOA features are now available in the current implementation of the {\i mORMot} framework (including service catalog aka "broker"). : Implemented features Here are the key features of the current implementation of services using interfaces in the {\i Synopse mORMot framework}: |%25%75 |\b Feature|Remarks\b0 |Service Orientation|Allow loosely-coupled relationship |Design by contract|Data Contracts are defined in Delphi code as standard {\f1\fs20 interface} custom types |Factory driven|Get an implementation instance from a given interface |Server factory|You can get an implementation on the server side |Client factory|You can get a "fake" implementation on the client side, remotely calling the server to execute the process |Auto marshaling|The contract is transparently implemented: no additional code is needed e.g. on the client side, and will handle simple types (strings, numbers, dates, sets and enumerations) and high-level types (objects, collections, records, dynamic arrays) from Delphi 6 up to XE3 |Flexible|Methods accept per-value or per-reference parameters |Instance lifetime|An implementation class can be:\line - Created on every call,\line - Shared among all calls,\line - Shared for a particular user or group,\line - Stay alive as long as the client-side interface is not released,\line - or as long as an @*authentication@ session exists |Stateless|Following a standard request/reply pattern |Signed|The contract is checked to be consistent before any remote execution |Secure|Every service and/or methods can be enabled or disabled on need |Safe|Using extended RESTful authentication - see @18@ |Multi-hosted\line (with DMZ)|Services are hosted by default within the main @*ORM@ server, but can have their own process, with a dedicated connection to the ORM core |
|
|
|
|
<
|
<
<
<
<
<
<
<
>
|
|
>
>
|
<
<
<
>
|
|
<
>
>
>
>
>
<
<
<
<
<
<
<
<
<
>
|
|
|
>
>
|
<
|
<
<
|
|
>
|
|
|
|
>
>
<
<
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
....
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
|
- By some RESTful @**service@s, implemented in the Server as {\i published methods}, and consumed in the Client via native Delphi methods; - Defining some {\i service @*contract@s} as standard Delphi {\f1\fs20 interface}, and then run it seamlesly on both client and client sides. The first of the two last items can be compared to the {\i DataSnap} Client-Server features, also @*JSON@-based, introduced in Delphi 2010. See for instance the following example available on the Internet at @http://docwiki.embarcadero.com/RADStudio/en/Developing_DataSnap_Applications The second is purely interface-based, so matches the "designed by contract" principle - see @47@ - as implemented by Microsoft's @*WCF@ technology. {\i Windows Communication Foundation} is the unified programming model provided by Microsoft for building service-oriented applications - see @http://msdn.microsoft.com/en-us/library/dd456779. We included most of the nice features made available in WCF in {\i mORMot}, in a KISS manner. :49 Client-Server services via methods To implement a service in the {\i Synopse mORMot framework}, the first method is to define @**published method@ Server-side, then use easy functions about JSON or URL-parameters to get the request encoded and decoded as expected, on Client-side. We'll implement the same example as in the official Embarcadero docwiki page above. Add two numbers. Very useful service, isn't it? : Publishing a service on the server On the server side, we need to customize the standard {\f1\fs20 TSQLRestServer} class definition (more precisely a {\f1\fs20 @*TSQLRestServerDB@} class which includes a {\i SQlite3} engine, or a lighter {\f1\fs20 @*TSQLRestServerFullMemory@} kind of server, which is enough for our purpose), by adding a new {\f1\fs20 published} method: !type ! TSQLRestServerTest = class(TSQLRestServerFullMemory) ! (...) !! published !! procedure Sum(var aParams: TSQLRestServerCallBackParams); ! end; The method name ("Sum") will be used for the URI encoding, and will be called remotely from {\i ModelRoot/Sum} URL. The {\i ModelRoot} is the one defined in the {\f1\fs20 Root} parameter of the {\i model} used by the application. This method, like all Server-side methods, MUST have the same exact parameter definition as in the {\f1\fs20 TSQLRestServerCallBack} prototype: !type ! TSQLRestServerCallBack = procedure(var aParams: TSQLRestServerCallBackParams) of object; Then we implement this method: !procedure TSQLRestServerTest.Sum(var aParams: TSQLRestServerCallBackParams); !var a,b: Extended; ! if UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin ! while aParams.Parameters<>nil do begin ! UrlDecodeExtended(aParams.Parameters,'A=',a); ! UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); ! end; ! aParams.Results([a+b]); ! end else ! aParams.Error('Missing Parameter'); !end; The only not obvious part of this code is the parameters marshaling, i.e. how the values are retrieved from the incoming {\f1\fs20 aParams.Parameters} text buffer, then converted into native local variables. On the Server side, typical implementation steps are therefore: - Use the {\f1\fs20 UrlDecodeNeedParameters} function to check that all expected parameters were supplied by the caller in {\f1\fs20 aParams.Parameters}; - Call {\f1\fs20 UrlDecodeInteger / UrlDecodeInt64 / UrlDecodeExtended / UrlDecodeValue / UrlDecodeObject} functions (all defined in {\f1\fs20 SynCommons.pas}) to retrieve each individual parameter from standard JSON content; - Implement the service (here it is just the {\f1\fs20 a+b} expression); - Then return the result calling {\f1\fs20 aParams.Results()} method or {\f1\fs20 aParams.Error()} in case of any error. The powerful {\f1\fs20 UrlDecodeObject} function (defined in {\f1\fs20 mORMot.pas}) can be used to un-serialize most class instance from its textual JSON representation ({\f1\fs20 @*TPersistent@, @*TSQLRecord@, TStringList}...). Using {\f1\fs20 aParams.Results()} will encode the specified values as a JSON object with one {\f1\fs20 "Result"} member, with default mime-type {\f1\fs20 JSON_CONTENT_TYPE}: $ {"Result":"OneValue"} or a JSON object containing an array: $ {"Result":["One","two"]} Using {\f1\fs20 aParams.Returns()} will let the method return the content in any format, e.g. as a JSON object (via the overloaded {\f1\fs20 aParams.Returns([])} method expecting field name/value pairs), or any content, since the returned mime-type can be defined as a parameter to {\f1\fs20 aParams.Returns()} - it may be useful to specify another mime-type than the default constant {\f1\fs20 JSON_CONTENT_TYPE}, i.e. {\f1\fs20 'application/json; charset=UTF-8'}, and returns plain text, HTML or binary. For instance, you can return directly a value as plain text as such: !procedure TSQLRestServer.TimeStamp(var aParams: TSQLRestServerCallBackParams); !begin ! aParams.Returns(Int64ToUtf8(ServerTimeStamp),HTML_SUCCESS,TEXT_CONTENT_TYPE_HEADER); !end; So you can consume these services, implemented Server-Side in fast Delphi code, with any @*AJAX@ application on the client side (if you use HTTP as communication protocol). An {\i important point} is to remember that the implementation of the callback method {\b must be thread-safe} - as stated by @25@. In fact, the {\f1\fs20 TSQLRestServer.URI} method expects such callbacks to handle the thread-safety on their side. It's perhaps some more work to handle a critical section in the implementation, but, in practice, it's the best way to achieve performance and scalability: the resource locking can be made at the tiniest code level. : Defining the client The client-side is implemented by calling some dedicated methods, and providing the service name ({\f1\fs20 'sum'}) and its associated parameters: !function Sum(aClient: TSQLRestClientURI; a, b: double): double; !var err: integer; !begin ! val(aClient.CallBackGetResult('sum',['a',a,'b',b]),Result,err); !end; You could even implement this method in a dedicated client method - which make sense: ................................................................................ You have to create the server instance, and the corresponding {\f1\fs20 TSQLRestClientURI} (or {\f1\fs20 TMyClient}), with the same database model, just as usual... On the Client side, you can use the {\f1\fs20 CallBackGetResult} method to call the service from its name and its expected parameters, or create your own caller using the {\f1\fs20 UrlEncode()} function. Note that you can specify most class instance into its JSON representation by using some {\f1\fs20 TObject} into the method arguments: !function TMyClient.SumMyObject(a, b: TMyObject): double; !var err: integer; !begin ! val(CallBackGetResult('summyobject',['a',a,'b',b]),Result,err); !end; This Client-Server protocol uses JSON here, as encoded server-side via {\f1\fs20 aParams.Results()} method, but you can serve any kind of data, binary, HTML, whatever... just by overriding the content type on the server with {\f1\fs20 aParams.Returns()}. : Advanced process on server side On server side, method definition has only one {\f1\fs20 aParams} parameter, which has several members at calling time, and publish all service calling features, including {\i RESTful} URI routing, session handling or low-level HTTP headers (if any). At first, {\f1\fs20 aParams} may indicate the expected {\f1\fs20 TSQLRecord} ID and {\f1\fs20 TSQLRecord} class, as decoded from {\i RESTful} URI. It means that a service can be related to any table/class of our @*ORM@ framework, so you would be able to create easily any RESTful compatible requests on URI like {\f1\fs20 ModelRoot/TableName/ID/MethodName}. The ID of the corresponding record is decoded from its {\i RESTful} scheme into {\f1\fs20 aParams.ID}, and the table is available in {\f1\fs20 aParams.Table} or {\f1\fs20 aParams.TableIndex} (if you need its index in the associated server Model). For example, here we return a @*BLOB@ field content as hexadecimal, according to its {\f1\fs20 TableName/Id}: !procedure TSQLRestServerTest.DataAsHex(var aParams: TSQLRestServerCallBackParams); !var aData: TSQLRawBlob; !begin ! if (self=nil) or (aParams.Table<>TSQLRecordPeople) or (aParams.ID<0) then ! aParams.Error('Need a valid record and its ID') else ! if RetrieveBlob(TSQLRecordPeople,aParams.ID,'Data',aData) then ! aParams.Results([SynCommons.BinToHex(aData)]) else ! aParams.Error('Impossible to retrieve the Data BLOB field'); !end; A corresponding client method may be: !function TSQLRecordPeople.DataAsHex(aClient: TSQLRestClientURI): RawUTF8; !begin ! Result := aClient.CallBackGetResult('DataAsHex',[],RecordClass,fID); !end; If @*authentication@ - see @18@ - is used, the current session, user and group IDs are available in {\f1\fs20 Session / SessionUser / SessionGroup} fields. If authentication is not available, those fields are meaningless: in fact, {\f1\fs20 aParams.Context.Session} will contain either 0 ({\f1\fs20 CONST_AUTHENTICATION_SESSION_NOT_STARTED}) if any @*session@ is not yet started, or 1 ({\f1\fs20 CONST_AUTHENTICATION_NOT_USED}) if authentication mode is not active. Server-side implementation can use the {\f1\fs20 TSQLRestServer.SessionGetUser} method to retrieve the corresponding user details (note that when using this method, the returned {\f1\fs20 TSQLAuthUser} instance is a local thread-safe copy which shall be freed when done). In {\f1\fs20 aParams.Call^} member, you can access low-level communication content, i.e. all incoming and outgoing values, including headers and message body. Depending on the transmission protocol used, you can retrieve e.g. HTTP header information. For instance, here is how you can access the caller remote IP address and client application user agent: ! aRemoteIP := FindIniNameValue(pointer(aParams.Call.InHead),'REMOTEIP: '); ! aUserAgent := FindIniNameValue(pointer(aParams.Call.InHead),'USER-AGENT: '); : Handling errors Calling either {\f1\fs20 aParams.Results()}, {\f1\fs20 aParams.Returns()}, {\f1\fs20 aParams.Success()} or {\f1\fs20 aParams.Error()} will specify the HTTP error code (e.g. 200 / "OK" for {\f1\fs20 Results()} and {\f1\fs20 Success()} methods by default, or 400 / "Bad Request" for {\f1\fs20 Error()}) as an {\f1\fs20 integer} value. For instance, here is how a service not returning any content can handle those error codes: !procedure TSQLRestServer.Batch(var aParams: TSQLRestServerCallBackParams); !begin ! if (aParams.Method=mPUT) and RunBatch(nil,nil,aParams) then ! aParams.Success else ! aParams.Error; !end; In case of an error on the server side, you may call {\f1\fs20 aParams.Error()} method (only the two valid status codes are {\f1\fs20 200} and {\f1\fs20 201}). The {\f1\fs20 aParams.Error()} method has an optional parameter to specify a custom error message in plain English, which will be returned to the client in case of an invalid status code. If no custom text is specified, the framework will return the corresponding generic HTTP status text (e.g. {\f1\fs20 "Bad Request"} for default status code {\f1\fs20 HTML_BADREQUEST} = 400). In this case, the client will receive a corresponding serialized JSON error object, e.g. for {\f1\fs20 aParams.Error('Missing Parameter',HTML_NOTFOUND)}: ${ $ "ErrorCode":404, $ "ErrorText":"Missing Parameter" $} If called from an AJAX client, or a browser, this content should be easy to interpret. Note that the framework core will catch any exception during the method execution, and will return a {\f1\fs20 "Internal Server Error" / HTML_SERVERERROR} = 500 error code with the associated textual exception details. : Benefits and limitations of this implementation Method-based services allow fast and direct access to all {\f1\fs20 mORMot} Client-Server {\f1\fs20 RESTful} features, over all usual protocols of our framework: @*HTTP@/1.1, Named Pipe, Windows GDI messages, direct in-memory/in-process access. The {\i mORMot} implementation of method-based services gives full access to the lowest-level of the framework core, so it has some advantages: - It can be tuned to fit any purpose (such as retrieving or returning some HTML or binary data, or modifying the HTTP headers on the fly); - It is integrated into the @*REST@ful URI model, so it can be related to any table/class of our @*ORM@ framework (like {\f1\fs20 DataAsHex} service above), or it can handle any remote query (e.g. any @*AJAX@ or @*SOAP@ requests); - It has a very low performance overhead, so can be used to reduce server workload for some common tasks. Note that due to this implementation pattern, the {\i mORMot} service implementation is very fast, and not sensitive to the "Hash collision attack" security issue, as reported with {\i Apache} - see @http://blog.synopse.info/post/2011/12/30/Hash-collision-attack for details. :63 Interface based services In real world, especially when your application relies heavily on services, the @49@ implementation pattern has some drawbacks: - Most content marshaling is to be done by hand, so may introduce implementation issues; - Client and server side code does not have the same implementation pattern, so you will have to code explicitly data marshaling twice, for both client and server ({\i DataSnap} and WCF both suffer from a similar issue, by which client classes shall be coded separately, most time generated by a Wizard); - The services do not have any hierarchy, and are listed as a plain list, which is not very convenient; - It is difficult to synchronize several service calls within a single context, e.g. when a workflow is to be handled during the application process (you have to code some kind of state machine on both sides, and use all session handling by hand); - @*Security@ is handled globally for the user, or should be checked by hand in the implementation method (using the {\f1\fs20 aParams} members). You can get rid of those limitations with the interface-based service implementation of {\i mORMot}. For a detailed introduction and best practice guide to @*SOA@, you can consult this classic article: @http://www.ibm.com/developerworks/webservices/library/ws-soa-design1 According to this document, all expected SOA features are now available in the current implementation of the {\i mORMot} framework (including service catalog aka "broker"). : Implemented features Here are the key features of the current implementation of services using interfaces in the {\i Synopse mORMot framework}: |%25%75 |\b Feature|Remarks\b0 |Service Orientation|Allow loosely-coupled relationship |Design by contract|Data Contracts are defined in Delphi code as standard {\f1\fs20 interface} custom types |Factory driven|Get an implementation instance from a given interface |Server factory|You can get an implementation on the server side |Client factory|You can get a "fake" implementation on the client side, remotely calling the server to execute the process |Auto marshaling|The contract is transparently implemented: no additional code is needed e.g. on the client side, and will handle simple types (strings, numbers, dates, sets and enumerations) and high-level types (objects, collections, records, dynamic arrays, variants) from Delphi 6 up to XE3 |Flexible|Methods accept per-value or per-reference parameters |Instance lifetime|An implementation class can be:\line - Created on every call,\line - Shared among all calls,\line - Shared for a particular user or group,\line - Stay alive as long as the client-side interface is not released,\line - or as long as an @*authentication@ session exists |Stateless|Following a standard request/reply pattern |Signed|The contract is checked to be consistent before any remote execution |Secure|Every service and/or methods can be enabled or disabled on need |Safe|Using extended RESTful authentication - see @18@ |Multi-hosted\line (with DMZ)|Services are hosted by default within the main @*ORM@ server, but can have their own process, with a dedicated connection to the ORM core |
Changes to SQLite3/Samples/06 - Remote JSON REST Service/Project06Server.dpr.
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
mORMot, SysUtils; type // TSQLRestServerFullMemory kind of server is light and enough for our purpose TServiceServer = class(TSQLRestServerFullMemory) published function Sum(var aParams: TSQLRestServerCallBackParams): Integer; end; { TServiceServer } function TServiceServer.Sum(var aParams: TSQLRestServerCallBackParams): Integer; var a,b: Extended; begin if not UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin result := 404; // invalid Request aParams.ErrorMsg^ := 'Missing Parameter'; // custom error message exit; end; while aParams.Parameters<>nil do begin UrlDecodeExtended(aParams.Parameters,'A=',a); UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); end; aParams.Resp := JSONEncodeResult([a+b]); // same as : aResp := JSONEncode(['result',a+b],TempMemoryStream); result := 200; // success end; var aModel: TSQLModel; begin aModel := TSQLModel.Create([],'service'); try |
| | | < < < < | | | | < < < > > > |
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
mORMot, SysUtils; type // TSQLRestServerFullMemory kind of server is light and enough for our purpose TServiceServer = class(TSQLRestServerFullMemory) published procedure Sum(var aParams: TSQLRestServerCallBackParams); end; { TServiceServer } procedure TServiceServer.Sum(var aParams: TSQLRestServerCallBackParams); var a,b: Extended; begin if UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin while aParams.Parameters<>nil do begin UrlDecodeExtended(aParams.Parameters,'A=',a); UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); end; aParams.Results([a+b]); end else aParams.Error('Missing Parameter'); end; var aModel: TSQLModel; begin aModel := TSQLModel.Create([],'service'); try |
Changes to SQLite3/Samples/MainDemo/FileServer.pas.
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
..
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
function OnDatabaseUpdateEvent(Sender: TSQLRestServer; Event: TSQLEvent; aTable: TSQLRecordClass; aID: integer): boolean; published /// a RESTful service used from the client side to add an event // to the TSQLAuditTrail table // - an optional database record can be specified in order to be // associated with the event function Event(var aParams: TSQLRestServerCallBackParams): Integer; end; implementation { TFileServer } ................................................................................ FreeAndNil(fTempAuditTrail); FreeAndNil(Server); finally inherited; end; end; function TFileServer.Event(var aParams: TSQLRestServerCallBackParams): Integer; var E: integer; begin if UrlDecodeInteger(aParams.Parameters,'EVENT=',E) and (E>0) and (E<=ord(High(TFileEvent))) then begin AddAuditTrail(TFileEvent(E),'',RecordReference(Model,aParams.Table,aParams.Context.ID)); result := HTML_SUCCESS; end else result := HTML_BADREQUEST; end; function TFileServer.OnDatabaseUpdateEvent(Sender: TSQLRestServer; Event: TSQLEvent; aTable: TSQLRecordClass; aID: integer): boolean; const EVENT_FROM_SQLEVENT: array[TSQLEvent] of TFileEvent = ( feRecordCreated, feRecordModified, feRecordDeleted); begin result := true; if aTable.InheritsFrom(TSQLFile) then AddAuditTrail(EVENT_FROM_SQLEVENT[Event], '', Model.RecordReference(aTable,aID)); end; end. |
|
|
|
|
|
|
|
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
..
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
function OnDatabaseUpdateEvent(Sender: TSQLRestServer; Event: TSQLEvent; aTable: TSQLRecordClass; aID: integer): boolean; published /// a RESTful service used from the client side to add an event // to the TSQLAuditTrail table // - an optional database record can be specified in order to be // associated with the event procedure Event(var aParams: TSQLRestServerCallBackParams); end; implementation { TFileServer } ................................................................................ FreeAndNil(fTempAuditTrail); FreeAndNil(Server); finally inherited; end; end; procedure TFileServer.Event(var aParams: TSQLRestServerCallBackParams); var E: integer; begin if UrlDecodeInteger(aParams.Parameters,'EVENT=',E) and (E>ord(feUnknownState)) and (E<=ord(High(TFileEvent))) then begin AddAuditTrail(TFileEvent(E),'',RecordReference(Model,aParams.Table,aParams.ID)); aParams.Success; end else aParams.Error; end; function TFileServer.OnDatabaseUpdateEvent(Sender: TSQLRestServer; Event: TSQLEvent; aTable: TSQLRecordClass; aID: integer): boolean; const EVENT_FROM_SQLEVENT: array[TSQLEvent] of TFileEvent = ( feRecordCreated, feRecordModified, feRecordDeleted); begin result := true; if aTable.InheritsFrom(TSQLFile) then AddAuditTrail(EVENT_FROM_SQLEVENT[Event], '', Model.RecordReference(aTable,aID)); end; end. |
Changes to SQLite3/mORMot.pas.
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 .... 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 .... 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 .... 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 .... 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 .... 6146 6147 6148 6149 6150 6151 6152 6153 6154 6155 6156 6157 6158 6159 6160 6161 6162 .... 6654 6655 6656 6657 6658 6659 6660 6661 6662 6663 6664 6665 6666 6667 6668 .... 7403 7404 7405 7406 7407 7408 7409 7410 7411 7412 7413 7414 7415 7416 7417 7418 .... 7517 7518 7519 7520 7521 7522 7523 7524 7525 7526 7527 7528 7529 7530 .... 7557 7558 7559 7560 7561 7562 7563 7564 7565 7566 7567 7568 7569 7570 .... 7645 7646 7647 7648 7649 7650 7651 7652 7653 7654 7655 7656 7657 7658 7659 7660 7661 7662 7663 7664 7665 7666 7667 7668 7669 7670 7671 7672 7673 7674 7675 7676 7677 7678 7679 7680 7681 7682 7683 .... 7785 7786 7787 7788 7789 7790 7791 7792 7793 7794 7795 7796 7797 7798 7799 7800 .... 7885 7886 7887 7888 7889 7890 7891 7892 7893 7894 7895 7896 7897 7898 7899 7900 7901 7902 7903 7904 7905 7906 7907 7908 7909 7910 7911 7912 7913 7914 7915 7916 7917 7918 7919 7920 7921 7922 7923 7924 7925 7926 7927 7928 7929 7930 7931 7932 7933 7934 7935 7936 7937 7938 7939 7940 7941 7942 7943 .... 8032 8033 8034 8035 8036 8037 8038 8039 8040 8041 8042 8043 8044 8045 8046 .... 8069 8070 8071 8072 8073 8074 8075 8076 8077 8078 8079 8080 8081 8082 8083 8084 8085 8086 8087 8088 8089 8090 8091 8092 8093 8094 8095 8096 8097 8098 8099 8100 8101 8102 8103 8104 8105 8106 8107 .... 9141 9142 9143 9144 9145 9146 9147 9148 9149 9150 9151 9152 9153 9154 9155 .... 9171 9172 9173 9174 9175 9176 9177 9178 9179 9180 9181 9182 9183 9184 9185 9186 .... 9774 9775 9776 9777 9778 9779 9780 9781 9782 9783 9784 9785 9786 9787 9788 9789 9790 9791 9792 9793 9794 9795 9796 9797 9798 9799 9800 9801 9802 9803 9804 .... 9895 9896 9897 9898 9899 9900 9901 9902 9903 9904 9905 9906 9907 9908 9909 ..... 19273 19274 19275 19276 19277 19278 19279 19280 19281 19282 19283 19284 19285 19286 19287 ..... 20409 20410 20411 20412 20413 20414 20415 20416 20417 20418 20419 20420 20421 20422 20423 20424 20425 20426 20427 20428 20429 20430 20431 20432 20433 20434 20435 20436 20437 20438 20439 20440 20441 20442 20443 20444 20445 20446 20447 20448 20449 20450 20451 20452 20453 20454 20455 20456 20457 20458 20459 20460 20461 20462 ..... 20523 20524 20525 20526 20527 20528 20529 20530 20531 20532 20533 20534 20535 20536 20537 20538 20539 20540 20541 20542 20543 20544 20545 20546 20547 20548 20549 20550 20551 20552 20553 20554 20555 20556 20557 20558 20559 20560 20561 20562 20563 20564 20565 20566 20567 20568 20569 20570 ..... 21121 21122 21123 21124 21125 21126 21127 21128 21129 21130 21131 21132 21133 21134 21135 21136 21137 21138 21139 21140 21141 21142 21143 21144 21145 21146 21147 21148 21149 21150 21151 21152 21153 21154 21155 21156 21157 21158 21159 21160 21161 21162 21163 21164 21165 21166 21167 21168 21169 21170 21171 21172 21173 21174 21175 21176 21177 21178 21179 21180 21181 21182 21183 21184 21185 21186 21187 21188 21189 21190 21191 21192 21193 21194 21195 21196 21197 ..... 21220 21221 21222 21223 21224 21225 21226 21227 21228 21229 21230 21231 21232 21233 21234 21235 21236 21237 21238 21239 21240 21241 21242 21243 21244 21245 21246 21247 21248 21249 21250 21251 21252 21253 21254 21255 21256 21257 21258 21259 21260 21261 21262 21263 21264 21265 21266 21267 21268 21269 21270 21271 21272 21273 21274 21275 21276 21277 21278 21279 21280 21281 21282 21283 21284 21285 21286 21287 21288 21289 21290 21291 21292 21293 21294 21295 21296 21297 21298 21299 21300 21301 21302 21303 21304 21305 21306 21307 21308 21309 21310 21311 21312 21313 21314 21315 21316 21317 21318 21319 21320 21321 21322 21323 21324 21325 21326 21327 21328 21329 21330 21331 21332 21333 21334 21335 21336 21337 21338 21339 21340 21341 21342 21343 21344 21345 21346 21347 21348 21349 21350 21351 ..... 21402 21403 21404 21405 21406 21407 21408 21409 21410 21411 21412 21413 21414 21415 21416 21417 21418 21419 21420 21421 21422 21423 21424 21425 21426 21427 21428 21429 21430 21431 21432 ..... 21456 21457 21458 21459 21460 21461 21462 21463 21464 21465 21466 21467 21468 21469 21470 21471 21472 21473 21474 21475 21476 21477 21478 21479 21480 21481 21482 21483 21484 21485 21486 21487 21488 21489 21490 21491 21492 21493 21494 21495 21496 21497 21498 21499 21500 ..... 21503 21504 21505 21506 21507 21508 21509 21510 21511 21512 21513 21514 21515 21516 21517 21518 21519 21520 21521 21522 21523 21524 21525 21526 21527 21528 21529 21530 21531 21532 21533 21534 21535 21536 21537 21538 21539 21540 21541 21542 21543 21544 21545 21546 21547 21548 21549 21550 21551 21552 21553 21554 21555 21556 21557 21558 21559 21560 21561 21562 21563 21564 21565 21566 21567 21568 21569 21570 21571 21572 21573 21574 21575 21576 21577 21578 21579 21580 21581 21582 21583 21584 21585 21586 21587 21588 21589 21590 21591 21592 21593 21594 21595 21596 21597 21598 21599 21600 21601 21602 21603 21604 21605 ..... 21624 21625 21626 21627 21628 21629 21630 21631 21632 21633 21634 21635 21636 21637 21638 21639 21640 21641 21642 21643 21644 21645 21646 21647 21648 21649 21650 21651 21652 21653 21654 21655 21656 21657 21658 21659 21660 21661 21662 21663 21664 21665 21666 21667 21668 21669 21670 21671 21672 21673 21674 21675 21676 21677 21678 21679 21680 21681 21682 21683 21684 21685 21686 21687 21688 21689 21690 21691 21692 21693 21694 21695 21696 21697 21698 21699 21700 21701 21702 21703 21704 21705 21706 21707 21708 21709 21710 21711 21712 21713 21714 21715 21716 21717 21718 21719 21720 21721 21722 21723 21724 21725 21726 21727 21728 21729 21730 21731 21732 21733 21734 21735 21736 21737 21738 21739 21740 21741 21742 21743 21744 21745 21746 21747 21748 21749 21750 21751 21752 21753 21754 21755 21756 21757 21758 21759 ..... 21774 21775 21776 21777 21778 21779 21780 21781 21782 21783 21784 21785 21786 21787 21788 21789 21790 21791 21792 21793 21794 21795 21796 21797 21798 21799 21800 21801 21802 21803 21804 21805 21806 21807 21808 21809 21810 21811 21812 21813 21814 21815 21816 21817 21818 21819 21820 21821 21822 21823 21824 21825 21826 21827 21828 21829 21830 21831 21832 21833 21834 21835 21836 21837 21838 21839 21840 21841 21842 21843 21844 21845 21846 21847 21848 21849 21850 21851 21852 21853 21854 21855 21856 21857 21858 21859 21860 21861 21862 21863 21864 21865 21866 21867 21868 21869 21870 21871 21872 21873 21874 21875 21876 21877 21878 21879 21880 21881 21882 21883 21884 21885 21886 21887 21888 21889 21890 21891 21892 21893 21894 21895 21896 21897 21898 21899 21900 21901 21902 21903 21904 21905 21906 21907 21908 21909 21910 21911 21912 21913 21914 21915 21916 21917 21918 21919 21920 21921 21922 21923 21924 21925 21926 21927 21928 21929 21930 21931 21932 21933 21934 21935 21936 21937 21938 21939 21940 21941 21942 21943 21944 21945 21946 21947 21948 21949 21950 21951 21952 21953 21954 21955 21956 21957 21958 21959 21960 21961 21962 21963 21964 21965 21966 21967 21968 21969 21970 21971 21972 21973 21974 21975 21976 21977 21978 21979 21980 21981 21982 21983 21984 21985 21986 21987 21988 21989 21990 21991 21992 21993 21994 21995 21996 21997 21998 21999 22000 22001 22002 22003 22004 22005 22006 22007 22008 22009 22010 22011 22012 22013 22014 22015 22016 22017 22018 22019 22020 22021 22022 22023 22024 22025 22026 22027 22028 22029 22030 22031 22032 22033 22034 22035 22036 22037 22038 22039 22040 22041 ..... 22051 22052 22053 22054 22055 22056 22057 22058 22059 22060 22061 22062 22063 22064 22065 22066 22067 22068 22069 22070 22071 22072 22073 22074 22075 22076 22077 22078 22079 22080 22081 22082 22083 22084 22085 ..... 22144 22145 22146 22147 22148 22149 22150 22151 22152 22153 22154 22155 22156 22157 22158 22159 22160 ..... 22173 22174 22175 22176 22177 22178 22179 22180 22181 22182 22183 22184 22185 22186 22187 22188 22189 22190 22191 22192 22193 22194 22195 22196 22197 22198 22199 22200 22201 22202 22203 22204 22205 22206 22207 22208 22209 22210 ..... 22211 22212 22213 22214 22215 22216 22217 22218 22219 22220 22221 22222 22223 22224 22225 22226 ..... 22601 22602 22603 22604 22605 22606 22607 22608 22609 22610 22611 22612 22613 22614 22615 22616 22617 22618 22619 22620 22621 ..... 22623 22624 22625 22626 22627 22628 22629 22630 22631 22632 22633 22634 22635 22636 22637 22638 22639 22640 22641 22642 22643 22644 22645 22646 22647 ..... 22656 22657 22658 22659 22660 22661 22662 22663 22664 22665 22666 22667 22668 22669 ..... 22723 22724 22725 22726 22727 22728 22729 22730 22731 22732 22733 22734 22735 22736 22737 ..... 29257 29258 29259 29260 29261 29262 29263 29264 29265 29266 29267 29268 29269 29270 29271 29272 29273 29274 29275 29276 29277 29278 29279 ..... 29281 29282 29283 29284 29285 29286 29287 29288 29289 29290 29291 29292 29293 29294 29295 29296 29297 29298 29299 29300 29301 29302 29303 29304 29305 29306 29307 29308 29309 29310 29311 29312 29313 29314 29315 29316 29317 29318 29319 29320 29321 29322 29323 29324 29325 29326 29327 29328 29329 29330 29331 29332 29333 29334 29335 29336 29337 29338 29339 29340 29341 29342 29343 29344 29345 29346 29347 29348 |
indeed no implementation requirement to force a specific class type - added aUseBatchMode optional parameter to TSQLRecordMany.ManyDelete() method - now JSON parser will handle #1..' ' chars as whitespace (not only ' ') - now huge service JSON response is truncated (to default 20 KB) in logs Version 1.18 - renamed SQLite3Commons.pas to mORMot.pas - deep code refactoring, introducing TSQLPropInfo* classes in order to decouple the ORM definitions from the RTTI - will allow definition of any class members, even if there is no RTTI generated or via custom properties attributes or a fluent interface - introduced more readable "stored AS_UNIQUE" published property definition in TSQLRecord (via the explicit AS_UNIQUE=false constant) - introducing TInterfaceStub and TInterfaceMock classes to define high-performance interface stubbing and mocking via a fluent interface - integrated Windows Authentication to the mORMot Client-Server layer: in order to enable it, define a SSPIAUTH conditional and call TSQLRestClientURI.SetUser() with an empty user name, and ensure that TSQLAuthUser.LoginName contains a matching 'DomainName\UserName' value - added process of Variant and WideString types in TSQLRecord properties - added JSON serialization of Variant and WideString types in JSONToObject() / ObjectToJSON() functions and WriteObject method - TSQLRestClientURINamedPipe and TSQLRestClientURIMessage are now thread-safe (i.e. protected by a system mutex) therefore can be called from a multi-threaded process, e.g. TSQLRestServer instances as proxies - modified named pipe client side to avoid unexpected file not found errors ................................................................................ /// maximum handled dimension for TSQLRecordRTree // - this value is the one used by SQLite3 R-Tree virtual table RTREE_MAX_DIMENSION = 5; /// used as "stored AS_UNIQUE" published property definition in TSQLRecord AS_UNIQUE = false; type /// generic parent class of all custom Exception types of this unit EORMException = class(ESynException); /// exception raised in case of wrong Model definition EModelException = class(EORMException); ................................................................................ // - will return the specified associated TSynFilterOrValidate instance function AddFilterOrValidate(const aFieldName: RawUTF8; aFilter: TSynFilterOrValidate): TSynFilterOrValidate; overload; {$ifdef HASINLINE}inline;{$endif} end; TAuthSession = class; /// the available THTTP methods transmitted between client and server TSQLURIMethod = (mNone, mGET, mPOST, mPUT, mDELETE, mBEGIN, mEND, mABORT, mLOCK, mUNLOCK, mSTATE); /// used to store the current execution context of a remote request // - if RESTful authentication is enabled, it will be filled as expected TSQLRestServerSessionContext = record /// the corresponding session TAuthSession.IDCardinal value // - equals 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED) if the session // is not started yet - i.e. if still in handshaking phase // - equals 1 (CONST_AUTHENTICATION_NOT_USED) if authentication mode // is not enabled - i.e. if TSQLRest.HandleAuthentication = FALSE Session: cardinal; /// the corresponding TAuthSession.User.ID value // - is undefined if Session is 0 or 1 (no authentication running) User: integer; /// the corresponding TAuthSession.User.GroupRights.ID value // - is undefined if Session is 0 or 1 (no authentication running) Group: integer; /// the associated TSQLRecord.ID, as decoded from URI scheme // - this property will be set from incoming URI, even if RESTful // authentication is not enabled ID: integer; /// the used Client-Server method (matching the corresponding HTTP Verb) // - this property will be set from incoming URI, even if RESTful // authentication is not enabled Method: TSQLURIMethod; end; /// store all parameters for a TSQLRestServerCallBack event handler // - having a dedicated record avoid changing the implementation methods // signature if the framework add some parameters to this structure // - see TSQLRestServerCallBack for general code use TSQLRestServerCallBackParams = record /// the corresponding authentication session ID // - Context.Session=1 (CONST_AUTHENTICATION_NOT_USED) if authentication // mode is not enabled // - Context.Session=0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED) if the // session not started yet // - if authentication is enabled, Context.User and Context.Group can be // checked to perform security during service execution // - Context.ID is the record ID as specified at the URI level (if any) Context: TSQLRestServerSessionContext; /// the Table as specified at the URI level (if any) Table: TSQLRecordClass; /// the index in the Model of the Table specified at the URI level (if any) TableIndex: integer; /// the index of the callback published method within the internal class list MethodIndex: integer; /// URI inlined parameters // - use UrlDecodeValue*() functions to retrieve the values Parameters: PUTF8Char; /// the URI address, just before parameters // - can be either the table name (in RESTful protocol), or a service name URI: RawUTF8; /// the message body (e.g. HTTP body) as send by the client SentData: RawUTF8; /// the response to be sent back to the client Resp: RawUTF8; /// a pointer to the Header to be sent back to the client // - you can use this value e.g. to change the result mime-type Head: PRawUTF8; /// a pointer to an optional error message to be sent back to the client // - to be used to specify the numerical error message returned as // integer result of the TSQLRestServerCallBack function ErrorMsg: PRawUTF8; end; (*/ method prototype which must be used to implement the Server-Side ModelRoot/[TableName/ID/]MethodName RESTful GET/PUT request of the Framework - this mechanism is able to handle some custom Client/Server request, similar to the DataSnap technology, but in a KISS way; it's fully integrated in the Client/Server architecture of our framework - just add a published method of this type to any TSQLRestServer descendant - when TSQLRestServer.URI receive a request for ModelRoot/MethodName ................................................................................ retrieved with a loop like this: ! if not UrlDecodeNeedParameters(aParams.Parameters,'SORT,COUNT') then ! exit; ! while aParams.Parameters<>nil do begin ! UrlDecodeValue(aParams.Parameters,'SORT=',aSortString); ! UrlDecodeValueInteger(aParams.Parameters,'COUNT=',aCountInteger,@aParams.Parameters); ! end; - aParams.SentData is set with incoming data from the GET/PUT method - aParams.Context will identify to the authentication session of the remote client (CONST_AUTHENTICATION_NOT_USED=1 if authentication mode is not enabled or CONST_AUTHENTICATION_SESSION_NOT_STARTED=0 if the session not started yet) - code may use SessionGetUser() protected method to retrieve the user details - aParams.Context.Method will indicate the used HTTP verb (e.g. GET/POST/PUT..) - implementation must return the HTTP error code (e.g. 200/HTML_SUCCESS) as an integer value, and any response in aParams.Resp as a JSON object by default (using e.g. TSQLRestServer.JSONEncodeResult), since default mime-type is JSON_CONTENT_TYPE: $ {"result":"OneValue"} or a JSON object containing an array: $ {"result":["One","two"]} - implementation can return an optional HTTP header (useful to set the response mime-type - see e.g. the TEXT_CONTENT_TYPE_HEADER constant) in aParams.Head^ - implementation can return an optional error text in aParams.ErrorMsg^ in order to specify the HTTP error code with plain text (which will be sent as JSON error object into the client) - a typical implementation may be: ! function TSQLRestServerTest.Sum(var aParams: TSQLRestServerCallBackParams): Integer; ! var a,b: Extended; ! begin ! if not UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin ! result := HTML_NOTFOUND; // invalid Request ! aParams.ErrorMsg^ := 'Missing Parameter'; ! exit; ! end; ! while Params.Parameters<>nil do begin ! UrlDecodeExtended(aParams.Parameters,'A=',a); ! UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); ! end; ! aParams.Resp := JSONEncodeResult([a+b]); ! // same as : aParams.Resp := JSONEncode(['result',a+b]); ! result := HTML_SUCCESS; ! end; - Client-Side can be implemented as you wish. By convention, it could be appropriate to define in either TSQLRestServer (if to be called as ModelRoot/MethodName), either TSQLRecord (if to be called as ModelRoot/TableName/MethodName[/ID]) a custom public or protected method, calling TSQLRestClientURI.URL with the appropriate parameters, and named (by convention) as MethodName; TSQLRestClientURI has dedicated methods ................................................................................ like CallBackGetResult, CallBackGet, and CallBackPut; see also TSQLModel.getURICallBack and JSONDecode function ! function TSQLRecordPeople.Sum(aClient: TSQLRestClientURI; a, b: double): double; ! var err: integer; ! begin ! val(aClient.CallBackGetResult('sum',['a',a,'b',b]),result,err); ! end; *) TSQLRestServerCallBack = function(var aParams: TSQLRestServerCallBackParams): Integer of object; /// the possible options for handling table names TSQLCheckTableName = (ctnNoCheck,ctnMustExist,ctnTrimExisting); /// internal data used by TSQLRecord.FillPrepare()/FillPrepareMany() methods // - using a dedicated class will reduce memory usage for each TSQLRecord // instance (which won't need these properties most of the time) ................................................................................ // - on success, aResp shall contain a serialized JSON object with one // nested result property, which may be a JSON array, containing the // all "var" or "out" parameters values, and then the method main result - // for instance, ExecuteMethod(..,'[1,2]') over ICalculator.Add will return: // $ {"result":[3],"id":0} // the returned "id" number is the Instance identifier to be used for any later // sicClientDriven remote call - or just 0 in case of sicSingle or sicShared function ExecuteMethod(const aContext: TSQLRestServerSessionContext; aMethodIndex, aInstanceID: cardinal; aParamsJSONArray: PUTF8Char; var aResp, aHead, aErrorMsg: RawUTF8): cardinal; /// this method will create an implementation instance function CreateInstance: TInterfacedObject; public /// initialize the service provider on the server side // - expect an direct server-side implementation class (inheriting from // TInterfacedClass or from TInterfacedObjectWithCustomCreate if you need // an overriden constructor) ................................................................................ function InternalDelete(Table: TSQLRecordClass; const SQLWhere: RawUTF8; var IDs: TIntegerDynArray): boolean; /// wait for the transaction critical section to be acquired // - used to implement a thread-safe and transaction-safe write to the DB // - returns FALSE in case of time out (see AcquireWriteTimeOut property) // - returns TRUE if it's safe to write to the DB - in this case, you must // call ReleaseWrite when done to release the fTransactionCriticalSession function AcquireWrite(const aContext: TSQLRestServerSessionContext): Boolean; /// release the fTransactionCriticalSession procedure ReleaseWrite; /// retrieve the server time stamp // - default implementation will use fServerTimeStampOffset to compute // the value from PC time (i.e. Now+fServerTimeStampOffset as TTimeLog) // - inherited classes may override this method, or set the appropriate // value in fServerTimeStampOffset protected field ................................................................................ // - use the TSQLAuthGroup.AccessRights CSV format function ToString: RawUTF8; /// unserialize the content from TEXT // - use the TSQLAuthGroup.AccessRights CSV format procedure FromString(P: PUTF8Char); end; PSQLAccessRights = ^TSQLAccessRights; TSQLRestServerStatic = class; TSQLRestServerStaticClass = class of TSQLRestServerStatic; TSQLRestServerStaticInMemory = class; TSQLVirtualTableModule = class; {/ table containing the available user access rights for authentication ................................................................................ fLastAccess: cardinal; fID: RawUTF8; fIDCardinal: cardinal; fTimeOut: cardinal; fAccessRights: TSQLAccessRights; fPrivateKey: RawUTF8; fPrivateSalt: RawUTF8; fPrivateSaltHash: Cardinal; fLastTimeStamp: Cardinal; public /// initialize a session instance with the supplied TSQLAuthUser instance // - this aUser instance will be handled by the class until Destroy // - raise an exception on any error // - on success, will also retrieve the aUser.Data BLOB field content ................................................................................ /// the number of millisedons a session is kept alive // - extracted from User.TSQLAuthGroup.SessionTimeout // - allow direct comparison with GetTickCount API call property Timeout: cardinal read fTimeOut; /// the hexadecimal private key as returned to the connected client // as 'SessionID+PrivateKey' property PrivateKey: RawUTF8 read fPrivateKey; end; { we need the RTTI information to be compiled for the published methods of this TSQLRestServer class and its children (like TPersistent), to enable Server-Side ModelRoot/[TableName/[ID/]]MethodName requests -> see TSQLRestServerCallBack } ................................................................................ /// this method is overriden for setting the NoAJAXJSON field // of all associated TSQLRestServerStatic servers procedure SetNoAJAXJSON(const Value: boolean); virtual; /// search for the corresponding TSQLRestServerCallBack in its published methods, // then launch it // - the aParams parameters will be used to set a default header for the callback // - return TRUE if the method was found and run, FALSE if method was not found function LaunchCallBack(var aParams: TSQLRestServerCallBackParams; var aResp: RawUTF8; var aResult: Cardinal): boolean; /// try to call a Service from a given URI // - this method will call any interface-based service previously registered // via ServerRegister() // - returns TRUE if the supplied method was a service name, and an error // code is returned in aResult (with an optional message in aErrorMsg) // - is in fact used internaly by the URI method: you are not likely to call // this method, but should rather call e.g. Services['Calculator'].Get(I) // to retrieve a working service interface to work with function LaunchService(const aParams: TSQLRestServerCallBackParams; var aResp: RawUTF8; var aResult: cardinal): boolean; /// execute a BATCH sequence // - expect input as generated by TSQLRestClientURI.Batch*() methods: // & '{"Table":["cmd":values,...]}' // or, in a table-independent way: // & '["cmd@table":values,...]' // - returns an array of integers: '[200,200,...]' function RunBatch(aStatic: TSQLRestServerStatic; aTable: TSQLRecordClass; Sent: PUTF8Char; var Resp, ErrorMsg: RawUTF8): boolean; /// fill the supplied context from the supplied aContext.Session ID // - returns nil if not found, or fill aContext.User/Group values if matchs // - this method will also check for outdated sessions, and delete them // - this method is not thread-safe: caller should use fSessionCriticalSection function SessionAccess(var aContext: TSQLRestServerSessionContext): TAuthSession; /// delete a session from its index in fSessions[] // - will perform any needed clean-up, and log the event // - this method is not thread-safe: caller should use fSessionCriticalSection procedure SessionDelete(aSessionIndex: integer); /// returns a copy of the user associated to a session ID // - returns nil if the session does not exist (e.g. if authentication is // disabled) ................................................................................ {$ifdef MSWINDOWS} /// declare the server on the local machine as a Named Pipe: allows // TSQLRestClientURINamedPipe local or remote client connection // - ServerApplicationName ('DBSERVER' e.g.) will be used to create a named // pipe server identifier, it is of UnicodeString type since Delphi 2009 // (use of Unicode FileOpen() version) // - this server identifier is appended to '\\.\pipe\Sqlite3_' to obtain // the full pipe name to initiate ('\\.\pipe\Sqlite3_DBSERVER' e.g.) // - this server identifier may also contain a fully qualified path // ('\\.\pipe\ApplicationName' e.g.) // - allows only one ExportServer*() by running process // - returns true on success, false otherwize (ServerApplicationName already used?) function ExportServerNamedPipe(const ServerApplicationName: TFileName): boolean; /// end any currently initialized named pipe server function CloseServerNamedPipe: boolean; ................................................................................ // if you call it from the main thread, it may fail to release resources // - it is set e.g. by TSQLite3HttpServer to be called from HTTP threads, // or by TSQLRestServerNamedPipeResponse for named-pipe server cleaning procedure EndCurrentThread(Sender: TObject); virtual; /// implement a generic local, piped or HTTP/1.1 provider // - this is the main entry point of the server, from the client side // - some GET/POST/PUT JSON data can be specified in SentData // - return in result.Lo the HTTP STATUS integer error or success code: // 404/HTML_NOTFOUND e.g. if the url doesn't start with Model.Root (caller // can try another TSQLRestServer) // - return in result.Hi the database internal status // - store the data to be sent into Resp, some headers in Head // - default implementation calls protected methods EngineList() Retrieve() // Add() Update() Delete() UnLock() EngineExecute() above, which must be overriden by // the TSQLRestServer child // - see TSQLRestClient to check how data is expected in our RESTful format // - AccessRights must be handled by the TSQLRestServer child, according // to the Application Security Policy (user logging, authentification and // rights management) - making access rights a parameter allows this method // to be handled as pure stateless, thread-safe and session-free // - handle enhanced REST codes: LOCK/UNLOCK/BEGIN/END/ABORT // - for 'GET ModelRoot/TableName', url parameters can be either "select" and // "where" (to specify a SQL Query, from the SQLFromSelectWhere function), // either "sort", "dir", "startIndex", "results", as expected by the YUI // DataSource Request Syntax for data pagination - see // http://developer.yahoo.com/yui/datatable/#data function URI(const url, method, SentData: RawUTF8; out Resp, Head: RawUTF8; RestAccessRights: PSQLAccessRights): Int64Rec; virtual; /// create an index for the specific FieldName // - will call CreateSQLMultiIndex() internaly function CreateSQLIndex(Table: TSQLRecordClass; const FieldName: RawUTF8; Unique: boolean; const IndexName: RawUTF8=''): boolean; overload; /// create one or multiple index(es) for the specific FieldName(s) function CreateSQLIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean): boolean; overload; /// create one index for all specific FieldNames at once function CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8=''): boolean; virtual; /// encode some value into a JSON "result":"value" UTF-8 encoded content // - wrapper around standard JSONEncode() function // - OneValue usually have only ONE parameter, e.g.: // ! result := JSONEncodeResult([Value]); // returned as '"result":100' if Value=100 // - if OneValue has more than one parameter, returns a JSON array of values // like '"result":["value1",value2]' // - this method will work outside of a true TSQLRestServer instance: you // can use e.g. // ! TSQLRestServer.JSONEncodeResult(['value1',10]) class function JSONEncodeResult(const OneValue: array of const): RawUTF8; /// call this method to disable Authentication method check for a given // published method name // - by default, only Auth and TimeStamp methods do not require the RESTful // authentication of the URI; you may call this method to add another method // to the list (e.g. for returning some HTML content from a public URI) procedure ServiceMethodByPassAuthentication(const aMethodName: RawUTF8); ................................................................................ // (should not to be used normaly, because it will add unnecessary overhead) property StaticVirtualTableDirect: boolean read fVirtualTableDirect write fVirtualTableDirect; published /// this method will be accessible from ModelRoot/Stat URI, and // will retrieve some statistics as a JSON object // - method parameters signature matches TSQLRestServerCallBack type function Stat(var aParams: TSQLRestServerCallBackParams): Integer; /// this method will be accessible from ModelRoot/Auth URI, and // will be called by the client for authentication and session management // - method parameters signature matches TSQLRestServerCallBack type // - to be called in a two pass "challenging" algorithm: // $ GET ModelRoot/auth?UserName=... // $ -> returns an hexadecimal nonce contents (valid for 5 minutes) // $ GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... ................................................................................ // user which opened the session via a successful call to the Auth service // - when you don't need the session any more (e.g. if the TSQLRestClientURI // instance is destroyed), you can call the service as such: // $ GET ModelRoot/auth?UserName=...&Session=... // - for a way of computing SHA-256 in JavaScript, see for instance // @http://www.webtoolkit.info/javascript-sha256.html // - this global callback method is thread-safe function Auth(var aParams: TSQLRestServerCallBackParams): Integer; /// this method will be accessible from the ModelRoot/TimeStamp URI, and // will return the server time stamp TTimeLog/Int64 value as RawUTF8 // - method parameters signature matches TSQLRestServerCallBack type function TimeStamp(var aParams: TSQLRestServerCallBackParams): Integer; /// this method will be accessible from the ModelRoot/CacheFlush URI, and // will flush the server cache // - this method shall be called by the clients when the Server cache could // be not refreshed // - ModelRoot/CacheFlush URI will flush the whole Server cache, for all tables // - ModelRoot/CacheFlush/TableName URI will flush the specified table cache // - ModelRoot/CacheFlush/TableName/ID URI will flush the content of the // specified record // - method parameters signature matches TSQLRestServerCallBack type function CacheFlush(var aParams: TSQLRestServerCallBackParams): Integer; /// this method will be accessible from the ModelRoot/Batch URI, and // will execute a set of RESTful commands // - expect input as JSON commands - see TSQLRestServer.RunBatch, i.e. // & '{"Table":["cmd":values,...]}' // or for multiple tables: // & '["cmd@Table":values,...]' // with cmd in POST/PUT with {object} as value or DELETE with ID // - only accepted context HTTP verb is PUT (for thread-safe and security // reasons) function Batch(var aParams: TSQLRestServerCallBackParams): Integer; end; /// REST server with direct access to an external database engine // - you can set an alternate per-table database engine by using this class // - this abstract class is to be overriden with a proper implementation (like // our TSQLRestServerStaticInMemory class) TSQLRestServerStatic = class(TSQLRestServer) ................................................................................ end; /// Rest client with remote access to a server through a Named Pipe // - named pipe is fast and optimized under Windows // - can be accessed localy or remotely TSQLRestClientURINamedPipe = class(TSQLRestClientURI) private /// handle for '\\.\pipe\Test_Local_Sqlite3' e.g. fServerPipe: cardinal; /// the pipe name fPipeName: TFileName; {$ifndef ANONYMOUSNAMEDPIPE} {$ifndef NOSECURITYFORNAMEDPIPECLIENTS} fPipeSecurityAttributes: TSecurityAttributes; fPipeSecurityDescriptor: array[0..SECURITY_DESCRIPTOR_MIN_LENGTH] of byte; ................................................................................ public /// connect to a server contained in a running application // - the server must have been declared by a previous // TSQLRestServer.ExportServer(ApplicationName) call // with ApplicationName as user-defined server identifier ('DBSERVER' e.g.) // - ApplicationName is of UnicodeString type since Delphi 2009 // (direct use of Wide Win32 API version) // - this server identifier is appended to '\\.\pipe\Sqlite3_' to obtain // the full pipe name to connect to ('\\.\pipe\Sqlite3_DBSERVER' e.g.) // - this server identifier may also contain a remote computer name, and // must be fully qualified ('\\ServerName\pipe\ApplicationName' e.g.) // - raise an exception if the server is not running or invalid constructor Create(aModel: TSQLModel; const ApplicationName: TFileName); /// release memory and handles destructor Destroy; override; end; ................................................................................ // and is handled in TSQLRibbon.RefreshClickHandled WM_TIMER_REFRESH_SCREEN = 1; /// timer identifier which indicates we must refresh the Report content // - used for User Interface generation // - is handled in TSQLRibbon.RefreshClickHandled WM_TIMER_REFRESH_REPORT = 2; /// HTML Status Code for "Success" HTML_SUCCESS = 200; /// HTML Status Code for "Created" HTML_CREATED = 201; /// HTML Status Code for "Bad Request" HTML_BADREQUEST = 400; /// HTML Status Code for "Forbidden" HTML_FORBIDDEN = 403; /// HTML Status Code for "Not Found" HTML_NOTFOUND = 404; /// HTML Status Code for "Request Time-out" HTML_TIMEOUT = 408; /// HTML Status Code for "Not Implemented" HTML_NOTIMPLEMENTED = 501; /// HTML Status Code for "Service Unavailable" HTML_UNAVAILABLE = 503; /// create a TRecordReference with the corresponding parameters function RecordReference(Model: TSQLModel; aTable: TSQLRecordClass; aID: integer): TRecordReference; /// convert a dynamic array of TRecordRef into its corresponding IDs procedure RecordRefToID(var aArray: TIntegerDynArray); ................................................................................ /// the currently running service factory // - it can be used within server-side implementation to retrieve the // associated TSQLRestServer instance Factory: TServiceFactoryServer; /// the currently runnning session identifier which launched the method // - make available the current session or authentication parameters // (including e.g. user details via Factory.RestServer.SessionGetUser) Session: ^TSQLRestServerSessionContext; /// the thread which launched the request // - is set by TSQLRestServer.BeginCurrentThread from multi-thread server // handlers - e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse RunningThread: TThread; end; threadvar ................................................................................ WhereClause := FormatUTF8('% MATCH :(''%''): ORDER BY rank(matchinfo(%)', [SQLTableName,MatchClause,SQLTableName]); for i := 0 to high(PerFieldWeight) do WhereClause := FormatUTF8('%,:(%):',[WhereClause,PerFieldWeight[i]]); result := FTSMatch(Table,WhereClause+') DESC',DocID); end; function TSQLRest.AcquireWrite(const aContext: TSQLRestServerSessionContext): Boolean; var Start, Now: Cardinal; begin if self<>nil then begin Start := GetTickCount; repeat if TryEnterCriticalSection(fTransactionCriticalSession) then begin if (fTransactionActive=0) or (fTransactionActive=aContext.Session) then begin ................................................................................ function TSQLRestClientURI.EngineUpdateField(Table: TSQLRecordClass; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; begin if (self=nil) or (Table=nil) then result := false else // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=.. result := URI(FormatUTF8('%?setname=%&set=%&wherename=%&where=%', [Model.URI[Table],SetFieldName,UrlEncode(SetValue),WhereFieldName,UrlEncode(WhereValue)]), 'PUT').Lo=HTML_SUCCESS; end; { TSQLRestServer } {$ifdef MSWINDOWS} const ServerPipeNamePrefix: TFileName = '\\.\pipe\mORMot_'; var GlobalURIRequestServer: TSQLRestServer = nil; function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl; function StringToPChar(const s: RawUTF8): PUTF8Char; var L: integer; begin L := length(s); if L=0 then result := nil else begin inc(L); // copy also last #0 from s if USEFASTMM4ALLOC then GetMem(result,L) else result := pointer(GlobalAlloc(GMEM_FIXED,L)); move(pointer(s)^,result^,L); end; end; var sResp, sHead: RawUTF8; begin if GlobalURIRequestServer=nil then begin Int64(result) := HTML_NOTIMPLEMENTED; // 501 exit; end; result := GlobalURIRequestServer.URI(url,method,SendData,sResp,sHead, @SUPERVISOR_ACCESS_RIGHTS); if Resp<>nil then Resp^ := StringToPChar(sResp); // make result copy as PAnsiChar if Head<>nil then Head^ := StringToPChar(sHead); end; function ReadString(Handle: cardinal): RawUTF8; var L, Read: cardinal; P: PUTF8Char; begin result := ''; ................................................................................ result := true; end; const MAGIC_SYN: cardinal = $A5ABA5AB; procedure TSQLRestServer.AnswerToMessage(var Msg: TWMCopyData); var url, method: RawUTF8; SentData: RawUTF8; Resp, Head, ResStr: RawUTF8; P: PUTF8Char; Res: record Magic: cardinal; LoHi: Int64Rec; end; Data: TCopyDataStruct; begin Msg.Result := HTML_NOTFOUND; if (self=nil) or (Msg.From=0) then exit; P := Msg.CopyDataStruct^.lpData; if (P=nil) or (Msg.CopyDataStruct^.cbData<=7) then exit; if PCardinal(P)^<>MAGIC_SYN then exit; // invalid layout: a broadcasted WM_COPYDATA message? :( inc(P,4); // #1 is a field delimiter below, since Get*Item() functions return nil for #0 Msg.Result := HTML_SUCCESS; // Send something back url := GetNextItem(P,#1); method := GetNextItem(P,#1); Head := GetNextItem(P,#1); SetString(SentData,P, PtrInt(Msg.CopyDataStruct^.cbData)-(P-Msg.CopyDataStruct^.lpData)); Res.Magic := MAGIC_SYN; Res.LoHi := URI(url,method,SentData,Resp,Head,@SUPERVISOR_ACCESS_RIGHTS); // note: it's up to URI overriden method to implement access rights SetString(ResStr,PAnsiChar(@Res),sizeof(Res)); Resp := ResStr+Head+#1+Resp; Data.dwData := fServerWindow; Data.cbData := length(Resp); Data.lpData := pointer(Resp); { TODO : 64-bits windows: can we store a PtrInt value in WM_COPYDATA? } SendMessage(Msg.From,WM_COPYDATA,fServerWindow,PtrInt(@Data)); end; function TSQLRestServer.CloseServerNamedPipe: boolean; begin if fExportServerNamedPipeThread<>nil then begin ................................................................................ begin result := true; for i := 0 to high(FieldNames) do if not CreateSQLMultiIndex(Table,[FieldNames[i]],Unique) then result := false; end; class function TSQLRestServer.JSONEncodeResult(const OneValue: array of const): RawUTF8; var W: TTextWriter; i,h: integer; begin result := ''; h := high(OneValue); if h<0 then exit; W := TTextWriter.CreateOwnedStream; try W.AddShort('{"result":'); if h=0 then // result is one value W.AddJSONEscape(OneValue[0]) else begin // result is one array of values W.Add('['); i := 0; repeat W.AddJSONEscape(OneValue[i]); if i=h then break; W.Add(','); inc(i); until false; W.Add(']'); end; W.Add('}'); W.SetText(result); finally W.Free; end; end; procedure TSQLRestServer.ServiceMethodByPassAuthentication(const aMethodName: RawUTF8); var i: Integer; begin if self=nil then exit; i := fPublishedMethods.Find(aMethodName); if i>=0 then AddInteger(fPublishedMethodsUnauthenticated,fPublishedMethodsUnauthenticatedCount,i,True); end; function TSQLRestServer.LaunchCallBack(var aParams: TSQLRestServerCallBackParams; var aResp: RawUTF8; var aResult: Cardinal): boolean; var Method: TMethod; Invoke: TSQLRestServerCallBack absolute Method; begin result := False; if (aParams.MethodIndex<0) or (self=nil) then exit; Method.Code := pointer(fPublishedMethods.List[aParams.MethodIndex].Tag); // launch the method found {$ifdef WITHLOG} SQLite3Log.Enter(self,pointer(fPublishedMethods.List[aParams.MethodIndex].Name)); {$endif} try Method.Data := Self; aResult := Invoke(aParams); aResp := aParams.Resp; inc(fStats.fServices); result := true; // mark method found and execution OK except on Exception do; // ignore any exception (return false) end; end; type TServiceInternalMethod = (imFree, imContract, imSignature); const ................................................................................ exit; if fServices=nil then fServices := TServiceContainerServer.Create(self); result := (fServices as TServiceContainerServer).AddInterface( aInterfaces,aInstanceCreation,aContractExpected); end; function TSQLRestServer.LaunchService(const aParams: TSQLRestServerCallBackParams; var aResp: RawUTF8; var aResult: cardinal): boolean; var Service: TServiceFactory; method, JSON: RawUTF8; Values: TPUtf8CharDynArray; ServiceParams: PUTF8Char; i, m, ServiceID: integer; internal: TServiceInternalMethod; begin result := false; if Services=nil then exit; // 1. retrieve request parameters according to routing scheme ServiceID := aParams.Context.ID; case ServicesRouting of rmRest: begin i := Services.fListInterfaceMethod.IndexOf(aParams.URI); if i<0 then exit; // no specific message: it may be a valid request {$ifdef WITHLOG} SQLite3Log.Enter(self,pointer(aParams.URI)); {$endif} i := PtrInt(Services.fListInterfaceMethod.Objects[i]); m := i shr 16; Service := Services.Index(i and $ffff); if aParams.SentData<>'' then // parameters sent as JSON array (the Delphi/AJAX way) ServiceParams := pointer(aParams.SentData) else begin JSON := UrlDecode(aParams.Parameters); // optional URI decoding (the HTML way) ServiceParams := pointer(JSON); end; if ServiceID<0 then ServiceID := 0; end; rmJSON_RPC: begin Service := Services[aParams.URI]; SetString(JSON,PAnsiChar(pointer(aParams.SentData)),length(aParams.SentData)); JSONDecode(JSON,['method','params','id'],Values,True); if Values[0]=nil then begin aParams.ErrorMsg^ := 'Method name required'; exit; end; method := Values[0]; ServiceParams := Values[1]; ServiceID := GetCardinal(Values[2]); m := Service.fInterface.FindMethodIndex(method); if m>=0 then inc(m,length(SERVICE_PSEUDO_METHOD)) else begin for internal := low(TServiceInternalMethod) to high(TServiceInternalMethod) do if IdemPropNameU(method,SERVICE_PSEUDO_METHOD[internal]) then begin m := ord(internal); break; end; if m<0 then begin aParams.ErrorMsg^ := 'Unknown method'; exit; end; end; {$ifdef WITHLOG} SQLite3Log.Enter(self,pointer(aParams.URI+'.'+method)); {$endif} end; else Exit; end; // 2. handle request if ServiceParams=nil then begin aParams.ErrorMsg^ := 'Parameters required'; exit; end; if Service=nil then exit; inc(fStats.fServices); case m of ord(imFree): // "method":"_free_" to release sicClientDriven..sicPerGroup if ServiceID<=0 then // expects an instance ID to be released exit else m := -1; // notify ExecuteMethod() to release the internal instance ord(imContract): begin // "method":"_contract_" to retrieve the implementation contract aResult := HTML_SUCCESS; // OK aResp := '{"result":['+Service.ContractExpected+'],"id":0}'; result := true; exit; // "id":0 for this method -> no instance was created end; ord(imSignature): begin // "method":"_signature_" to retrieve the implementation signature if TServiceContainerServer(Services).PublishSignature then begin aResult := HTML_SUCCESS; // OK aResp := '{"result":['+Service.Contract+'],"id":0}'; result := true; // "id":0 for this method -> no instance was created end; exit; // not allowed to publish signature end; else dec(m,length(SERVICE_PSEUDO_METHOD)); // index of operation in fMethods[] end; if (aParams.Context.Session>CONST_AUTHENTICATION_NOT_USED) and (m>=0) and (aParams.Context.Group-1 in Service.fExecution[m].Denied) then begin aParams.ErrorMsg^ := 'Unauthorized method'; exit; end; aResult := TServiceFactoryServer(Service).ExecuteMethod( aParams.Context,m,ServiceID,ServiceParams,aResp,aParams.Head^,aParams.ErrorMsg^); result := true; // notify method found (any error status is in aResult) end; function TSQLRestServer.RunBatch(aStatic: TSQLRestServerStatic; aTable: TSQLRecordClass; Sent: PUTF8Char; var Resp, ErrorMsg: RawUTF8): boolean; var EndOfObject: AnsiChar; wasString, OK: boolean; TableName, Value: RawUTF8; URIMethod, RunningBatchURIMethod: TSQLURIMethod; RunningBatchStatic: TSQLRestServerStatic; { TODO: allow nested batch between tables? } Method, MethodTable: PUTF8Char; Props: TSQLRecordProperties; i, ID, Count: integer; Results: TIntegerDynArray; RunTable: TSQLRecordClass; RunStatic: TSQLRestServerStatic; begin result := false; if (self=nil) or (Sent=nil) then exit; if aTable<>nil then begin // unserialize expected sequence array as '{"Table":["cmd":values,...]}' while Sent^<>'{' do inc(Sent); if Sent^<>'{' then exit; ................................................................................ RunningBatchURIMethod := URIMethod; end; // process CRUD method operation case URIMethod of mDELETE: begin // '{"Table":[...,"DELETE":ID,...]}' or '[...,"DELETE@Table":ID,...]' ID := GetInteger(GetJSONField(Sent,Sent,@wasString,@EndOfObject)); if (ID<=0) or wasString or not RecordCanBeUpdated(RunTable,ID,seDelete,@ErrorMsg) then exit; if RunStatic<>nil then OK := RunStatic.EngineDelete(RunTable,ID) else OK := EngineDelete(RunTable,ID); if OK then begin fCache.NotifyDeletion(RunTable,ID); if (RunningBatchStatic<>nil) or AfterDeleteForceCoherency(RunTable,ID) then Results[Count] := HTML_SUCCESS; // 200 OK end; end; mPOST: begin // '{"Table":[...,"POST":{object},...]}' or '[...,"POST@Table":{object},...]' Value := JSONGetObject(Sent,nil,EndOfObject); if (Sent=nil) or not RecordCanBeUpdated(RunTable,0,seAdd,@ErrorMsg) then exit; if RunStatic<>nil then ID := RunStatic.EngineAdd(RunTable,Value) else ID := EngineAdd(RunTable,Value); Results[Count] := ID; fCache.Notify(RunTable,ID,Value,soInsert); end; mPUT: begin // '{"Table":[...,"PUT":{object},...]}' or '[...,"PUT@Table":{object},...]' ................................................................................ while Sent^<>'}' do inc(Sent); result := Sent^='}'; end else result := true; // send back operation status array for i := 0 to Count-1 do if Results[i]<>HTML_SUCCESS then begin Resp := IntegerDynArrayToCSV(Results,Count,'[',']'); exit; end; Resp := '["OK"]'; // to save bandwith if no adding end; procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8); begin case Code of 100: result := 'Continue'; HTML_SUCCESS: result := 'OK'; HTML_CREATED: result := 'Created'; 202: result := 'Accepted'; 203: result := 'Non-Authoritative Information'; 204: result := 'No Content'; 300: result := 'Multiple Choices'; 301: result := 'Moved Permanently'; 302: result := 'Found'; 303: result := 'See Other'; 304: result := 'Not Modified'; 307: result := 'Temporary Redirect'; HTML_BADREQUEST: result := 'Bad Request'; 401: result := 'Unauthorized'; HTML_FORBIDDEN: result := 'Forbidden'; HTML_NOTFOUND: result := 'Not Found'; 405: result := 'Method Not Allowed'; 406: result := 'Not Acceptable'; 500: result := 'Internal Server Error'; HTML_UNAVAILABLE:result := 'Service Unavailable'; else result := 'Invalid Request'; end; end; function StringToMethod(const method: RawUTF8): TSQLURIMethod; const NAME: array[mGET..high(TSQLURIMethod)] of string[7] = ( 'GET','POST','PUT','DELETE','BEGIN','END','ABORT','LOCK','UNLOCK','STATE'); var URIMethodUp: string[7]; begin ................................................................................ for result := low(NAME) to high(NAME) do if URIMethodUp=NAME[result] then exit; end; result := mNone; end; function TSQLRestServer.URI(const url, method, SentData: RawUTF8; out Resp, Head: RawUTF8; RestAccessRights: PSQLAccessRights): Int64Rec; var BlobFieldName: RawUTF8; Static: TSQLRestServerStatic; Engine: TSQLRestServer; i,j,L: PtrInt; SQLSelect, SQLWhere, SQLSort, SQLDir, SQL, ErrorMsg: RawUTF8; SQLStartIndex, SQLResults: integer; StaticKind: (sNone, sInMemory, sVirtual); SQLisSelect, OK: boolean; URI: TSQLRestServerCallBackParams; Session: TAuthSession; SessionAccessRights: TSQLAccessRights; // session may be deleted meanwhile P: PUTF8Char; Blob: PPropInfo; {$ifdef WITHSTATPROCESS} timeStart,timeEnd: Int64; {$endif} {$ifdef WITHLOG} Log: ISynLog; {$endif} procedure SetRespFromError; begin inc(fStats.fInvalid); if ErrorMsg='' then StatusCodeToErrorMsg(result.Lo,ErrorMsg); {$ifdef WITHLOG} Log.Log(sllServer,'% % ERROR=% (%)',[method,url,result.Lo,ErrorMsg],self); {$endif} if Resp='' then // return error content as JSON object with TTextWriter.CreateOwnedStream do try AddShort('{'#13#10'"ErrorCode":'); Add(result.Lo); AddShort(#13#10'"ErrorText":"'); AddJSONEscape(pointer(ErrorMsg)); AddShort('"'#13#10'}'); SetText(resp); finally Free; end; end; begin {$ifdef WITHLOG} Log := SQLite3Log.Enter; {$endif} // 0. always return internal database state count (even if URI is '') {$ifdef WITHSTATPROCESS} QueryPerformanceCounter(timeStart); {$endif} result.Hi := InternalState; // get first, since other threads may change it L := length(url); inc(fStats.fIncomingBytes,L+length(method)+length(SentData)+12); // 1. retrieve URI expecting 'ModelRoot[/TableName[/ID[/BlobFieldName]]]' format i := 0; if (url<>'') and (url[1]='/') then inc(i); // URL may be '/path' j := length(Model.Root); if (i+j>L) or (not(url[i+j+1] in [#0,'/','?'])) or (StrCompIL(pointer(PtrInt(url)+i),pointer(Model.Root),j,0)<>0) then begin result.Lo := HTML_NOTFOUND; // server does not match the Request-URI ErrorMsg := 'Invalid Root'; SetRespFromError; exit; // bad ModelRoot -> caller can try another TSQLRestServer end; result.Lo := HTML_BADREQUEST; // default error code is 400 BAD REQUEST URI.URI := copy(url,j+i+2,maxInt); i := PosEx(RawUTF8('/'),URI.URI,1); if i>0 then begin URI.Parameters := @URI.URI[i+1]; URI.Context.ID := GetNextItemCardinal(URI.Parameters,'/'); if (URI.Context.ID>0) and (URI.Parameters<>nil) then begin // for URL like "ModelRoot/TableName/ID/BlobFieldName" P := PosChar(URI.Parameters,'?'); if P=nil then BlobFieldName := URI.Parameters else SetString(BlobFieldName,PAnsiChar(URI.Parameters),P-URI.Parameters); end; SetLength(URI.URI,i-1); j := PosEx(RawUTF8('?'),url,1); if j>0 then // '?select=...&where=...' or '?where=...' URI.Parameters := @url[j+1] else URI.Parameters := nil; end else begin URI.Context.ID := -1; i := PosEx(RawUTF8('?'),url,1); if i>0 then begin // '?select=...&where=...' or '?where=...' URI.Parameters := @url[i+1]; i := PosEx(RawUTF8('?'),URI.URI); if i>0 then dec(i); SetLength(URI.URI,i); end else URI.Parameters := nil; // no parameter end; ................................................................................ end; if Static<>nil then Engine := Static; end; // 2. handle security Session := nil; if HandleAuthentication then begin URI.Context.Session := CONST_AUTHENTICATION_SESSION_NOT_STARTED; // check session_signature=... parameter if URI.Parameters<>nil then begin // expected format is 'session_signature='Hexa8(SessionID)+Hexa8(TimeStamp)+ // Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+ // Hexa8(TimeStamp)+url)) i := L-(17+24); if (i>0) and // should be LAST parameter in URL IdemPChar(@url[i],'SESSION_SIGNATURE=') and HexDisplayToCardinal(@url[i+18],URI.Context.Session) then begin EnterCriticalSection(fSessionCriticalSection); try Session := SessionAccess(URI.Context); if Session.IsValidURL(url,i-2) then begin // supplied RestAccessRights is ignored and replaced by the user rights {$ifdef WITHLOG} Log.Log(sllUserAuth,'%/%',[Session.User.LogonName,URI.Context.Session],self); {$endif} move(Session.fAccessRights,SessionAccessRights,sizeof(TSQLAccessRights)); RestAccessRights := @SessionAccessRights; end else // mark invalid query authentication Session := nil; finally LeaveCriticalSection(fSessionCriticalSection); end; end; end; if (Session=nil) and ((URI.MethodIndex<0) or not IntegerScanExists(pointer(fPublishedMethodsUnauthenticated), fPublishedMethodsUnauthenticatedCount,URI.MethodIndex)) then begin // /auth + /timestamp are e.g. allowed services without signature result.Lo := HTML_FORBIDDEN; // 403 in case of authentication failure // 401 Unauthorized response MUST include a WWW-Authenticate header, // which is not what we used, so we won't send 401 error code but 403 SetRespFromError; exit; // authentication error -> caller can try to open another session end; end else begin // default unique session if authentication is not enabled URI.Context.Session := CONST_AUTHENTICATION_NOT_USED; URI.Context.User := 0; URI.Context.Group := 0; end; // 3. call appropriate database commands URI.Context.Method := StringToMethod(method); URI.SentData := SentData; URI.Head := @Head; URI.ErrorMsg := @ErrorMsg; with URI.Context do case Method of mLOCK,mGET: begin if URI.Table=nil then begin if (Method<>mLOCK) and // GET ModelRoot/MethodName + parameters sent in URI ((URI.MethodIndex<0) or not LaunchCallBack(URI,Resp,result.Lo)) then if (URI.URI='') or not (reService in RestAccessRights^.AllowRemoteExecute) or // GET ModelRoot/Service.Method[/ID] + parameters sent as JSON or in URI not LaunchService(URI,Resp,result.Lo) then begin if (SentData='') and (URI.Parameters<>nil) and (reUrlEncodedSQL in RestAccessRights^.AllowRemoteExecute) then begin // GET with a SQL statement sent in URI, as sql=.... while not UrlDecodeValue(URI.Parameters,'SQL=',SQL,@URI.Parameters) do if URI.Parameters=nil then break; end else // GET with a SQL statement sent as UTF-8 body SQL := SentData; SQLisSelect := isSelect(pointer(SQL)); if (SQL<>'') and (SQLisSelect or (reSQL in RestAccessRights^.AllowRemoteExecute)) then begin // no user check for SELECT: see TSQLAccessRights.GET comment Static := InternalAdaptSQL(Model.GetTableIndexFromSQLSelect(SQL,false),SQL); if Static<>nil then Engine := Static; Resp := Engine.EngineList(SQL); // security note: only first statement is run by EngineList() if Resp<>'' then begin // got JSON list '[{...}]' ? result.Lo := HTML_SUCCESS; // 200 OK if not SQLisSelect then inc(fStats.fModified); end; end; end; end else // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1] if not (URI.TableIndex in RestAccessRights^.GET) then // check User Access result.Lo := HTML_FORBIDDEN else begin if ID>0 then begin // GET ModelRoot/TableName/ID[/BlobFieldName] to retrieve one member, // with or w/out locking, or a specified BLOB field content if Method=mLOCK then // LOCK is to be followed by PUT -> check user if not (URI.TableIndex in RestAccessRights^.PUT) then result.Lo := HTML_FORBIDDEN else if Model.Lock(URI.TableIndex,ID) then Method := mGET; // mark successfully locked if Method<>mLOCK then if BlobFieldName<>'' then begin // GET ModelRoot/TableName/ID/BlobFieldName: retrieve BLOB field content Blob := URI.Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName); if Blob<>nil then begin if Engine.EngineRetrieveBlob(URI.Table,ID,Blob,TSQLRawBlob(Resp)) then begin result.Lo := HTML_SUCCESS; // 200 OK Head := HEADER_CONTENT_TYPE+GetMimeContentType(pointer(Resp),Length(Resp)); end; end else begin // GET ModelRoot/TableName/ID/MethodName: try MethodName URI.MethodIndex := fPublishedMethods.Find(BlobFieldName); if URI.MethodIndex>=0 then LaunchCallBack(URI,Resp,result.Lo); end; end else begin // GET ModelRoot/TableName/ID: retrieve a member content, JSON encoded Resp := fCache.Retrieve(URI.TableIndex,ID); if Resp='' then begin Resp := Engine.EngineRetrieve(URI.TableIndex,ID); // get JSON object '{...}' fCache.Notify(URI.TableIndex,ID,Resp,soSelect); // cache if expected end; if Resp<>'' then // if something was found result.Lo := HTML_SUCCESS; // 200 OK end; end else // ModelRoot/TableName with 'select=..&where=' or YUI paging if Method<>mLOCK then begin // LOCK not available here SQLSelect := 'RowID'; // if no select is specified (i.e. ModelRoot/TableName) // all IDs of this table are returned to the client SQLWhere := ''; if URI.Parameters<>nil then begin // '?select=...&where=...' or '?where=...' SQLStartIndex := 0; SQLResults := 0; if URI.Parameters^<>#0 then ................................................................................ end; if (SQLResults<>0) and not ContainsUTF8(pointer(SQLWhere),'LIMIT ') then SQLWhere := FormatUTF8('% LIMIT % OFFSET %',[SQLWhere,SQLResults,SQLStartIndex]); end; SQL := Model.Props[URI.Table].SQLFromSelectWhere(SQLSelect,trim(SQLWhere)); if (Static<>nil) and (StaticKind=sInMemory) then // manual retrieval (no SQLite3 module available for fStaticData[]) Resp := Static.EngineList(SQL) else // TSQLVirtualTableJSON/External will rely on their SQLite3 module Resp := EngineList(SQL); if Resp<>'' then // got JSON list '[{...}]' ? result.Lo := HTML_SUCCESS; // 200 OK end; end; end; mUNLOCK: begin // ModelRoot/TableName/ID to unlock a member if not (URI.TableIndex in RestAccessRights^.PUT) then result.Lo := HTML_FORBIDDEN else if (URI.Table<>nil) and (ID>0) and Model.UnLock(URI.Table,ID) then result.Lo := HTML_SUCCESS; // 200 OK end; mSTATE: begin // STATE method for TSQLRestClientURI.ServerInternalState // this method is called with Root (-> Table=nil -> Static=nil) // we need a specialized method in order to avoid fStats.Invalid increase result.Lo := HTML_SUCCESS; for i := 0 to high(fStaticData) do if fStaticData[i]<>nil then if fStaticData[i].RefreshedAndModified then begin inc(InternalState); // force refresh break; end; end else // write methods (mPOST, mPUT, mDELETE...) are handled separately if (URI.Table<>nil) or (Method<>mPOST) or // check thread-safe call of service not (reService in RestAccessRights^.AllowRemoteExecute) or // POST ModelRoot/Service.Method[/ID] + parameters sent as JSON or in URI not LaunchService(URI,Resp,result.Lo) then // now we have to handle a write to the DB (POST/PUT/DELETE...) if AcquireWrite(URI.Context) then // make it thread-safe and transaction-safe try case Method of mPOST: begin // POST=ADD=INSERT if URI.Table=nil then begin // ModelRoot with free SQL statement sent as UTF-8 (only for Admin group) // security note: multiple SQL statements can be run in EngineExecuteAll() if (reSQL in RestAccessRights^.AllowRemoteExecute) and (ErrorMsg='') and EngineExecuteAll(SentData) then begin result.Lo := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end else // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1] if not (URI.TableIndex in RestAccessRights^.POST) then // check User result.Lo := HTML_FORBIDDEN else if ID<0 then begin // ModelRoot/TableName with possible JSON SentData: create a new member ID := Engine.EngineAdd(URI.Table,SentData); if ID<>0 then begin result.Lo := HTML_CREATED; // 201 Created Head := 'Location: '+URI.URI+'/'+ {$ifndef ENHANCEDRTL}Int32ToUtf8{$else}IntToStr{$endif}(ID); fCache.Notify(URI.TableIndex,ID,SentData,soInsert); inc(fStats.fModified); end; end else // ModelRoot/TableName/0 = BATCH sequence '{"Table":["cmd":values,...]}' if not (URI.TableIndex in RestAccessRights^.PUT) or not (URI.TableIndex in RestAccessRights^.DELETE) then // POST already checked result.Lo := HTML_FORBIDDEN else if RunBatch(Static,URI.Table,pointer(SentData),Resp,ErrorMsg) then result.Lo := HTML_SUCCESS; end; mPUT: begin // PUT=UPDATE if URI.MethodIndex>=0 then // PUT ModelRoot/MethodName (e.g. ModelRoot/Batch) LaunchCallBack(URI,Resp,result.Lo) else if ID>0 then begin // PUT ModelRoot/TableName/ID[/BlobFieldName] to update member/BLOB content if not RecordCanBeUpdated(URI.Table,ID,seUpdate,@ErrorMsg) or not (URI.TableIndex in RestAccessRights^.PUT) then // check User result.Lo := HTML_FORBIDDEN else begin OK := false; if BlobFieldName<>'' then begin // PUT ModelRoot/TableName/ID/BlobFieldName: update BLOB field content Blob := URI.Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName); if Blob<>nil then begin OK := Engine.EngineUpdateBlob(URI.Table,ID,Blob,SentData); end else begin // PUT ModelRoot/TableName/ID/MethodName: try MethodName URI.MethodIndex := fPublishedMethods.Find(BlobFieldName); if URI.MethodIndex>=0 then LaunchCallBack(URI,Resp,result.Lo); end; end else begin // ModelRoot/TableName/ID with JSON SentData: update a member OK := Engine.EngineUpdate(URI.Table,ID,SentData); if OK then fCache.NotifyDeletion(URI.TableIndex,ID); // flush (no CreateTime in JSON) end; if OK then begin result.Lo := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end; end else if URI.Parameters<>nil then // e.g. from TSQLRestClient.EngineUpdateField // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=.. if not (URI.TableIndex in RestAccessRights^.PUT) then // check User result.Lo := HTML_FORBIDDEN else begin repeat UrlDecodeValue(URI.Parameters,'SETNAME=',SQLSelect); UrlDecodeValue(URI.Parameters,'SET=',SQLDir); UrlDecodeValue(URI.Parameters,'WHERENAME=',SQLSort); UrlDecodeValue(URI.Parameters,'WHERE=',SQLWhere,@URI.Parameters); until URI.Parameters=nil; if (SQLSelect<>'') and (SQLDir<>'') and (SQLSort<>'') and (SQLWhere<>'') then if Engine.EngineUpdateField(URI.Table,SQLSelect,SQLDir,SQLSort,SQLWhere) then begin result.Lo := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end; end; mDELETE: if URI.Table<>nil then if ID>0 then // ModelRoot/TableName/ID to delete a member if not RecordCanBeUpdated(URI.Table,ID,seDelete,@ErrorMsg) or not (URI.TableIndex in RestAccessRights^.DELETE) then // check User result.Lo := HTML_FORBIDDEN else begin if Engine.EngineDelete(URI.Table,ID) and AfterDeleteForceCoherency(URI.Table,ID) then begin result.Lo := HTML_SUCCESS; // 200 OK fCache.NotifyDeletion(URI.TableIndex,ID); inc(fStats.fModified); end; end else if URI.Parameters<>nil then if (not (URI.TableIndex in RestAccessRights^.DELETE)) or (not (reUrlEncodedDelete in RestAccessRights^.AllowRemoteExecute)) then result.Lo := HTML_FORBIDDEN else begin // ModelRoot/TableName?WhereClause to delete members SQLWhere := Trim(UrlDecode(URI.Parameters)); if SQLWhere<>'' then begin if Delete(URI.Table,SQLWhere) then begin result.Lo := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end; end; mBEGIN: begin // BEGIN TRANSACTION // TSQLVirtualTableJSON/External will rely on SQLite3 module // and also TSQLRestServerStaticInMemory, since COMMIT/ROLLBACK have Static=nil if TransactionBegin(URI.Table,Session) then begin if (Static<>nil) and (StaticKind=sVirtual) then Static.TransactionBegin(URI.Table,Session); result.Lo := HTML_SUCCESS; // 200 OK end; end; mEND: begin // END=COMMIT // this method is called with Root (-> Table=nil -> Static=nil) if fTransactionTable<>nil then Static := StaticVirtualTable[fTransactionTable]; Commit(Session); if Static<>nil then Static.Commit(Session); result.Lo := HTML_SUCCESS; // 200 OK end; mABORT: begin // ABORT=ROLLBACK // this method is called with Root (-> Table=nil -> Static=nil) if fTransactionTable<>nil then Static := StaticVirtualTable[fTransactionTable]; RollBack(Session); if Static<>nil then Static.RollBack(Session); result.Lo := HTML_SUCCESS; // 200 OK end; end; finally ReleaseWrite; end else // AcquireWrite(SessionID) returned false (e.g. endless transaction) result.Lo := HTML_TIMEOUT; // 408 Request Time-out end; // 4. returns expected result to the client if result.Lo in [HTML_SUCCESS,HTML_CREATED] then begin inc(fStats.fResponses); {$ifdef WITHLOG} Log.Log(sllServer,'% % -> %',[method,url,result.Lo],self); {$endif} end else SetRespFromError; inc(fStats.fOutcomingBytes,length(Resp)+length(Head)+16); if (Static<>nil) and (StaticKind=sInMemory) then result.Hi := cardinal(-1) else // force always refresh for Static table result.Hi := InternalState; // database state may have changed above {$ifdef WITHSTATPROCESS} QueryPerformanceCounter(timeEnd); inc(fStats.ProcessTimeCounter,timeEnd-timeStart); {$endif} end; function TSQLRestServer.Stat(var aParams: TSQLRestServerCallBackParams): Integer; begin aParams.Resp := Stats.DebugMessage; // transmitted as JSON object result := HTML_SUCCESS; end; function TSQLRestServer.TimeStamp(var aParams: TSQLRestServerCallBackParams): Integer; begin aParams.Head^ := TEXT_CONTENT_TYPE_HEADER; aParams.Resp := Int64ToUtf8(ServerTimeStamp); result := HTML_SUCCESS; end; function TSQLRestServer.CacheFlush(var aParams: TSQLRestServerCallBackParams): Integer; begin if aParams.Table=nil then Cache.Flush else if aParams.Context.ID=0 then Cache.Flush(aParams.Table) else Cache.SetCache(aParams.Table,aParams.Context.ID); result := HTML_SUCCESS; end; function TSQLRestServer.Batch(var aParams: TSQLRestServerCallBackParams): Integer; begin if (aParams.Context.Method=mPUT) and RunBatch(nil,nil,pointer(aParams.SentData),aParams.Resp,aParams.ErrorMsg^) then result := HTML_SUCCESS else result := HTML_BADREQUEST; end; function Nonce(Previous: boolean): RawUTF8; var Tix: cardinal; tmp: RawByteString; begin Tix := GetTickCount div (1000*60*5); // valid for 5*60*1000 ms = 5 minutes if Previous then dec(Tix); SetString(tmp,PAnsiChar(@Tix),sizeof(Tix)); result := SHA256(tmp); end; function TSQLRestServer.Auth(var aParams: TSQLRestServerCallBackParams): Integer; procedure CreateNewSession(var User: TSQLAuthUser; var aParams: TSQLRestServerCallBackParams); var Session: TAuthSession; begin if User.fID=0 then begin {$ifdef WITHLOG} SQLite3Log.Family.SynLog.Log(sllUserAuth, 'User.LogonName=% not found in AuthUser table',[User.LogonName],self); {$endif} exit; // unknown user -> error 404 end; Session := TAuthSession.Create(self,User); try aParams.Resp := JSONEncode(['result',Session.fPrivateSalt,'logonname',User.LogonName]); User := nil; // will be freed by TAuthSession.Destroy if fSessions=nil then fSessions := TObjectList.Create; fSessions.Add(Session); Session := nil; // will be freed by fSessions finally Session.Free; ................................................................................ InDataEnc: RawUTF8; CtxArr: TDynArray; Now: QWord; SecCtxIdx: Integer; OutData: RawByteString; {$endif} begin result := HTML_NOTFOUND; if not UrlDecodeNeedParameters(aParams.Parameters,'UserName') then exit; EnterCriticalSection(fSessionCriticalSection); try if UrlDecodeNeedParameters(aParams.Parameters,'Session') then begin // GET ModelRoot/auth?UserName=...&Session=... -> release session while aParams.Parameters<>nil do begin UrlDecodeValue(aParams.Parameters,'USERNAME=',aUserName); UrlDecodeCardinal(aParams.Parameters,'SESSION=',aSessionID,@aParams.Parameters); end; if (fSessions<>nil) and // allow only to delete its own session - ticket [7723fa7ebd] (aSessionID=aParams.Context.Session) then for i := 0 to fSessions.Count-1 do with TAuthSession(fSessions.List[i]) do if fIDCardinal=aSessionID then begin SessionDelete(i); result := HTML_SUCCESS; // mark success break; end; exit; // unknown session -> error 404 end else if UrlDecodeNeedParameters(aParams.Parameters,'PassWord,ClientNonce') then begin // GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... -> handshaking while aParams.Parameters<>nil do begin UrlDecodeValue(aParams.Parameters,'USERNAME=',aUserName); UrlDecodeValue(aParams.Parameters,'PASSWORD=',aPassWord); ................................................................................ SecCtxIdx := CtxArr.New; // add a new entry to fSSPIAuthContexts[] InvalidateSecContext(fSSPIAuthContexts[SecCtxIdx]); fSSPIAuthContexts[SecCtxIdx].ID := fSSPIAuthCounter; Inc(fSSPIAuthCounter); end; // call SSPI provider if ServerSSPIAuth(fSSPIAuthContexts[SecCtxIdx], Base64ToBin(InDataEnc), OutData) then begin aParams.Resp := JSONEncode(['result','','id',Int64(fSSPIAuthContexts[SecCtxIdx].ID), 'data',BinToBase64(OutData)]); Result := HTML_SUCCESS; exit; // 1st call: send back OutData to the client end; // 2nd call: user was authenticated -> release used context ServerSSPIAuthUser(fSSPIAuthContexts[SecCtxIdx], aUserName); {$ifdef WITHLOG} SQLite3Log.Family.SynLog.Log(sllUserAuth, 'Windows Authentication success for %',[aUserName],self); ................................................................................ CreateNewSession(User,aParams); finally User.Free; end; {$endif} end else // only UserName=... -> return hexadecimal nonce content valid for 5 minutes aParams.Resp := JSONEncodeResult([Nonce(false)]); finally LeaveCriticalSection(fSessionCriticalSection); end; result := HTML_SUCCESS; end; procedure TSQLRestServer.SessionDelete(aSessionIndex: integer); begin if (self<>nil) and (Cardinal(aSessionIndex)<Cardinal(fSessions.Count)) then with TAuthSession(fSessions.List[aSessionIndex]) do begin if Services is TServiceContainerServer then TServiceContainerServer(Services).OnCloseSession(IDCardinal); {$ifdef WITHLOG} SQLite3Log.Family.SynLog.Log(sllUserAuth,'Deleted session %/%',[User.LogonName,IDCardinal],self); {$endif} fSessions.Delete(aSessionIndex); end; end; function TSQLRestServer.SessionAccess(var aContext: TSQLRestServerSessionContext): TAuthSession; var i: integer; Now: cardinal; begin if (self<>nil) and (fSessions<>nil) then begin // first check for outdated sessions to be deleted Now := GetTickCount; for i := fSessions.Count-1 downto 0 do with TAuthSession(fSessions.List[i]) do if Now<LastAccess then // 32 bit overflow occured fLastAccess := Now else ................................................................................ if QWord(Now)>QWord(LastAccess)+QWord(TimeOut) then SessionDelete(i); // retrieve session for i := 0 to fSessions.Count-1 do begin result := TAuthSession(fSessions.List[i]); if result.IDCardinal=aContext.Session then begin result.fLastAccess := Now; // refresh session access timestamp aContext.User := result.User.fID; aContext.Group := result.User.GroupRights.fID; exit; end; end; end; result := nil; end; ................................................................................ fChild[fMasterThreadChildIndex] := nil; InterlockedDecrement(fChildCount); end; inherited; end; procedure TSQLRestServerNamedPipeResponse.Execute; var url, method: RawUTF8; SentData: RawUTF8; Resp, Head: RawUTF8; Code: integer; Ticks, Sleeper, ClientTimeOut: cardinal; Available: cardinal; Res: Int64Rec; begin if (fPipe=0) or (fPipe=INVALID_HANDLE_VALUE) or (fServer=nil) then exit; fServer.BeginCurrentThread(self); Ticks := 0; Sleeper := 0; ClientTimeOut := GetTickCount+30*60*1000; // disconnect client after 30 min of inactivity ................................................................................ while not Terminated do if // (WaitForSingleObject(fPipe,200)=WAIT_OBJECT_0) and = don't wait PeekNamedPipe(fPipe,nil,0,nil,@Available,nil) and (Available>=4) then begin FileRead(fPipe,Code,4); if (Code=integer(MAGIC_SYN)) // magic word for URI like request and not Terminated then try url := ReadString(fPipe); method := ReadString(fPipe); SentData := ReadString(fPipe); //writeln('TSQLRestServerNamedPipeResponse.EngineExecute ',method,' ',url); Res := fServer.URI(url,method,SentData,Resp,Head,@SUPERVISOR_ACCESS_RIGHTS); // it's up to URI overriden method to implement access rights FileWrite(fPipe,Res,sizeof(Res)); WriteString(fPipe,Head); WriteString(fPipe,Resp); FlushFileBuffers(fPipe); // Flush the pipe to allow the client to read // if (Win32Platform=VER_PLATFORM_WIN32_NT) and (Win32MajorVersion<6) then Ticks := GetTickCount+20; // start sleeping after 20 ms ClientTimeOut := Ticks+30*60*1000; if ClientTimeOut<Ticks then // avoid 32 bits overflow ClientTimeOut := 30*60*1000; Sleeper := 0; Sleep(0); except ................................................................................ Sleep(Sleeper); // doesn't slow down connection but decreases CSwitch Ticks := 0; if GetTickCount>ClientTimeOut then break; // disconnect client after 30 min of inactivity end else Sleep(0); finally DisconnectNamedPipe(fPipe); CloseHandle(fPipe); end; end; { TSQLRestClientURINamedPipe } ................................................................................ {$endif} CreatePipe; if (Pipe=INVALID_HANDLE_VALUE) or (Pipe=ERROR_PIPE_BUSY) then for Retry := 1 to NAMEDPIPE_RETRYCOUNT_IFBUSY do begin if WaitNamedPipe(pointer(fPipeName),1000) then begin // 1000 since we have sleep(128) in TSQLRestServerNamedPipe.EngineExecute CreatePipe; if Pipe=INVALID_HANDLE_VALUE then break; end; sleep(200); // wait for TSQLRestServerNamedPipe.EngineExecute to be reached end; if (Pipe=INVALID_HANDLE_VALUE) or (Pipe=ERROR_PIPE_BUSY) then begin {$ifdef WITHLOG} Log.Log(sllError,'"%" when connecting to %',[ ................................................................................ function TServiceFactoryServer.CreateInstance: TInterfacedObject; begin if fImplementationClassWithCustomCreate then result := TInterfacedObjectWithCustomCreateClass(fImplementationClass).Create else result := fImplementationClass.Create; end; function TServiceFactoryServer.ExecuteMethod(const aContext: TSQLRestServerSessionContext; aMethodIndex, aInstanceID: cardinal; aParamsJSONArray: PUTF8Char; var aResp, aHead, aErrorMsg: RawUTF8): cardinal; var Inst: TServiceFactoryServerInstance; WR: TTextWriter; entry: PInterfaceEntry; ThreadServer: PServiceRunningContext; begin result := HTML_BADREQUEST; // 1. initialize Inst.Instance and Inst.InstanceID Inst.InstanceID := 0; Inst.Instance := nil; case InstanceCreation of sicSingle: if aMethodIndex>=fInterface.fMethodsCount then exit else ................................................................................ sicShared: if aMethodIndex>=fInterface.fMethodsCount then exit else Inst.Instance := fSharedInstance; sicClientDriven, sicPerSession, sicPerUser, sicPerGroup: begin if InstanceCreation=sicClientDriven then Inst.InstanceID := aInstanceID else if aContext.Session>CONST_AUTHENTICATION_NOT_USED then case InstanceCreation of // authenticated user -> handle context sicPerSession: Inst.InstanceID := aContext.Session; sicPerUser: Inst.InstanceID := aContext.User; sicPerGroup: Inst.InstanceID := aContext.Group; end else aErrorMsg := '% mode expects an authenticated session'; if aErrorMsg='' then if InternalInstanceRetrieve(Inst,aMethodIndex) then begin result := HTML_SUCCESS; exit; // {"method":"_free_", "params":[], "id":1234} end; end; end; if Inst.Instance=nil then begin if aErrorMsg='' then aErrorMsg := '% instance id:% not found or deprecated'; aErrorMsg := FormatUTF8(pointer(aErrorMsg), [UnCamelCase(ServiceInstanceImplementationToText(InstanceCreation)), Inst.InstanceID]); exit; end; // 2. call method implementation try entry := Inst.Instance.GetInterfaceEntry(fInterface.fInterfaceIID); if entry=nil then exit; ThreadServer := @ServiceContext; WR := TJSONSerializer.CreateOwnedStream; try with ThreadServer^ do begin Factory := self; Session := @aContext; end; // RunningThread is already set at thread initialization // root/calculator {"method":"add","params":[1,2]} -> {"result":[3],"id":0} try WR.AddShort('{"result":['); if not fInterface.fMethods[aMethodIndex].InternalExecute( [PAnsiChar(Inst.Instance)+entry^.IOffset],aParamsJSONArray,WR,aHead, fExecution[aMethodIndex].Options) then exit; // wrong request if aHead='' then begin // aHead<>'' for TServiceCustomAnswer WR.AddShort('],"id":'); WR.Add(Inst.InstanceID); // only used in sicClientDriven mode WR.AddShort('}'); end; WR.SetText(aResp); result := HTML_SUCCESS; // success except on E: Exception do begin result := 500; // Internal Server Error aErrorMsg := FormatUTF8('%: %',[E.ClassName,E.Message]); exit; end; end; finally with ThreadServer^ do begin Factory := nil; Session := nil; end; WR.Free; |
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > < | > | < | | | | | | | | | | | | | | | | | | | > > > > > > > > > > > < < > > > > > | < < < < < < > > > > > > > > < < < < < < < < < < < < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | | | | | | | | | | | < < | | < < < < | | | | < > | < > > > | | | < | < < > > > > > > > > | < | < | | | | < < < < < < < < < < < < | < < < < < < < < < < < < | | | | | | | | < < < < < < < < < < < < < < < < < | | | | | | | > > | > > > | < > | | | < < < > < > > > | | | < | < > | > > > > < > | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < | < > | | | | | | | | | | | | | | | | | | | | | | > < < > > < > > > > > > > > > > > > > > > > > < < < | > > > > > > > > > > > > > > > > > > > > | < | | > | < < > | > > | < | < < < > > | | > > > | | | | | | > | | | | | | | < < < | < | | | | | < | | | > | > > | > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < | < | < < < < | < < < | < < < < < < < < < < < < < < < < < < | < > > | | | | | | < | < > | | | | | | | | | | | | | | | | | | | | | | < < < < | | | | | | | | | | | | | | | | | | | | | | > > | < | | | > | > | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < | | > > | | | | | | | | | < | | > > | | | | | | | | | | | | | | | | | | | | | | > > > | | > | > | | < | < | < | | | | | | | | < | | > > > > | < | > > | | | | | | | | | < | < | | | | | | < < < | | | < | > | | | < > | | > | < > > > > > < | | | | | | | > | < > | | < | < < < | | | | | | | | | < < > |
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 .... 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 .... 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 .... 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 .... 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 .... 6236 6237 6238 6239 6240 6241 6242 6243 6244 6245 6246 6247 6248 6249 6250 6251 .... 6743 6744 6745 6746 6747 6748 6749 6750 6751 6752 6753 6754 6755 6756 6757 .... 7492 7493 7494 7495 7496 7497 7498 7499 7500 7501 7502 7503 7504 7505 .... 7604 7605 7606 7607 7608 7609 7610 7611 7612 7613 7614 7615 7616 7617 7618 7619 .... 7646 7647 7648 7649 7650 7651 7652 7653 7654 7655 7656 7657 7658 7659 7660 7661 7662 7663 7664 7665 .... 7740 7741 7742 7743 7744 7745 7746 7747 7748 7749 7750 7751 7752 7753 7754 7755 7756 7757 7758 7759 7760 7761 7762 7763 7764 7765 7766 7767 7768 7769 7770 7771 7772 7773 7774 7775 7776 .... 7878 7879 7880 7881 7882 7883 7884 7885 7886 7887 7888 7889 7890 7891 7892 7893 .... 7978 7979 7980 7981 7982 7983 7984 7985 7986 7987 7988 7989 7990 7991 7992 7993 7994 7995 7996 7997 7998 7999 8000 8001 8002 8003 8004 8005 8006 8007 8008 8009 8010 8011 8012 .... 8101 8102 8103 8104 8105 8106 8107 8108 8109 8110 8111 8112 8113 8114 8115 .... 8138 8139 8140 8141 8142 8143 8144 8145 8146 8147 8148 8149 8150 8151 8152 8153 8154 8155 8156 8157 8158 8159 8160 8161 8162 8163 8164 8165 8166 8167 8168 8169 8170 8171 8172 8173 8174 8175 8176 .... 9210 9211 9212 9213 9214 9215 9216 9217 9218 9219 9220 9221 9222 9223 9224 .... 9240 9241 9242 9243 9244 9245 9246 9247 9248 9249 9250 9251 9252 9253 9254 9255 .... 9843 9844 9845 9846 9847 9848 9849 9850 9851 9852 9853 9854 9855 9856 .... 9947 9948 9949 9950 9951 9952 9953 9954 9955 9956 9957 9958 9959 9960 9961 ..... 19325 19326 19327 19328 19329 19330 19331 19332 19333 19334 19335 19336 19337 19338 19339 ..... 20461 20462 20463 20464 20465 20466 20467 20468 20469 20470 20471 20472 20473 20474 20475 20476 20477 20478 20479 20480 20481 20482 20483 20484 20485 20486 20487 20488 20489 20490 20491 20492 20493 20494 20495 20496 20497 20498 20499 20500 20501 20502 20503 20504 20505 20506 20507 20508 20509 20510 20511 20512 20513 20514 20515 20516 20517 20518 20519 ..... 20580 20581 20582 20583 20584 20585 20586 20587 20588 20589 20590 20591 20592 20593 20594 20595 20596 20597 20598 20599 20600 20601 20602 20603 20604 20605 20606 20607 20608 20609 20610 20611 20612 20613 20614 20615 20616 20617 20618 20619 20620 20621 20622 20623 20624 20625 20626 20627 20628 20629 20630 ..... 21181 21182 21183 21184 21185 21186 21187 21188 21189 21190 21191 21192 21193 21194 21195 21196 21197 21198 21199 21200 21201 21202 21203 21204 21205 21206 21207 21208 21209 21210 21211 21212 21213 21214 21215 21216 21217 21218 21219 21220 21221 21222 21223 21224 21225 21226 21227 21228 21229 21230 21231 21232 21233 21234 21235 21236 21237 21238 21239 21240 21241 21242 21243 21244 21245 21246 21247 21248 21249 21250 21251 21252 21253 21254 21255 21256 21257 21258 21259 21260 21261 21262 21263 21264 21265 21266 21267 21268 21269 21270 21271 21272 21273 21274 21275 21276 21277 21278 21279 21280 21281 21282 21283 21284 21285 21286 21287 21288 21289 21290 21291 21292 21293 21294 21295 21296 21297 21298 21299 21300 21301 21302 21303 21304 21305 21306 21307 21308 21309 21310 21311 21312 21313 21314 21315 21316 21317 21318 21319 21320 21321 21322 21323 21324 21325 21326 21327 21328 21329 21330 21331 21332 21333 21334 21335 21336 21337 21338 21339 21340 21341 21342 21343 21344 ..... 21367 21368 21369 21370 21371 21372 21373 21374 21375 21376 21377 21378 21379 21380 21381 21382 21383 21384 21385 21386 21387 21388 21389 21390 21391 21392 21393 21394 21395 21396 21397 21398 21399 21400 21401 21402 21403 21404 21405 21406 21407 21408 21409 21410 21411 21412 21413 21414 21415 21416 21417 21418 21419 21420 21421 21422 21423 21424 21425 21426 21427 21428 21429 21430 21431 21432 21433 21434 21435 21436 21437 21438 21439 21440 21441 21442 21443 21444 21445 21446 21447 21448 21449 21450 21451 21452 21453 21454 21455 21456 21457 21458 21459 21460 21461 21462 21463 21464 21465 21466 21467 21468 21469 21470 21471 21472 21473 21474 21475 21476 21477 21478 21479 21480 21481 21482 21483 21484 21485 21486 21487 21488 21489 21490 21491 21492 21493 21494 21495 21496 ..... 21547 21548 21549 21550 21551 21552 21553 21554 21555 21556 21557 21558 21559 21560 21561 21562 21563 21564 21565 21566 21567 21568 21569 21570 21571 21572 21573 21574 21575 21576 21577 21578 21579 21580 21581 ..... 21605 21606 21607 21608 21609 21610 21611 21612 21613 21614 21615 21616 21617 21618 21619 21620 21621 21622 ..... 21625 21626 21627 21628 21629 21630 21631 21632 21633 21634 21635 21636 21637 21638 21639 21640 21641 21642 21643 21644 21645 21646 21647 21648 21649 21650 21651 21652 21653 21654 21655 21656 21657 21658 21659 21660 21661 21662 21663 21664 21665 21666 21667 21668 21669 21670 21671 21672 21673 21674 21675 21676 21677 21678 21679 21680 21681 21682 21683 21684 21685 21686 21687 21688 21689 21690 21691 21692 21693 21694 21695 21696 21697 21698 21699 21700 21701 ..... 21720 21721 21722 21723 21724 21725 21726 21727 21728 21729 21730 21731 21732 21733 21734 21735 21736 21737 21738 21739 21740 21741 21742 21743 21744 21745 21746 21747 21748 21749 21750 21751 21752 21753 21754 21755 21756 21757 21758 21759 21760 21761 21762 21763 21764 21765 21766 21767 21768 21769 21770 21771 21772 21773 21774 21775 21776 21777 21778 21779 21780 21781 21782 21783 21784 21785 21786 21787 21788 21789 21790 21791 21792 21793 21794 21795 21796 21797 21798 21799 21800 21801 21802 21803 21804 21805 21806 21807 21808 21809 21810 21811 21812 21813 21814 21815 21816 21817 21818 21819 21820 21821 21822 21823 21824 21825 21826 21827 21828 21829 21830 21831 21832 21833 21834 21835 21836 21837 21838 21839 21840 21841 21842 21843 21844 21845 21846 21847 21848 21849 21850 21851 21852 21853 21854 ..... 21869 21870 21871 21872 21873 21874 21875 21876 21877 21878 21879 21880 21881 21882 21883 21884 21885 21886 21887 21888 21889 21890 21891 21892 21893 21894 21895 21896 21897 21898 21899 21900 21901 21902 21903 21904 21905 21906 21907 21908 21909 21910 21911 21912 21913 21914 21915 21916 21917 21918 21919 21920 21921 21922 21923 21924 21925 21926 21927 21928 21929 21930 21931 21932 21933 21934 21935 21936 21937 21938 21939 21940 21941 21942 21943 21944 21945 21946 21947 21948 21949 21950 21951 21952 21953 21954 21955 21956 21957 21958 21959 21960 21961 21962 21963 21964 21965 21966 21967 21968 21969 21970 21971 21972 21973 21974 21975 21976 21977 21978 21979 21980 21981 21982 21983 21984 21985 21986 21987 21988 21989 21990 21991 21992 21993 21994 21995 21996 21997 21998 21999 22000 22001 22002 22003 22004 22005 22006 22007 22008 22009 22010 22011 22012 22013 22014 22015 22016 22017 22018 22019 22020 22021 22022 22023 22024 22025 22026 22027 22028 22029 22030 22031 22032 22033 22034 22035 22036 22037 22038 22039 22040 22041 22042 22043 22044 22045 22046 22047 22048 22049 22050 22051 22052 22053 22054 22055 22056 22057 22058 22059 22060 22061 22062 22063 22064 22065 22066 22067 22068 22069 22070 22071 22072 22073 22074 22075 22076 22077 22078 22079 22080 22081 22082 22083 22084 22085 22086 22087 22088 22089 22090 22091 22092 22093 22094 22095 22096 22097 22098 22099 22100 22101 22102 22103 22104 22105 22106 22107 22108 22109 22110 22111 22112 22113 22114 22115 22116 22117 22118 22119 22120 22121 22122 22123 22124 22125 22126 22127 22128 22129 22130 22131 22132 22133 22134 22135 22136 22137 22138 22139 22140 22141 22142 22143 ..... 22153 22154 22155 22156 22157 22158 22159 22160 22161 22162 22163 22164 22165 22166 22167 22168 22169 22170 22171 22172 22173 22174 22175 22176 22177 22178 22179 22180 22181 22182 22183 22184 22185 22186 22187 22188 ..... 22247 22248 22249 22250 22251 22252 22253 22254 22255 22256 22257 22258 22259 22260 22261 22262 ..... 22275 22276 22277 22278 22279 22280 22281 22282 22283 22284 22285 22286 22287 22288 22289 22290 22291 22292 22293 22294 22295 22296 22297 22298 22299 22300 22301 22302 22303 22304 22305 22306 22307 22308 22309 22310 22311 ..... 22312 22313 22314 22315 22316 22317 22318 22319 22320 22321 22322 22323 22324 22325 22326 22327 ..... 22702 22703 22704 22705 22706 22707 22708 22709 22710 22711 22712 22713 22714 22715 22716 22717 22718 22719 ..... 22721 22722 22723 22724 22725 22726 22727 22728 22729 22730 22731 22732 22733 22734 22735 22736 22737 22738 22739 22740 22741 22742 22743 22744 ..... 22753 22754 22755 22756 22757 22758 22759 22760 22761 22762 22763 22764 22765 22766 22767 ..... 22821 22822 22823 22824 22825 22826 22827 22828 22829 22830 22831 22832 22833 22834 22835 ..... 29355 29356 29357 29358 29359 29360 29361 29362 29363 29364 29365 29366 29367 29368 29369 29370 29371 29372 29373 29374 29375 29376 29377 29378 29379 29380 29381 ..... 29383 29384 29385 29386 29387 29388 29389 29390 29391 29392 29393 29394 29395 29396 29397 29398 29399 29400 29401 29402 29403 29404 29405 29406 29407 29408 29409 29410 29411 29412 29413 29414 29415 29416 29417 29418 29419 29420 29421 29422 29423 29424 29425 29426 29427 29428 29429 29430 29431 29432 29433 29434 29435 29436 29437 29438 29439 29440 29441 29442 29443 29444 29445 29446 |
indeed no implementation requirement to force a specific class type - added aUseBatchMode optional parameter to TSQLRecordMany.ManyDelete() method - now JSON parser will handle #1..' ' chars as whitespace (not only ' ') - now huge service JSON response is truncated (to default 20 KB) in logs Version 1.18 - renamed SQLite3Commons.pas to mORMot.pas - BREAKING CHANGE in TSQLRestServerCallBackParams use: all method-based services should now be a procedure, and use aParams.Results()/Error() methods to return the content - new definition of aParams features now full access to incoming/outgoing context and parameters - deep code refactoring, introducing TSQLPropInfo* classes in order to decouple the ORM definitions from the RTTI - will allow definition of any class members, even if there is no RTTI generated or via custom properties attributes or a fluent interface - introduced more readable "stored AS_UNIQUE" published property definition in TSQLRecord (via the explicit AS_UNIQUE=false constant) - introducing TInterfaceStub and TInterfaceMock classes to define high-performance interface stubbing and mocking via a fluent interface - integrated Windows Authentication to the mORMot Client-Server layer: in order to enable it, define a SSPIAUTH conditional and call TSQLRestClientURI.SetUser() with an empty user name, and ensure that TSQLAuthUser.LoginName contains a matching 'DomainName\UserName' value - TSQLRestServer.URI() method uses now one TSQLRestServerURIParams parameter - added TAuthSession.SentHeaders and RemoteIP properties (for HTTP) - added process of Variant and WideString types in TSQLRecord properties - added JSON serialization of Variant and WideString types in JSONToObject() / ObjectToJSON() functions and WriteObject method - TSQLRestClientURINamedPipe and TSQLRestClientURIMessage are now thread-safe (i.e. protected by a system mutex) therefore can be called from a multi-threaded process, e.g. TSQLRestServer instances as proxies - modified named pipe client side to avoid unexpected file not found errors ................................................................................ /// maximum handled dimension for TSQLRecordRTree // - this value is the one used by SQLite3 R-Tree virtual table RTREE_MAX_DIMENSION = 5; /// used as "stored AS_UNIQUE" published property definition in TSQLRecord AS_UNIQUE = false; /// HTML Status Code for "Success" HTML_SUCCESS = 200; /// HTML Status Code for "Created" HTML_CREATED = 201; /// HTML Status Code for "Not Modified" HTML_NOTMODIFIED = 304; /// HTML Status Code for "Bad Request" HTML_BADREQUEST = 400; /// HTML Status Code for "Forbidden" HTML_FORBIDDEN = 403; /// HTML Status Code for "Not Found" HTML_NOTFOUND = 404; /// HTML Status Code for "Request Time-out" HTML_TIMEOUT = 408; /// HTML Status Code for "Internal Server Error" HTML_SERVERERROR = 500; /// HTML Status Code for "Not Implemented" HTML_NOTIMPLEMENTED = 501; /// HTML Status Code for "Service Unavailable" HTML_UNAVAILABLE = 503; type /// generic parent class of all custom Exception types of this unit EORMException = class(ESynException); /// exception raised in case of wrong Model definition EModelException = class(EORMException); ................................................................................ // - will return the specified associated TSynFilterOrValidate instance function AddFilterOrValidate(const aFieldName: RawUTF8; aFilter: TSynFilterOrValidate): TSynFilterOrValidate; overload; {$ifdef HASINLINE}inline;{$endif} end; TAuthSession = class; PSQLAccessRights = ^TSQLAccessRights; /// the available THTTP methods transmitted between client and server TSQLURIMethod = (mNone, mGET, mPOST, mPUT, mDELETE, mBEGIN, mEND, mABORT, mLOCK, mUNLOCK, mSTATE); /// store all parameters for a TSQLRestServer.URI() method call // - see TSQLRestClient to check how data is expected in our RESTful format TSQLRestServerURIParams = packed record /// input parameter containing the caller URI Url: RawUTF8; /// input parameter containing the caller method // - handle enhanced REST codes: LOCK/UNLOCK/BEGIN/END/ABORT Method: RawUTF8; /// input parameter containing the caller message headers InHead: RawUTF8; /// input parameter containing the caller message body // - e.g. some GET/POST/PUT JSON data can be specified here InBody: RawUTF8; /// output parameter to be set to the response message header OutHead: RawUTF8; /// output parameter to be set to the response message body OutBody: RawUTF8; /// output parameter to be set to the HTTP status integer code // - HTML_NOTFOUND=404 e.g. if the url doesn't start with Model.Root (caller // can try another TSQLRestServer) OutStatus: cardinal; /// output parameter to be set to the database internal state OutInternalState: cardinal; /// associated RESTful access rights // - AccessRights must be handled by the TSQLRestServer child, according // to the Application Security Policy (user logging, authentification and // rights management) - making access rights a parameter allows this method // to be handled as pure stateless, thread-safe and session-free RestAccessRights: PSQLAccessRights; end; /// used to map set of parameters for a TSQLRestServer.URI() method PSQLRestServerURIParams = ^TSQLRestServerURIParams; /// store all parameters for a TSQLRestServerCallBack event handler // - having a dedicated record avoid changing the implementation methods // signature if the framework add some parameters to this structure // - see TSQLRestServerCallBack for general code use {$ifdef UNICODE} TSQLRestServerCallBackParams = record {$else} TSQLRestServerCallBackParams = object {$endif} /// the used Client-Server method (matching the corresponding HTTP Verb) // - this property will be set from incoming URI, even if RESTful // authentication is not enabled Method: TSQLURIMethod; /// the URI address, just before parameters // - can be either the table name (in RESTful protocol), or a service name URI: RawUTF8; /// the Table as specified at the URI level (if any) Table: TSQLRecordClass; /// the index in the Model of the Table specified at the URI level (if any) TableIndex: integer; /// the associated TSQLRecord.ID, as decoded from URI scheme // - this property will be set from incoming URI, even if RESTful // authentication is not enabled ID: integer; /// the index of the callback published method within the internal class list MethodIndex: integer; /// URI inlined parameters // - use UrlDecodeValue*() functions to retrieve the values Parameters: PUTF8Char; /// access to all input/output parameters at TSQLRestServer.URI() level // - process should better call Results() or Success() methods to set the // appropriate answer or Error() method in case of an error // - low-level access to the call parameters can be made via this pointer Call: PSQLRestServerURIParams; /// the corresponding session TAuthSession.IDCardinal value // - equals 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED) if the session // is not started yet - i.e. if still in handshaking phase // - equals 1 (CONST_AUTHENTICATION_NOT_USED) if authentication mode // is not enabled - i.e. if TSQLRest.HandleAuthentication = FALSE Session: cardinal; /// the corresponding TAuthSession.User.ID value // - is undefined if Session is 0 or 1 (no authentication running) SessionUser: integer; /// the corresponding TAuthSession.User.GroupRights.ID value // - is undefined if Session is 0 or 1 (no authentication running) SessionGroup: integer; {$ifdef WITHLOG} /// associated logging instance // - you can use it to log some process on the server side Log: ISynLog; {$endif} /// use this method to send back directly a result value to the caller // - expects Status to be either HTML_SUCCESS or HTML_CREATED, and will // return as answer the supplied Result content with no transformation // - if Status is an error code, it will call Error() method // - CustomHeader optional parameter can be set e.g. to // TEXT_CONTENT_TYPE_HEADER if the default JSON_CONTENT_TYPE is not OK procedure Returns(const Result: RawUTF8; Status: integer=HTML_SUCCESS; const CustomHeader: RawUTF8=''); overload; /// use this method to send back a JSON object to the caller // - this method will encode the supplied values e.g. as // ! JSONEncode(['name','John','year',1972]) = '{"name":"John","year":1972}' // - implementation is just a wrapper around Returns(JSONEncode([])) // - note that cardinal values should be type-casted to Int64() (otherwise // the integer mapped value will be transmitted, therefore wrongly) // - expects Status to be either HTML_SUCCESS or HTML_CREATED procedure Returns(const NameValuePairs: array of const; Status: integer=HTML_SUCCESS); overload; /// use this method to send back a JSON object with a "result" field // - this method will encode the supplied values as a {"result":"...} // JSON object, as such for one value: // $ {"result":"OneValue"} // (with one value, you can just call TSQLRestClientURI.CallBackGetResult // method to call and decode this value) // or as a JSON object containing an array of values: // $ {"result":["One","two"]} // - expects Status to be either HTML_SUCCESS or HTML_CREATED procedure Results(const Values: array of const; Status: integer=HTML_SUCCESS); /// use this method if the caller expect no data, just a status // - just wrap the overloaded Returns() method with no result value // - if Status is an error code, it will call Error() method // - by default, calling this method will mark process as successfull procedure Success(Status: integer=HTML_SUCCESS); /// use this method to send back an error to the caller // - expects Status to not be HTML_SUCCESS neither HTML_CREATED, // and will send back a JSON error message to the caller, with the // supplied error text // - if no ErrorMessage is specified, will return a default text // corresponding to the Status code procedure Error(const ErrorMessage: RawUTF8=''; Status: integer=HTML_BADREQUEST); overload; /// use this method to send back an error to the caller // - implementation is just a wrapper over Error(FormatUTF8(Format,Args)) procedure Error(Format: PUTF8Char; const Args: array of const; Status: integer=HTML_BADREQUEST); overload; end; (*/ method prototype which must be used to implement the Server-Side ModelRoot/[TableName/ID/]MethodName RESTful GET/PUT request of the Framework - this mechanism is able to handle some custom Client/Server request, similar to the DataSnap technology, but in a KISS way; it's fully integrated in the Client/Server architecture of our framework - just add a published method of this type to any TSQLRestServer descendant - when TSQLRestServer.URI receive a request for ModelRoot/MethodName ................................................................................ retrieved with a loop like this: ! if not UrlDecodeNeedParameters(aParams.Parameters,'SORT,COUNT') then ! exit; ! while aParams.Parameters<>nil do begin ! UrlDecodeValue(aParams.Parameters,'SORT=',aSortString); ! UrlDecodeValueInteger(aParams.Parameters,'COUNT=',aCountInteger,@aParams.Parameters); ! end; - aParams.Call.InBody is set with bulk incoming data from the GET/PUT method - aParams.Context will identify to the authentication session of the remote client (CONST_AUTHENTICATION_NOT_USED=1 if authentication mode is not enabled or CONST_AUTHENTICATION_SESSION_NOT_STARTED=0 if the session not started yet) - code may use SessionGetUser() protected method to retrieve the user details - aParams.Context.Method will indicate the used HTTP verb (e.g. GET/POST/PUT..) - if process succeeded, implementation shall call aParams.Results([]) method to set a JSON response object with one "result" field name or aParams.Returns([]) with a JSON object described in Name/Value pairs; if the returned value is not JSON_CONTENT_TYPE, use aParams.Returns() and its optional CustomHeader parameter can specify a custom header like TEXT_CONTENT_TYPE_HEADER - if process succeeded, and no data is expected to be returned to the caller, implementation shall call overloaded aParams.Success() method with the expected status (i.e. just aParams.Success will return HTML_SUCCESS) - if process failed, implementation shall call aParams.Error() method to set the corresponding error message and error code number - a typical implementation may be: ! procedure TSQLRestServerTest.Sum(var aParams: TSQLRestServerCallBackParams); ! var a,b: Extended; ! begin ! if UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin ! while Params.Parameters<>nil do begin ! UrlDecodeExtended(aParams.Parameters,'A=',a); ! UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); ! end; ! aParams.Results([a+b]); ! // same as: aParams.Returns(JSONEncode(['result',a+b])); ! // same as: aParams.Returns(['result',a+b]); ! end else ! aParams.Error('Missing Parameter'); ! end; - Client-Side can be implemented as you wish. By convention, it could be appropriate to define in either TSQLRestServer (if to be called as ModelRoot/MethodName), either TSQLRecord (if to be called as ModelRoot/TableName/MethodName[/ID]) a custom public or protected method, calling TSQLRestClientURI.URL with the appropriate parameters, and named (by convention) as MethodName; TSQLRestClientURI has dedicated methods ................................................................................ like CallBackGetResult, CallBackGet, and CallBackPut; see also TSQLModel.getURICallBack and JSONDecode function ! function TSQLRecordPeople.Sum(aClient: TSQLRestClientURI; a, b: double): double; ! var err: integer; ! begin ! val(aClient.CallBackGetResult('sum',['a',a,'b',b]),result,err); ! end; *) TSQLRestServerCallBack = procedure(var aParams: TSQLRestServerCallBackParams) of object; /// the possible options for handling table names TSQLCheckTableName = (ctnNoCheck,ctnMustExist,ctnTrimExisting); /// internal data used by TSQLRecord.FillPrepare()/FillPrepareMany() methods // - using a dedicated class will reduce memory usage for each TSQLRecord // instance (which won't need these properties most of the time) ................................................................................ // - on success, aResp shall contain a serialized JSON object with one // nested result property, which may be a JSON array, containing the // all "var" or "out" parameters values, and then the method main result - // for instance, ExecuteMethod(..,'[1,2]') over ICalculator.Add will return: // $ {"result":[3],"id":0} // the returned "id" number is the Instance identifier to be used for any later // sicClientDriven remote call - or just 0 in case of sicSingle or sicShared procedure ExecuteMethod(var aParams: TSQLRestServerCallBackParams; aMethodIndex, aInstanceID: cardinal; aParamsJSONArray: PUTF8Char); /// this method will create an implementation instance function CreateInstance: TInterfacedObject; public /// initialize the service provider on the server side // - expect an direct server-side implementation class (inheriting from // TInterfacedClass or from TInterfacedObjectWithCustomCreate if you need // an overriden constructor) ................................................................................ function InternalDelete(Table: TSQLRecordClass; const SQLWhere: RawUTF8; var IDs: TIntegerDynArray): boolean; /// wait for the transaction critical section to be acquired // - used to implement a thread-safe and transaction-safe write to the DB // - returns FALSE in case of time out (see AcquireWriteTimeOut property) // - returns TRUE if it's safe to write to the DB - in this case, you must // call ReleaseWrite when done to release the fTransactionCriticalSession function AcquireWrite(const aContext: TSQLRestServerCallBackParams): Boolean; /// release the fTransactionCriticalSession procedure ReleaseWrite; /// retrieve the server time stamp // - default implementation will use fServerTimeStampOffset to compute // the value from PC time (i.e. Now+fServerTimeStampOffset as TTimeLog) // - inherited classes may override this method, or set the appropriate // value in fServerTimeStampOffset protected field ................................................................................ // - use the TSQLAuthGroup.AccessRights CSV format function ToString: RawUTF8; /// unserialize the content from TEXT // - use the TSQLAuthGroup.AccessRights CSV format procedure FromString(P: PUTF8Char); end; TSQLRestServerStatic = class; TSQLRestServerStaticClass = class of TSQLRestServerStatic; TSQLRestServerStaticInMemory = class; TSQLVirtualTableModule = class; {/ table containing the available user access rights for authentication ................................................................................ fLastAccess: cardinal; fID: RawUTF8; fIDCardinal: cardinal; fTimeOut: cardinal; fAccessRights: TSQLAccessRights; fPrivateKey: RawUTF8; fPrivateSalt: RawUTF8; fSentHeaders: RawUTF8; fRemoteIP: RawUTF8; fPrivateSaltHash: Cardinal; fLastTimeStamp: Cardinal; public /// initialize a session instance with the supplied TSQLAuthUser instance // - this aUser instance will be handled by the class until Destroy // - raise an exception on any error // - on success, will also retrieve the aUser.Data BLOB field content ................................................................................ /// the number of millisedons a session is kept alive // - extracted from User.TSQLAuthGroup.SessionTimeout // - allow direct comparison with GetTickCount API call property Timeout: cardinal read fTimeOut; /// the hexadecimal private key as returned to the connected client // as 'SessionID+PrivateKey' property PrivateKey: RawUTF8 read fPrivateKey; /// the transmitted HTTP headers, if any // - can contain e.g. 'RemoteIP: 127.0.0.1' or 'User-Agent: Mozilla/4.0' property SentHeaders: RawUTF8 read fSentHeaders; /// the remote IP, if any // - is extracted from SentHeaders properties property RemoteIP: RawUTF8 read fRemoteIP; end; { we need the RTTI information to be compiled for the published methods of this TSQLRestServer class and its children (like TPersistent), to enable Server-Side ModelRoot/[TableName/[ID/]]MethodName requests -> see TSQLRestServerCallBack } ................................................................................ /// this method is overriden for setting the NoAJAXJSON field // of all associated TSQLRestServerStatic servers procedure SetNoAJAXJSON(const Value: boolean); virtual; /// search for the corresponding TSQLRestServerCallBack in its published methods, // then launch it // - the aParams parameters will be used to set a default header for the callback // - return TRUE if the method was found and run, FALSE if method was not found function LaunchCallBack(var aParams: TSQLRestServerCallBackParams): boolean; /// try to call a Service from a given URI // - this method will call any interface-based service previously registered // via ServerRegister() // - returns TRUE if the supplied method was a service name, and an error // code is returned in aResult (with an optional message in aErrorMsg) // - is in fact used internaly by the URI method: you are not likely to call // this method, but should rather call e.g. Services['Calculator'].Get(I) // to retrieve a working service interface to work with function LaunchService(var aParams: TSQLRestServerCallBackParams): boolean; /// execute a BATCH sequence // - expect input as generated by TSQLRestClientURI.Batch*() methods: // & '{"Table":["cmd":values,...]}' // or, in a table-independent way: // & '["cmd@table":values,...]' // - returns an array of integers: '[200,200,...]' function RunBatch(aStatic: TSQLRestServerStatic; aTable: TSQLRecordClass; var aCall: TSQLRestServerCallBackParams): boolean; /// fill the supplied context from the supplied aContext.Session ID // - returns nil if not found, or fill aContext.User/Group values if matchs // - this method will also check for outdated sessions, and delete them // - this method is not thread-safe: caller should use fSessionCriticalSection function SessionAccess(var aContext: TSQLRestServerCallBackParams): TAuthSession; /// delete a session from its index in fSessions[] // - will perform any needed clean-up, and log the event // - this method is not thread-safe: caller should use fSessionCriticalSection procedure SessionDelete(aSessionIndex: integer); /// returns a copy of the user associated to a session ID // - returns nil if the session does not exist (e.g. if authentication is // disabled) ................................................................................ {$ifdef MSWINDOWS} /// declare the server on the local machine as a Named Pipe: allows // TSQLRestClientURINamedPipe local or remote client connection // - ServerApplicationName ('DBSERVER' e.g.) will be used to create a named // pipe server identifier, it is of UnicodeString type since Delphi 2009 // (use of Unicode FileOpen() version) // - this server identifier is appended to '\\.\pipe\mORMot_' to obtain // the full pipe name to initiate ('\\.\pipe\mORMot_DBSERVER' e.g.) // - this server identifier may also contain a fully qualified path // ('\\.\pipe\ApplicationName' e.g.) // - allows only one ExportServer*() by running process // - returns true on success, false otherwize (ServerApplicationName already used?) function ExportServerNamedPipe(const ServerApplicationName: TFileName): boolean; /// end any currently initialized named pipe server function CloseServerNamedPipe: boolean; ................................................................................ // if you call it from the main thread, it may fail to release resources // - it is set e.g. by TSQLite3HttpServer to be called from HTTP threads, // or by TSQLRestServerNamedPipeResponse for named-pipe server cleaning procedure EndCurrentThread(Sender: TObject); virtual; /// implement a generic local, piped or HTTP/1.1 provider // - this is the main entry point of the server, from the client side // - default implementation calls protected methods EngineList() Retrieve() // Add() Update() Delete() UnLock() EngineExecute() above, which must be overriden by // the TSQLRestServer child // - for 'GET ModelRoot/TableName', url parameters can be either "select" and // "where" (to specify a SQL Query, from the SQLFromSelectWhere function), // either "sort", "dir", "startIndex", "results", as expected by the YUI // DataSource Request Syntax for data pagination - see // http://developer.yahoo.com/yui/datatable/#data procedure URI(var Call: TSQLRestServerURIParams); virtual; /// create an index for the specific FieldName // - will call CreateSQLMultiIndex() internaly function CreateSQLIndex(Table: TSQLRecordClass; const FieldName: RawUTF8; Unique: boolean; const IndexName: RawUTF8=''): boolean; overload; /// create one or multiple index(es) for the specific FieldName(s) function CreateSQLIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean): boolean; overload; /// create one index for all specific FieldNames at once function CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8=''): boolean; virtual; /// call this method to disable Authentication method check for a given // published method name // - by default, only Auth and TimeStamp methods do not require the RESTful // authentication of the URI; you may call this method to add another method // to the list (e.g. for returning some HTML content from a public URI) procedure ServiceMethodByPassAuthentication(const aMethodName: RawUTF8); ................................................................................ // (should not to be used normaly, because it will add unnecessary overhead) property StaticVirtualTableDirect: boolean read fVirtualTableDirect write fVirtualTableDirect; published /// this method will be accessible from ModelRoot/Stat URI, and // will retrieve some statistics as a JSON object // - method parameters signature matches TSQLRestServerCallBack type procedure Stat(var aParams: TSQLRestServerCallBackParams); /// this method will be accessible from ModelRoot/Auth URI, and // will be called by the client for authentication and session management // - method parameters signature matches TSQLRestServerCallBack type // - to be called in a two pass "challenging" algorithm: // $ GET ModelRoot/auth?UserName=... // $ -> returns an hexadecimal nonce contents (valid for 5 minutes) // $ GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... ................................................................................ // user which opened the session via a successful call to the Auth service // - when you don't need the session any more (e.g. if the TSQLRestClientURI // instance is destroyed), you can call the service as such: // $ GET ModelRoot/auth?UserName=...&Session=... // - for a way of computing SHA-256 in JavaScript, see for instance // @http://www.webtoolkit.info/javascript-sha256.html // - this global callback method is thread-safe procedure Auth(var aParams: TSQLRestServerCallBackParams); /// this method will be accessible from the ModelRoot/TimeStamp URI, and // will return the server time stamp TTimeLog/Int64 value as RawUTF8 // - method parameters signature matches TSQLRestServerCallBack type procedure TimeStamp(var aParams: TSQLRestServerCallBackParams); /// this method will be accessible from the ModelRoot/CacheFlush URI, and // will flush the server cache // - this method shall be called by the clients when the Server cache could // be not refreshed // - ModelRoot/CacheFlush URI will flush the whole Server cache, for all tables // - ModelRoot/CacheFlush/TableName URI will flush the specified table cache // - ModelRoot/CacheFlush/TableName/ID URI will flush the content of the // specified record // - method parameters signature matches TSQLRestServerCallBack type procedure CacheFlush(var aParams: TSQLRestServerCallBackParams); /// this method will be accessible from the ModelRoot/Batch URI, and // will execute a set of RESTful commands // - expect input as JSON commands - see TSQLRestServer.RunBatch, i.e. // & '{"Table":["cmd":values,...]}' // or for multiple tables: // & '["cmd@Table":values,...]' // with cmd in POST/PUT with {object} as value or DELETE with ID // - only accepted context HTTP verb is PUT (for thread-safe and security // reasons) procedure Batch(var aParams: TSQLRestServerCallBackParams); end; /// REST server with direct access to an external database engine // - you can set an alternate per-table database engine by using this class // - this abstract class is to be overriden with a proper implementation (like // our TSQLRestServerStaticInMemory class) TSQLRestServerStatic = class(TSQLRestServer) ................................................................................ end; /// Rest client with remote access to a server through a Named Pipe // - named pipe is fast and optimized under Windows // - can be accessed localy or remotely TSQLRestClientURINamedPipe = class(TSQLRestClientURI) private /// handle for '\\.\pipe\mORMot_TEST' e.g. fServerPipe: cardinal; /// the pipe name fPipeName: TFileName; {$ifndef ANONYMOUSNAMEDPIPE} {$ifndef NOSECURITYFORNAMEDPIPECLIENTS} fPipeSecurityAttributes: TSecurityAttributes; fPipeSecurityDescriptor: array[0..SECURITY_DESCRIPTOR_MIN_LENGTH] of byte; ................................................................................ public /// connect to a server contained in a running application // - the server must have been declared by a previous // TSQLRestServer.ExportServer(ApplicationName) call // with ApplicationName as user-defined server identifier ('DBSERVER' e.g.) // - ApplicationName is of UnicodeString type since Delphi 2009 // (direct use of Wide Win32 API version) // - this server identifier is appended to '\\.\pipe\mORMot_' to obtain // the full pipe name to connect to ('\\.\pipe\mORMot__DBSERVER' e.g.) // - this server identifier may also contain a remote computer name, and // must be fully qualified ('\\ServerName\pipe\ApplicationName' e.g.) // - raise an exception if the server is not running or invalid constructor Create(aModel: TSQLModel; const ApplicationName: TFileName); /// release memory and handles destructor Destroy; override; end; ................................................................................ // and is handled in TSQLRibbon.RefreshClickHandled WM_TIMER_REFRESH_SCREEN = 1; /// timer identifier which indicates we must refresh the Report content // - used for User Interface generation // - is handled in TSQLRibbon.RefreshClickHandled WM_TIMER_REFRESH_REPORT = 2; /// create a TRecordReference with the corresponding parameters function RecordReference(Model: TSQLModel; aTable: TSQLRecordClass; aID: integer): TRecordReference; /// convert a dynamic array of TRecordRef into its corresponding IDs procedure RecordRefToID(var aArray: TIntegerDynArray); ................................................................................ /// the currently running service factory // - it can be used within server-side implementation to retrieve the // associated TSQLRestServer instance Factory: TServiceFactoryServer; /// the currently runnning session identifier which launched the method // - make available the current session or authentication parameters // (including e.g. user details via Factory.RestServer.SessionGetUser) Session: ^TSQLRestServerCallBackParams; /// the thread which launched the request // - is set by TSQLRestServer.BeginCurrentThread from multi-thread server // handlers - e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse RunningThread: TThread; end; threadvar ................................................................................ WhereClause := FormatUTF8('% MATCH :(''%''): ORDER BY rank(matchinfo(%)', [SQLTableName,MatchClause,SQLTableName]); for i := 0 to high(PerFieldWeight) do WhereClause := FormatUTF8('%,:(%):',[WhereClause,PerFieldWeight[i]]); result := FTSMatch(Table,WhereClause+') DESC',DocID); end; function TSQLRest.AcquireWrite(const aContext: TSQLRestServerCallBackParams): Boolean; var Start, Now: Cardinal; begin if self<>nil then begin Start := GetTickCount; repeat if TryEnterCriticalSection(fTransactionCriticalSession) then begin if (fTransactionActive=0) or (fTransactionActive=aContext.Session) then begin ................................................................................ function TSQLRestClientURI.EngineUpdateField(Table: TSQLRecordClass; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; begin if (self=nil) or (Table=nil) then result := false else // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=.. result := URI(FormatUTF8('%?setname=%&set=%&wherename=%&where=%', [Model.URI[Table],SetFieldName,UrlEncode(SetValue),WhereFieldName, UrlEncode(WhereValue)]),'PUT').Lo=HTML_SUCCESS; end; { TSQLRestServer } {$ifdef MSWINDOWS} const ServerPipeNamePrefix: TFileName = '\\.\pipe\mORMot_'; var GlobalURIRequestServer: TSQLRestServer = nil; function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl; function StringToPCharCopy(const s: RawUTF8): PUTF8Char; var L: integer; begin L := length(s); if L=0 then result := nil else begin inc(L); // copy also last #0 from s if USEFASTMM4ALLOC then GetMem(result,L) else result := pointer(GlobalAlloc(GMEM_FIXED,L)); move(pointer(s)^,result^,L); end; end; var call: TSQLRestServerURIParams; begin if GlobalURIRequestServer=nil then begin Int64(result) := HTML_NOTIMPLEMENTED; // 501 exit; end; call.Url := url; call.Method := method; call.InBody := SendData; call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; GlobalURIRequestServer.URI(call); result.Lo := call.OutStatus; result.Hi := call.OutInternalState; if Head<>nil then Head^ := StringToPCharCopy(call.OutHead); if Resp<>nil then Resp^ := StringToPCharCopy(call.OutBody); end; function ReadString(Handle: cardinal): RawUTF8; var L, Read: cardinal; P: PUTF8Char; begin result := ''; ................................................................................ result := true; end; const MAGIC_SYN: cardinal = $A5ABA5AB; procedure TSQLRestServer.AnswerToMessage(var Msg: TWMCopyData); var call: TSQLRestServerURIParams; P: PUTF8Char; Res: packed record Magic: cardinal; Status: cardinal; InternalState: cardinal; end; Data: TCopyDataStruct; ResStr: RawUTF8; begin Msg.Result := HTML_NOTFOUND; if (self=nil) or (Msg.From=0) then exit; P := Msg.CopyDataStruct^.lpData; if (P=nil) or (Msg.CopyDataStruct^.cbData<=7) then exit; if PCardinal(P)^<>MAGIC_SYN then exit; // invalid layout: a broadcasted WM_COPYDATA message? :( inc(P,4); // #1 is a field delimiter below, since Get*Item() functions return nil for #0 Msg.Result := HTML_SUCCESS; // Send something back call.Url := GetNextItem(P,#1); call.Method := GetNextItem(P,#1); call.InHead := GetNextItem(P,#1); with Msg.CopyDataStruct^ do SetString(call.InBody,P,PtrInt(cbData)-(P-lpData)); call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; // note: it's up to URI overriden method to implement access rights URI(call); Res.Magic := MAGIC_SYN; Res.Status := call.OutStatus; Res.InternalState := call.OutInternalState; SetString(ResStr,PAnsiChar(@Res),sizeof(Res)); ResStr := ResStr+call.OutHead+#1+call.OutBody; Data.dwData := fServerWindow; Data.cbData := length(ResStr); Data.lpData := pointer(ResStr); { TODO : 64-bits windows: can we store a PtrInt value in WM_COPYDATA? } SendMessage(Msg.From,WM_COPYDATA,fServerWindow,PtrInt(@Data)); end; function TSQLRestServer.CloseServerNamedPipe: boolean; begin if fExportServerNamedPipeThread<>nil then begin ................................................................................ begin result := true; for i := 0 to high(FieldNames) do if not CreateSQLMultiIndex(Table,[FieldNames[i]],Unique) then result := false; end; procedure TSQLRestServer.ServiceMethodByPassAuthentication(const aMethodName: RawUTF8); var i: Integer; begin if self=nil then exit; i := fPublishedMethods.Find(aMethodName); if i>=0 then AddInteger(fPublishedMethodsUnauthenticated,fPublishedMethodsUnauthenticatedCount,i,True); end; procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8); begin case Code of 100: result := 'Continue'; HTML_SUCCESS: result := 'OK'; HTML_CREATED: result := 'Created'; 202: result := 'Accepted'; 203: result := 'Non-Authoritative Information'; 204: result := 'No Content'; 300: result := 'Multiple Choices'; 301: result := 'Moved Permanently'; 302: result := 'Found'; 303: result := 'See Other'; HTML_NOTMODIFIED:result := 'Not Modified'; 307: result := 'Temporary Redirect'; HTML_BADREQUEST: result := 'Bad Request'; 401: result := 'Unauthorized'; HTML_FORBIDDEN: result := 'Forbidden'; HTML_NOTFOUND: result := 'Not Found'; 405: result := 'Method Not Allowed'; 406: result := 'Not Acceptable'; HTML_SERVERERROR:result := 'Internal Server Error'; HTML_UNAVAILABLE:result := 'Service Unavailable'; else result := 'Invalid Request'; end; end; procedure TSQLRestServerCallBackParams.Returns(const Result: RawUTF8; Status: integer; const CustomHeader: RawUTF8); begin if Status in [HTML_SUCCESS,HTML_CREATED] then begin Call.OutStatus := Status; Call.OutBody := Result; end else Error(Result,Status); end; procedure TSQLRestServerCallBackParams.Returns(const NameValuePairs: array of const; Status: integer); begin Returns(JSONEncode(NameValuePairs),Status); end; procedure TSQLRestServerCallBackParams.Results(const Values: array of const; Status: integer); var i,h: integer; begin h := high(Values); if h<0 then Call.OutBody := '{"result":null}' else with TTextWriter.CreateOwnedStream do try AddShort('{"result":'); if h=0 then // result is one value AddJSONEscape(Values[0]) else begin // result is one array of values Add('['); i := 0; repeat AddJSONEscape(Values[i]); if i=h then break; Add(','); inc(i); until false; Add(']'); end; Add('}'); SetText(Call.OutBody); finally Free; end; Call.OutStatus := Status; end; procedure TSQLRestServerCallBackParams.Success(Status: integer); begin if Status in [HTML_SUCCESS,HTML_CREATED] then Call.OutStatus := Status else Error('',Status); end; procedure TSQLRestServerCallBackParams.Error(Format: PUTF8Char; const Args: array of const; Status: integer); begin Error(FormatUTF8(Format,Args),Status); end; procedure TSQLRestServerCallBackParams.Error(const ErrorMessage: RawUTF8; Status: integer); var ErrorMsg: RawUTF8; begin if Status in [HTML_SUCCESS,HTML_CREATED] then begin // not an error Call.OutStatus := Status; Call.OutBody := ErrorMessage; exit; end; if ErrorMessage='' then StatusCodeToErrorMsg(Status,ErrorMsg) else ErrorMsg := ErrorMessage; {$ifdef WITHLOG} Log.Log(sllServer,'% % ERROR=% (%)',[Call.Method,URI,Call.OutStatus,ErrorMsg]); {$endif} if Call.OutBody='' then // return error content as JSON object, if not set with TTextWriter.CreateOwnedStream do try AddShort('{'#13#10'"ErrorCode":'); Add(call.OutStatus); AddShort(#13#10'"ErrorText":"'); AddJSONEscape(pointer(ErrorMsg)); AddShort('"'#13#10'}'); SetText(Call.OutBody); finally Free; end; end; function TSQLRestServer.LaunchCallBack(var aParams: TSQLRestServerCallBackParams): boolean; var Method: TMethod; Invoke: TSQLRestServerCallBack absolute Method; begin result := false; if (aParams.MethodIndex<0) or (self=nil) then exit; Method.Code := pointer(fPublishedMethods.List[aParams.MethodIndex].Tag); // launch the method found {$ifdef WITHLOG} aParams.Log.Log(sllServiceCall,fPublishedMethods.List[aParams.MethodIndex].Name,self); {$endif} result := true; // mark method found and executed try Method.Data := Self; Invoke(aParams); inc(fStats.fServices); except on E: Exception do // execution errors are intercepted and returned as such aParams.Error('Exception %: %', [PShortString(PPointer(PtrInt(E)+vmtClassName)^)^,E.Message], HTML_SERVERERROR); end; end; type TServiceInternalMethod = (imFree, imContract, imSignature); const ................................................................................ exit; if fServices=nil then fServices := TServiceContainerServer.Create(self); result := (fServices as TServiceContainerServer).AddInterface( aInterfaces,aInstanceCreation,aContractExpected); end; function TSQLRestServer.LaunchService(var aParams: TSQLRestServerCallBackParams): boolean; var Service: TServiceFactory; method, JSON: RawUTF8; Values: TPUtf8CharDynArray; ServiceParams: PUTF8Char; i, m, ServiceID: integer; internal: TServiceInternalMethod; begin result := false; if Services=nil then exit; // 1. retrieve request parameters according to routing scheme ServiceID := aParams.ID; case ServicesRouting of rmRest: begin i := Services.fListInterfaceMethod.IndexOf(aParams.URI); if i<0 then exit; // no specific message: it may be a valid request i := PtrInt(Services.fListInterfaceMethod.Objects[i]); m := i shr 16; Service := Services.Index(i and $ffff); if Service=nil then exit; if aParams.Call.InBody<>'' then // parameters sent as JSON array (the Delphi/AJAX way) ServiceParams := pointer(aParams.Call.InBody) else begin JSON := UrlDecode(aParams.Parameters); // optional URI decoding (the HTML way) ServiceParams := pointer(JSON); end; if ServiceID<0 then ServiceID := 0; {$ifdef WITHLOG} aParams.Log.Log(sllServiceCall,aParams.URI,self); {$endif} end; rmJSON_RPC: begin Service := Services[aParams.URI]; if Service=nil then // Unknown service exit; // not a valid JSON-RPC service JSON := aParams.Call.InBody; // in-place parsing -> private copy JSONDecode(JSON,['method','params','id'],Values,True); if Values[0]=nil then // Method name required exit; result := true; // sounds like a valid JSON-RPC signature method := Values[0]; ServiceParams := Values[1]; ServiceID := GetCardinal(Values[2]); m := Service.fInterface.FindMethodIndex(method); if m>=0 then inc(m,length(SERVICE_PSEUDO_METHOD)) else begin for internal := low(TServiceInternalMethod) to high(TServiceInternalMethod) do if IdemPropNameU(method,SERVICE_PSEUDO_METHOD[internal]) then begin m := ord(internal); break; end; if m<0 then begin aParams.Error('Unknown method'); exit; end; end; {$ifdef WITHLOG} aParams.Log.Log(sllServiceCall,'%.%',[aParams.URI,method],self); {$endif} end; else exit; end; // 2. this is a valid service call -> handle request if ServiceParams=nil then begin aParams.Error('Parameters required'); exit; end; inc(fStats.fServices); case m of ord(imFree): // "method":"_free_" to release sicClientDriven..sicPerGroup if ServiceID<=0 then // expects an instance ID to be released exit else m := -1; // notify ExecuteMethod() to release the internal instance ord(imContract): begin // "method":"_contract_" to retrieve the implementation contract aParams.Returns('{"result":['+Service.ContractExpected+'],"id":0}'); result := true; exit; // "id":0 for this method -> no instance was created end; ord(imSignature): begin // "method":"_signature_" to retrieve the implementation signature if TServiceContainerServer(Services).PublishSignature then begin aParams.Returns('{"result":['+Service.Contract+'],"id":0}'); result := true; // "id":0 for this method -> no instance was created end; exit; // not allowed to publish signature end; else dec(m,length(SERVICE_PSEUDO_METHOD)); // index of operation in fMethods[] end; if (aParams.Session>CONST_AUTHENTICATION_NOT_USED) and (m>=0) and (aParams.SessionGroup-1 in Service.fExecution[m].Denied) then begin aParams.Error('Unauthorized method'); exit; end; TServiceFactoryServer(Service).ExecuteMethod(aParams,m,ServiceID,ServiceParams); result := true; // notify method found (any error status is in aResult) end; function TSQLRestServer.RunBatch(aStatic: TSQLRestServerStatic; aTable: TSQLRecordClass; var aCall: TSQLRestServerCallBackParams): boolean; var EndOfObject: AnsiChar; wasString, OK: boolean; TableName, Value, ErrMsg: RawUTF8; URIMethod, RunningBatchURIMethod: TSQLURIMethod; RunningBatchStatic: TSQLRestServerStatic; { TODO: allow nested batch between tables? } Sent, Method, MethodTable: PUTF8Char; Props: TSQLRecordProperties; i, ID, Count: integer; Results: TIntegerDynArray; RunTable: TSQLRecordClass; RunStatic: TSQLRestServerStatic; begin result := false; Sent := pointer(aCall.Call.InBody); if (self=nil) or (Sent=nil) then exit; if aTable<>nil then begin // unserialize expected sequence array as '{"Table":["cmd":values,...]}' while Sent^<>'{' do inc(Sent); if Sent^<>'{' then exit; ................................................................................ RunningBatchURIMethod := URIMethod; end; // process CRUD method operation case URIMethod of mDELETE: begin // '{"Table":[...,"DELETE":ID,...]}' or '[...,"DELETE@Table":ID,...]' ID := GetInteger(GetJSONField(Sent,Sent,@wasString,@EndOfObject)); if (ID<=0) or wasString or not RecordCanBeUpdated(RunTable,ID,seDelete,@ErrMsg) then begin aCall.Error(ErrMsg,HTML_NOTMODIFIED); exit; end; if RunStatic<>nil then OK := RunStatic.EngineDelete(RunTable,ID) else OK := EngineDelete(RunTable,ID); if OK then begin fCache.NotifyDeletion(RunTable,ID); if (RunningBatchStatic<>nil) or AfterDeleteForceCoherency(RunTable,ID) then Results[Count] := HTML_SUCCESS; // 200 OK end; end; mPOST: begin // '{"Table":[...,"POST":{object},...]}' or '[...,"POST@Table":{object},...]' Value := JSONGetObject(Sent,nil,EndOfObject); if (Sent=nil) or not RecordCanBeUpdated(RunTable,0,seAdd,@ErrMsg) then begin aCall.Error(ErrMsg,HTML_NOTMODIFIED); exit; end; if RunStatic<>nil then ID := RunStatic.EngineAdd(RunTable,Value) else ID := EngineAdd(RunTable,Value); Results[Count] := ID; fCache.Notify(RunTable,ID,Value,soInsert); end; mPUT: begin // '{"Table":[...,"PUT":{object},...]}' or '[...,"PUT@Table":{object},...]' ................................................................................ while Sent^<>'}' do inc(Sent); result := Sent^='}'; end else result := true; // send back operation status array for i := 0 to Count-1 do if Results[i]<>HTML_SUCCESS then begin aCall.Call.OutBody := IntegerDynArrayToCSV(Results,Count,'[',']'); exit; end; aCall.Call.OutBody := '["OK"]'; // to save bandwith if no adding end; function StringToMethod(const method: RawUTF8): TSQLURIMethod; const NAME: array[mGET..high(TSQLURIMethod)] of string[7] = ( 'GET','POST','PUT','DELETE','BEGIN','END','ABORT','LOCK','UNLOCK','STATE'); var URIMethodUp: string[7]; begin ................................................................................ for result := low(NAME) to high(NAME) do if URIMethodUp=NAME[result] then exit; end; result := mNone; end; procedure TSQLRestServer.URI(var Call: TSQLRestServerURIParams); var BlobFieldName: RawUTF8; Static: TSQLRestServerStatic; Engine: TSQLRestServer; i,j,L: PtrInt; SQLSelect, SQLWhere, SQLSort, SQLDir, SQL, ErrMsg: RawUTF8; SQLStartIndex, SQLResults: integer; StaticKind: (sNone, sInMemory, sVirtual); SQLisSelect, OK: boolean; URI: TSQLRestServerCallBackParams; Session: TAuthSession; SessionAccessRights: TSQLAccessRights; // session may be deleted meanwhile P: PUTF8Char; Blob: PPropInfo; {$ifdef WITHSTATPROCESS} timeStart,timeEnd: Int64; {$endif} begin {$ifdef WITHLOG} URI.Log := SQLite3Log.Enter; {$endif} {$ifdef WITHSTATPROCESS} QueryPerformanceCounter(timeStart); {$endif} URI.Call := @Call; // 0. always return internal database state count (even if URI is '') Call.OutInternalState := InternalState; // other threads may change it L := length(Call.url); inc(fStats.fIncomingBytes,L+length(call.method)+length(call.InHead)+length(call.InBody)+12); // 1. retrieve URI expecting 'ModelRoot[/TableName[/ID[/BlobFieldName]]]' format i := 0; if (Call.url<>'') and (Call.url[1]='/') then inc(i); // URL may be '/path' j := length(Model.Root); if (i+j>L) or (not(Call.url[i+j+1] in [#0,'/','?'])) or (StrCompIL(pointer(PtrInt(Call.url)+i),pointer(Model.Root),j,0)<>0) then begin URI.Error('Invalid Root',HTML_NOTFOUND); inc(fStats.fInvalid); exit; // bad ModelRoot -> caller can try another TSQLRestServer end; Call.OutStatus := HTML_BADREQUEST; // default error code is 400 BAD REQUEST URI.URI := copy(Call.url,j+i+2,maxInt); i := PosEx(RawUTF8('/'),URI.URI,1); if i>0 then begin URI.Parameters := @URI.URI[i+1]; URI.ID := GetNextItemCardinal(URI.Parameters,'/'); if (URI.ID>0) and (URI.Parameters<>nil) then begin // for URL like "ModelRoot/TableName/ID/BlobFieldName" P := PosChar(URI.Parameters,'?'); if P=nil then BlobFieldName := URI.Parameters else SetString(BlobFieldName,PAnsiChar(URI.Parameters),P-URI.Parameters); end; SetLength(URI.URI,i-1); j := PosEx(RawUTF8('?'),Call.url,1); if j>0 then // '?select=...&where=...' or '?where=...' URI.Parameters := @Call.url[j+1] else URI.Parameters := nil; end else begin URI.ID := -1; i := PosEx(RawUTF8('?'),Call.url,1); if i>0 then begin // '?select=...&where=...' or '?where=...' URI.Parameters := @Call.url[i+1]; i := PosEx(RawUTF8('?'),URI.URI); if i>0 then dec(i); SetLength(URI.URI,i); end else URI.Parameters := nil; // no parameter end; ................................................................................ end; if Static<>nil then Engine := Static; end; // 2. handle security Session := nil; if HandleAuthentication then begin URI.Session := CONST_AUTHENTICATION_SESSION_NOT_STARTED; // check session_signature=... parameter if URI.Parameters<>nil then begin // expected format is 'session_signature='Hexa8(SessionID)+Hexa8(TimeStamp)+ // Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+ // Hexa8(TimeStamp)+url)) i := L-(17+24); if (i>0) and // should be LAST parameter in URL IdemPChar(@Call.url[i],'SESSION_SIGNATURE=') and HexDisplayToCardinal(@Call.url[i+18],URI.Session) then begin EnterCriticalSection(fSessionCriticalSection); try Session := SessionAccess(URI); if Session.IsValidURL(Call.url,i-2) then begin // supplied RestAccessRights is ignored and replaced by the user rights {$ifdef WITHLOG} URI.Log.Log(sllUserAuth,'%/%',[Session.User.LogonName,URI.Session],self); {$endif} move(Session.fAccessRights,SessionAccessRights,sizeof(TSQLAccessRights)); Call.RestAccessRights := @SessionAccessRights; end else // mark invalid query authentication Session := nil; finally LeaveCriticalSection(fSessionCriticalSection); end; end; end; if (Session=nil) and ((URI.MethodIndex<0) or not IntegerScanExists(pointer(fPublishedMethodsUnauthenticated), fPublishedMethodsUnauthenticatedCount,URI.MethodIndex)) then begin // /auth + /timestamp are e.g. allowed services without signature URI.Error('',HTML_FORBIDDEN); // 403 in case of authentication failure // 401 Unauthorized response MUST include a WWW-Authenticate header, // which is not what we used, so we won't send 401 error code but 403 inc(fStats.fInvalid); exit; // authentication error -> caller can try to open another session end; end else begin // default unique session if authentication is not enabled URI.Session := CONST_AUTHENTICATION_NOT_USED; URI.SessionUser := 0; URI.SessionGroup := 0; end; // 3. call appropriate database commands URI.Method := StringToMethod(call.method); case URI.Method of mLOCK,mGET: begin if URI.Table=nil then begin if (URI.Method<>mLOCK) and // GET ModelRoot/MethodName + parameters sent in URI ((URI.MethodIndex<0) or not LaunchCallBack(URI)) then if (URI.URI='') or not (reService in Call.RestAccessRights^.AllowRemoteExecute) or // GET ModelRoot/Service.Method[/ID] + parameters sent as JSON or in URI not LaunchService(URI) then begin if (Call.InBody='') and (URI.Parameters<>nil) and (reUrlEncodedSQL in Call.RestAccessRights^.AllowRemoteExecute) then begin // GET with a SQL statement sent in URI, as sql=.... while not UrlDecodeValue(URI.Parameters,'SQL=',SQL,@URI.Parameters) do if URI.Parameters=nil then break; end else // GET with a SQL statement sent as UTF-8 body SQL := Call.InBody; SQLisSelect := isSelect(pointer(SQL)); if (SQL<>'') and (SQLisSelect or (reSQL in Call.RestAccessRights^.AllowRemoteExecute)) then begin // no user check for SELECT: see TSQLAccessRights.GET comment Static := InternalAdaptSQL(Model.GetTableIndexFromSQLSelect(SQL,false),SQL); if Static<>nil then Engine := Static; Call.OutBody := Engine.EngineList(SQL); // security note: only first statement is run by EngineList() if Call.OutBody<>'' then begin // got JSON list '[{...}]' ? Call.OutStatus := HTML_SUCCESS; // 200 OK if not SQLisSelect then inc(fStats.fModified); end; end; end; end else // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1] if not (URI.TableIndex in Call.RestAccessRights^.GET) then // check User Access Call.OutStatus := HTML_FORBIDDEN else begin if URI.ID>0 then begin // GET ModelRoot/TableName/ID[/BlobFieldName] to retrieve one member, // with or w/out locking, or a specified BLOB field content if URI.Method=mLOCK then // LOCK is to be followed by PUT -> check user if not (URI.TableIndex in Call.RestAccessRights^.PUT) then Call.OutStatus := HTML_FORBIDDEN else if Model.Lock(URI.TableIndex,URI.ID) then URI.Method := mGET; // mark successfully locked if URI.Method<>mLOCK then if BlobFieldName<>'' then begin // GET ModelRoot/TableName/ID/BlobFieldName: retrieve BLOB field content Blob := URI.Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName); if Blob<>nil then begin if Engine.EngineRetrieveBlob(URI.Table,URI.ID,Blob,TSQLRawBlob(Call.OutBody)) then begin Call.OutHead := HEADER_CONTENT_TYPE+ GetMimeContentType(pointer(Call.OutBody),Length(Call.OutBody)); Call.OutStatus := HTML_SUCCESS; // 200 OK end; end else begin // GET ModelRoot/TableName/ID/MethodName: try MethodName URI.MethodIndex := fPublishedMethods.Find(BlobFieldName); if URI.MethodIndex>=0 then LaunchCallBack(URI); end; end else begin // GET ModelRoot/TableName/ID: retrieve a member content, JSON encoded Call.OutBody := fCache.Retrieve(URI.TableIndex,URI.ID); if Call.OutBody='' then begin // get JSON object '{...}' Call.OutBody := Engine.EngineRetrieve(URI.TableIndex,URI.ID); // cache if expected fCache.Notify(URI.TableIndex,URI.ID,Call.OutBody,soSelect); end; if Call.OutBody<>'' then // if something was found Call.OutStatus := HTML_SUCCESS; // 200 OK end; end else // ModelRoot/TableName with 'select=..&where=' or YUI paging if URI.Method<>mLOCK then begin // LOCK not available here SQLSelect := 'RowID'; // if no select is specified (i.e. ModelRoot/TableName) // all IDs of this table are returned to the client SQLWhere := ''; if URI.Parameters<>nil then begin // '?select=...&where=...' or '?where=...' SQLStartIndex := 0; SQLResults := 0; if URI.Parameters^<>#0 then ................................................................................ end; if (SQLResults<>0) and not ContainsUTF8(pointer(SQLWhere),'LIMIT ') then SQLWhere := FormatUTF8('% LIMIT % OFFSET %',[SQLWhere,SQLResults,SQLStartIndex]); end; SQL := Model.Props[URI.Table].SQLFromSelectWhere(SQLSelect,trim(SQLWhere)); if (Static<>nil) and (StaticKind=sInMemory) then // manual retrieval (no SQLite3 module available for fStaticData[]) Call.OutBody := Static.EngineList(SQL) else // TSQLVirtualTableJSON/External will rely on their SQLite3 module Call.OutBody := EngineList(SQL); if Call.OutBody<>'' then // got JSON list '[{...}]' ? Call.OutStatus := HTML_SUCCESS; // 200 OK end; end; end; mUNLOCK: begin // ModelRoot/TableName/ID to unlock a member if not (URI.TableIndex in Call.RestAccessRights^.PUT) then Call.OutStatus := HTML_FORBIDDEN else if (URI.Table<>nil) and (URI.ID>0) and Model.UnLock(URI.Table,URI.ID) then Call.OutStatus := HTML_SUCCESS; // 200 OK end; mSTATE: begin // STATE method for TSQLRestClientURI.ServerInternalState // this method is called with Root (-> Table=nil -> Static=nil) // we need a specialized method in order to avoid fStats.Invalid increase Call.OutStatus := HTML_SUCCESS; for i := 0 to high(fStaticData) do if fStaticData[i]<>nil then if fStaticData[i].RefreshedAndModified then begin inc(InternalState); // force refresh break; end; end else // write methods (mPOST, mPUT, mDELETE...) are handled separately if (URI.Table<>nil) or (URI.Method<>mPOST) or // check thread-safe call of service not (reService in Call.RestAccessRights^.AllowRemoteExecute) or // POST ModelRoot/Service.Method[/ID] + parameters sent as JSON or in URI not LaunchService(URI) then // now we have to handle a write to the DB (POST/PUT/DELETE...) if AcquireWrite(URI) then // make it thread-safe and transaction-safe try case URI.Method of mPOST: begin // POST=ADD=INSERT if URI.Table=nil then begin // ModelRoot with free SQL statement sent as UTF-8 (only for Admin group) // security note: multiple SQL statements can be run in EngineExecuteAll() if (reSQL in Call.RestAccessRights^.AllowRemoteExecute) and EngineExecuteAll(Call.InBody) then begin Call.OutStatus := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end else // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1] if not (URI.TableIndex in Call.RestAccessRights^.POST) then // check User Call.OutStatus := HTML_FORBIDDEN else if URI.ID<0 then begin // ModelRoot/TableName with possible JSON SentData: create a new member URI.ID := Engine.EngineAdd(URI.Table,Call.InBody); if URI.ID<>0 then begin Call.OutStatus := HTML_CREATED; // 201 Created Call.OutHead := 'Location: '+URI.URI+'/'+ {$ifndef ENHANCEDRTL}Int32ToUtf8{$else}IntToStr{$endif}(URI.ID); fCache.Notify(URI.TableIndex,URI.ID,Call.InBody,soInsert); inc(fStats.fModified); end; end else // ModelRoot/TableName/0 = BATCH sequence '{"Table":["cmd":values,...]}' if not (URI.TableIndex in Call.RestAccessRights^.PUT) or not (URI.TableIndex in Call.RestAccessRights^.DELETE) then // POST already checked Call.OutStatus := HTML_FORBIDDEN else if RunBatch(Static,URI.Table,URI) then Call.OutStatus := HTML_SUCCESS; end; mPUT: begin // PUT=UPDATE if URI.MethodIndex>=0 then // PUT ModelRoot/MethodName (e.g. ModelRoot/Batch) LaunchCallBack(URI) else if URI.ID>0 then begin // PUT ModelRoot/TableName/ID[/BlobFieldName] to update member/BLOB content if not (URI.TableIndex in Call.RestAccessRights^.PUT) then // check User Call.OutStatus := HTML_FORBIDDEN else if not RecordCanBeUpdated(URI.Table,URI.ID,seUpdate,@ErrMsg) then Call.OutStatus := HTML_NOTMODIFIED else begin OK := false; if BlobFieldName<>'' then begin // PUT ModelRoot/TableName/ID/BlobFieldName: update BLOB field content Blob := URI.Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName); if Blob<>nil then begin OK := Engine.EngineUpdateBlob(URI.Table,URI.ID,Blob,Call.InBody); end else begin // PUT ModelRoot/TableName/ID/MethodName: try MethodName URI.MethodIndex := fPublishedMethods.Find(BlobFieldName); if URI.MethodIndex>=0 then LaunchCallBack(URI); end; end else begin // ModelRoot/TableName/ID with JSON SentData: update a member OK := Engine.EngineUpdate(URI.Table,URI.ID,Call.InBody); if OK then fCache.NotifyDeletion(URI.TableIndex,URI.ID); // flush (no CreateTime in JSON) end; if OK then begin Call.OutStatus := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end; end else if URI.Parameters<>nil then // e.g. from TSQLRestClient.EngineUpdateField // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=.. if not (URI.TableIndex in Call.RestAccessRights^.PUT) then // check User Call.OutStatus := HTML_FORBIDDEN else begin repeat UrlDecodeValue(URI.Parameters,'SETNAME=',SQLSelect); UrlDecodeValue(URI.Parameters,'SET=',SQLDir); UrlDecodeValue(URI.Parameters,'WHERENAME=',SQLSort); UrlDecodeValue(URI.Parameters,'WHERE=',SQLWhere,@URI.Parameters); until URI.Parameters=nil; if (SQLSelect<>'') and (SQLDir<>'') and (SQLSort<>'') and (SQLWhere<>'') then if Engine.EngineUpdateField(URI.Table,SQLSelect,SQLDir,SQLSort,SQLWhere) then begin Call.OutStatus := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end; end; mDELETE: if URI.Table<>nil then if URI.ID>0 then // ModelRoot/TableName/ID to delete a member if not (URI.TableIndex in Call.RestAccessRights^.DELETE) then // check User Call.OutStatus := HTML_FORBIDDEN else if not RecordCanBeUpdated(URI.Table,URI.ID,seDelete,@ErrMsg) then Call.OutStatus := HTML_NOTMODIFIED else begin if Engine.EngineDelete(URI.Table,URI.ID) and AfterDeleteForceCoherency(URI.Table,URI.ID) then begin Call.OutStatus := HTML_SUCCESS; // 200 OK fCache.NotifyDeletion(URI.TableIndex,URI.ID); inc(fStats.fModified); end; end else if URI.Parameters<>nil then if (not (URI.TableIndex in Call.RestAccessRights^.DELETE)) or (not (reUrlEncodedDelete in Call.RestAccessRights^.AllowRemoteExecute)) then Call.OutStatus := HTML_FORBIDDEN else begin // ModelRoot/TableName?WhereClause to delete members SQLWhere := Trim(UrlDecode(URI.Parameters)); if SQLWhere<>'' then begin if Delete(URI.Table,SQLWhere) then begin Call.OutStatus := HTML_SUCCESS; // 200 OK inc(fStats.fModified); end; end; end; mBEGIN: begin // BEGIN TRANSACTION // TSQLVirtualTableJSON/External will rely on SQLite3 module // and also TSQLRestServerStaticInMemory, since COMMIT/ROLLBACK have Static=nil if TransactionBegin(URI.Table,URI.Session) then begin if (Static<>nil) and (StaticKind=sVirtual) then Static.TransactionBegin(URI.Table,URI.Session); Call.OutStatus := HTML_SUCCESS; // 200 OK end; end; mEND: begin // END=COMMIT // this method is called with Root (-> Table=nil -> Static=nil) if fTransactionTable<>nil then Static := StaticVirtualTable[fTransactionTable]; Commit(URI.Session); if Static<>nil then Static.Commit(URI.Session); Call.OutStatus := HTML_SUCCESS; // 200 OK end; mABORT: begin // ABORT=ROLLBACK // this method is called with Root (-> Table=nil -> Static=nil) if fTransactionTable<>nil then Static := StaticVirtualTable[fTransactionTable]; RollBack(URI.Session); if Static<>nil then Static.RollBack(URI.Session); Call.OutStatus := HTML_SUCCESS; // 200 OK end; end; finally ReleaseWrite; end else // AcquireWrite(SessionID) returned false (e.g. endless transaction) Call.OutStatus := HTML_TIMEOUT; // 408 Request Time-out end; // 4. returns expected result to the client if Call.OutStatus in [HTML_SUCCESS,HTML_CREATED] then begin inc(fStats.fResponses); {$ifdef WITHLOG} URI.Log.Log(sllServer,'% % -> %',[Call.Method,URI.URI,Call.OutStatus],self); {$endif} end else begin inc(fStats.fInvalid); if Call.OutBody='' then // if no custom error message, compute it now as JSON URI.Error(ErrMsg,Call.OutStatus); end; inc(fStats.fOutcomingBytes,length(Call.OutHead)+length(Call.OutBody)+16); if (Static<>nil) and (StaticKind=sInMemory) then // force always refresh for Static table Call.OutInternalState := cardinal(-1) else // database state may have changed above Call.OutInternalState := InternalState; {$ifdef WITHSTATPROCESS} QueryPerformanceCounter(timeEnd); inc(fStats.ProcessTimeCounter,timeEnd-timeStart); {$endif} end; procedure TSQLRestServer.Stat(var aParams: TSQLRestServerCallBackParams); begin aParams.Returns(Stats.DebugMessage); // transmitted as JSON object end; procedure TSQLRestServer.TimeStamp(var aParams: TSQLRestServerCallBackParams); begin aParams.Returns(Int64ToUtf8(ServerTimeStamp),HTML_SUCCESS,TEXT_CONTENT_TYPE_HEADER); end; procedure TSQLRestServer.CacheFlush(var aParams: TSQLRestServerCallBackParams); begin if aParams.Table=nil then Cache.Flush else if aParams.ID=0 then Cache.Flush(aParams.Table) else Cache.SetCache(aParams.Table,aParams.ID); aParams.Success; end; procedure TSQLRestServer.Batch(var aParams: TSQLRestServerCallBackParams); begin if (aParams.Method=mPUT) and RunBatch(nil,nil,aParams) then aParams.Success else aParams.Error; end; function Nonce(Previous: boolean): RawUTF8; var Tix: cardinal; tmp: RawByteString; begin Tix := GetTickCount div (1000*60*5); // valid for 5*60*1000 ms = 5 minutes if Previous then dec(Tix); SetString(tmp,PAnsiChar(@Tix),sizeof(Tix)); result := SHA256(tmp); end; procedure TSQLRestServer.Auth(var aParams: TSQLRestServerCallBackParams); procedure CreateNewSession(var User: TSQLAuthUser; var aParams: TSQLRestServerCallBackParams); var Session: TAuthSession; begin if User.fID=0 then begin {$ifdef WITHLOG} aParams.Log.Log(sllUserAuth, 'User.LogonName=% not found in AuthUser table',[User.LogonName],self); {$endif} exit; // unknown user -> error 404 end; Session := TAuthSession.Create(self,User); try if aParams.Call.InHead<>'' then begin Session.fSentHeaders := aParams.Call.InHead; Session.fRemoteIP := FindIniNameValue(pointer(aParams.Call.InHead),'REMOTEIP: '); end; aParams.Returns(['result',Session.fPrivateSalt,'logonname',User.LogonName]); User := nil; // will be freed by TAuthSession.Destroy if fSessions=nil then fSessions := TObjectList.Create; fSessions.Add(Session); Session := nil; // will be freed by fSessions finally Session.Free; ................................................................................ InDataEnc: RawUTF8; CtxArr: TDynArray; Now: QWord; SecCtxIdx: Integer; OutData: RawByteString; {$endif} begin if not UrlDecodeNeedParameters(aParams.Parameters,'UserName') then begin aParams.Error('Expect UserName parameter'); exit; end; EnterCriticalSection(fSessionCriticalSection); try if UrlDecodeNeedParameters(aParams.Parameters,'Session') then begin // GET ModelRoot/auth?UserName=...&Session=... -> release session while aParams.Parameters<>nil do begin UrlDecodeValue(aParams.Parameters,'USERNAME=',aUserName); UrlDecodeCardinal(aParams.Parameters,'SESSION=',aSessionID,@aParams.Parameters); end; if (fSessions<>nil) and // allow only to delete its own session - ticket [7723fa7ebd] (aSessionID=aParams.Session) then for i := 0 to fSessions.Count-1 do with TAuthSession(fSessions.List[i]) do if fIDCardinal=aSessionID then begin SessionDelete(i); aParams.Success; break; end; exit; // unknown session -> error 404 end else if UrlDecodeNeedParameters(aParams.Parameters,'PassWord,ClientNonce') then begin // GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... -> handshaking while aParams.Parameters<>nil do begin UrlDecodeValue(aParams.Parameters,'USERNAME=',aUserName); UrlDecodeValue(aParams.Parameters,'PASSWORD=',aPassWord); ................................................................................ SecCtxIdx := CtxArr.New; // add a new entry to fSSPIAuthContexts[] InvalidateSecContext(fSSPIAuthContexts[SecCtxIdx]); fSSPIAuthContexts[SecCtxIdx].ID := fSSPIAuthCounter; Inc(fSSPIAuthCounter); end; // call SSPI provider if ServerSSPIAuth(fSSPIAuthContexts[SecCtxIdx], Base64ToBin(InDataEnc), OutData) then begin aParams.Returns(['result','','id',Int64(fSSPIAuthContexts[SecCtxIdx].ID), 'data',BinToBase64(OutData)]); exit; // 1st call: send back OutData to the client end; // 2nd call: user was authenticated -> release used context ServerSSPIAuthUser(fSSPIAuthContexts[SecCtxIdx], aUserName); {$ifdef WITHLOG} SQLite3Log.Family.SynLog.Log(sllUserAuth, 'Windows Authentication success for %',[aUserName],self); ................................................................................ CreateNewSession(User,aParams); finally User.Free; end; {$endif} end else // only UserName=... -> return hexadecimal nonce content valid for 5 minutes aParams.Results([Nonce(false)]); finally LeaveCriticalSection(fSessionCriticalSection); end; end; procedure TSQLRestServer.SessionDelete(aSessionIndex: integer); begin if (self<>nil) and (cardinal(aSessionIndex)<cardinal(fSessions.Count)) then with TAuthSession(fSessions.List[aSessionIndex]) do begin if Services is TServiceContainerServer then TServiceContainerServer(Services).OnCloseSession(IDCardinal); {$ifdef WITHLOG} SQLite3Log.Family.SynLog.Log(sllUserAuth,'Deleted session %/%',[User.LogonName,IDCardinal],self); {$endif} fSessions.Delete(aSessionIndex); end; end; function TSQLRestServer.SessionAccess(var aContext: TSQLRestServerCallBackParams): TAuthSession; var i: integer; Now: cardinal; begin // caller shall be locked via fSessionCriticalSection if (self<>nil) and (fSessions<>nil) then begin // first check for outdated sessions to be deleted Now := GetTickCount; for i := fSessions.Count-1 downto 0 do with TAuthSession(fSessions.List[i]) do if Now<LastAccess then // 32 bit overflow occured fLastAccess := Now else ................................................................................ if QWord(Now)>QWord(LastAccess)+QWord(TimeOut) then SessionDelete(i); // retrieve session for i := 0 to fSessions.Count-1 do begin result := TAuthSession(fSessions.List[i]); if result.IDCardinal=aContext.Session then begin result.fLastAccess := Now; // refresh session access timestamp aContext.SessionUser := result.User.fID; aContext.SessionGroup := result.User.GroupRights.fID; exit; end; end; end; result := nil; end; ................................................................................ fChild[fMasterThreadChildIndex] := nil; InterlockedDecrement(fChildCount); end; inherited; end; procedure TSQLRestServerNamedPipeResponse.Execute; var call: TSQLRestServerURIParams; Code: integer; Ticks, Sleeper, ClientTimeOut: cardinal; Available: cardinal; begin if (fPipe=0) or (fPipe=INVALID_HANDLE_VALUE) or (fServer=nil) then exit; fServer.BeginCurrentThread(self); Ticks := 0; Sleeper := 0; ClientTimeOut := GetTickCount+30*60*1000; // disconnect client after 30 min of inactivity ................................................................................ while not Terminated do if // (WaitForSingleObject(fPipe,200)=WAIT_OBJECT_0) and = don't wait PeekNamedPipe(fPipe,nil,0,nil,@Available,nil) and (Available>=4) then begin FileRead(fPipe,Code,4); if (Code=integer(MAGIC_SYN)) // magic word for URI like request and not Terminated then try call.Url := ReadString(fPipe); call.Method := ReadString(fPipe); call.InBody := ReadString(fPipe); call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; // it's up to URI overriden method to implement access rights fServer.URI(call); FileWrite(fPipe,call.OutStatus,sizeof(cardinal)*2); WriteString(fPipe,call.OutHead); WriteString(fPipe,call.OutBody); FlushFileBuffers(fPipe); // Flush the pipe to allow the client to read Ticks := GetTickCount+20; // start sleeping after 20 ms ClientTimeOut := Ticks+30*60*1000; if ClientTimeOut<Ticks then // avoid 32 bits overflow ClientTimeOut := 30*60*1000; Sleeper := 0; Sleep(0); except ................................................................................ Sleep(Sleeper); // doesn't slow down connection but decreases CSwitch Ticks := 0; if GetTickCount>ClientTimeOut then break; // disconnect client after 30 min of inactivity end else Sleep(0); finally fServer.Stats.ClientDisconnect; DisconnectNamedPipe(fPipe); CloseHandle(fPipe); end; end; { TSQLRestClientURINamedPipe } ................................................................................ {$endif} CreatePipe; if (Pipe=INVALID_HANDLE_VALUE) or (Pipe=ERROR_PIPE_BUSY) then for Retry := 1 to NAMEDPIPE_RETRYCOUNT_IFBUSY do begin if WaitNamedPipe(pointer(fPipeName),1000) then begin // 1000 since we have sleep(128) in TSQLRestServerNamedPipe.EngineExecute CreatePipe; if Pipe<>ERROR_PIPE_BUSY then break; end; sleep(200); // wait for TSQLRestServerNamedPipe.EngineExecute to be reached end; if (Pipe=INVALID_HANDLE_VALUE) or (Pipe=ERROR_PIPE_BUSY) then begin {$ifdef WITHLOG} Log.Log(sllError,'"%" when connecting to %',[ ................................................................................ function TServiceFactoryServer.CreateInstance: TInterfacedObject; begin if fImplementationClassWithCustomCreate then result := TInterfacedObjectWithCustomCreateClass(fImplementationClass).Create else result := fImplementationClass.Create; end; procedure TServiceFactoryServer.ExecuteMethod( var aParams: TSQLRestServerCallBackParams; aMethodIndex, aInstanceID: cardinal; aParamsJSONArray: PUTF8Char); procedure Error(Msg: PUTF8Char); begin aParams.Error(Msg, [UnCamelCase(ServiceInstanceImplementationToText(InstanceCreation))]); end; var Inst: TServiceFactoryServerInstance; WR: TTextWriter; entry: PInterfaceEntry; ThreadServer: PServiceRunningContext; begin // 1. initialize Inst.Instance and Inst.InstanceID Inst.InstanceID := 0; Inst.Instance := nil; case InstanceCreation of sicSingle: if aMethodIndex>=fInterface.fMethodsCount then exit else ................................................................................ sicShared: if aMethodIndex>=fInterface.fMethodsCount then exit else Inst.Instance := fSharedInstance; sicClientDriven, sicPerSession, sicPerUser, sicPerGroup: begin if InstanceCreation=sicClientDriven then Inst.InstanceID := aInstanceID else if aParams.Session>CONST_AUTHENTICATION_NOT_USED then case InstanceCreation of // authenticated user -> handle context sicPerSession: Inst.InstanceID := aParams.Session; sicPerUser: Inst.InstanceID := aParams.SessionUser; sicPerGroup: Inst.InstanceID := aParams.SessionGroup; end else begin Error('% mode expects an authenticated session'); exit; end; if InternalInstanceRetrieve(Inst,aMethodIndex) then begin aParams.Success; exit; // {"method":"_free_", "params":[], "id":1234} end; end; end; if Inst.Instance=nil then begin Error('% instance not found or deprecated'); exit; end; // 2. call method implementation try entry := Inst.Instance.GetInterfaceEntry(fInterface.fInterfaceIID); if entry=nil then exit; ThreadServer := @ServiceContext; WR := TJSONSerializer.CreateOwnedStream; try with ThreadServer^ do begin Factory := self; Session := @aParams; end; // RunningThread is already set at thread initialization // root/calculator {"method":"add","params":[1,2]} -> {"result":[3],"id":0} try WR.AddShort('{"result":['); if not fInterface.fMethods[aMethodIndex].InternalExecute( [PAnsiChar(Inst.Instance)+entry^.IOffset],aParamsJSONArray,WR, aParams.Call.OutHead,fExecution[aMethodIndex].Options) then exit; // wrong request if aParams.Call.OutHead='' then begin // <>'' for TServiceCustomAnswer WR.AddShort('],"id":'); WR.Add(Inst.InstanceID); // only used in sicClientDriven mode WR.AddShort('}'); end; WR.SetText(aParams.Call.OutBody); aParams.Call.OutStatus := HTML_SUCCESS; except on E: Exception do aParams.Error('Exception %: %', [PShortString(PPointer(PtrInt(E)+vmtClassName)^)^,E.Message], HTML_SERVERERROR); end; finally with ThreadServer^ do begin Factory := nil; Session := nil; end; WR.Free; |
Changes to SQLite3/mORMotFastCgiServer.pas.
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 |
procedure TFastCGIServer.LogOut; begin ; // do nothing by default end; procedure TFastCGIServer.ProcessRequest(const Request: RawUTF8); var Head: RawUTF8; begin if Server=nil then ResetRequest else with Server.URI(fRequestURL, fRequestMethod, Request, fResponseContent, Head, @SUPERVISOR_ACCESS_RIGHTS) do fResponseHeaders := FormatUTF8( 'Status: %'#13#10'Server-InternalState: %'#13#10+ 'X-Powered-By: TFastCGIServer http://synopse.info'#13#10+ 'Content-Type: '+JSON_CONTENT_TYPE+#13#10+ 'Content-Length: %'#13#10+ '%', // a void line will be appened in SendResponse() method [Lo,Hi,length(fResponseContent),Head]); end; function TFastCGIServer.ReadPacked: RawUTF8; {$ifdef MSWINDOWS} var L: integer; {$endif} begin |
| | > > > | > | | | | | | < > > |
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 |
procedure TFastCGIServer.LogOut; begin ; // do nothing by default end; procedure TFastCGIServer.ProcessRequest(const Request: RawUTF8); var call: TSQLRestServerURIParams; begin if Server=nil then ResetRequest else with call do begin Url := fRequestURL; Method := fRequestMethod; InBody := Request; RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; Server.URI(call); fResponseHeaders := FormatUTF8( 'Status: %'#13#10'Server-InternalState: %'#13#10+ 'X-Powered-By: TFastCGIServer http://synopse.info'#13#10+ 'Content-Type: '+JSON_CONTENT_TYPE+#13#10+ 'Content-Length: %'#13#10+ '%', // a void line will be appened in SendResponse() method [OutStatus,OutInternalState,length(OutBody),OutHead]); end; end; function TFastCGIServer.ReadPacked: RawUTF8; {$ifdef MSWINDOWS} var L: integer; {$endif} begin |
Changes to SQLite3/mORMotHttpServer.pas.
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 |
if (Self<>nil) and (cardinal(Index)<cardinal(length(fDBServers))) then fDBServers[Index].RestAccessRights := Value; end; function TSQLHttpServer.Request( const InURL, InMethod, InHeaders, InContent, InContentType: RawByteString; out OutContent, OutContentType, OutCustomHeader: RawByteString): cardinal; var URL, Head: RawUTF8; i: integer; P: PUTF8Char; begin if (InURL='') or (InMethod='') or (OnlyJSONRequests and not IdemPChar(pointer(InContentType),'APPLICATION/JSON')) then // wrong Input parameters or not JSON request: 400 BAD REQUEST result := 400 else begin if InURL[1]='/' then // trim any left '/' from URL URL := copy(InURL,2,maxInt) else URL := InURL; result := 404; // page not found by default (in case of wrong URL) for i := 0 to high(fDBServers) do with fDBServers[i] do if Server.Model.URIMatch(URL) then with Server.URI(URL,InMethod,InContent,RawUTF8(OutContent),Head,RestAccessRights) do begin result := Lo; P := pointer(Head); if IdemPChar(P,'CONTENT-TYPE: ') then begin // change mime type if modified in HTTP header (e.g. GET blob fields) OutContentType := GetNextLine(P+14,P); Head := P; end else // default content type is JSON OutContentType := JSON_CONTENT_TYPE; Head := Trim(Trim(Head)+#13#10'Server-InternalState: '+Int32ToUtf8(Hi)); OutCustomHeader := Head; break; end; end; end; procedure TSQLHttpServer.HttpThreadTerminate(Sender: TObject); var i: integer; |
| | | | | > > > > | | | | | > |
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 |
if (Self<>nil) and (cardinal(Index)<cardinal(length(fDBServers))) then fDBServers[Index].RestAccessRights := Value; end; function TSQLHttpServer.Request( const InURL, InMethod, InHeaders, InContent, InContentType: RawByteString; out OutContent, OutContentType, OutCustomHeader: RawByteString): cardinal; var call: TSQLRestServerURIParams; i: integer; P: PUTF8Char; begin if (InURL='') or (InMethod='') or (OnlyJSONRequests and not IdemPChar(pointer(InContentType),'APPLICATION/JSON')) then // wrong Input parameters or not JSON request: 400 BAD REQUEST result := 400 else begin if InURL[1]='/' then // trim any left '/' from URL call.Url := copy(InURL,2,maxInt) else call.Url := InURL; result := 404; // page not found by default (in case of wrong URL) for i := 0 to high(fDBServers) do with fDBServers[i] do if Server.Model.URIMatch(call.Url) then begin call.Method := InMethod; call.InHead := InHeaders; call.InBody := InContent; call.RestAccessRights := RestAccessRights; Server.URI(call); result := call.OutStatus; P := pointer(call.OutHead); if IdemPChar(P,'CONTENT-TYPE: ') then begin // change mime type if modified in HTTP header (e.g. GET blob fields) OutContentType := GetNextLine(P+14,P); call.OutHead := P; end else // default content type is JSON OutContentType := JSON_CONTENT_TYPE; OutCustomHeader := Trim(Trim(call.OutHead)+#13#10'Server-InternalState: '+ Int32ToUtf8(call.OutInternalState)); OutContent := call.OutBody; break; end; end; end; procedure TSQLHttpServer.HttpThreadTerminate(Sender: TObject); var i: integer; |
Changes to SQLite3/mORMotSQLite3.pas.
1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 |
on ESQLite3Exception do result := nil; end; end; function TSQLRestClientDB.InternalURI(const url, method: RawUTF8; Resp, Head, SendData: PRawUTF8): Int64Rec; var R,H,S: RawUTF8; // temp '' string to be used when no PString is provided begin if Resp=nil then Resp := @R; if Head=nil then Head := @H; if SendData=nil then SendData := @S; result := fServer.URI(url,method,SendData^,Resp^,Head^,@FULL_ACCESS_RIGHTS); if (result.Hi=0) and (fServer.DB.InternalState<>nil) then result.Hi := fServer.DB.InternalState^; // manual update if necessary end; function TSQLRestClientDB.InternalCheckOpen: boolean; begin result := true; end; |
| | | | | > > > > > > > | | > |
1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 |
on ESQLite3Exception do result := nil; end; end; function TSQLRestClientDB.InternalURI(const url, method: RawUTF8; Resp, Head, SendData: PRawUTF8): Int64Rec; var call: TSQLRestServerURIParams; begin call.Url := url; call.Method := method; if SendData<>nil then call.InBody := SendData^; call.RestAccessRights := @FULL_ACCESS_RIGHTS; fServer.URI(call); if Head<>nil then Head^ := call.OutHead; if Resp<>nil then Resp^ := call.OutBody; result.Lo := call.OutStatus; if (call.OutInternalState=0) and (fServer.DB.InternalState<>nil) then result.Hi := fServer.DB.InternalState^ else // manual update if necessary result.Hi := call.OutInternalState; end; function TSQLRestClientDB.InternalCheckOpen: boolean; begin result := true; end; |
Changes to SynCrtSock.pas.
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
....
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
|
result := result*10+c;
inc(P);
end;
end;
const
DEFAULT_AGENT = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows; FREE)';
function THttpClientSocket.Request(const url, method: RawByteString;
KeepAlive: cardinal; const Header, Data, DataType: RawByteString; retry: boolean): integer;
procedure DoRetry(Error: integer);
begin
if retry then // retry once -> return error if already retried
result := Error else begin
................................................................................
Suspended := False;
end;
constructor THttpApiServer.CreateClone(From: THttpApiServer);
begin
inherited Create(false);
fReqQueue := From.fReqQueue;
fOnRequest := From.OnRequest;
fCompress := From.fCompress;
OnHttpThreadTerminate := From.OnHttpThreadTerminate;
fCompressAcceptEncoding := From.fCompressAcceptEncoding;
end;
destructor THttpApiServer.Destroy;
var i: Integer;
|
|
|
|
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
....
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
|
result := result*10+c; inc(P); end; end; const DEFAULT_AGENT = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows; FREE)'; function THttpClientSocket.Request(const url, method: RawByteString; KeepAlive: cardinal; const Header, Data, DataType: RawByteString; retry: boolean): integer; procedure DoRetry(Error: integer); begin if retry then // retry once -> return error if already retried result := Error else begin ................................................................................ Suspended := False; end; constructor THttpApiServer.CreateClone(From: THttpApiServer); begin inherited Create(false); fReqQueue := From.fReqQueue; fOnRequest := From.fOnRequest; fCompress := From.fCompress; OnHttpThreadTerminate := From.OnHttpThreadTerminate; fCompressAcceptEncoding := From.fCompressAcceptEncoding; end; destructor THttpApiServer.Destroy; var i: Integer; |
Changes to SynSelfTests.pas.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 ... 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 .... 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 .... 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 .... 5422 5423 5424 5425 5426 5427 5428 5429 5430 5431 5432 5433 5434 5435 .... 5637 5638 5639 5640 5641 5642 5643 5644 5645 5646 5647 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 5658 5659 5660 .... 5858 5859 5860 5861 5862 5863 5864 5865 5866 5867 5868 5869 5870 5871 5872 5873 5874 5875 5876 5877 5878 5879 5880 5881 5882 5883 5884 5885 5886 5887 5888 5889 5890 5891 5892 5893 5894 5895 5896 5897 5898 5899 5900 |
{$endif} SysUtils, {$ifndef FPC} {$ifndef LVCL} Contnrs, SynDB, {$endif} {$ifndef DELPHI5OROLDER} {$ifndef CPU64} SynSQLite3, {$ifndef LVCL} SynDBSQLite3, mORMotDB, {$endif} mORMot, mORMotSQLite3, mORMotHttpServer, ................................................................................ - if called from ModelRoot/DataAsHex with GET or PUT methods, TSQLRestServer.URI will leave aRecord=nil before launching it - implementation must return the HTTP error code (e.g. 200 as success) - Table is overloaded as TSQLRecordPeople here, and still match the TSQLRestServerCallBack prototype: but you have to check the class at runtime: it can be called by another similar but invalid URL, like ModelRoot/OtherTableName/ID/DataAsHex } function DataAsHex(var aParams: TSQLRestServerCallBackParams): Integer; {/ method used to test the Server-Side ModelRoot/Sum or ModelRoot/People/Sum Requests - implementation of this method returns the sum of two floating-points, named A and B, as in the public TSQLRecordPeople.Sum() method, which implements the Client-Side of this service - Table nor ID are never used here } function Sum(var aParams: TSQLRestServerCallBackParams): Integer; end; {$ifndef LVCL} /// a test case which will test most external DB functions of the mORMotDB unit // - the external DB will be in fact a SynDBSQLite3 instance, expecting a // test.db3 SQlite3 file available in the current directory, populated with // some TSQLRecordPeople rows ................................................................................ Check(V[3]=nil); J := BinToBase64WithMagic(U); check(PInteger(J)^ and $00ffffff=JSON_BASE64_MAGIC); {$ifndef DELPHI5OROLDER} check(BlobToTSQLRawBlob(pointer(J))=U); Base64MagicToBlob(@J[4],K); check(BlobToTSQLRawBlob(pointer(K))=U); J := TSQLRestServer.JSONEncodeResult([r]); Check(SameValue(GetExtended(pointer(JSONDecode(J)),err),r)); {$ifdef USEVARIANTS} with TTextWriter.CreateOwnedStream do try AddVariantJSON(a); Add(','); AddVariantJSON(r); Add(','); ................................................................................ FV: TFV; ModelC: TSQLModel; Client: TSQLRestClientDB; ClientDist: TSQLRestClientURI; Server: TSQLRestServer; aStatic: TSQLRestServerStaticInMemory; Curr: Currency; DaVinci, s, h: RawUTF8; Refreshed: boolean; J: TSQLTableJSON; i, n, nupd, ndx: integer; IntArray, Results: TIntegerDynArray; Data: TSQLRawBlob; DataS: THeapMemoryStream; a,b: double; ................................................................................ end; result := true; finally Free; T.Free; end; end; begin V := TSQLRecordPeople.Create; VA := TSQLRecordPeopleArray.Create; {$ifndef LVCL} VO := TSQLRecordPeopleObject.Create; {$endif} V2 := nil; ................................................................................ Check(aStatic<>nil); aStatic.LoadFromJSON(JS); // test Add() for i := 0 to aStatic.Count-1 do begin Check(Client.Retrieve(aStatic.ID[i],V),'test statement+bind speed'); Check(V.SameRecord(aStatic.Items[i]),'static retrieve'); end; // test our 'REST-minimal' SELECT statement SQL engine Server.URI(Client.SessionSign('/root/People?select=%2A&where=id%3D012'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS); Check(Hash32(S)=$9B10BE36); Server.URI(Client.SessionSign('/root/People?select=%2A&where=id%3D:(012):'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS); Check(Hash32(S)=$9B10BE36); Server.URI(Client.SessionSign('/root/People?select=%2A&where=LastName%3D%22M%C3%B4net%22'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS); Check(Hash32(S)=$7D90E964); Server.URI(Client.SessionSign('/root/People?select=%2A&where=YearOfBirth%3D1873'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS); Check(Hash32(S)=$F0818745); Server.URI(Client.SessionSign('/root/People?select=%2A'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS); Check(Hash32(S)=$17AE45E3); // test Retrieve() and Delete() Server.ExportServer; // initialize URIRequest() with the aStatic database USEFASTMM4ALLOC := true; // getmem() is 2x faster than GlobalAlloc() ClientDist := TSQLRestClientURIDll.Create(ModelC,URIRequest); try SetLength(IntArray,(aStatic.Count-1)shr 2); for i := 0 to high(IntArray) do begin ................................................................................ {$ifdef UNICODE} {$WARNINGS ON} // don't care about implicit string cast in tests {$endif} { TSQLRestServerTest } function TSQLRestServerTest.DataAsHex(var aParams: TSQLRestServerCallBackParams): Integer; var aData: TSQLRawBlob; begin result := 404; // invalid Request if (self=nil) or (aParams.Table=nil) or not aParams.Table.InheritsFrom(TSQLRecord) or (aParams.Context.ID<0) then exit; // we need a valid record and its ID if not RetrieveBlob(TSQLRecordPeople,aParams.Context.ID,'Data',aData) then exit; // impossible to retrieve the Data BLOB field aParams.Resp := JSONEncodeResult([SynCommons.BinToHex(aData)]); // idem: aResp := JSONEncode(['result',BinToHex(aRecord.fData)],TempMemoryStream); result := 200; // success end; function TSQLRestServerTest.Sum(var aParams: TSQLRestServerCallBackParams): Integer; var a,b: Extended; begin if not UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin result := 404; // invalid Request exit; end; while aParams.Parameters<>nil do begin UrlDecodeExtended(aParams.Parameters,'A=',a); UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); end; aParams.Resp := JSONEncodeResult([a+b]); // same as : aResp := JSONEncode(['result',a+b],TempMemoryStream); result := 200; // success end; {$ifndef LVCL} { TSQLRecordPeopleObject } |
< > | | | | | > > > > > > > > > | < | < | < | < | < | < | < < | | > | < < < | | < < < | | | | < < < > > > |
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 ... 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 .... 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 .... 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 .... 5422 5423 5424 5425 5426 5427 5428 5429 5430 5431 5432 5433 5434 5435 5436 5437 5438 5439 5440 5441 5442 5443 5444 .... 5646 5647 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 5658 5659 5660 5661 5662 5663 5664 .... 5862 5863 5864 5865 5866 5867 5868 5869 5870 5871 5872 5873 5874 5875 5876 5877 5878 5879 5880 5881 5882 5883 5884 5885 5886 5887 5888 5889 5890 5891 5892 5893 5894 5895 5896 |
{$endif} SysUtils, {$ifndef FPC} {$ifndef LVCL} Contnrs, SynDB, {$endif} {$ifndef CPU64} SynSQLite3, {$ifndef DELPHI5OROLDER} {$ifndef LVCL} SynDBSQLite3, mORMotDB, {$endif} mORMot, mORMotSQLite3, mORMotHttpServer, ................................................................................ - if called from ModelRoot/DataAsHex with GET or PUT methods, TSQLRestServer.URI will leave aRecord=nil before launching it - implementation must return the HTTP error code (e.g. 200 as success) - Table is overloaded as TSQLRecordPeople here, and still match the TSQLRestServerCallBack prototype: but you have to check the class at runtime: it can be called by another similar but invalid URL, like ModelRoot/OtherTableName/ID/DataAsHex } procedure DataAsHex(var aParams: TSQLRestServerCallBackParams); {/ method used to test the Server-Side ModelRoot/Sum or ModelRoot/People/Sum Requests - implementation of this method returns the sum of two floating-points, named A and B, as in the public TSQLRecordPeople.Sum() method, which implements the Client-Side of this service - Table nor ID are never used here } procedure Sum(var aParams: TSQLRestServerCallBackParams); end; {$ifndef LVCL} /// a test case which will test most external DB functions of the mORMotDB unit // - the external DB will be in fact a SynDBSQLite3 instance, expecting a // test.db3 SQlite3 file available in the current directory, populated with // some TSQLRecordPeople rows ................................................................................ Check(V[3]=nil); J := BinToBase64WithMagic(U); check(PInteger(J)^ and $00ffffff=JSON_BASE64_MAGIC); {$ifndef DELPHI5OROLDER} check(BlobToTSQLRawBlob(pointer(J))=U); Base64MagicToBlob(@J[4],K); check(BlobToTSQLRawBlob(pointer(K))=U); { J := TSQLRestServer.JSONEncodeResult([r]); Check(SameValue(GetExtended(pointer(JSONDecode(J)),err),r)); } {$ifdef USEVARIANTS} with TTextWriter.CreateOwnedStream do try AddVariantJSON(a); Add(','); AddVariantJSON(r); Add(','); ................................................................................ FV: TFV; ModelC: TSQLModel; Client: TSQLRestClientDB; ClientDist: TSQLRestClientURI; Server: TSQLRestServer; aStatic: TSQLRestServerStaticInMemory; Curr: Currency; DaVinci, s: RawUTF8; Refreshed: boolean; J: TSQLTableJSON; i, n, nupd, ndx: integer; IntArray, Results: TIntegerDynArray; Data: TSQLRawBlob; DataS: THeapMemoryStream; a,b: double; ................................................................................ end; result := true; finally Free; T.Free; end; end; procedure Direct(const URI: RawUTF8; Hash: cardinal); var call: TSQLRestServerURIParams; begin call.Method :='GET'; call.Url := Client.SessionSign(URI); call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; Server.URI(call); Check(Hash32(call.OutBody)=Hash); end; begin V := TSQLRecordPeople.Create; VA := TSQLRecordPeopleArray.Create; {$ifndef LVCL} VO := TSQLRecordPeopleObject.Create; {$endif} V2 := nil; ................................................................................ Check(aStatic<>nil); aStatic.LoadFromJSON(JS); // test Add() for i := 0 to aStatic.Count-1 do begin Check(Client.Retrieve(aStatic.ID[i],V),'test statement+bind speed'); Check(V.SameRecord(aStatic.Items[i]),'static retrieve'); end; // test our 'REST-minimal' SELECT statement SQL engine Direct('/root/People?select=%2A&where=id%3D012',$9B10BE36); Direct('/root/People?select=%2A&where=id%3D:(012):',$9B10BE36); Direct('/root/People?select=%2A&where=LastName%3D%22M%C3%B4net%22',$7D90E964); Direct('/root/People?select=%2A&where=YearOfBirth%3D1873',$F0818745); Direct('/root/People?select=%2A',$17AE45E3); // test Retrieve() and Delete() Server.ExportServer; // initialize URIRequest() with the aStatic database USEFASTMM4ALLOC := true; // getmem() is 2x faster than GlobalAlloc() ClientDist := TSQLRestClientURIDll.Create(ModelC,URIRequest); try SetLength(IntArray,(aStatic.Count-1)shr 2); for i := 0 to high(IntArray) do begin ................................................................................ {$ifdef UNICODE} {$WARNINGS ON} // don't care about implicit string cast in tests {$endif} { TSQLRestServerTest } procedure TSQLRestServerTest.DataAsHex(var aParams: TSQLRestServerCallBackParams); var aData: TSQLRawBlob; begin if (self=nil) or (aParams.Table<>TSQLRecordPeople) or (aParams.ID<0) then aParams.Error('Need a valid record and its ID') else if RetrieveBlob(TSQLRecordPeople,aParams.ID,'Data',aData) then aParams.Results([SynCommons.BinToHex(aData)]) else aParams.Error('Impossible to retrieve the Data BLOB field'); end; procedure TSQLRestServerTest.Sum(var aParams: TSQLRestServerCallBackParams); var a,b: Extended; begin if UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin while aParams.Parameters<>nil do begin UrlDecodeExtended(aParams.Parameters,'A=',a); UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters); end; aParams.Results([a+b]); end else aParams.Error('Missing Parameter'); end; {$ifndef LVCL} { TSQLRecordPeopleObject } |