Acting as a SOAP Server

KCML can act as a server for methods exposed through the SOAP protocol. This is in addition to client support available through the CREATE statement. A KCML SOAP server can either run as a standalone daemon or be invoked by a web server such as Apache.

In both cases a program must be run via the command line. The program will need to create a dynamic SOAP server object that creates a SOAP server instance. The object also processes the Foreground program to create the RPC methods exposed to the client. For the SOAP server to be configured an embedded XML data island must be used. The data island is represented by a DEFSOAP statement.

As soon as the server object has been created and configured the program should call the method ProcessRequests() which handles all incoming SOAP requests. This is a blocking call that will only return on an error or when the SOAP client has disconnected.

Creating the SOAP object

The first step in creating a SOAP server object is to define its interfaces, namespaces, and endpoints. This can be done via a DEFSOAP statement. Entering a DEFSOAP statement via the workbench will create a basic DEFSOAP statement that can be edited. Once we have a DEFSOAP statement we can use it as an argument in the construction of the SOAP server object.

DEFSOAP mySoapServer
...
OBJECT oSoap = CREATE "dynamic","soap",USING DEFSOAP mySoapServer

After creating the SOAP server object you may wish to configure several options. You can change endpoints and timeouts programatically using the methods defined. If any method fails on the SOAP server object it will return an error code (zero equals success). It is also possible to use the methods GetLastError() and GetLastErrorString() to retrieve info about the last error to occur.

OBJECT oInterface = oSoap.GetInterface("myInterface")
IF (OBJECT oInterface <> NULL)
	oInterface.Endpoint$ = "localhost:8080/myInterface"
ENDIF

You will need to specify the location of an existing definition file (WSDL) or the name of a definition file you wish to create. It is important that you don't create the WSDL file every time this program is invoked because it can be a very expensive operation.

// Use existing WSDL or generate new file
rc = oSoap.OpenDefinition("soapserver.wsdl",TRUE)
IF (rc <> 0)
	REDIM serror$ = oSoap.GetLastErrorString$()
ENDIF

Processing SOAP requests

When the object has been configured correctly it is time to start processing the SOAP requests. You will do this by making a blocking call to ProcessRequests().

// Enter processing loop
rc = oSoap.ProcessRequests()
IF (rc <> 0)
	REDIM serror$ = oSoap.GetLastErrorString$()
END IF
...
OBJECT oSoap = NULL
$END

This will only return if an error occurred or the SOAP client has disconnected.

Writing SOAP methods

Any non-nested PUBLIC DEFSUB in the Foreground can represent a SOAP method.

A DEFSUB does need to be marked inorder to be included as a SOAP method. To mark a DEFSUB you will need to use the workbench to add an XML data island to each DEFSUB documenting the attributes of that DEFSUB. If you have a valid DEFSOAP statement in your program the workbench will allow you to associate a SOAP interface with a DEFSUB. This option will be available from the documentation window.

Documentation window

The documentation window will also allow you to set more detailed type info about each argument. This is required because the XSD schemas SOAP and WSDL use have more types than KCML.

WSDL conventions

When generating the WSDL file KCML follows certain conventions:

Implementation specifics

Web Interface

A GET request to the service path i.e. http://www.myhost.com/path/myservice will return an HTML page describing the web services exposed at the site.

Error messages

All the SOAP method calls will return 0 if they are successful. It is good programming practice to check the return codes from all subroutines.

In the event of a problem there are two methods implemented for error reporting.

...
error_number = oSoap.GetLastError()
...
Returns the last SOAP error number.
error_string$ = oSoap.GetLastErrorString$()
Returns the last SOAP error text.

Running the KCML server from the Connection Manager

Running from the Connection Manager has the advantage of being operating system independent and providing a common environment shared with interactive sessions.

An existing service can be reused for starting a number of different SOAP servers in the same environment by defining a list of <soapservice> directives in kconf.xml under the <service><soap> node.

Each soapservice has a base <url> tag that is used to identify the SOAP interface. The value of the <start> tag is used to define the name of the program to be run for SOAP requests. The environment variable SOAPSTART will be set to this value. Any START environment variable will be disregarded by the SOAP server process.

The connection manager service name should be encoded in the SOAP endpoint URL as the first component. The second component of the URL will be the value of the SOAP service's base <url> tag.

For example, with the service shown below you would connect to the SOAP service using the endpoint URL http://pjcserver.com:790/K8-ORA-6/MySoapService. It would start a KCML running as the user with the appropriate environment for the K8-ORA-6 service and an initial program of ~pjc/SOAPTEST

	<service>
		<name>K8-ORA-6</name>
		<description>K8 on Oracle</description>
		...
		<soap>
			<soapservice>
				<url>MySoapService</url>
				<start>SOAPTEST</start>
				<user>pjc</user>
			</soapservice>
		</soap>
	</service>

You might have a KCML SOAP client that connects to this service by creating this SOAP object:

OBJECT s = CREATE "SOAP", "http://pjcserver:790/K8-ORA-6/MySoapService?wsdl"

Tags allowed under <soapservice> are:

TagPurpose
urlThe second component of the endpoint URL following the service name.
userThe user to run as. This must be a local user on the app server and be a valid user in kconf.xml. The home directory of this user will be the working directory for the service.
startThe initial program to run. This can be either a source file with a .src extension, which need not be specified here, or a compiled program with no extension.

Running the KCML server as a Unix daemon

You first need to allocate a port number to use. This should not be used by any other service and should probably be greater than 1024 as on Unix systems these ports are reserved for the superuser. The server should be started with the -b command line switch indicating that it is a SOAP server and the -p switch specifying the program to run. STR($MACHINE, 58,1) is set by the -b switch indicating to application programs that they are running as a SOAP server.

On Unix add a line to /etc/services for the port, say on 8081,

kcmlsoap     8081/tcp
and add the following to /etc/inetd.conf
kcmlsoap    stream  tcp     nowait  soapuser    /usr/local/kcml/kcml      /usr/local/kcml/kcml -b -p /home/pjc/SOAPSVR.kcml

This example shows the server being run as the user soapuser rather than root. The KCML process persists, serving only requests from the client that caused inetd to create it, until that client terminates.

Running the KCML server as an NT service

This is best done through the Kerridge service administrator. Add a service using the Services|Add... menu and select KCML as the service provided. Click the Listen radio button and set the port to be the chosen port. Remember to use that port number in the client's endpoint URL.

Developing KCML SOAP server programs

KCML SOAP server programs can be developed on the KCML Database machine using the normal development environment of KClient and the KCML Workbench.

By using the ProcessRequestsDebug(iPort) in place of the ProcessRequests() method it is possible to start a SOAP server instance from within the workbench listening out on the specified port of localhost. This special SOAP server automatically creates a separate temporary WSDL file.

This allows you to debug just like you would any normal program. Set your intial breakpoints before ProcessRequestDebug(iPort) is called and they will fire if a request is processed that hits a breakpoint. You may want to enclose the ProcessRequestsDebug() method inside a loop as it completes whenever the client closes its socket, e.g. after a GET for the WSDL.

#ifdef SOAPDEBUG
WHILE TRUE DO
    PRINT "waiting for request..."
    rc = oSoap.ProcessRequestsDebug(800)
    PRINT "Request completed", rc
    IF (rc <> 0)
       PRINT oSoap.GetLastErrorString$()
       BREAK
    END IF
WEND
#else
oSoap.ProcessRequests()
#endif

SOAP security

SOAP is essentially a Web application, so all the security fundamentals that apply to Web applications apply to SOAP servers. The SOAP specification makes no direct reference to authentication or authorization and these are presumed to be layered into the transport or handled by the application. Clearly the client must be able to support what the server uses.

The typical approach would be to use use SSL to authenticate the server and to provide encryption on the HTTP transport which will, in turn, permits authentication of the client using standard HTTP basic authentication.
See: Setting up an SOAP server to use SSL

Basic authentication is the only widely supported authentication scheme but because the passwords are not strongly encrypted, it should not be used without SSL. The virtual directory used for the endpoint should be configured as requiring basic authentication so the KCML Connection Manager, Apache or MS IIS will check for the presence of authentication header lines in the incoming request and send a suitable 401 error if missing or incorrect.

The SOAP client will need to specify the username and password as properties e.g. with the KCML SOAP client using CREATE

OBJECT s = CREATE "SOAP", "http://someserver/someservice?WSDL","Auth=fred:secretpwd"

Other clients will have similar properties.

The KCML program should check the request's HTTP headers looking for the Authorization line using the GetUsername() method.

...
DIM user$32,rc
user$ = oSoap.GetUsername$()
...

The name will be blank if BASIC authentication is not in use. Remember don't use this simple scheme unless you are encrypting the link with SSL. This need only be done once as the user name and other details inferred from it should be transferred to a session variable. To be extra secure you should then blank any variables used in determining the userid as this KCML instance will be reused by other clients.

Example Server

00010 $COMPLIANCE 3
    : 
    : DEFSOAP SOAPServerDefinition
    : <xmlData>
<soap service='SOAPService'>
<interface name='Interface'>
<endpoint value='http://localhost/SOAPService' />
</interface>
</soap>
</xmlData>
    : 
    : 'main()
    : $END
    : 
    : PRIVATE DIM OBJECT oSoap
    : 
    : DEFSUB 'Main()
    : LOCAL DIM rc
    : LOCAL DIM sError$
    : 
    : OBJECT oSoap = CREATE "dynamic","soap",USING DEFSOAP SOAPServerDefinition
    : 
    : rc = oSoap.OpenDefinition("demo.wsdl",TRUE)
    : 
    : IF ((rc<>0))
    :     REDIM serror$ = oSoap.GetLastErrorString$()
    : ELSE
    :     
    :     rc = oSoap.ProcessRequests()
    :     IF (rc<>0)
    :         REDIM serror$ = oSoap.GetLastErrorString$()
    :     END IF
    : END IF
    : 
    : OBJECT oSoap = NULL
    : END SUB
    : 
    : // 
    : // echoString
    : // 
    : DEFSUB 'echoString$(inputString$)
    : <xmlData type='help' subtype='embedded'><brief>echo given string</brief>
	<detailed>echo given string</detailed>
	<args><param name='inputString$' flag='in' type='string'>input string to be echo returned</param>
	</args>
	<return type='string'>return input string</return>
	<soap><method transport='rpc' encoding='encoded' />
	<interface name='Interface' />
	</soap>
	</xmlData>
    : RETURN inputstring$
    : END SUB
    : 

Example Client

00010 DIM r$
    : DIM OBJECT oSoap
    : 
    : OBJECT oSoap = CREATE "SOAP","http://localhost:790/AppService/SOAPService?WSDL"
    : 
    : REDIM r$ = oSoap.echoString$("bert")
    : 
    : PRINT r$
    : 
    : OBJECT oSoap = NULL
    : 
    : $END

Example kconf.xml Service

	<service>
	<name>AppService</name>
	<type>kdb</type>
	<connection>true</connection>
	<soap>
		<soapservice>
			<url>SOAPService</url>
			<start>c:/demo_server.src</start>
		</soapservice>
	</soap>
	</service>

Method call hook routine.

A SOAP server's method calls can be protected by a hook routine which is registered, before ProcessRequests(), by calling the SetMethodCallBack() method. The hook routine is passed the following parameters :

DEFSUB 'MethodHook(OBJECT oSoap, sMethodName$, BYREF rHookInfo$_KCML_SOAP_HookInfo) AS Bool

If the hook routine returns TRUE, the method call is executed as normal. If it returns FALSE, it should specify values for a the .SOAP_HookInfo_FaultString$, .SOAP_HookInfo_FaultActor$ & .SOAP_HookInfo_FaultDetail$ fields of the rHookInfo$ record. These values will then be used to construct a SOAP-fault response which is sent back to the SOAP client.

Information can be passed between the SOAP server's code and the hook routine by setting the .SOAP_HookInfo_UserSym to the SYM of a user-defined buffer.

The hook routine is called before & after a method, the SOAP_HookInfo_State field of the rHookInfo$ record describes the state of the SOAP server. The request to the method, and its reponse (when .SOAP_HookInfo_State == KCML_SOAPHOOK_POSTMETHOD), can be found by calling the GetMethodRequest$() and GetMethodResponse$() SOAP object methods.

Example method-call hook routine

    : DEFSUB 'MethodHook(OBJECT oSoap, sMethodName$, BYREF rInfo$_KCML_SOAP_HookInfo) AS Bool
    : LOCAL DIM rc AS Bool
    : LOCAL DIM OBJECT oIface
    : LOCAL DIM sRequest$0
    : LOCAL DIM sResponse$0
    : rc = TRUE
    : SELECT CASE FLD(r$.SOAP_HookInfo_State)
    : CASE _KCML_SOAPHOOK_PREMETHOD
    :     REM Hook being called before a method invocation.
    :     REM Return code of the hook routine determines if the method can be called.
    :     rc = 'IsMethodAllowed(sMethodName$)
    :     IF (rc == FALSE)
    :         REDIM sRequest = oSoap.GetMethodRequest$()
    :         FLD(rInfo$.SOAP_HookInfo_FaultString$) = "Service not available"
    :         FLD(rInfo$.SOAP_HookInfo_FaultDetail$) = "Method call not allowed at this time"
    :         OBJECT oIface = oSoap.GetInterface("Interface")
    :         FLD(rInfo$.SOAP_HookInfo_FaultActor$) = oIface.EndPoint$
    :     END IF
    : CASE _KCML_SOAPHOOK_POSTMETHOD
    :    REM Hook called after a method has been invoked
    :    REDIM sResponse$ = oSoap.GetMethodResponse$()
    : CASE ELSE
    :     WRITE LOG "Unknown state"
    : END SELECT
    : RETURN rc

This could be registered in the above SOAP server example by calling

    : oSoap.SetMethodCallBack(BYREF 'MethodHook, SYM(someStateVar$))

after creating the server object.

See Also

SOAP server methods and properties