Friday, March 27, 2009

Asterisk FastAGI server in PHP

Here is a new usage of the PEAR Net_Server class. This time a FastAGI server for Asterisk. In the previous example I used the "Sequential" driver, but this time we'll use the "Fork" driver. We are also using the System_Daemon class from PEAR.

First the server script:

require_once 'System/Daemon.php';

System_Daemon::setOption("appName", "FastAGI");
System_Daemon::setOption("authorEmail", "mortena@tpn.no");

System_Daemon::start();

ob_implicit_flush(true);

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

require_once 'Net/Server.php';

require_once 'lib/FastAGI.php';

$server = Net_Server::create('fork', 'serveraddress', portnumber);
$server->setEndCharacter("\n\n");
$server->setCallbackObject(new FastAGI());

$server->start();

System_Daemon::stop();


And here is the Net_Server handler class:

require_once 'Net/Server/Handler.php';
require_once 'lib/FastAGI/Command.php';

/* * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/**
 *
 * @author Morten Amundsen
 */
final class FastAGI extends Net_Server_Handler
{

    /**
     *
     * @param integer $clientId
     * @param string $data
     */
    public function onReceiveData($clientId = 0, $data = '')
    {
        try {
            $cmd = new FastAGI_Command($data, $this->_server);

            $retval = $cmd->execute();

            $this->convertAndReturn($retval);

            $this->setAsteriskVar('fastagi_status', 'OK');
        } catch (Exception $e) {
            $this->setAsteriskVar('fastagi_error_message', $e->getMessage());
            $this->setAsteriskVar('fastagi_status', 'ERROR');
        }

        $this->_server->closeConnection();
    }

    /**
     *
     * @param mixed $retval
     */
    protected function convertAndReturn($retval)
    {
        if (is_array($retval) or is_object($retval)) {
            foreach ($retval as $key => $value) {
                if (!is_object($value) and ! is_array($value)) {
                    $this->setAsteriskVar($key, $value);
                }
            }
        } else {
            $this->setAsteriskVar('return_value', $retval);
        }
    }

    /**
     *
     * @param string $var Asterisk variable name
     * @param string $value Asterisk variable value
     */
    protected function setAsteriskVar($var, $value)
    {
        $this->_server->sendData("SET VARIABLE \"{$var}\" \"{$value}\"");
    }
}

/**
 * Description of Command
 *
 * @author Morten Amundsen
 */
class FastAGI_Command {

    protected $class;
    protected $method;
    protected $params;
    protected $channel;
    protected $lang;
    protected $type;
    protected $uniqueid;
    protected $callerid;
    protected $calleridname;
    protected $dnid;
    protected $context;
    protected $exten;
    protected $pri;
    protected $connection;

    public function __construct($msg, $connection)
    {
        $lines = explode("\n", $msg);

        $this->connection = $connection;

        foreach ($lines as $line) {
            $parts = explode(':', trim($line));
            switch ($parts[0]) {
                case 'agi_request':
                    unset($parts[0]);
                    $query = implode(':', $parts);

                    if ($data = parse_url(trim($query))) {
                        if (!empty($data['query'])) {
                            parse_str($data['query'], $this->params);
                        } else {
                            $this->params = array();
                        }

                        $pathparts = explode('/', substr($data['path'], 1, strlen($data['path']) - 1));
                        $cmd = $pathparts[count($pathparts) - 1];
                        unset($pathparts[count($pathparts) - 1]);
                        if (!count($pathparts)) {
                            $this->class = $cmd;
                            $this->method = 'default';
                        } else {
                            $this->class = implode('_', $pathparts);
                            $this->method = $cmd;
                        }
                    } else {
                        throw new Exception("Query not understood: {$query}");
                    }
                    break;
                case 'agi_channel':
                    $this->channel = $parts[1];
                    break;
                case 'agi_uniqueid':
                    $this->uniqueid = $parts[1];
                    break;
                case 'agi_callerid':
                    $this->callerid = $parts[1];
                    break;
                case 'agi_context':
                    $this->context = $parts[1];
                    break;
                case 'agi_extension':
                    $this->exten = $parts[1];
                    break;
                case 'agi_priority':
                    $this->pri = $parts[1];
                    break;
            }
        }
    }

    /**
     *
     * @return mixed
     */
    public function execute()
    {
        Zend_Loader::loadClass($this->class);

        if (class_exists($this->class)) {
            $class = $this->class;
            $obj = new $class;

            if (method_exists($obj, $this->method)) {
                return call_user_func_array(array($obj, $this->method), $this->params);
            } else {
                throw new Exception("Method {$this->method} does not exist in class {$this->class}");
            }
        } else {
            throw new Exception("No such class '{$this->class}'");
        }
    }
}




It should now be possible to call your FastAGI server from Asterisk like this:

exten => s,1,AGI(agi://[server]:[port]/Foo/Bar/hello?name=Morten


This would autoload the class Foo_Bar, and call the method hello with the parameter name=Morten, and have the result as new variables in your Asterisk call.

5 comments:

  1. Where are you getting the FastAGI library? I would like to try this but I can't find a 'FastAGI' library with 'Command.php'

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. So I've tried to come up with a working solution and it seems that this post requires Dr. Clue's solution:
    http://code.google.com/p/fastagi-php-drclue/downloads/list
    Still open questions about 'lib/FastAGI/Command.php'

    ReplyDelete
  5. lib/FastAGI/Command.php should probably contain everything from

    class FastAGI_Command {

    and below

    ReplyDelete