Reference13r2:Concept Talking to the v13 Application Platform using PHP: Difference between revisions

From innovaphone wiki
Jump to navigation Jump to search
 
(34 intermediate revisions by 2 users not shown)
Line 1: Line 1:
Work in progress
This is a slight rewrite of the [[Reference13r1:Concept Talking to the v13 Application Platform using PHP | older article]] in the Reference13r1 namespace.  While there is nothing technically wrong with the old article, some of the authentication methods described there are no longer recommended for use with scripts.  In particular, it is not recommended that a script authenticate as a PBX user.  Instead, it should always authenticate against a PBX ''App'' type object.  If the script needs to access other App services, it should use the ''Services'' API described in https://sdk.innovaphone.com/13r2/doc/appwebsocket/Services.htm.
 
This is a slight rewrite of the [[Reference13r1:Concept Talking to the v13 Application Platform using PHP | older article]] in the Reference13r1 name space.  While there is technically nothing wrong in the old article, some of the authentication methods described there are not recommended any more for script usage.  In particular, it is not recommended to have a script authenticate as a PBX user.  Instead, it should authenticate against a PBX ''App'' type object always.  If the scripts needs to have access to other App services, it shall use the ''Services'' API described in https://sdk.innovaphone.com/13r2/doc/appwebsocket/Services.htm.


==Applies To==
==Applies To==
This information applies to
This information applies to


* innovaphone platform running the v13 ''App Platform ''
* innovaphone platform running the 13r2 (or later) ''App Platform ''
* PBX running v13 firmware  
* PBX running 13r2 (or later) firmware  
* PHP script accessing the ''App Services'' available on the ''App Platform''
* PHP script accessing the ''App Services'' available on the ''App Platform''


==More Information==
==More Information==
===Problem Details===
===Problem Details===
The v13 App Platform runs various ''App Services'' providing services which are used by ''Apps'' running in the ''myApps'' client context''Apps'' (written in JavaScript) are loaded from the ''App Platform'' and launched by the ''myApps'' client.  They communicate to the ''App Services'' using a JSON based WebSocket protocol.  ''Apps'' may also be loaded from the PBX (so called ''PBX Apps'').
The v13 App Platform runs several ''App Services'' that provide services used by ''Apps'' running in the context of the ''myApps'' client.  Apps (written in JavaScript) are loaded from the App Platform and launched by the myApps client.  They communicate with the App Services using a JSON-based WebSocket protocol.  ''Apps'' can also be loaded from the PBX (so called ''PBX Apps'').


The ''vanilla'' way to go when it comes to 3rd party integration is to write a ''custom App Service'' and install it on the ''App Platform''.  Creation of such ''App Services'' is facilitated by the [https://sdk.innovaphone.com/13r2/doc/sdk.htm ''v13 SDK''].  
The ''vanilla'' way to go when it comes to 3rd party integration is to write a ''custom App Service'' and install it on the ''App Platform''.  The creation of such ''App Services'' is facilitated by the [https://sdk.innovaphone.com/13r2/doc/sdk.htm ''v13 SDK''].  


NB: the mechanisms described here are available in the SDK from 13r2 and hence all links to the SDK are given for the 13r2 version.  To access other versions of the SDK once available, refer to the [https://sdk.innovaphone.com SDK root] and follow the links to the version of interest.
Note: The mechanisms described here are available in the 13r2 SDK, so all links to the SDK are given for the 13r2 version.  To access other versions of the SDK as they become available, go to [https://sdk.innovaphone.com SDK root] and follow the links to the version of interest.


[[Image:app-platform-php-appplatform-simplified.png]]
[[Image:app-platform-php-appplatform-simplified.png]]


However, in some scenarios you may want to talk to the PBX or existing ''App Services'' from your own application directly (that is, not by virtue of an ''App'' running in the ''myApps'' client).  This article is on how to do that.  General considerations are given which are true for all programming languages, some sample code is given in PHP.
However, in some scenarios you may want to talk to the PBX or existing App Services directly from your own application (i.e. not via an App running in the myApps client).  This article describes how to do this.  General considerations are given that apply to all programming languages, and some sample code is given in PHP.


[[Image:app-platform-php-appplatform-simplified-3rd-party.png]]
[[Image:app-platform-php-appplatform-simplified-3rd-party.png]]


====  Authentication ====
====  Authentication ====
To talk to the PBX or another ''App Service'', your application (written in PHP or any other language that can talk WebSocket) needs to be logged in to the PBX.  While an ''App'' does not need to care about it, as the ''myApps'' client (which is the context all ''Apps'' run in) would take care of this, an App Service needs to implement the protocol itself.
To talk to the PBX or another ''App Service'', your application (written in PHP or any other language that can talk to a WebSocket) needs to be logged in to the PBX.  While an ''App'' does not need to worry about this, as the ''myApps'' client (which is the context in which all ''Apps'' run) would take care of this, an App Service needs to implement the protocol itself.


The process is as follows:
The process is as follows:
* an ''App'' type object must be created on the PBX for your application
* An ''App'' type object must be created on the PBX for your application.
* the ''App'' object defines the credentials for authentication (''Name'' and ''Password'') as well as the access rights of your application (that is, the APIs in the ''App'' tab, the licenses in the ''License'' tab and the Apps in the ''Apps'' tab)
* The ''App'' object defines the authentication credentials (''Name'' and ''Password'') as well as the access rights of your application (i.e., the APIs in the ''App'' tab, the licenses in the ''License'' tab, and the Apps in the ''Apps'' tab)
* the application must log-in to the PBX using the credentials configured for the ''App'' type object that corresponds to the application. In other words, it logs in as the App, not as a particular user
* The application must log in to the PBX using the credentials configured for the ''App'' type object that corresponds to the application. In other words, it logs in as the App, not as a specific user
* the application can then use the allowed PBX APIs (as per the definitions on the ''Grant access to APIs'' section in the ''App'' tab of the ''App'' object)
* The application can then use the allowed PBX APIs (according to the definitions in the ''Grant access to APIs'' section in the ''App'' tab of the ''App'' object).
* the application can authenticate towards other allowed App services (as per the definitions in the ''Apps'' tab of the ''App'' object) by virtue of the PBX ''Services'' API. It can then request services from those App services
* The application can authenticate to other allowed App services (as defined in the ''Apps'' tab of the ''App'' object) using the PBX ''Services'' API. It can then request services from these App Services


==== WebSocket ====
==== WebSocket ====
Line 44: Line 42:
Although JSON is related to JavaScript, most programming languages can deal with it.  For example, PHP has the <code>json_encode()</code> and <code>json_decode()</code> functions, C# has the <code> DataContractJsonSerializer</code> class.   
Although JSON is related to JavaScript, most programming languages can deal with it.  For example, PHP has the <code>json_encode()</code> and <code>json_decode()</code> functions, C# has the <code> DataContractJsonSerializer</code> class.   


Here is a full example of the JSON message that might be used to initiate a log-in to the PBX
Here is a full example of the JSON message that might be used to authenticate towards the PBX
<pre>
<syntaxhighlight lang="json">
{
{
     "mt": "Login",
     "mt": "AppLogin",
     "type": "session",
     "digest": "955da4b8351557c54c25b99fa8aad9e8b4ba7a4090ac5c2664e7ef7a301e6586",
     "userAgent": "websocket.class.php (PHP WebSocket CKL-CELSIUS-W10)"
     "domain": "",
    "sip": "",
    "guid": "",
    "dn": "",
    "app": "myapplication",
    "info": {}
}
}
</pre>
</syntaxhighlight>


When working with ''App Services'', there are three message members which are somehow special for all such services.   
When working with ''App Services'', there are three message members which are somehow special for all such services.   
; mt (string) : the ''m''essage ''t''ype. Each message must have an <code>mt</code> member which denotes the message type
; mt (string) : the ''m''essage ''t''ype. Each message must have an <code>mt</code> member which denotes the message type
; api (string optional) : some App services (and in particular the PBX) support different defined sets of messages (referred to as ''api''). In this case, the ''mt'' message member is not sufficient to specify the type of message. Instead, the API identifier must be given in the ''api'' message member.  The [https://sdk.innovaphone.com/13r2/doc/appwebsocket/RCC.htm RCC api] provided by the PBX would be an example where all messages sent to or received from the PBX  for this API must have an ''api'' message member with the (string) value <code>"RCC"</code>
; api (string, optional) : some App services (and in particular the PBX) support different defined sets of messages (referred to as ''api''). In this case, the ''mt'' message member is not sufficient to specify the type of message. Instead, the API identifier must be given in the ''api'' message member.  The [https://sdk.innovaphone.com/13r2/doc/appwebsocket/RCC.htm RCC api] provided by the PBX would be an example where all messages sent to or received from the PBX  for this API must have an ''api'' message member with the (string) value <code>"RCC"</code>
; src (string, optional) : this member may be sent along with messages sent to an ''App Service''.  If the message triggers responses, the ''App Service'' will copy the <code>src</code> member in to the responses.  This allows the client to associate responses to the message that initiated them.
; src (string, optional) : this member may be sent along with messages sent to an ''App Service''.  If the message triggers responses, the ''App Service'' will copy the <code>src</code> member in to the responses.  This allows the client to associate responses to the message that initiated them.
All other members (if any) are defined by the application protocol.
All other members (if any) are defined by the application protocol.
Line 63: Line 66:


=== PHP Sample Code ===
=== PHP Sample Code ===
These scripts use an optional configuration from a file called ''my-pbx-data.php''.  If it is not present, default values are used.
These scripts use a configuration from a file called ''my-pbx-data.php''.  


To provide proper data for your environment to the scripts, create the file ''my-pbx-data.php'' as follows:
To provide proper data for your environment to the scripts, create the file ''my-pbx-data.php'' as follows:
Line 73: Line 76:
</syntaxhighlight>
</syntaxhighlight>


==== Access App services using user credentials ====
Our little example (available in <code>application.php</code>) will connect to 2 ''App Services'' on the ''App Platform'', ''Devices'' and ''Users'' to retrieve some configuration data.  To authenticate towards the App service instances, a dedicated PBX ''App'' object in the PBX with appropriate rights is used.
Our little example (available in <code>app-sample.php</code>) will connect to 2 ''App Services'' on the ''App Platform'', ''Devices'' and ''Users'' to retrieve some configuration data.  To authenticate towards the App service instances, a dedicated PBX ''App'' objects with appropriate rights is used.


===== PBX configuration =====
===== PBX configuration =====
You will need a PBX which has been set up using the standard ''Install'' procedure (http://$pbxdns /install.htm).
You will need a PBX which has been set up using the standard ''Install'' procedure (http://$pbxdns /install.htm).
On that PBX, you additionally need to create an ''App'' type object
* whose ''Name'' property is equal to the value of ''$pbxapp''
* whose ''Password'' is equal to the value of ''$pbxpw''
* that has ticked ''Services'' in the ''Grant access to APIs'' section of the ''App'' tab
* that has ticked ''devices-api'' and ''users-admin'' in the ''Apps'' tab


===== Login =====
===== Login =====
Logging-in to the PBX requires the following steps:
Logging-in to the PBX requires the following steps:
* request a challenge from the PBX using the ''AppChallenge'' message
* request a challenge from the PBX using the ''AppChallenge'' message
: <code>sent->PBXWS {"mt":"AppChallenge"}</code>
: <syntaxhighlight lang="json">sent->PBXWS {"mt":"AppChallenge"}</syntaxhighlight >
* receive the challenge from the PBX
: <syntaxhighlight lang="json">received<-PBXWS {"mt":"AppChallengeResult","challenge":"8b7d8a1a3a281efd"}</syntaxhighlight >
* compute the digest based on the App data, the secret and the challenge
: <syntaxhighlight lang="json">sent->PBXWS
{
    "mt": "AppLogin",
    "digest": "1a07f3c20d2a4f6b117c64d845b9bed7b884b49b82868f0e2b73200553c40e2d",
    "domain": "",
    "sip": "",
    "guid": "",
    "dn": "",
    "app": "myapplication",
    "info": {}
}</syntaxhighlight >
* receive the confirmation from the PBX
<syntaxhighlight lang="json">received<-PBXWS {"mt":"AppLoginResult","ok":true}</syntaxhighlight >
 
This handshake is implemented in the ''AppPlatform\AppLoginAutomaton'' class available in classes\websocket.class.php.  The thing that needs to be added is the WebSocket connection to the PBX (an ''AppPlatform\WSClient'' object required as constructor argument for the ''AppPlatform\AppLoginAutomaton'' class). We do this using a derived class named ''PbxAppLoginAutomaton'':
 
<syntaxhighlight lang="php">
class PbxAppLoginAutomaton extends AppLoginAutomaton {
 
    function __construct($pbx, AppServiceCredentials $cred, $useWS = false) {
        $this->pbxUrl = (strpos($pbx, "s://") !== false) ? $pbx :
                $this->pbxUrl = ($useWS ? "ws" : "wss") . "://$pbx/PBX0/APPS/websocket";
        // create websocket towards the well known PBX URI
        $this->pbxWS  = new WSClient("PBXWS", $this->pbxUrl);
        parent::__construct($this->pbxWS, $cred);
    }
}
</syntaxhighlight>
 
This class takes the URL to the PBX and the credentials (wrapped in an object of class ''AppPlatform\AppServiceCredentials'') as constructor arguments:
   
   
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
// login to PBX and devices and users
$app = new PbxAppLoginAutomaton($pbxdns, new AppPlatform\AppServiceCredentials($pbxapp, $pbxpw));
$connector = new AppPlatform\AppServiceLogin(
        $pbxdns, new AppPlatform\AppUserCredentials($pbxuser, $pbxpw), array(
    $devicesspec = new AppPlatform\AppServiceSpec("innovaphone-devices"),
    $usersspec = new AppPlatform\AppServiceSpec("innovaphone-users"),
        ),
        true
);
$connector->connect();
</syntaxhighlight>
</syntaxhighlight>


To perform the necessary steps, an instance of class <code>AppServiceLogin</code> (note that in our sample code, all library code provided is in the <code>AppPlatform</code> name space) is used. The constructor arguments are
Our new class ultimately is a derivative of the ''AppPlattform\FinitStateAutomaton'' class, so we can run the automaton like:
; $PBX (string) : either the FQDN or the full websocket URI of the PBX . In the sample code, we use the FQDN <code>sindelfingen.sample.dom</code>
 
; $credentials (AppUserCredentials) : the users ''Name'' and ''Password'' wrapped in an object of type ''AppUserCredentials''
<syntaxhighlight lang="php">
; $appServiceSpec (AppServiceSpec[]) : an array of ''App Service'' specifications to select the ones to connect to, each wrapped in an object of type ''AppServiceSpec''
$app->run();
</syntaxhighlight>


The call to <code>connect()</code> creates and authenticates the WebSocket connections to the ''App Services'' and the ''PBX''.  Note that a connection is made only if the user used for log-in has access to the selected ''App Service''.
The automaton will do the authentication towards the PBX as shown above and then terminate (which causes the ''run()'' member function to return).


The code then verifies that the log-in to the PBX succeeded:
The code then verifies that the log-in to the PBX succeeded:


<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
// look at the PBX login
if (!$app->getIsLoggedIn()) {
if ($connector->getPbxA()->getIsLoggedIn()) {
     die("login to the PBX failed - check credentials");
     AppPlatform\Log::log("Logged in to PBX");
    $pbxws = $connector->getPbxWS();
    $pbxloginresult = ($connector->getPbxA()->getResults());
} else {
    AppPlatform\Log::log("Failed to log-in to PBX");
    exit;
}
}
</syntaxhighlight>
</syntaxhighlight>


If the login succeeded, the WebSocket connection to the PBX and the log-in messages are retrievedNext step is to verify the respective connections to the ''App Services'':
Eh voilà, we are connected to the PBX.   
 
That was the easy part :-)


<syntaxhighlight lang="php">
Note that the code for the ''PbxAppLoginAutomaton'' can be found in the classes/websocket.class.php file.
// look at devices
if ($connector->getAppAutomaton($devicesspec)->getIsLoggedIn()) {
    AppPlatform\Log::log("Logged in to Devices");
    $devicesws = $connector->getAppAutomaton($devicesspec)->getWs();
} else {
    AppPlatform\Log::log("Failed to log-in to Devices");
    exit;
}


// look at users
===== Determination of available services =====
if ($connector->getAppAutomaton($usersspec)->getIsLoggedIn()) {
Now that we are successfully connected to the PBX, we can determine the services that are available to our application.  This is done using the ''Services'' API available in the PBX (which is described in the SDK's [https://sdk.innovaphone.com/13r3/doc/appwebsocket/Services.htm ''Services'' page]).


    AppPlatform\Log::log("Logged in to Users");
Only a few steps are required:
    $usersws = $connector->getAppAutomaton($usersspec)->getWs();
* subscribe to the available services information
} else {
: <syntaxhighlight lang="json">sent->PBXWS {"mt":"SubscribeServices","api":"Services"}</syntaxhighlight>
     AppPlatform\Log::log("Failed to log-in to Users");
: note the additional <code>api</code> member
    exit;
* receive the respones
: <syntaxhighlight lang="json">received<-PBXWS {"api":"Services","mt":"SubscribeServicesResult"}</syntaxhighlight>
: note that there is no further information in this message.  The actual list of services will be received later on
* unsubscribe from the list of services as we do not need dynamic updates
: <syntaxhighlight lang="json">sent->PBXWS {"mt":"UnsubscribeServices","api":"Services"}</syntaxhighlight>
* receive the actual list of services available to us
: <code>received< - PBXWS </code>
: <syntaxhighlight lang="json">
{
     "api": "Services",
    "mt": "ServicesInfo",
    "services": [{
            "name": "devices-api",
            "title": "DevicesApi",
            "url": "https://192.168.178.71/sample.dom/devices/innovaphone-devices-api",
            "info": {
                "apis": {
                    "com.innovaphone.devices": {}
                }
            }
        }, {
            "name": "users-admin",
            "title": "Users Admin",
            "url": "https://192.168.178.71/sample.dom/usersapp/innovaphone-usersadmin"
        }]
}
}
</syntaxhighlight>
</syntaxhighlight>
: note that the actual list of services depends on the configuration of the ''Apps'' tab in the ''App'' object for our application


Eh voilà, we are connected to the 2 ''App Services'' we are interested in. 


<syntaxhighlight lang="php">
Life was easy so far as we have derived the ''PbxAppLoginAutomaton'' utility class which simply did what we wanted so far, except for the initialization in its constructor.  Now we want to implement further conversation with the PBX, which requires some more fundamental understanding of the finite state automaton classes.
// now we have the authenticated websockets to our AppServices, so we can release the connector
$connector = null;
</syntaxhighlight>
That was the easy part :-)


===== Implementing the WebSocket Protocol =====
===== The finite state automaton classes =====
Life was easy so far as we have used the ''AppServiceLogin'' utility class.  Now we want to implement a conversation with our ''App Services'' using the WebSocket connections we have set up before.
Generally, to talk to the PBX or any App service, you can of course use just any WebSocket library directly.  We are using (and recommending) a derivative of the [https://github.com/Textalk/websocket-php Textalk] websocket classes.  We simplified the code a bit and also added support for receiving WebSocket messages asynchronously. You will find this code in <code>classes/textalk.class.php</code>.   
 
Generally, you can of course use just any WebSocket library directly.  We are using (and recommending) a derivative of the [https://github.com/Textalk/websocket-php Textalk] websocket classes.  We simplified the code a bit and also added support for receiving WebSocket messages asynchronously. You will find this code in <code>classes/textalk.class.php</code>.   


However, solely using the textalk classes leaves you with quite a bit of work to do.  As discussed above, there are some challenges:
However, solely using the textalk classes leaves you with quite a bit of work to do.  As discussed above, there are some challenges:
* websocket is async by nature
* websocket is async by nature
: you will need some support for receiving messages asynchronously at least. Aside from the support for asynchronous receipt of messages added to the Textalk classes, the sample code has some classes which allow you to easily create ''event driven'' code which processes messages whenever they come in and not when you expect them to come in
: you will need some support for receiving messages asynchronously at least. Aside from the support for asynchronous receipt of messages added to the Textalk classes, the sample code has some classes which allow you to easily create ''event driven'' code which processes messages whenever they come in and not when you expect them to come in
* PBX login requires some fiddling with encryption schemes and netlogon protocol peculiarities
* PBX login requires some fiddling with encryption schemes  
 
The sample code includes some classes to create ''finite state automatons''.  Also it has some classes derived from this, which actually implement the protocols required to log-in to the PBX and the ''App Services''
The sample code includes some classes to create ''finite state automatons''.  Also it has some classes derived from this, which actually implement the protocols required to log-in to the PBX and the ''App Services''


Those extensions can be found in <code>classes/websocket.class.php</code>.
Those extensions can be found in <code>classes/websocket.class.php</code>.


===== The Asynchronous Programming Model =====
===== The asynchronous programming model =====
PHP is not really nicely prepared for asynchronous programming.  To deal with that, we have created a utility class called <code>FinitStateAutomaton</code>.  This class handles the communication to a single ''App Service''.  This is why it has a WebSocket connection as argument to the constructor (our WebSocket implementation is actually called <code>WSClient</code>).  The class will use this WebSocket to talk to the ''App Service''.  As we have seen above, the <code>AppServiceLogin</code> utility class creates such <code>WSClient</code> objects (which we retrieved by calling the <code>getAppWebSocket()</code> member function.  
PHP is not really nicely prepared for asynchronous programming.  To deal with that, we have created a utility class called ''FinitStateAutomaton''.  This class handles the communication to a single ''App Service''.  This is why it has a WebSocket connection as argument to the constructor (our WebSocket implementation is actually called ''WSClient'').  The class will use this WebSocket to talk to the ''App Service''.  As we have seen above, the ''PbxAppLoginAutomaton'' utility class creates such an ''WSClient'' object and passes it to the ''AppLoginAutomaton'' (which extends the ''FinitStateAutomaton'' class).


However, if you try to instantiate such a class (like <code>$mya = new FinitStateAutomaton($mywebsocket)</code> you will see that this is not possible, as the class is ''abstract''.  This means that there are some  member functions missing which must be implemented by a derived class.  These functions are those which know how to handle messages received from the ''App Service''.
However, if you try to instantiate such a class (like <syntaxhighlight lang="php">$mya = new FinitStateAutomaton($mywebsocket)</syntaxhighlight> you will see that this is not possible, as the class is ''abstract''.  This means that there are some  member functions missing which must be implemented by a derived class.  These functions are those which know how to handle messages received from the ''App Service''.
 
Let us look at how the ''AppLoginAutomaton '' class does this:


So here we go and define a derived class:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
// an automaton which lists all devices in Devices
class AppLoginAutomaton extends FinitStateAutomaton {
class DeviceLister extends AppPlatform\FinitStateAutomaton {
    public function ReceiveInitialStart(Message $msg) {
...
 
        $this->log("requesting challenge");
        $this->sendMessage(new Message("AppChallenge"));
 
    }
}
}
</syntaxhighlight>
</syntaxhighlight>
It defines an override for the abstract ''ReceiveInitialStart'' member function.  This function, as all functions whose name begins with ''Receive'' is called when an event is fed into the automaton.  Usually, this event is a message (of type ''AppPlatform\Message'') received from an app service.  In this special case however, it is a pseudo event generated by the system indicating that the automaton should start (hence the name ''ReceiveInitial'''Start''''').  So it implements the first action the automaton performs.


''Devices'' by the way is the ''App Service'' that manages all physical devices that belong to a single innovaphone PBX installation.  Our sample task now is to talk to ''Devices'' to find out which devices exist.
In our case, as discussed above, it sends an ''AppChallenge'' message to the PBX.  Recall that such an automaton is always instantiated with a single ''WSClient'' argument, which is the WebSocket connection used to talk to the App service (or the PBX which behaves like an App service). In other words, a single instance of a ''FinitStateAutomaton'' always talks to a single App service only. This is why we can simply say
 
When we instantiate this derived class, we will provide the ''WSClient'' towards the ''Devices App Service''.


<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$dl = new DeviceLister($devicesws);
        $this->sendMessage(new Message("AppChallenge"));
</syntaxhighlight>
</syntaxhighlight>
without specifying a destination and resulting in an AppChallenge message
<syntaxhighlight lang="json">
sent->PBXWS {"mt":"AppChallenge"}
</syntaxhighlight>
sent to the PBX.


In the base class, there is only one function declared as ''abstract'':
Eventually, the PBX will respond with an ''AppChallengeResult'' message. 
<syntaxhighlight lang="php">
<syntaxhighlight lang="json">
    // this function at least must be overriden by any derived class
received<-PBXWS {"mt":"AppChallengeResult","challenge":"95ed003a24c3fc89"}
    abstract public function ReceiveInitialStart(\AppPlatform\Message $msg);
</syntaxhighlight>
</syntaxhighlight>
 
When this happens, the system will call the ''ReceiveInitial'''AppChallengeResult''''' member function:
This function will be called by the base class, when a message with ''message type'' (mt, see above) of ''Start'' is received in the automaton state ''Initial''.  When a message is received and there is no appropriate ''Receive'' function defined, the message is discarded.  Otherwise the message is passed to the ''Receive'' function and the derived class can handle it.
 
The ''Start'' is a little special.  In fact, the ''FinitStateAutomaton'' class will send this pseudo-message to the derived class to start the automaton defined by the derived class.  This message has nothing but the ''mt'' member.  The derived class will usually send the first message to its ''App Service'' in this ''Receive'' function.


<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
     public function ReceiveInitialStart(\AppPlatform\Message $msg) {
     public function ReceiveInitialAppChallengeResult(Message $msg) {
         AppPlatform\Log::log("Requesting Device List");
         $infoObj          = new \stdClass();
         $this->sendMessage(new AppPlatform\Message("GetDevices"));
        $infoHashString  = json_encode($infoObj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $hashcode        = hash('sha256', $sha              = "{$this->cred->app}:::::{$infoHashString}:{$msg->challenge}:{$this->cred->pw}");
        // $this->log("computed challenge $sha", "debug");  // do not output in any other category than "debug" coz it includes the password
        $this->sessionKey = hash('sha256', "innovaphoneAppSessionKey:{$msg->challenge}:{$this->cred->pw}");
         $this->sendMessage($this->loginData  = new Message("AppLogin", "digest", $hashcode, "domain", "", "sip", "", "guid", "", "dn", "", "app", $this->cred->app, "info", $infoObj));
     }
     }
</syntaxhighlight>
</syntaxhighlight>


It is the ''sendMessage'' member function of the base class that is used to send a message to the ''App Service''.  The message itself is an instance of a class of type ''Message''.  The first argument of its constructor is the ''mt'' member of the message.  All further arguments are optional.  If they appear, they must appear in pairs of ''member/value''.  In the code above, the ''mt'' is "GetDevices".
It does some cryptographic magic to compute a digest from the available information and then sends an ''AppLogin'' message to the PBX:


''Devices'' will respond to this message with a message that looks like so:
<syntaxhighlight lang="php">
<pre>
        $this->sendMessage($this->loginData  = new Message("AppLogin", "digest", $hashcode, "domain", "", "sip", "", "guid", "", "dn", "", "app", $this->cred->app, "info", $infoObj));
{
</syntaxhighlight>
    "mt": "GetDevicesResult",
resulting in a message such as
    "devices": [{
<syntaxhighlight lang="json">
            "id": 2,
sent - >PBXWS {
            "hwId": "029033410109",
    "mt": "AppLogin",
            "name": "AP - sample.dom",
    "digest": "955da4b8351557c54c25b99fa8aad9e8b4ba7a4090ac5c2664e7ef7a301e6586",
            "domain": 1,
    "domain": "",
            "product": "AppPlatform ARM",
    "sip": "",
            "version": "60002 dvl",
    "guid": "",
            "type": "APP_PLATFORM",
    "dn": "",
            "pbxActive": false,
    "app": "myapplication",
            "online": true
    "info": {}
        }, {
            "id": 3,
            "hwId": "0090333000af",
            "name": "ckl's IP232",
            "domain": 1,
            "product": "IP232",
            "version": "13r1 dvl [13.1102/125119/501]",
            "type": "PHONE",
            "pbxActive": false,
            "online": true
        }, {
            "id": 1,
            "hwId": "009033410109",
            "name": "PBX - sindelfingen.sample.dom",
            "domain": 1,
            "product": "IP811",
            "version": "13r1 dvl [13.1133/131094/200]",
            "type": "GW",
            "pbxActive": true,
            "online": true
        }]
}
}
</pre>
</syntaxhighlight>
As we see, the message type ''mt'' is "GetDevicesResult" so the ''ReceiveInitialGetDevicesResult'' member function of our derived class will be called:
being sent to the PBX.


<syntaxhighlight lang="php">
The PBX would verify the correctness of the provided digest and then return an ''AppLoginResult'' message. 
    public function ReceiveInitialGetDevicesResult(\AppPlatform\Message $msg) {
<syntaxhighlight lang="json">
        AppPlatform\Log::log("got " . count($msg->devices) . " Device Info(s)");
received<-PBXWS {"mt":"AppLoginResult","ok":true}
        $this->devices = array_merge($this->devices, $msg->devices);
        if (isset($msg->last) && $msg->last) {
            AppPlatform\Log::log("Last chunk");
            return "Dead";
        }
    }
</syntaxhighlight>
</syntaxhighlight>
 
Due to the reception of such a message, the ''ReceiveInitialAppLoginResult'' member function is calledIt does a bit of internal housekeeping and then does two interesting things:
Here we save the data in some class-local storage (<code>$this->devices</code>).  More interesting is what we do if the message contains a member ''last'' which is set to true. In this case, the ''Receive'' function returns the name of the new state the automaton is supposed to be inIn our case, it is "Dead".  "Dead" is a magic state name (just as "Initial" is) in that it tells the system that your automaton has finished.  You could as well change the state name to something else (say, "NewState"). If further ''GetDevicesResult'' messages would appear, a member function Receive''NewState''GetDevicesResult() would be called.
 
So here is the complete derived class:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
// an automaton which lists all devices in Devices
    public function ReceiveInitialAppLoginResult(Message $msg) {
class DeviceLister extends AppPlatform\FinitStateAutomaton {


    protected $devices = array();
        $response->setMt("AppLoginSuccess");
        $this->postEvent($response);


    public function getDevices() {
         return "Dead";
         return $this->devices;
    }
 
    public function ReceiveInitialStart(\AppPlatform\Message $msg) {
        AppPlatform\Log::log("Requesting Device List");
        $this->sendMessage(new AppPlatform\Message("GetDevices"));
    }
 
    public function ReceiveInitialGetDevicesResult(\AppPlatform\Message $msg) {
        AppPlatform\Log::log("got " . count($msg->devices) . " Device Info(s)");
        $this->devices = array_merge($this->devices, $msg->devices);
        if (isset($msg->last) && $msg->last) {
            AppPlatform\Log::log("Last chunk");
            return "Dead";
        }
     }
     }
}
</syntaxhighlight>
</syntaxhighlight>
Its as simple as that!
This code creates a ''$response'' message and sets the ''mt'' member to <code>AppLoginSuccess</code>.  It returns the string "Dead" then.


Have a look at the second derived class in the sample code (<code>websocket-sample.php</code>) called ''UserLister''.  It is a bit more complicated (as it handles more message types) but you will see the same mechanisms.
The event function (''ReceiveInitialAppChallengeResult'') we looked at so far did not have any explicit return statement. In PHP terms that means that it returns a ''null'' valueWhen an event returns a string however, it indicates that the automaton shall move to a new state.  


===== Running the Automatons =====
In our case, the new state is named "Dead" and it has a special meaningAn automaton in state "Dead" is considered to be terminated.  The system will not listen for incoming messages on the automaton's WSClient socket any further.  
So far, we have not talked about who actually  runs the automatons we create in our derived classes.  You could think that this is done by calling something like <code>$myauto = new myAuto($ws); $myauto->run()</code>However, it is done a little different.  The reason for this is that it comes in handy once in a while when you can run several automatons concurrently.


Think of the log-in scenario we discussed aboveIn this scenario, we must talk both to the PBX and to one or more ''App Services''.  As we learned above, a single automaton only talks to a single ''App Service'' (or actually a PBX, as a PBX can act just like an ''App Service''.  This is known as ''PBX App'').  
Note that the initial state of any automaton is named <code>Initial</code>This is why event member functions are named ''Receive'''Initial'''Start'' for exampleIf a member function returns a new state name other than <code>Dead</code>, the member functions called upon subsequent incoming messages will be named ''Receive'''NewStateName'''Event''.  Also, the first thing the system will do is call the ''Receive'''NewStateName'''Start'' member function.


Therefore, automatons are run by another utility class called ''Transitioner''.  This is instantiated with a list of automatons (which are in turn instantiated with their respective ''WSClients'').  We then can start and run all those automatons concurrently:
===== Working with multiple automatons =====
Obviously, applications may want to talk to multiple destinations, e.g. to the PBX for authentication and to one or more other App services.  As a single ''FiniteStateAutomaton'' always talks to a single ''WSClient'' connection, we need to instantiate multiple automatons to be able to talk to multiple destinations:


<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$dl = new DeviceLister($devicesws);
$a1 = new Type1Automaton();
$ul = new UserLister($usersws, $pbxloginresult->loginResultMsg);
$a2 = new Type2Automaton();
$t = new AppPlatform\Transitioner($dl, $ul);
$transitioner = new AppPlatform\Transitioner($a1, $a2);
$t->run();
$transitioner->run();
</syntaxhighlight>
</syntaxhighlight>


For convenience however, the ''FiniteStateAutomaton'' also has a (trivial) member function ''run'' indeed.  It will run only the automaton itself and comes in handy if you want to run a single automaton only.
''AppPlatform\Transitioner'' is a helper class that takes a number of automatons (either listed as multiple arguments or wrapped in an array and given as single argument) as constructor arguments. When its ''run'' member function is called, it will in turn call all of the automatons ''ReceiveInitialStart'' member functions and then listen for messages coming in on any of the automatons ''WSClient'' connections.
 
By the way, the ''FinitStateAutomaton'''s own ''run'' member function is trivial:
 
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
     /**
     /**
     * utility function for the simple case you want to run a single (i.e. this) automaton only
     * utility function for the simple case you want to run a single (i.e. this) automaton only
     */
     */
     public function run() {
     public function run($sockettimeout = 5) {
         $auto = new Transitioner($this);
         $auto = new Transitioner($this);
         $auto->run();
         $auto->run($sockettimeout);
     }
     }
</syntaxhighlight>
</syntaxhighlight>
===== Communicating between Automatons =====
 
When automatons run concurrently, you most likely will need to be able to exchange message between them.  This can be done by the ''FinitStateAutomaton'' classes ''postEvent'' member function.
The remaining question is how different automaton instances communicate to each other.  This is done using the ''FinitStateAutomaton'''s ''postMessage'' member function as in
 
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
    /**
        $this->postEvent($response);
    * post an Event to other automatons, this function is synchronous (i.e. Receive*() member functions will be called directly)
    * @param Message $msg
    */
    protected function postEvent(Message $msg, $dst = null);
</syntaxhighlight>
</syntaxhighlight>
The function allows you to send a message to the other automatons (when ''$dst'' is ''null'') or to a specific automaton.  For an example, see the ''UserPBXLoginWithAppAutomaton'' and ''AppLoginViaPBXAutomaton'' which work in tandem using the ''postEvent'' mechanism. 


Please note that messages sent by this function are handled synchronously by the target automatons (i.e the target automatons have already processed the posted event when ''postEvent'' returns).
shown above in the ''ReceiveInitialAppLoginResult'' event function.  When ''postEvent'' is called, the system would call the corresponding event functions in all currently active automatons. Note that these member functions are called synchronously, that is, they are already executed when the ''postEvent'' function returns.  


==== Access App services using the service's secret ====
===== Tandems =====
This sample code (in file ''pbxapi.php'') accesses an App service using the secret as defined in an App objectNo user account is required therefore.  As a sample, the [https://sdk.innovaphone.com/13r2/doc/appwebsocket/PbxAdminApi.htm PbxAdminApi] provided by the PBX itself is used. 
The ''postEvent'' mechanisms allows two or more automatons to work in tandemTo see how that works, we can have a look at the ''Pbx2AppAuthenticator'' class.  


This sample is new from build 1006.
<syntaxhighlight lang="php">
===== PBX configuration =====
/**
To access an API provided by the PBX, an ''App'' object is required. In this sample, we assume that
* class that authenticates to an App service with help from the PBX  
* an ''App'' type object called <code>pbxadminapi</code> (this is the ''Name'' property) is created
  */
* it has <code>ip411</code> as ''Password''  
class Pbx2AppAuthenticator extends \AppPlatform\FinitStateAutomaton {
* in the object's ''App'' tab, the 'Admin' and 'PbxApi' check-marks are ticked in the ''Grant access to APIs'' section
</syntaxhighlight>
This class takes a ''ServiceConnector'' as constructor argument (for example one delivered from the ''PbxAppLoginAutomaton'') and creates a WebSocket connection (actually a ''WSClient'' object) towards the App service described by the ''ServiceConnector''.


===== Authentication =====
<syntaxhighlight lang="php">
To authenticate towards an App service directly, the ''AppPlatform\AppLoginAutomaton'' (available in websocket.class.php) is used.  As we intend to talk to an API provided by the PBX itself, we derive the class ''PbxAppLoginAutomaton''.  It only overrides the constructor to create the proper URL to connect to the PBX.
    public function __construct(ServiceConnector $svc) {
        $this->svc = $svc;
        parent::__construct($svc->connect(), $svc->name);
    }
</syntaxhighlight>
The first thing it then does is to send an ''AppChallenge'' message to the service:
<syntaxhighlight lang="php">
    public function ReceiveInitialStart(Message  $msg) {
        $this->sendMessage(new Message ("AppChallenge"));
    }
</syntaxhighlight>
When the service returns the challenge, the class needs help from the ''PbxAppLoginAutomaton'' class (which talks to the PBX as we have seen before). To get help, it posts a ''GotChallenge'' message which includes the challenge received from the service and the name of that service.
<syntaxhighlight lang="php">
    public function ReceiveInitialAppChallengeResult(Message  $msg) {
        $this->postEvent(new Message ("GotChallenge", "from", $this->svc->name, "challenge", $msg->challenge));
    }
</syntaxhighlight>


When the ''PbxAppLoginAutomaton'' class (which has already been used to authenticate towards the PBX) is activated again (resulting in a call of its ''ReceiveInitialStart'' event function) it will determine that it already obtained a service list from the PBX (see above) and will therefore move into the new state named ''SvcAuthenticate''.
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
/*
    public function ReceiveInitialStart(Message  $msg) {
* authenticate towards the PBX as an App service
        if (empty($this->services)) {
  */
            // initial login to the PBX
class PbxAppLoginAutomaton extends AppPlatform\AppLoginAutomaton {
            return parent::ReceiveInitialStart($msg);
        }
        else {
            // PBX assisted login towards App services
            return "SvcAuthenticate";
        }
    }
</syntaxhighlight>
When the ''GotChallenge'' event is posted, the ''ReceiveSvcAuthenticateGotChallenge'' event function is called
<syntaxhighlight lang="php">
    public function ReceiveSvcAuthenticateGotChallenge(Message $msg) {
        $this->log("got challenge info from $msg->from");
        $this->sendMessage(new Message ("GetServiceLogin", "api", "Services", "app", $msg->from, "challenge", $msg->challenge, "src", $msg->from));
    }
</syntaxhighlight>
and sends a ''GetServiceLogin'' message to the PBX which includes the service name and the challenge. It also includes a ''src'' property set to the name of the App.  This will instruct the PBX to include the same property when the response is sent, so that the returned information can be associated with the proper service later on.


The PBX will eventually respond with a ''GetServiceLoginResult'' message. This needs to be passed to the ''Pbx2AppAuthenticator'' class instance which will send it to the service.  This is done using the ''postEvent'' mechanism again:
<syntaxhighlight lang="php">
    public function ReceiveSvcAuthenticateGetServiceLoginResult(Message  $msg) {
        $this->postEvent(new Message ("GotChallengeResult", "for", $msg->src, "msg", $msg));
</syntaxhighlight>
The ''Pbx2AppAuthenticator'' class instance which is interested in exactly this ''GetServiceLoginResult'' response has a ''ReceiveInitialGotChallengeResult'' event function which is called due to this ''postEvent'':
<syntaxhighlight lang="php">
    public function ReceiveInitialGotChallengeResult(Message  $msg) {
        if ($msg->for == $this->svc->name) {
            $this->sendMessage(new Message ("AppLogin",
                                                      "app", $msg->msg->app,
                                                      "domain", $msg->msg->domain,
                                                      "sip", $msg->msg->sip,
                                                      "guid", $msg->msg->guid,
                                                      "dn", $msg->msg->dn,
                                                      "digest", $msg->msg->digest,
                                                      "pbxObj", $msg->msg->pbxObj,
                                                      "info", $msg->msg->info));
        }
    }
</syntaxhighlight>
The function verifies that it has been called for the challenge result it is actually interested in and then passes it to its service.  The service will return an ''AppLoginResult'' message which is processed by the ''ReceiveInitialAppLoginResult'' function:
<syntaxhighlight lang="php">
    public function ReceiveInitialAppLoginResult(Message  $msg) {
        if (empty($msg->ok) || $msg->ok != 1) {
            $this->log("FAILED to log in to $msg->app ($this->svc->name", "error");
        }
        else {
            $this->log("logged in to $msg->app ({$this->svc->name})", "runtime");
            $this->svc->authenticated = true;
        }
        return "User";
    }
</syntaxhighlight>
The event function checks if the login was successfull (it should have been) and then moves into the new "User" state.  This will trigger the ''ReceiveUserStart'' to be called which simply moves to the ''Dead'' state (that is, it terminates the automaton). 
<syntaxhighlight lang="php">
     /**
     /**
     * @var string PBX IP address
    * this simply ends the automaton.  Override it if you derive a class from this
     * @param Message  $msg
    * @return string  
     */
     */
     protected $pbxUrl;
     public function ReceiveUserStart(Message  $msg) {
        //
        return "Dead";
    }
</syntaxhighlight>


     /**
So why is this?  This is just convenience.  In a real application, we obviously would want to continue talking to the service but the ''Pbx2AppAuthenticator'' class doesn't know how to.  So we would certainly create a new class of our own which extends the ''Pbx2AppAuthenticator'' class and overrides the '' event function. A very simple example can be seen in the ''application.php'' script:
    * @var \WebSocket\WSClient websocket to PBX
<syntaxhighlight lang="php">
    */
class UsersLister extends AppPlatform\Pbx2AppAuthenticator {
     protected $pbxWS;
     public function ReceiveUserStart(\AppPlatform\Message $msg) {
        $this->sendMessage(new AppPlatform\Message("UserData",
                                                  "maxID", 9999, "offset", 0, "filter", "%", "update", false, "col", "id", "asc", true));
     }


     function __construct($pbx, AppPlatform\AppServiceCredentials $cred, $useWS = false) {
     public function ReceiveUserUserDataInfo(\AppPlatform\Message $msg) {
         $this->pbxUrl = (strpos($pbx, "s://") !== false) ? $pbx :
         $this->log("from {$this->svc->name}: user: id $msg->id, name $msg->username", "runtime");
                $this->pbxUrl = ($useWS ? "ws" : "wss") . "://$pbx/PBX0/APPS/websocket";
         return "Dead";
        // create websocket towards the well known PBX URI
        $this->pbxWS = new AppPlatform\WSClient("PBXWS", $this->pbxUrl);
         parent::__construct($this->pbxWS, $cred);
     }
     }
}
</syntaxhighlight>
This sample class simply sends a ''UserData'' message to the ''users-admin'' service and prints out the user information from the resulting ''UserDataInfo'' response.


So this was an example of how automatons can work in tandem.  However, the mechanism can also be used to work with more than two automatons.  In the sample code, an instance of the ''Pbx2AppAuthenticator'' class (more precisely, a derived class such as the ''UsersLister'' class above) is created for each of the services listed by the PBX as available to our App.  They are pushed into an array of ''FinitStateAutomaton''s in addition to the instance of the ''PbxAppLoginAutomaton'' class used to talk to the PBX:
<syntaxhighlight lang="php">
// connect to services we need
$authenticators = [$app];
// scan list of available app services for those we are interested ion
foreach ($app->getServices() as $svc) {
    /* consider if we need this */
    switch ($svc->type) {
        case "innovaphone-devices-api":
            $authenticators[] = new DevicesLister($svc);
            break;
        case "innovaphone-usersadmin":
            $authenticators[] = new UsersLister($svc);
            break;
    }
}
}
</syntaxhighlight>
</syntaxhighlight>
Please note the relative URL path ''/PBX0/APPS/websocket'' usedThis is required for direct API accessThe path ''/PBX0/APPCLIENT/130000/websocket'' which is used in the ''AppServiceLogin'' (as shown in the previous sample code) can only be used for a user login. Apart from that, the provedure to access an API provided by an App service on an App platform is exactly the same.
The resulting list of tasks is then run using the ''Transitioner'' helper class:
<syntaxhighlight lang="php">
// tell class talking to PBX how many authentications we need to do
$app->setNumberOfAuthentications(count($authenticators) - 1);
 
$tr = new AppPlatform\Transitioner($authenticators);
$tr->run();
</syntaxhighlight>
 
=== More sample code ===
==== Using the PbxAdminApi ====
This is a simple piece of code that uses the [https://sdk.innovaphone.com/13r3/doc/appwebsocket/PbxAdminApi.htm PbxAdminApi] to create a duplicate of the App object we were using in the first sample code (''application.php'')It is available in ''pbxadminapi.php''Before you can use the sample, you must make sure that the ''Admin'' check-mark is ticked in the ''App'' tab of your App object.
 
It first uses the ''PbxAppLoginAutomaton'' to log in to the PBX.


To authenticate towards the PBX, we run the
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$app = new PbxAppLoginAutomaton($pbxdns, new AppPlatform\AppServiceCredentials($pbxapp, $pbxpw));
// Login to PBX
$app = new AppPlatform\PbxAppLoginAutomaton($pbxdns, new AppPlatform\AppServiceCredentials($pbxapp, $pbxpw));
$app->run();
$app->run();
if (!$app->getIsLoggedIn()) {
    die("login to the PBX failed - check credentials");
}
</syntaxhighlight>
</syntaxhighlight>


As soon as this is completed, we are logged in to the PBX (we could verify the login success by calling ''$app->getIsLoggedIn()''.
It then uses a new derivative of the ''FiniteStateAutomaton'' class to create the cloned object, passing the WSClient object used by the  ''PbxAppLoginAutomaton'' class to its constructor:
===== Using the API =====
<syntaxhighlight lang="php">
To use the API, we define a new automaton class (so it is derived from ''AppPlatform\FinitStateAutomaton''):
/**
* class to create a new PBX App object just like the one we use for this application
*/
class AppObjectCreator extends AppPlatform\FinitStateAutomaton {
 
    public function ReceiveInitialStart(AppPlatform\Message $msg) {
        //
        {
            return "CopyObject";
        }
    }


<syntaxhighlight lang="php">
    /**
// the class utilizes the PbxApi
    *
class PbxApiSample extends AppPlatform\FinitStateAutomaton {
    * @global string $pbxapp h323-name of source App object
    * @param AppPlatform\Message $msg
    */
    public function ReceiveCopyObjectStart(AppPlatform\Message $msg) {
        global $pbxapp;
        $this->sendMessage(new AppPlatform\Message(
                        "GetObject",
                        "api", "PbxAdminApi",
                        "h323", $pbxapp));
    }


     public function ReceiveInitialStart(\AppPlatform\Message $msg) {
     public function ReceiveCopyObjectGetObjectResult(AppPlatform\Message $msg) {
         $this->sendMessage(new AppPlatform\Message("GetStun", "api", "PbxAdminApi"));
        // patch $msg so it can be used for object creation
         $msg->setMt("UpdateObject");
        // a new one shall be created, no update of the exiting one
        unset($msg->guid);
        // assert unique identifiers
        $msg->h323 .= "-clone";
        $msg->cn  .= " (clone)";
        // we don't want any "Devices" entries
        unset($msg->devices);
       
        $this->sendMessage($msg);
     }
     }


     public function ReceiveInitialGetStunResult(\AppPlatform\Message $msg) {
     public function ReceiveCopyObjectUpdateObjectResult(AppPlatform\Message $msg) {
        if (isset($msg->guid)) {
            $this->log("App object clone created with Guid $msg->guid", "runtime");
        } else {
            $this->log("App object clone could not be created: $msg->error", "runtime");
        }
         return "Dead";
         return "Dead";
     }
     }
}
}
$me = new AppObjectCreator($app->getWs());
$me->run();
</syntaxhighlight>
</syntaxhighlight>


This automaton will merely send a ''GetStun'' message to the API (note the additional ''api'' member that always needs to be set to the API identifier, ''PbxAdminApi'' in our case) and wait for the result.  It terminates as soon as the result is received.
==== Using the RCC API ====
To run this sample code, you must make sure
* there is a waiting queue with ''Name'' (h323/sip) <code>sink</code>
: * it has an ''Alert Timeout'' set to a reasonable number (some seconds, e.g. 3)
: * it has the ''1st Announcement URL'' set to <code>MOH</code>
* there must be a user object
: * it has our application (''myapplication'') check-mark ticked in its ''Apps'' tab
: * it has a phone registered or a softphone provisioned
* the App object for our application (''myapplication'') must have the ''RCC'' check-mark ticked in its ''App'' tab


We instantiate the automaton and run it with the authenticated WebSocket we obtained earlier (retrieved from the ''PbxAppLoginAutomaton'' class using it's ''getWs()'' member function.
This sample code demonstrates use of the [http://sdk.innovaphone.com/13r2/doc/appwebsocket/RCC.htm RCC] API.  It monitors some PBX user objects and shows all related calls.  If a peer named ''sink'' is called, the call is forcefully terminated by our script. It is available in the ''rccapi.php'' sample code file.
 
The code uses a derivative of the ''FinitStateAutomaton'' class called ''RemoteControlUser''.  The first thing it does is to send an ''Initialize'' message to the PBX which initializes the use of the ''RCC'' api.


<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$pbxapi = new PbxApiSample($app->getWs(), "PBX");
class RemoteControlUser extends AppPlatform\FinitStateAutomaton {
$pbxapi->run();
</syntaxhighlight>


===== Using the RCC API =====
    private $myusers = [];
If you want to use the [http://sdk.innovaphone.com/13r2/doc/appwebsocket/RCC.htm RCC] API instead, you can take a look at the sample ''rccapi.php''.<br>
    private $mycalls = [];
<br>
To use this example, you need:
* a PBX App Object named ''rccapi''
* the PBX App Object must have the password ''ip411'' (just for using the example of course ...)
* the PBX App Object doesn't need an App URL set, as it points to the local PBX by default
* the PBX App Object must have the RCC flag set under App
<br>
In the PHP code you'll see the Initialize message which must be sent first. You'll receive UserInfo messages for all users, '''which''' have the '''rccapi''' App flag set in their Apps.


<syntaxhighlight lang="php">
    public function ReceiveInitialStart(AppPlatform\Message $msg) {
public function ReceiveInitialStart(\AppPlatform\Message $msg) {
        // move to Monitoring state
    $this->sendMessage(new AppPlatform\Message("Initialize", "api", "RCC"));
        return "Monitoring";
}
    }


public function ReceiveInitialUserInfo(\AppPlatform\Message $msg) {
    public function ReceiveMonitoringStart(AppPlatform\Message $msg) {
    $this->log("UserInfo");
        // Initialize RCC Api
}
        $this->sendMessage(new AppPlatform\Message(
 
                        "Initialize",
public function ReceiveInitialInitializeResult(\AppPlatform\Message $msg) {
                        "api", "RCC"
    $this->log("InitializeResult");
        ));
    return "Dead";
    }
}
</syntaxhighlight>
</syntaxhighlight>


This sample code is available from build 1012.
The PBX will send a number of ''UserInfo'' messages in response to the ''Initialize'' message.  One message will be sent for each user that has our application check-mark ticked in its ''Apps'' tab. Those users then are said to be monitored by the application using the RCC API.


==== Access a PBX App service using a PBX user login ====
For each new monitored user that is announced this way, the user information is stored in a class-local array. Also, an ''UserInitialize'' message is sent.
You might think: if the websocket authentication procedure used towards an API provided by the PBX is identical to the procedure for an API provided by an App service running on the Apps platform, why can't I use the approach shown in the first sample (''websocket-sample.php'') for the PBX too?


In fact, you can!  The code would look much like the first example in ''websocket-sample.php'', except you need to use a different service specification.
<syntaxhighlight lang="php">
 
    public function ReceiveMonitoringUserInfo(AppPlatform\Message $msg) {
This sample is new from build 1007.
        // remember UserInfo and do UserInitialize on the user
===== PBX configuration =====
        if (isset($this->myusers[$msg->h323])) {
To make this work, you must create the same App object (''pbxadminapi'') as before in ''pbxapi.php''.  However, in addition to that you must fill in its ''URL'' property so that it points to the PBX's API service access point: <code>http://</code>''your-pbx-dns''<code>/PBX0/APPS/websocket</code>
            $this->log("user '$msg->h323' updated", "runtime");
        }
        else {
            $this->log("new user '$msg->h323'", "runtime");
            $this->myusers[$msg->h323] = new stdClass();
            $this->sendMessage(new AppPlatform\Message(
                            "UserInitialize",
                            "api", "RCC",
                            "cn", $msg->cn,
                            "src", $msg->h323 // use "src" to be able to associate response
            ));
        }
        $this->myusers[$msg->h323]->info = $msg;
    }
</syntaxhighlight>


===== Authentication =====
The ''UserInitializeResponse'' message will include a client-local identifier for the initialized user called ''user''.  We remember it in our class-local array of users:


To login, you would use the ''AppPlatform\AppServiceLogin'' class again:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
// login to PBX and devices and users
    public function ReceiveMonitoringUserInitializeResult(AppPlatform\Message $msg) {
$connector = new AppPlatform\AppServiceLogin(
        // remember local user id returned from UserInitialize (associated by "src")
        $pbxdns, new AppPlatform\AppUserCredentials($pbxuser, $pbxpw), array(
         $this->myusers[$msg->src]->user = $msg->user;
    $apispec = new AppPlatform\AppServiceSpec("websocket", "APPS", "PBX0"),
    }
         ),
        true
);
$connector->connect();
</syntaxhighlight>
</syntaxhighlight>
Note the new AppServiceSpec though!


The code will authenticate towards the PBX and towards the API websocket now (just like it did towards the PBX and the App platform services in the ''websocket-sample.php'' sample).   
When a user object is monitored (that is, when there was a successful ''UserInitializeResponse'' message), the PBX will start to send additional ''CallInfo'' messages  for each call related to the user and for all call-state changes.
===== Using the API =====
 
The API can then be used just like before:
We look at those messages and determine if the remote party (the ''peer'') is called ''sink''. If so, we remember this callIf such remembered call later announces to be connected (a ''CallInfo'' message with ''msg'' member ''r-conn'' is received, we terminate the call by sending an appropriate ''UserEnd'' message.


<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
// the class utilizes the PbxApi
    public function ReceiveMonitoringCallInfo(AppPlatform\Message $msg) {
class PbxApiSample extends AppPlatform\FinitStateAutomaton {
        // a call state update
        $this->log("user $msg->user call $msg->call event $msg->msg", "runtime");
        // see if the call is towards the waiting queue "sink"
        if (isset($msg->peer) && isset($msg->peer->h323) && $msg->peer->h323 == "sink") {
            $this->log("user $msg->user call $msg->call with peer h323=sink (notified with $msg->msg)", "runtime");
            // remember this call for monitoring
            $this->mycalls[$msg->call] = $msg;
        }


    public function ReceiveInitialStart(\AppPlatform\Message $msg) {
        // check if we monitor this call and if we are connected
        $this->sendMessage(new AppPlatform\Message("GetStun", "api", "PbxAdminApi"));
        if (isset($this->mycalls[$msg->call])) {
            switch ($msg->msg) {
                case "r-conn" :
                    $this->log("call $msg->call connected ($msg->msg) - disconnecting", "runtime");
                    $this->sendMessage(new AppPlatform\Message(
                                    "UserClear",
                                    "api", "RCC",
                                    "call", $msg->call,
                                    "cause", 88, // "Incompatible destination" see https://wiki.innovaphone.com/index.php?title=Reference:ISDN_Cause_Codes
                    ));
                    break;
                case "del" :
                    $this->log("call $msg->call ended ($msg->msg) - terminating", "runtime");
                    return "Dead";
            }
        }
     }
     }
</syntaxhighlight>


     public function ReceiveInitialGetStunResult(\AppPlatform\Message $msg) {
There is one other interesting mechanism which is used in this script:
         return "Dead";
<syntaxhighlight lang="php">
     public function timeout() {
         $this->log("timeout", "runtime");
     }
     }
}
// AppPlatform\Log::setLogLevel("", "debug", true);
$pbxapi = new PbxApiSample($apiws, "PBX");
$pbxapi->run();
</syntaxhighlight>
</syntaxhighlight>


==== Access the App manager ====
When the system waits for incoming events, there is (by default) a timeout of 5 seconds. If there are no incoming events during this time, an error message is issued and all the automatons are terminated.  However, if an automaton implements an override for the ''timeout'' function and it does not return true, the timeout will be ignored.  This is why our script waits a long time for the end of the call, occasionally spitting out a log message.
<small>(thanks to Forum user james99 who [http://forum.innovaphone.com/moodle2/mod/forum/discuss.php?d=25205#p67980 provided this solution])</small> {{Template:3rd Party Input}}
<syntaxhighlight lang="php">
$connector = new AppPlatform\AppServiceLogin($pbxdns, new AppPlatform\AppUserCredentials($pbxuser, $pbxpw),
        array($managerspec = new AppPlatform\AppServiceSpec("manager")
        ), true);
$connector->connect();
 
// manager socket connection
if ($connector->getAppAutomaton($managerspec)->getIsLoggedIn()) {
    AppPlatform\Log::log("Logged in to app manager");
    $managerws = $connector->getAppAutomaton($managerspec)->getWs();
} else {
    AppPlatform\Log::log("Failed to log in to app manager");
    exit;
}
</syntaxhighlight>


===System Requirements===
===System Requirements===
To run the sample code, you need
To run the sample code, you need
* a platform that is able to run v13r1 PBX firmware (e.g. an IP411, but any other will do too)
* a platform that is able to run v13r2 PBX firmware (e.g. an IP411, but any other will do too)
* a platform that can run the v13r1 app platform (the same IP411 would do), so if you choose to use a gateway, you will need an SSD
* a platform that can run the v13r2 app platform (the same IP411 would do), so if you choose to use a gateway, you will need an SSD
* a web server running PHP 5.4 or up (the code has not been tested with PHP 7, but should run with no problems)
* a web server running PHP 8.x or up (the code has not been tested with PHP 5.6 or 7, but it may run with no problems)
* your favourite PHP IDE
* your favourite PHP IDE


Line 503: Line 658:
==== PBX / ''App Platform'' ====
==== PBX / ''App Platform'' ====
* if you choose to use a gateway platform, install an SSD
* if you choose to use a gateway platform, install an SSD
* upgrade your box (or IPVA) to the latest v13r1
* upgrade your box (or IPVA) to the latest v13r2 (or later)
* you will need two extra IP address. One for the ''App Platform'', one for the PBX
* you will need two extra IP address. One for the ''App Platform'', one for the PBX
* perform a factory reset
* perform a factory reset
* access the box and you will see the installer
* access the box and you will see the installer (if not, use <code>htps://<ip-of-our-pbx/install.htm</code>)
* before you can run the installer, you will need do create a DNS name for your PBX and ''App Platform''
* complete the installer using IP addresses instead of DNS names, so that it installs a PBX and a fresh ''App Platform'' (you can of course also use DNS names during the Install if you have them operational for the PBX and App platform IP addresses. For running the sample code, it does not matter)
** the easiest way to do this, is to use your box itself as an (additional) DNS server
** access your PBX using the URL <code>https://</code>''your-pbx-ip-address''<code>/admin.xml?xsl=admin.xsl</code> to bypass the installer for a moment
** go to ''Services/DNS''
** tick ''Enable DNS Server''
** add a ''New Resource Record'' of type <code>A</code>
** use <code>sindelfingen.sample.dom</code> as ''Name''
** use ''your-pbx-ip-address'' as ''IP Address''
** add another ''New Resource Record'' of type <code>A</code>
** use <code>apps.sample.dom</code> as ''Name''
** use ''your-app-platform-ip-address'' as ''IP Address''
* restart the box and access the installer again
* complete the installer using DNS names set above, so that it installs a PBX and a fresh ''App Platform''
* be sure to note the ''Admin Password'' shown in the installer
* be sure to note the ''Admin Password'' shown in the installer
* the installer will ask for the name of an ''admin account''.  Be sure to note name and password. To make sure the sample code will run right away, use <code>ckl</code> as name here and <code>pwd</code> as password
* the installer will ask for the name of an ''admin account''.  Be sure to note name and password.  
* for convenience, consider to not turn on ''two factor authentication''
* for convenience, consider to not turn on ''two factor authentication''


Line 527: Line 670:
* unpack the sample sources in to your web server's content directories
* unpack the sample sources in to your web server's content directories
* make sure PHP scripts can be executed in the ''.../sample/sources'' directory
* make sure PHP scripts can be executed in the ''.../sample/sources'' directory
* open <code>websocket-sample.php</code>
* configure the PBX according to the hints given with the individual sample code above
 
* open the file ''application.php'' in your browser
You should now see the following output:
 
<code>
07/09/18 12:21:33.5742 0.000 0000.000 smsg AppPlatform\Transitioner:  sent->PBXWS {"mt":"Login","type":"user","userAgent":"websocket.class.php (PHP WebSocket CKL-CELSIUS-W10)"}<br>
07/09/18 12:21:33.5750 0.000 0000.000 smsg AppPlatform\Transitioner:  received<-PBXWS {"mt":"Authenticate","type":"user","method":"digest","domain":"sample.dom","challenge":"3e6bf6a1ba658959"}<br>
...
</code>


===Known Problems===
===Known Problems===
Line 545: Line 681:


=== Download ===
=== Download ===
The sample code can be downloaded [http://wiki.innovaphone.com/index.php?title=Howto:Wiki_Sources#websocketphp5 here ]
The sample code can be downloaded [http://wiki.innovaphone.com/index.php?title=Howto:Wiki_Sources#websocketphp8 here ]
<!-- == Related Articles == -->
<!-- == Related Articles == -->



Latest revision as of 11:46, 10 July 2023

This is a slight rewrite of the older article in the Reference13r1 namespace. While there is nothing technically wrong with the old article, some of the authentication methods described there are no longer recommended for use with scripts. In particular, it is not recommended that a script authenticate as a PBX user. Instead, it should always authenticate against a PBX App type object. If the script needs to access other App services, it should use the Services API described in https://sdk.innovaphone.com/13r2/doc/appwebsocket/Services.htm.

Applies To

This information applies to

  • innovaphone platform running the 13r2 (or later) App Platform
  • PBX running 13r2 (or later) firmware
  • PHP script accessing the App Services available on the App Platform

More Information

Problem Details

The v13 App Platform runs several App Services that provide services used by Apps running in the context of the myApps client. Apps (written in JavaScript) are loaded from the App Platform and launched by the myApps client. They communicate with the App Services using a JSON-based WebSocket protocol. Apps can also be loaded from the PBX (so called PBX Apps).

The vanilla way to go when it comes to 3rd party integration is to write a custom App Service and install it on the App Platform. The creation of such App Services is facilitated by the v13 SDK.

Note: The mechanisms described here are available in the 13r2 SDK, so all links to the SDK are given for the 13r2 version. To access other versions of the SDK as they become available, go to SDK root and follow the links to the version of interest.

App-platform-php-appplatform-simplified.png

However, in some scenarios you may want to talk to the PBX or existing App Services directly from your own application (i.e. not via an App running in the myApps client). This article describes how to do this. General considerations are given that apply to all programming languages, and some sample code is given in PHP.

App-platform-php-appplatform-simplified-3rd-party.png

Authentication

To talk to the PBX or another App Service, your application (written in PHP or any other language that can talk to a WebSocket) needs to be logged in to the PBX. While an App does not need to worry about this, as the myApps client (which is the context in which all Apps run) would take care of this, an App Service needs to implement the protocol itself.

The process is as follows:

  • An App type object must be created on the PBX for your application.
  • The App object defines the authentication credentials (Name and Password) as well as the access rights of your application (i.e., the APIs in the App tab, the licenses in the License tab, and the Apps in the Apps tab)
  • The application must log in to the PBX using the credentials configured for the App type object that corresponds to the application. In other words, it logs in as the App, not as a specific user
  • The application can then use the allowed PBX APIs (according to the definitions in the Grant access to APIs section in the App tab of the App object).
  • The application can authenticate to other allowed App services (as defined in the Apps tab of the App object) using the PBX Services API. It can then request services from these App Services

WebSocket

App Services (as well as the aforementioned PBX APIs) communicate using messages sent through WebSockets. The content of those messages has JSON syntax.

Although WebSockets are based on the HTTP protocol (for example, they usually use ports 80/443), they behave fundamentally different than HTTP connections in that they are asynchronous. With HTTP, all requests are synchronous, that is, each request is responded to by an answer. Also, only the HTTP client can send requests, not the server. With WebSocket however, both sides can (once they have agreed to upgrade the HTTP connection to a WebSocket connection) send messages at any time. Such messages may trigger 0, 1 or more response messages, depending on the application protocol. The protocol itself does not define a relation between a message and its response messages. As there is no such thing as a synchronous request/response cycle with WebSocket, an asynchronous programming model is required to take full advantage of the WebSocket approach.

JSON

JSON stands for JavaScriptObjectNotation. It is a subset of the syntax JavaScript uses to denote (complex) data constants. Here is an example: {"mystring":"a","myint":42}. As such, it allows to store the value of an object as a string and also to set the value of an object from a string (a process known as serialization/unserialization in many programming languages). JSON allows us to put the value of an object in to a WebSocket message and also of course to retrieve such value from a WebSocket message. Compared to XML (which more or less allows the same), it is much more compact.

Although JSON is related to JavaScript, most programming languages can deal with it. For example, PHP has the json_encode() and json_decode() functions, C# has the DataContractJsonSerializer class.

Here is a full example of the JSON message that might be used to authenticate towards the PBX

{
    "mt": "AppLogin",
    "digest": "955da4b8351557c54c25b99fa8aad9e8b4ba7a4090ac5c2664e7ef7a301e6586",
    "domain": "",
    "sip": "",
    "guid": "",
    "dn": "",
    "app": "myapplication",
    "info": {}
}

When working with App Services, there are three message members which are somehow special for all such services.

mt (string)
the message type. Each message must have an mt member which denotes the message type
api (string, optional)
some App services (and in particular the PBX) support different defined sets of messages (referred to as api). In this case, the mt message member is not sufficient to specify the type of message. Instead, the API identifier must be given in the api message member. The RCC api provided by the PBX would be an example where all messages sent to or received from the PBX for this API must have an api message member with the (string) value "RCC"
src (string, optional)
this member may be sent along with messages sent to an App Service. If the message triggers responses, the App Service will copy the src member in to the responses. This allows the client to associate responses to the message that initiated them.

All other members (if any) are defined by the application protocol.

Definition of Application Protocols

Message types, their members and the message flow are part of the documentation in the software development kit (SDK) that comes with the App Platform. In this article, we only touch them for educational purposes.

PHP Sample Code

These scripts use a configuration from a file called my-pbx-data.php.

To provide proper data for your environment to the scripts, create the file my-pbx-data.php as follows:

<?php
$pbxdns = "your-pbx-dns-or-ip";
$pbxapp = "your-app-object-name";
$pbxpw = "your-pbx-app-password";

Our little example (available in application.php) will connect to 2 App Services on the App Platform, Devices and Users to retrieve some configuration data. To authenticate towards the App service instances, a dedicated PBX App object in the PBX with appropriate rights is used.

PBX configuration

You will need a PBX which has been set up using the standard Install procedure (http://$pbxdns /install.htm).

On that PBX, you additionally need to create an App type object

  • whose Name property is equal to the value of $pbxapp
  • whose Password is equal to the value of $pbxpw
  • that has ticked Services in the Grant access to APIs section of the App tab
  • that has ticked devices-api and users-admin in the Apps tab
Login

Logging-in to the PBX requires the following steps:

  • request a challenge from the PBX using the AppChallenge message
sent->PBXWS {"mt":"AppChallenge"}
  • receive the challenge from the PBX
received<-PBXWS {"mt":"AppChallengeResult","challenge":"8b7d8a1a3a281efd"}
  • compute the digest based on the App data, the secret and the challenge
sent->PBXWS 
{
    "mt": "AppLogin",
    "digest": "1a07f3c20d2a4f6b117c64d845b9bed7b884b49b82868f0e2b73200553c40e2d",
    "domain": "",
    "sip": "",
    "guid": "",
    "dn": "",
    "app": "myapplication",
    "info": {}
}
  • receive the confirmation from the PBX
received<-PBXWS {"mt":"AppLoginResult","ok":true}

This handshake is implemented in the AppPlatform\AppLoginAutomaton class available in classes\websocket.class.php. The thing that needs to be added is the WebSocket connection to the PBX (an AppPlatform\WSClient object required as constructor argument for the AppPlatform\AppLoginAutomaton class). We do this using a derived class named PbxAppLoginAutomaton:

class PbxAppLoginAutomaton extends AppLoginAutomaton {

    function __construct($pbx, AppServiceCredentials $cred, $useWS = false) {
        $this->pbxUrl = (strpos($pbx, "s://") !== false) ? $pbx :
                $this->pbxUrl = ($useWS ? "ws" : "wss") . "://$pbx/PBX0/APPS/websocket";
        // create websocket towards the well known PBX URI
        $this->pbxWS  = new WSClient("PBXWS", $this->pbxUrl);
        parent::__construct($this->pbxWS, $cred);
    }
}

This class takes the URL to the PBX and the credentials (wrapped in an object of class AppPlatform\AppServiceCredentials) as constructor arguments:

$app = new PbxAppLoginAutomaton($pbxdns, new AppPlatform\AppServiceCredentials($pbxapp, $pbxpw));

Our new class ultimately is a derivative of the AppPlattform\FinitStateAutomaton class, so we can run the automaton like:

$app->run();

The automaton will do the authentication towards the PBX as shown above and then terminate (which causes the run() member function to return).

The code then verifies that the log-in to the PBX succeeded:

if (!$app->getIsLoggedIn()) {
    die("login to the PBX failed - check credentials");
}

Eh voilà, we are connected to the PBX.

That was the easy part :-)

Note that the code for the PbxAppLoginAutomaton can be found in the classes/websocket.class.php file.

Determination of available services

Now that we are successfully connected to the PBX, we can determine the services that are available to our application. This is done using the Services API available in the PBX (which is described in the SDK's Services page).

Only a few steps are required:

  • subscribe to the available services information
sent->PBXWS {"mt":"SubscribeServices","api":"Services"}
note the additional api member
  • receive the respones
received<-PBXWS {"api":"Services","mt":"SubscribeServicesResult"}
note that there is no further information in this message. The actual list of services will be received later on
  • unsubscribe from the list of services as we do not need dynamic updates
sent->PBXWS {"mt":"UnsubscribeServices","api":"Services"}
  • receive the actual list of services available to us
received< - PBXWS
{
    "api": "Services",
    "mt": "ServicesInfo",
    "services": [{
            "name": "devices-api",
            "title": "DevicesApi",
            "url": "https://192.168.178.71/sample.dom/devices/innovaphone-devices-api",
            "info": {
                "apis": {
                    "com.innovaphone.devices": {}
                }
            }
        }, {
            "name": "users-admin",
            "title": "Users Admin",
            "url": "https://192.168.178.71/sample.dom/usersapp/innovaphone-usersadmin"
        }]
}
note that the actual list of services depends on the configuration of the Apps tab in the App object for our application


Life was easy so far as we have derived the PbxAppLoginAutomaton utility class which simply did what we wanted so far, except for the initialization in its constructor. Now we want to implement further conversation with the PBX, which requires some more fundamental understanding of the finite state automaton classes.

The finite state automaton classes

Generally, to talk to the PBX or any App service, you can of course use just any WebSocket library directly. We are using (and recommending) a derivative of the Textalk websocket classes. We simplified the code a bit and also added support for receiving WebSocket messages asynchronously. You will find this code in classes/textalk.class.php.

However, solely using the textalk classes leaves you with quite a bit of work to do. As discussed above, there are some challenges:

  • websocket is async by nature
you will need some support for receiving messages asynchronously at least. Aside from the support for asynchronous receipt of messages added to the Textalk classes, the sample code has some classes which allow you to easily create event driven code which processes messages whenever they come in and not when you expect them to come in
  • PBX login requires some fiddling with encryption schemes

The sample code includes some classes to create finite state automatons. Also it has some classes derived from this, which actually implement the protocols required to log-in to the PBX and the App Services

Those extensions can be found in classes/websocket.class.php.

The asynchronous programming model

PHP is not really nicely prepared for asynchronous programming. To deal with that, we have created a utility class called FinitStateAutomaton. This class handles the communication to a single App Service. This is why it has a WebSocket connection as argument to the constructor (our WebSocket implementation is actually called WSClient). The class will use this WebSocket to talk to the App Service. As we have seen above, the PbxAppLoginAutomaton utility class creates such an WSClient object and passes it to the AppLoginAutomaton (which extends the FinitStateAutomaton class).

However, if you try to instantiate such a class (like

$mya = new FinitStateAutomaton($mywebsocket)

you will see that this is not possible, as the class is abstract. This means that there are some member functions missing which must be implemented by a derived class. These functions are those which know how to handle messages received from the App Service.

Let us look at how the AppLoginAutomaton class does this:

class AppLoginAutomaton extends FinitStateAutomaton {
    public function ReceiveInitialStart(Message $msg) {

        $this->log("requesting challenge");
        $this->sendMessage(new Message("AppChallenge"));

    }
}

It defines an override for the abstract ReceiveInitialStart member function. This function, as all functions whose name begins with Receive is called when an event is fed into the automaton. Usually, this event is a message (of type AppPlatform\Message) received from an app service. In this special case however, it is a pseudo event generated by the system indicating that the automaton should start (hence the name ReceiveInitialStart). So it implements the first action the automaton performs.

In our case, as discussed above, it sends an AppChallenge message to the PBX. Recall that such an automaton is always instantiated with a single WSClient argument, which is the WebSocket connection used to talk to the App service (or the PBX which behaves like an App service). In other words, a single instance of a FinitStateAutomaton always talks to a single App service only. This is why we can simply say

        $this->sendMessage(new Message("AppChallenge"));

without specifying a destination and resulting in an AppChallenge message

sent->PBXWS {"mt":"AppChallenge"}

sent to the PBX.

Eventually, the PBX will respond with an AppChallengeResult message.

received<-PBXWS {"mt":"AppChallengeResult","challenge":"95ed003a24c3fc89"}

When this happens, the system will call the ReceiveInitialAppChallengeResult member function:

    public function ReceiveInitialAppChallengeResult(Message $msg) {
        $infoObj          = new \stdClass();
        $infoHashString   = json_encode($infoObj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $hashcode         = hash('sha256', $sha              = "{$this->cred->app}:::::{$infoHashString}:{$msg->challenge}:{$this->cred->pw}");
        // $this->log("computed challenge $sha", "debug");  // do not output in any other category than "debug" coz it includes the password
        $this->sessionKey = hash('sha256', "innovaphoneAppSessionKey:{$msg->challenge}:{$this->cred->pw}");
        $this->sendMessage($this->loginData  = new Message("AppLogin", "digest", $hashcode, "domain", "", "sip", "", "guid", "", "dn", "", "app", $this->cred->app, "info", $infoObj));
    }

It does some cryptographic magic to compute a digest from the available information and then sends an AppLogin message to the PBX:

        $this->sendMessage($this->loginData  = new Message("AppLogin", "digest", $hashcode, "domain", "", "sip", "", "guid", "", "dn", "", "app", $this->cred->app, "info", $infoObj));

resulting in a message such as

sent - >PBXWS {
    "mt": "AppLogin",
    "digest": "955da4b8351557c54c25b99fa8aad9e8b4ba7a4090ac5c2664e7ef7a301e6586",
    "domain": "",
    "sip": "",
    "guid": "",
    "dn": "",
    "app": "myapplication",
    "info": {}
}

being sent to the PBX.

The PBX would verify the correctness of the provided digest and then return an AppLoginResult message.

received<-PBXWS {"mt":"AppLoginResult","ok":true}

Due to the reception of such a message, the ReceiveInitialAppLoginResult member function is called. It does a bit of internal housekeeping and then does two interesting things:

    public function ReceiveInitialAppLoginResult(Message $msg) {

        $response->setMt("AppLoginSuccess");
        $this->postEvent($response);

        return "Dead";
    }

This code creates a $response message and sets the mt member to AppLoginSuccess. It returns the string "Dead" then.

The event function (ReceiveInitialAppChallengeResult) we looked at so far did not have any explicit return statement. In PHP terms that means that it returns a null value. When an event returns a string however, it indicates that the automaton shall move to a new state.

In our case, the new state is named "Dead" and it has a special meaning. An automaton in state "Dead" is considered to be terminated. The system will not listen for incoming messages on the automaton's WSClient socket any further.

Note that the initial state of any automaton is named Initial. This is why event member functions are named ReceiveInitialStart for example. If a member function returns a new state name other than Dead, the member functions called upon subsequent incoming messages will be named ReceiveNewStateNameEvent. Also, the first thing the system will do is call the ReceiveNewStateNameStart member function.

Working with multiple automatons

Obviously, applications may want to talk to multiple destinations, e.g. to the PBX for authentication and to one or more other App services. As a single FiniteStateAutomaton always talks to a single WSClient connection, we need to instantiate multiple automatons to be able to talk to multiple destinations:

$a1 = new Type1Automaton();
$a2 = new Type2Automaton();
$transitioner = new AppPlatform\Transitioner($a1, $a2);
$transitioner->run();

AppPlatform\Transitioner is a helper class that takes a number of automatons (either listed as multiple arguments or wrapped in an array and given as single argument) as constructor arguments. When its run member function is called, it will in turn call all of the automatons ReceiveInitialStart member functions and then listen for messages coming in on any of the automatons WSClient connections.

By the way, the FinitStateAutomaton's own run member function is trivial:

    /**
     * utility function for the simple case you want to run a single (i.e. this) automaton only
     */
    public function run($sockettimeout = 5) {
        $auto = new Transitioner($this);
        $auto->run($sockettimeout);
    }

The remaining question is how different automaton instances communicate to each other. This is done using the FinitStateAutomaton's postMessage member function as in

        $this->postEvent($response);

shown above in the ReceiveInitialAppLoginResult event function. When postEvent is called, the system would call the corresponding event functions in all currently active automatons. Note that these member functions are called synchronously, that is, they are already executed when the postEvent function returns.

Tandems

The postEvent mechanisms allows two or more automatons to work in tandem. To see how that works, we can have a look at the Pbx2AppAuthenticator class.

/**
 * class that authenticates to an App service with help from the PBX 
 */
class Pbx2AppAuthenticator extends \AppPlatform\FinitStateAutomaton {

This class takes a ServiceConnector as constructor argument (for example one delivered from the PbxAppLoginAutomaton) and creates a WebSocket connection (actually a WSClient object) towards the App service described by the ServiceConnector.

    public function __construct(ServiceConnector $svc) {
        $this->svc = $svc;
        parent::__construct($svc->connect(), $svc->name);
    }

The first thing it then does is to send an AppChallenge message to the service:

    public function ReceiveInitialStart(Message   $msg) {
        $this->sendMessage(new Message ("AppChallenge"));
    }

When the service returns the challenge, the class needs help from the PbxAppLoginAutomaton class (which talks to the PBX as we have seen before). To get help, it posts a GotChallenge message which includes the challenge received from the service and the name of that service.

    public function ReceiveInitialAppChallengeResult(Message   $msg) {
        $this->postEvent(new Message ("GotChallenge", "from", $this->svc->name, "challenge", $msg->challenge));
    }

When the PbxAppLoginAutomaton class (which has already been used to authenticate towards the PBX) is activated again (resulting in a call of its ReceiveInitialStart event function) it will determine that it already obtained a service list from the PBX (see above) and will therefore move into the new state named SvcAuthenticate.

    public function ReceiveInitialStart(Message   $msg) {
        if (empty($this->services)) {
            // initial login to the PBX
            return parent::ReceiveInitialStart($msg);
        }
        else {
            // PBX assisted login towards App services
            return "SvcAuthenticate";
        }
    }

When the GotChallenge event is posted, the ReceiveSvcAuthenticateGotChallenge event function is called

    public function ReceiveSvcAuthenticateGotChallenge(Message  $msg) {
        $this->log("got challenge info from $msg->from");
        $this->sendMessage(new Message ("GetServiceLogin", "api", "Services", "app", $msg->from, "challenge", $msg->challenge, "src", $msg->from));
    }

and sends a GetServiceLogin message to the PBX which includes the service name and the challenge. It also includes a src property set to the name of the App. This will instruct the PBX to include the same property when the response is sent, so that the returned information can be associated with the proper service later on.

The PBX will eventually respond with a GetServiceLoginResult message. This needs to be passed to the Pbx2AppAuthenticator class instance which will send it to the service. This is done using the postEvent mechanism again:

    public function ReceiveSvcAuthenticateGetServiceLoginResult(Message  $msg) {
        $this->postEvent(new Message ("GotChallengeResult", "for", $msg->src, "msg", $msg));

The Pbx2AppAuthenticator class instance which is interested in exactly this GetServiceLoginResult response has a ReceiveInitialGotChallengeResult event function which is called due to this postEvent:

    public function ReceiveInitialGotChallengeResult(Message  $msg) {
        if ($msg->for == $this->svc->name) {
            $this->sendMessage(new Message ("AppLogin",
                                                       "app", $msg->msg->app,
                                                       "domain", $msg->msg->domain,
                                                       "sip", $msg->msg->sip,
                                                       "guid", $msg->msg->guid,
                                                       "dn", $msg->msg->dn,
                                                       "digest", $msg->msg->digest,
                                                       "pbxObj", $msg->msg->pbxObj,
                                                       "info", $msg->msg->info));
        }
    }

The function verifies that it has been called for the challenge result it is actually interested in and then passes it to its service. The service will return an AppLoginResult message which is processed by the ReceiveInitialAppLoginResult function:

    public function ReceiveInitialAppLoginResult(Message  $msg) {
        if (empty($msg->ok) || $msg->ok != 1) {
            $this->log("FAILED to log in to $msg->app ($this->svc->name", "error");
        }
        else {
            $this->log("logged in to $msg->app ({$this->svc->name})", "runtime");
            $this->svc->authenticated = true;
        }
        return "User";
    }

The event function checks if the login was successfull (it should have been) and then moves into the new "User" state. This will trigger the ReceiveUserStart to be called which simply moves to the Dead state (that is, it terminates the automaton).

    /**
     * this simply ends the automaton.  Override it if you derive a class from this
     * @param Message  $msg
     * @return string 
     */
    public function ReceiveUserStart(Message  $msg) {
        // 
        return "Dead";
    }

So why is this? This is just convenience. In a real application, we obviously would want to continue talking to the service but the Pbx2AppAuthenticator class doesn't know how to. So we would certainly create a new class of our own which extends the Pbx2AppAuthenticator class and overrides the event function. A very simple example can be seen in the application.php script:

class UsersLister extends AppPlatform\Pbx2AppAuthenticator {
    public function ReceiveUserStart(\AppPlatform\Message $msg) {
        $this->sendMessage(new AppPlatform\Message("UserData",
                                                   "maxID", 9999, "offset", 0, "filter", "%", "update", false, "col", "id", "asc", true));
    }

    public function ReceiveUserUserDataInfo(\AppPlatform\Message $msg) {
        $this->log("from {$this->svc->name}: user: id $msg->id, name $msg->username", "runtime");
        return "Dead";
    }
}

This sample class simply sends a UserData message to the users-admin service and prints out the user information from the resulting UserDataInfo response.

So this was an example of how automatons can work in tandem. However, the mechanism can also be used to work with more than two automatons. In the sample code, an instance of the Pbx2AppAuthenticator class (more precisely, a derived class such as the UsersLister class above) is created for each of the services listed by the PBX as available to our App. They are pushed into an array of FinitStateAutomatons in addition to the instance of the PbxAppLoginAutomaton class used to talk to the PBX:

// connect to services we need
$authenticators = [$app];

// scan list of available app services for those we are interested ion
foreach ($app->getServices() as $svc) {
    /* consider if we need this */
    switch ($svc->type) {
        case "innovaphone-devices-api":
            $authenticators[] = new DevicesLister($svc);
            break;
        case "innovaphone-usersadmin":
            $authenticators[] = new UsersLister($svc);
            break;
    }
}

The resulting list of tasks is then run using the Transitioner helper class:

// tell class talking to PBX how many authentications we need to do
$app->setNumberOfAuthentications(count($authenticators) - 1);

$tr = new AppPlatform\Transitioner($authenticators);
$tr->run();

More sample code

Using the PbxAdminApi

This is a simple piece of code that uses the PbxAdminApi to create a duplicate of the App object we were using in the first sample code (application.php). It is available in pbxadminapi.php. Before you can use the sample, you must make sure that the Admin check-mark is ticked in the App tab of your App object.

It first uses the PbxAppLoginAutomaton to log in to the PBX.

// Login to PBX
$app = new AppPlatform\PbxAppLoginAutomaton($pbxdns, new AppPlatform\AppServiceCredentials($pbxapp, $pbxpw));
$app->run();
if (!$app->getIsLoggedIn()) {
    die("login to the PBX failed - check credentials");
}

It then uses a new derivative of the FiniteStateAutomaton class to create the cloned object, passing the WSClient object used by the PbxAppLoginAutomaton class to its constructor:

/**
 * class to create a new PBX App object just like the one we use for this application
 */
class AppObjectCreator extends AppPlatform\FinitStateAutomaton {

    public function ReceiveInitialStart(AppPlatform\Message $msg) {
        // 
        {
            return "CopyObject";
        }
    }

    /**
     * 
     * @global string $pbxapp h323-name of source App object
     * @param AppPlatform\Message $msg
     */
    public function ReceiveCopyObjectStart(AppPlatform\Message $msg) {
        global $pbxapp;
        $this->sendMessage(new AppPlatform\Message(
                        "GetObject",
                        "api", "PbxAdminApi",
                        "h323", $pbxapp));
    }

    public function ReceiveCopyObjectGetObjectResult(AppPlatform\Message $msg) {
        // patch $msg so it can be used for object creation
        $msg->setMt("UpdateObject");
        // a new one shall be created, no update of the exiting one
        unset($msg->guid);
        // assert unique identifiers
        $msg->h323 .= "-clone";
        $msg->cn   .= " (clone)";
        // we don't want any "Devices" entries
        unset($msg->devices);
        
        $this->sendMessage($msg);
    }

    public function ReceiveCopyObjectUpdateObjectResult(AppPlatform\Message $msg) {
        if (isset($msg->guid)) {
            $this->log("App object clone created with Guid $msg->guid", "runtime");
        } else {
            $this->log("App object clone could not be created: $msg->error", "runtime");
        }
        return "Dead";
    }
}
$me = new AppObjectCreator($app->getWs());
$me->run();

Using the RCC API

To run this sample code, you must make sure

  • there is a waiting queue with Name (h323/sip) sink
* it has an Alert Timeout set to a reasonable number (some seconds, e.g. 3)
* it has the 1st Announcement URL set to MOH
  • there must be a user object
* it has our application (myapplication) check-mark ticked in its Apps tab
* it has a phone registered or a softphone provisioned
  • the App object for our application (myapplication) must have the RCC check-mark ticked in its App tab

This sample code demonstrates use of the RCC API. It monitors some PBX user objects and shows all related calls. If a peer named sink is called, the call is forcefully terminated by our script. It is available in the rccapi.php sample code file.

The code uses a derivative of the FinitStateAutomaton class called RemoteControlUser. The first thing it does is to send an Initialize message to the PBX which initializes the use of the RCC api.

class RemoteControlUser extends AppPlatform\FinitStateAutomaton {

    private $myusers = [];
    private $mycalls = [];

    public function ReceiveInitialStart(AppPlatform\Message $msg) {
        // move to Monitoring state
        return "Monitoring";
    }

    public function ReceiveMonitoringStart(AppPlatform\Message $msg) {
        // Initialize RCC Api
        $this->sendMessage(new AppPlatform\Message(
                        "Initialize",
                        "api", "RCC"
        ));
    }

The PBX will send a number of UserInfo messages in response to the Initialize message. One message will be sent for each user that has our application check-mark ticked in its Apps tab. Those users then are said to be monitored by the application using the RCC API.

For each new monitored user that is announced this way, the user information is stored in a class-local array. Also, an UserInitialize message is sent.

    public function ReceiveMonitoringUserInfo(AppPlatform\Message $msg) {
        // remember UserInfo and do UserInitialize on the user
        if (isset($this->myusers[$msg->h323])) {
            $this->log("user '$msg->h323' updated", "runtime");
        }
        else {
            $this->log("new user '$msg->h323'", "runtime");
            $this->myusers[$msg->h323] = new stdClass();
            $this->sendMessage(new AppPlatform\Message(
                            "UserInitialize",
                            "api", "RCC",
                            "cn", $msg->cn,
                            "src", $msg->h323 // use "src" to be able to associate response
            ));
        }
        $this->myusers[$msg->h323]->info = $msg;
    }

The UserInitializeResponse message will include a client-local identifier for the initialized user called user. We remember it in our class-local array of users:

    public function ReceiveMonitoringUserInitializeResult(AppPlatform\Message $msg) {
        // remember local user id returned from UserInitialize (associated by "src")
        $this->myusers[$msg->src]->user = $msg->user;
    }

When a user object is monitored (that is, when there was a successful UserInitializeResponse message), the PBX will start to send additional CallInfo messages for each call related to the user and for all call-state changes.

We look at those messages and determine if the remote party (the peer) is called sink. If so, we remember this call. If such remembered call later announces to be connected (a CallInfo message with msg member r-conn is received, we terminate the call by sending an appropriate UserEnd message.

    public function ReceiveMonitoringCallInfo(AppPlatform\Message $msg) {
        // a call state update
        $this->log("user $msg->user call $msg->call event $msg->msg", "runtime");
        // see if the call is towards the waiting queue "sink" 
        if (isset($msg->peer) && isset($msg->peer->h323) && $msg->peer->h323 == "sink") {
            $this->log("user $msg->user call $msg->call with peer h323=sink (notified with $msg->msg)", "runtime");
            // remember this call for monitoring
            $this->mycalls[$msg->call] = $msg;
        }

        // check if we monitor this call and if we are connected
        if (isset($this->mycalls[$msg->call])) {
            switch ($msg->msg) {
                case "r-conn" :
                    $this->log("call $msg->call connected ($msg->msg) - disconnecting", "runtime");
                    $this->sendMessage(new AppPlatform\Message(
                                    "UserClear",
                                    "api", "RCC",
                                    "call", $msg->call,
                                    "cause", 88, // "Incompatible destination" see https://wiki.innovaphone.com/index.php?title=Reference:ISDN_Cause_Codes
                    ));
                    break;
                case "del" : 
                    $this->log("call $msg->call ended ($msg->msg) - terminating", "runtime");
                    return "Dead";
            }
        }
    }

There is one other interesting mechanism which is used in this script:

    public function timeout() {
        $this->log("timeout", "runtime");
    }

When the system waits for incoming events, there is (by default) a timeout of 5 seconds. If there are no incoming events during this time, an error message is issued and all the automatons are terminated. However, if an automaton implements an override for the timeout function and it does not return true, the timeout will be ignored. This is why our script waits a long time for the end of the call, occasionally spitting out a log message.

System Requirements

To run the sample code, you need

  • a platform that is able to run v13r2 PBX firmware (e.g. an IP411, but any other will do too)
  • a platform that can run the v13r2 app platform (the same IP411 would do), so if you choose to use a gateway, you will need an SSD
  • a web server running PHP 8.x or up (the code has not been tested with PHP 5.6 or 7, but it may run with no problems)
  • your favourite PHP IDE

You can download the PBX firmware from store.innovaphone.com.

Installation

The PBX and App Platform is installed using the Install.

PBX / App Platform

  • if you choose to use a gateway platform, install an SSD
  • upgrade your box (or IPVA) to the latest v13r2 (or later)
  • you will need two extra IP address. One for the App Platform, one for the PBX
  • perform a factory reset
  • access the box and you will see the installer (if not, use htps://<ip-of-our-pbx/install.htm)
  • complete the installer using IP addresses instead of DNS names, so that it installs a PBX and a fresh App Platform (you can of course also use DNS names during the Install if you have them operational for the PBX and App platform IP addresses. For running the sample code, it does not matter)
  • be sure to note the Admin Password shown in the installer
  • the installer will ask for the name of an admin account. Be sure to note name and password.
  • for convenience, consider to not turn on two factor authentication

PHP Script

  • unpack the sample sources in to your web server's content directories
  • make sure PHP scripts can be executed in the .../sample/sources directory
  • configure the PBX according to the hints given with the individual sample code above
  • open the file application.php in your browser

Known Problems

Missing php_openssl extension

In case following error appears, make sure to enable php_openssl in your php environment configuration:

Fatal error: Uncaught Error: Call to undefined function AppPlatform\openssl_random_pseudo_bytes()

Download

The sample code can be downloaded here