Howto:Talking to the v13 Application Platform using PHP

From innovaphone-wiki

Jump to: navigation, search

Contents

Applies To

This information applies to

  • innovaphone platform running the v13 App Platform
  • PBX running v13 firmware
  • PHP script accessing the App Services available on the App Platform

More Information

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 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 v13 SDK (to appear soon).

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

However, in some scenarios you may want to talk to 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.

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

Authentication

To talk to an App Service, you need to be logged in to the PBX. 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.

The process is as follows:

  • the application must log-in to the PBX
    • depending on the configuration of the PBX, the authentication type can be digest (the PBX itself knows the user credentials) or ntlm (the PBX passes the user credentials to a 3rd party authentication service using netlogon)
    • depending on the configuration of the PBX, two factor authentication may be required. In this case, the calling application must be prepared that the login process may take a long time (the time needed to complete the two factor process, e.g. to click on the verification link sent by email)
  • the PBX will create and store a permanent session for this login and return special session credentials for this new session
  • subsequent log-ins can be done using the session credentials instead of the user credentials

Please note that the PBX will create new session credentials for each such login. Also, the PBX will only store the last 8 such sessions. Excessive older sessions are deleted. For this reason, it is important that an application stores the session credentials received and uses them for all subsequent log-ins. Otherwise, some older sessions (which may be sessions created by the myApps client or other applications) would be removed.

Once the calling application is logged in to the PBX, the authentication towards the App Service can take place. The process is as follows:

  • the calling application requests a challenge from the App Service
  • the calling application passes this challenge to the PBX
  • the PBX (knowing the shared secret defined for the AppService) creates a hash from some information about the user
  • the calling application passes the hash to the App Service
  • the App Service computes the same hash and compares it to the one received from the application
  • if both match, the application is authenticated

WebSocket

App Services 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 two 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
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 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

Our little example (available in websocket-sample.php) will connect to 2 App Services on the App Platform, Devices and Users to retrieve some configuration data.

Login

As discussed above, to do so, we need to log-in to the PBX and then connect and authenticate to the respective App Services.

// login to PBX and devices and users
$connector = new AppPlatform\AppServiceLogin(
"sindelfingen.sample.dom", new AppPlatform\AppUserCredentials("ckl", "pwd"), array(
$devicesspec = new AppPlatform\AppServiceSpec("\$innovaphone-devices"),
$usersspec = new AppPlatform\AppServiceSpec("users"),
)
);
$connector->connect();

To perform the necessary steps, an instance of class AppServiceLogin (note that in our sample code, all library code provided is in the AppPlatform name space) is used. The constructor arguments are

$PBX (string) 
either the FQDN or the full websocket URI of the PBX . In the sample code, we use the FQDN sindelfingen.sample.dom
$credentials (AppUserCredentials) 
the users Name and Password wrapped in an object of type AppUserCredentials
$appServiceSpec (AppServiceSpec[]) 
an array of App Service specifications to select the ones to connect to, each wrapped in an object of type AppServiceSpec

The call to connect() 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 code then verifies that the log-in to the PBX succeeded:

// look at the PBX login
if ($connector->getPbxA()->getIsLoggedIn()) {
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;
}

If the login succeeded, the WebSocket connection to the PBX and the log-in messages are retrieved. Next step is to verify the respective connections to the App Services:

// 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
if ($connector->getAppAutomaton($usersspec)->getIsLoggedIn()) {
 
AppPlatform\Log::log("Logged in to Users");
$usersws = $connector->getAppAutomaton($usersspec)->getWs();
} else {
AppPlatform\Log::log("Failed to log-in to Users");
exit;
}

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

// now we have the authenticated websockets to our AppServices, so we can release the connector
$connector = null;

That was the easy part :-)


Implementing the WebSocket Protocol

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, 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 and netlogon protocol peculiarities

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 AppServiceLogin utility class creates such WSClient objects (which we retrieved by calling the getAppWebSocket() member function.

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.

So here we go and define a derived class:

// an automaton which lists all devices in Devices
class DeviceLister extends AppPlatform\FinitStateAutomaton {
...
}

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.

When we instantiate this derived class, we will provide the WSClient towards the Devices App Service.

$dl = new DeviceLister($devicesws);

In the base class, there is only one function declared as abstract:

// this function at least must be overriden by any derived class
abstract public function ReceiveInitialStart(\AppPlatform\Message $msg);

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.

public function ReceiveInitialStart(\AppPlatform\Message $msg) {
AppPlatform\Log::log("Requesting User List");
$me = $this->myInfo->info->user;
$this->sendMessage(new AppPlatform\Message("NumUsers", "user", $me->sip, "domain", "@{$me->domain}", "visible", true, "filter", "%"));
}

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 "NumUsers" and we have some more message members called "user", "domain", "visible" and "filter".

Devices will respond to this message with a message that looks like so:

{
    "mt": "GetDevicesResult",
    "devices": [{
            "id": 2,
            "hwId": "029033410109",
            "name": "AP - sample.dom",
            "domain": 1,
            "product": "AppPlatform ARM",
            "version": "60002 dvl",
            "type": "APP_PLATFORM",
            "pbxActive": false,
            "online": true
        }, {
            "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
        }]
}

As we see, the message type mt is "GetDevicesResult" so the ReceiveInitialGetDevicesResult member function of our derived class will be called:

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";
}
}

Here we save the data in some class-local storage ($this->devices). 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 in. In 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 ReceiveNewStateGetDevicesResult() would be called.

So here is the complete derived class:

// an automaton which lists all devices in Devices
class DeviceLister extends AppPlatform\FinitStateAutomaton {
 
protected $devices = array();
 
public function getDevices() {
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";
}
}
 
}

Its as simple as that!

Have a look at the second derived class in the sample code (websocket-sample.php) called UserLister. It is a bit more complicated (as it handles more message types) but you will see the same mechanisms.

Running the Automatons

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 $myauto = new myAuto($ws); $myauto->run(). 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 above. In 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).

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:

$dl = new DeviceLister($devicesws);
$ul = new UserLister($usersws, $pbxloginresult->loginResultMsg);
$t = new AppPlatform\Transitioner($dl, $ul);
$t->run();

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.

/**
* utility function for the simple case you want to run a single (i.e. this) automaton only
*/

public function run() {
$auto = new Transitioner($this);
$auto->run();
}

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.

/**
* 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);

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).

System Requirements

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), build 131133 or later
  • a platform that can run the v13r1 app platform (the same IP411 would do), build 131133 or later (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)
  • your favourite PHP IDE

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

Installation

PBX / App Platform

  • download the latest (or at least 131133) v13r1 build from webbuild.innovaphone.com
  • if you choose to use a gateway platform, install an SSD
  • upgrade the firmware on the box
  • 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 so and access the installer again
  • complete the installer 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
  • when the installer has finished, for convenience, create a link to the App Platform manager (although you can also reach it through the Devices app)
    • in the PBX, create a new object of type App
    • Enter Manager as Long Name
    • Enter manager as Name
    • Enter the password you took note of as Admin Password in the installer as Password
    • Enter https://apps.sample.dom/manager/manager as URL in the Apps' tab
  • for the PBX admin user you have created (ckl), go to the user's Apps and check the manager app
  • for the Config Admin and Config User template objects, open the Visibility form and add an entry with Name set to @sample.dom and Visible checked

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
  • open websocket-sample.php

You should now see the following output:

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)"}
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"}
...

Known Problems

This is Sample Code based on yet unreleased Firmware

As of today (July 2018), this sample code is based on yet unreleased firmware. As such, it is subject to change without notice. Also, the code may or may not work with newer builds.

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 from download.innovaphone.com/ice/wiki-src#websocketphp5

Personal tools