As promised 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:
Code: Select all
soServer:FilterRequest := {|o| FilterRequest(o)}
This is the code, I added some comments to it:
Code: Select all
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) + " (" + soServer:ErrorText(soServer:ErrorCode) + ")" )
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 := oClient:HTTPRequest:Path()
Local cRef := oClient:HTTPRequest:Referrer()
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 := oThread:HTTPRequest:Path() , bFunction , nCode := 0
Local cCommand := oThread:httpRequest:command , cEndpoint := "" , aUrl := {}
Local cOrigin := oThread:HTTPRequest:origin() , cDossier := ""
cOrigin := "*"
// always add your headers to avoid CORS problems.
oThread:HTTPResponse:setheader('Access-Control-Allow-Origin', cOrigin)
oThread:HTTPResponse:setheader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS' )
oThread:HTTPResponse:setheader('Access-Control-Allow-Headers', 'Origin, Content-Type, Content-Disposition, Content-Transfer-Encoding, X-Auth-Token, Authorization, BOA-Header')
oThread:HTTPResponse:setheader('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.
rec:=json():new()
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. Second security.
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. Again extra security.
if !averder[1] // jwt_token() is your own function to check that token, in this case it returns an array with info I need.
rec:=json():new()
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] // adding rest_ in front of the function make sure potential hackers can't use regular functions as quit, so important for security!
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