API server code sample for BOA.

Announcements about new features or events.
Post Reply
Message
Author
skiman
Posts: 1183
Joined: Thu Jan 28, 2010 1:22 am
Location: Sijsele, Belgium
Contact:

API server code sample for BOA.

#1 Post by skiman »

Hi,

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
As you can see above, it is quite similar as setting up a soapserver.
Best regards,

Chris.
www.aboservice.be

Post Reply