Reference13r2:Concept Talking to the v13 Application Platform using PHP

From innovaphone wiki
Revision as of 23:28, 5 July 2023 by Ckl (talk | contribs) (→‎Tandems)
Jump to navigation Jump to search
There are also other versions of this article available: Reference13r1 | Reference13r2 (this version)

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 initiate a log-in to the PBX

{
    "mt": "Login",
    "type": "session",
    "userAgent": "websocket.class.php (PHP WebSocket CKL-CELSIUS-W10)"
}

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 an 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 Authenticate.

    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 teh service. This is done using the postEvent mechanism of course:

    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 a 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-adin 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:

// 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;
    }
}
// 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 wre 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-makr 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.

The code uses a derivative of the 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 message 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 of 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 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 v13r1
  • 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
  • before you can run the installer, you will need do create a DNS name for your PBX and App Platform
    • the easiest way to do this, is to use your box itself as an (additional) DNS server
    • access your PBX using the URL https://your-pbx-ip-address/admin.xml?xsl=admin.xsl to bypass the installer for a moment
    • go to Services/DNS
    • tick Enable DNS Server
    • add a New Resource Record of type A
    • use sindelfingen.sample.dom as Name
    • use your-pbx-ip-address as IP Address
    • add another New Resource Record of type A
    • use apps.sample.dom 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
  • 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 ckl as name here and pwd as 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

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