Notifications
Clear all
Hi,
Hereby a sample to setup a REST-server. I'm using xb2bnet, but I suppose it won't be difficult to modify it to the httpEndpoint of Xbase++. In this code you find three functions to add/implement in the webserve.prg of xb2net. For the function httpserver() you only need to add this line:
soServer:FilterRequest := {|o| FilterRequest(o)}
This is the code, I added some comments to it:
FUNCTION HTTPServer(cPort)
***********************
Local i, nPort
if (i := At("/PORT:", scParamLine)) > 0
nPort := Val(SubStr(scParamLine, i+6))
endif
if Empty(nPort)
nPort := Val(cPort)
endif
soServer := xbHTTPServer():new( INADDR_ANY, nPort )
if soServer:ErrorCode > 0
ErrorLog("Unable to start HTTP server!" + CRLF +;
"Error code: " + NTrim(soServer:ErrorCode) + " (" + soServerErrorCode) + ")" )
Return NIL
endif
soServer:MaxConnections := 100 // max # of concurrent client threads (note: one client may spawn more than one concurrent connection)
soServer:RootDir := dc_curpath() //".\www_root" // this is the root directory where our html files are located
soServer:IndexFile := nil // "index.htm" // default file sent to client
soServer:onGET := {|o| HTTPHandler(o,.f.)} // this codeblock will be fired whenever an HTTP GET request is received
soServer:onPOST := {|o| HTTPHandler(o,.f.)}
soServer:onPUT := {|o| HTTPHandler(o,.f.)}
soServer:onDELETE := {|o| HTTPHandler(o,.f.)}
soServer:onSOAP := {|o| SOAPHandler(o)}
soServer:onMaxConnect := {|o| OnMaxConnect(o)} // this is the response that will be sent when the max # of concurrent connections is reached
soServer:onNotFound := {|o| OnNotFound(o)} // log access errors
soServer:FilterRequest := {|o| FilterRequest(o)} // pre-process all requests before they get to the standard HTTP handlers
soServer:start()
Return soServer
//-----------------------------------------------------------------------------
FUNCTION FilterRequest( oClient )
Local cUserAgent, cFileExt
Local cPath := oClientPath()
Local cRef := oClientReferrer()
cPath := if(empty(cpath),"",Lower(cPath))
if "v1.0" $ cPath // I'm using a version in my API. Only if that version is in the request, it will be processed. All the other requests are ignored.
// This is a first level of security.
RestHandler(oClient)
return .F.
endif
// this is an example of how to block some spam and hacker probes
// this can be expanded and customized by for example loading data from a config or database file
if ".php" $ cPath .or. ".cgi" $ cPath .or. "cgi-bin" $ cPath .or. ".asp" $ cPath
oClient:NoLog := .t. // don't want to record this in the log file
oClient:close() // don't bother to respond, just close the connection
Return .F.
elseif !empty(cRef) // limit referrer spam
cRef := lower(cRef)
if "best-seo" $ cRef .or. "r-e-f-e-r-e-r" $ cRef
oClient:NoLog := .t.
oClient:close()
Return .F.
endif
endif
Return .T.
STATIC PROCEDURE RESTHandler( oThread )
**********************************************
Local cWebFunction , rec , aVerder := {} , cTaal := "" , cUser:="" , cCodeVert:=""
Local cAction := oThreadPath() , bFunction , nCode := 0
Local cCommand := oThreadcommand , cEndpoint := "" , aUrl := {}
Local cOrigin := oThreadorigin() , cDossier := ""
cOrigin := "*"
// always add your headers to avoid CORS problems.
oThreadsetheader('Access-Control-Allow-Origin', cOrigin)
oThreadsetheader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS' )
oThreadsetheader('Access-Control-Allow-Headers', 'Origin, Content-Type, Content-Disposition, Content-Transfer-Encoding, X-Auth-Token, Authorization, BOA-Header')
oThreadsetheader('Access-Control-Expose-Headers', 'Content-Disposition')
// The OPTIONS commnand is a pre-evalute of the browser (See https://boa-platform.com/manual/options_command.htm)
if upper(cCommand) == "OPTIONS" // don't validate, we don't want to loose time.
recnew()
rec:error := "OPTIONS, browser pre test."
sendjson(rec)
return
endif
aUrl := dc_tokenarray(substr(cAction,2),"/")
// examples of URL's.
// /v1.0/customers/123
// /v1.0/customers/key=ABC
// /v1.0/customers/123?labels=1
if len(aUrl) < 2 // url start always with version, so an URL has at least /version/command. If not stop.
return
endif
if !upper(aUrl[2]) $ "LOGIN LICENCE" // validate the token before proceding. In case the request is LOGIN or LICENCE, the token has to be created.
averder := jwt_token("check") // The token you generated at login is send with each request. If gives the possibility to verify the access rights of the user.
if !averder[1] // jwt_token() is your own function to check that token, in this case it returns an array with info I need.
recnew()
rec:error := "Token is expired."
sendjson(rec)
return
endif
cDossier := aVerder[2] // These vars are from the token that is send. They are send as a parameter to each rest_ function.
cTaal := aVerder[3]
cUser := aVerder[4]
cCodeVert := aVerder[5]
nCode := aVerder[7]
amain(3,1,cDossier)
endif
cEndpoint := "REST_"+aUrl[2]
cWebFunction := "{|cCommand,aUrl,cDossier,cTaal,cUser,cCodeVert,nCode| " + cEndpoint+"(cCommand,aUrl,cDossier,cTaal,cUser,cCodeVert,nCode) }"
if IsFunction(cEndpoint)
// macro compile and execute the requested function
bFunction := &(cWebFunction)
eval(bFunction,cCommand,aUrl,cDossier,cTaal,cUser,cCodeVert,nCode)
// The functions gets the following as parameters.
// rest_xxx(cCommand,aPara,cDossier,cTaal,cUser,cCodeVert,nCode)
// cCommand: GET, PUT, POST, DELETE
// aPara: array made from the URL
// cDossier specific for my case, since we have a multi-company solution.
// cTaal: language of the user
// cUser: Username which defines the rights in our Application. Same business logic as our Windows application
// cCodeVert: is used to set a filter when opening files, same system as our Windows application. This way an external salesman only see his customers and his sales.
// nCode: defines which menu items are active, same logis as our Windows application.
else
oThread:NotFound()
endif
ABOCLoseAll() // close all my tables.
Return
As you can see above, it is quite similar as setting up a soapserver.
Best regards,
Chris.
Topic starter
Posted : 12/05/2022 12:30 pm