Initial commit.

This commit is contained in:
Julien Fontanet 2013-02-06 10:29:52 +01:00
commit abb2f50e10
16 changed files with 2443 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/src/config/local.php
/src/database.json
/src/log
/vendor/

20
composer.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "vates/xo-server",
"description": "Xen Orchestra server.",
"license": "GPL-3.0",
"authors": [
{
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
}
],
"require": {
"php": ">=5.3.0",
"ext-xmlrpc": "*",
"monolog/monolog": "~1.2",
"ircmaxell/password-compat": "dev-master"
},
"autoload": {
"psr-0": { "": "src/lib" }
}
}

152
composer.lock generated Normal file
View File

@ -0,0 +1,152 @@
{
"hash": "d65aa7e3aad71264202dfcffcccb3cc1",
"packages": [
{
"name": "ircmaxell/password-compat",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/ircmaxell/password_compat.git",
"reference": "fc4ad2d46794ace07cbf04fe654a8bf546fc6764"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/fc4ad2d46794ace07cbf04fe654a8bf546fc6764",
"reference": "fc4ad2d46794ace07cbf04fe654a8bf546fc6764",
"shasum": ""
},
"time": "2013-02-04 16:45:02",
"type": "library",
"autoload": {
"files": [
"lib/password.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Anthony Ferrara",
"email": "ircmaxell@php.net",
"homepage": "http://blog.ircmaxell.com"
}
],
"description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash",
"homepage": "https://github.com/ircmaxell/password_compat",
"keywords": [
"hashing",
"password"
]
},
{
"name": "monolog/monolog",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog",
"reference": "1.3.1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/1.3.1",
"reference": "1.3.1",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"psr/log": ">=1.0,<2.0"
},
"require-dev": {
"doctrine/couchdb": "dev-master",
"mlehner/gelf-php": "1.0.*",
"raven/raven": "0.3.*"
},
"suggest": {
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-mongo": "Allow sending log messages to a MongoDB server",
"mlehner/gelf-php": "Allow sending log messages to a GrayLog2 server",
"raven/raven": "Allow sending log messages to a Sentry server"
},
"time": "2013-01-11 10:23:20",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"psr-0": {
"Monolog": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be",
"role": "Developer"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "http://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
]
},
{
"name": "psr/log",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log",
"reference": "1.0.0"
},
"dist": {
"type": "zip",
"url": "https://github.com/php-fig/log/archive/1.0.0.zip",
"reference": "1.0.0",
"shasum": ""
},
"time": "2012-12-21 11:40:51",
"type": "library",
"autoload": {
"psr-0": {
"Psr\\Log\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"keywords": [
"log",
"psr",
"psr-3"
]
}
],
"packages-dev": null,
"aliases": [
],
"minimum-stability": "stable",
"stability-flags": {
"ircmaxell/password-compat": 20
}
}

39
src/config/global.php Normal file
View File

@ -0,0 +1,39 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
return array(
'database' => array(
'json' => '#{root_dir}/database.json',
),
'listen' => array(
'tcp://0.0.0.0:1024',
),
'log' => array(
//'email' => 'your.email@example.net',
'file' => '#{root_dir}/log',
),
);

52
src/config/local.php.dist Normal file
View File

@ -0,0 +1,52 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*
* Local Variables:
* mode: php
* End:
*/
/* Local settings
*
* This file contains all settings related to this current
* installation of Xen Orchestra Server.
*
* You MUST define the following settings for which no default
* values exists.
*
* But, you MAY override any settings which already exists in
* “global.php”.
*/
return array(
// For now, XCP servers/pools must be defined in this file.
'xcp' => array(
'pool 1' => array(
'url' => 'https://xcp1.example.net',
'username' => 'username',
'password' => 'password'
),
),
);

863
src/lib/Application.php Normal file
View File

@ -0,0 +1,863 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
*
*/
final class Application extends Base
{
/**
*
*/
const NONE = 0;
/**
*
*/
const READ = 1;
/**
*
*/
const WRITE = 2;
/**
*
*/
const ADMIN = 3;
/**
*
*/
function __construct(DI $di)
{
parent::__construct();
$this->_di = $di;
}
/**
*
*/
function api_session_signInWithPassword($id, array $params, Client $c)
{
// Checks parameters.
if (!isset($params[0], $params[1]))
{
return -32602; // Invalid params.
}
list($name, $password) = $params;
// Checks the client is not already authenticated.
if ($c->isAuthenticated())
{
return array(0, 'already authenticated');
}
// Checks the user exists.
if (!isset($this->_usersByName[$name]))
{
return array(1, 'invalid credential');
}
$uid = $this->_usersByName[$name];
$hash = &$this->_users[$uid]['password'];
// Checks the password matches.
if (!password_verify($password, $hash))
{
return array(1, 'invalid credential');
}
// Checks whether the hash needs to be updated.
if (password_needs_rehash($hash, PASSWORD_DEFAULT))
{
$hash = password_hash($password, PASSWORD_DEFAULT);
$this->_saveDatabase();
}
// Marks the client as authenticated.
$c->uid = $uid;
// Returns success.
$c->respond($id, true);
}
/**
*
*/
function api_session_signInWithToken($id, array $params, Client $c)
{
// Checks parameters.
if (!isset($params[0]))
{
return -32602; // Invalid params.
}
$token = $params[0];
// Checks the client is not already authenticated.
if ($c->isAuthenticated())
{
return array(0, 'already authenticated');
}
// Checks the token exists.
if (!isset($this->_tokens[$token]))
{
return array(1, 'invalid token');
}
$record = $this->_tokens[$token];
// Checks the token is valid.
if ($record['expiration'] < time())
{
unset($this->_tokens[$token]);
return array(1, 'invalid token');
}
// Marks the client as authenticated.
$c->uid = $record['uid'];
// Returns success.
$c->respond($id, true);
}
/**
*
*/
function api_session_getUser($id, array $params, Client $c)
{
if (!$c->isAuthenticated())
{
return array(0, 'not authenticated');
}
$c->respond($id, array(
'id' => (string) $c->uid,
'name' => $this->_users[$c->uid]['name'],
'permission' => $this->_users[$c->uid]['permission'],
));
}
/**
*
*/
function api_session_createToken($id, array $params, Client $c)
{
// Checks the client is authenticated.
if (!$c->isAuthenticated())
{
return array(0, 'not authenticated');
}
// Generates the token and makes sure it is unique.
do
{
/* If available, we use OpenSSL to create more secure tokens.
*
* @todo Maybe we should also use a time-resistant token comparison
* algorithm in authentication.
*
* @todo Move the if outside of this function and furthermore of
* this loop for performance concerns.
*/
if (function_exists('openssl_random_pseudo_bytes'))
{
$token = bin2hex(openssl_random_pseudo_bytes(32));
}
else
{
$token = uniqid('', true);
}
} while (isset($this->_tokens[$token]));
// Registers it.
$this->_tokens[$token] = array(
'expiration' => time() + 604800, // One week
'uid' => $c->uid,
);
$this->_saveDatabase();
// Returns it.
$c->respond($id, $token);
}
/**
*
*/
function api_session_destroyToken($id, array $params, Client $c)
{
// Checks the token exists.
if (!isset($this->_tokens[$token]))
{
return array(0, 'invalid token');
}
// Deletes it.
unset($this->_tokens[$token]);
$this->_saveDatabase();
// Returns success.
$c->respond($id, true);
}
/**
*
*/
function api_user_create($id, array $params, Client $c)
{
// Checks parameters.
if (!isset($params[0], $params[1]))
{
return -32602; // Invalid params.
}
list($name, $password) = $params;
// Checks credentials.
if (!$c->isAuthenticated()
|| !$this->_checkPermission($c->uid, self::ADMIN))
{
return array(0, 'not authorized');
}
// Checks the provided user name.
if (!is_string($name)
|| !preg_match('/^[a-z0-9]+(?:[-_.][a-z0-9]+)*$/', $name))
{
return array(1, 'invalid user name');
}
// Checks the provided password.
if (!is_string($password)
|| !preg_match('/^.{8,}$/', $password))
{
return array(2, 'invalid password');
}
// Checks provided permission.
if (isset($params[2]))
{
$permission = self::_permissionFromString($params[2]);
if ($permission === false)
{
return array(3, 'invalid permission');
}
}
else
{
$permission = self::NONE;
}
// Checks if the user name is already used.
if (isset($this->_usersByName[$name]))
{
return array(4, 'user name already taken');
}
// Creates the user.
$this->_users[] = array(
'name' => $name,
'password' => password_hash($password, PASSWORD_DEFAULT),
'permission' => $permission,
);
end($this->_users);
$uid = (string) key($this->_users);
$this->_usersByName[$name] = $uid;
$this->_saveDatabase();
// Returns the identifier.
$c->respond($id, $uid);
}
/**
*
*/
function api_user_delete($id, array $params, Client $c)
{
// Checks parameter.
if (!isset($params[0]))
{
return -32602; // Invalid params.
}
$uid = $params[0];
// Checks credentials.
if (!$c->isAuthenticated()
|| !$this->_checkPermission($c->uid, self::ADMIN))
{
return array(0, 'not authorized');
}
// Checks user exists and is not the current user.
if (!isset($this->_users[$uid])
|| ($uid === $c->uid))
{
return array(1, 'invalid user');
}
// Deletes the user.
$name = $this->_users[$uid]['name'];
unset($this->_users[$uid], $this->_usersByName[$name]);
$this->_saveDatabase();
// Returns success.
$c->respond($id, true);
}
/**
*
*/
function api_user_changePassword($id, array $params, Client $c)
{
// Checks parameters.
if (!isset($params[0], $params[1]))
{
return -32602; // Invalid params.
}
list($old, $new) = $params;
// Checks the client is authenticated.
if (!$c->isAuthenticated())
{
return array(0, 'not authenticated');
}
$hash = &$this->_users[$c->uid]['password'];
// Checks the old password matches.
if (!password_verify($old, $hash))
{
return array(1, 'invalid credential');
}
// Checks the new password is valid.
if (!is_string($new)
|| !preg_match('/^.{8,}$/', $new))
{
return array(2, 'invalid password');
}
$hash = password_hash($new, PASSWORD_DEFAULT);
$this->_saveDatabase();
// Returns success.
$c->respond($id, true);
}
/**
*
*/
function api_user_getAll($id, array $params, Client $c)
{
// Checks credentials.
if (!$c->isAuthenticated()
|| !$this->_checkPermission($c->uid, self::ADMIN))
{
return array(0, 'not authorized');
}
$users = array();
foreach ($this->_users as $uid => $user)
{
$users[] = array(
'id' => $uid,
'name' => $user['name'],
'permission' => self::_permissionToString($user['permission']),
);
}
$c->respond($id, $users);
}
/**
*
*/
function api_vm_getAll($id, array $params, Client $c)
{
// @todo Handles parameter.
$c->respond($id, $this->_xenVms);
}
/**
*
*/
function handleServer($handle, $data)
{
if (feof($handle))
{
// Stops listening to this socket.
return false;
}
$handle = @stream_socket_accept($handle, 10);
if (!$handle)
{
trigger_error(
'error while handling an incoming connection',
E_USER_ERROR
);
}
/* Here we build a map for all available methods.
*
* This technic provides fast case sensitive matching (compare to
* “is_callable()).
*/
static $methods;
if ($methods === null)
{
$methods = array();
foreach (get_class_methods($this) as $method)
{
if (!substr_compare($method, 'api_', 0, 4))
{
$_ = strtr(substr($method, 4), '_', '.');
$methods[$_] = array($this, $method);
}
}
}
new Client(
$data['loop'],
$handle,
$methods
);
echo "new client connected\n";
}
/**
*
*/
function handleXenEvents(array $events)
{
static $keys;
$objects = array();
foreach ($events as $event)
{
$_ = array_keys($event);
if (!$keys)
{
$keys = $_;
var_export($keys); echo PHP_EOL;
}
elseif ($_ !== $keys)
{
$keys = array_intersect($keys, $_);
var_export($keys); echo PHP_EOL;
}
$class = $event['class'];
$ref = $event['ref'];
$snapshot = $event['snapshot']; // Not present in the documentation.
echo "$class - $ref\n";
$objects[$class][$ref] = $snapshot;
}
isset($objects['pool'])
and $this->updateXenPools($objects['pool']);
isset($objects['host'])
and $this->updateXenHosts($objects['host']);
isset($objects['vm'])
and $this->updateXenVms($objects['vm']);
// Requeue this request.
return true;
}
/**
*
*/
function updateXenPools(array $pools)
{
foreach ($pools as $ref => $pool)
{
$this->_update($this->_xenPools[$ref], $pool);
}
}
/**
*
*/
function updateXenHosts(array $hosts)
{
foreach ($hosts as $ref => $host)
{
$this->_update($this->_xenHosts[$ref], $host);
}
}
/**
*
*/
function updateXenVms(array $vms)
{
foreach ($vms as $ref => $vm)
{
if ($vm['is_a_template'])
{
$_ = 'template';
}
elseif ($vm['is_a_snapshot'])
{
$_ = 'snapshot';
}
elseif ($vm['is_control_domain'])
{
$_ = 'control_domain';
}
else
{
$_ = 'normal';
}
$this->_update(
$this->_xenVms[$_][$ref],
$vm
);
}
}
/**
*
*/
function run()
{
$this->_loadDatabase();
//--------------------------------------
$config = $this->_di->get('config');
$loop = $this->_di->get('loop');
//--------------------------------------
// Creates master sockets.
foreach ($config->get('listen') as $uri)
{
$handle = self::_createServer($uri);
$loop->addRead($handle, array($this, 'handleServer'));
}
//--------------------------------------
foreach ($config->get('xcp') as $_)
{
$xcp = new XCP($loop, $_['url'], $_['username'], $_['password']);
$xcp->queue(
'VM.get_all_records',
null,
array($this, 'updateXenVms')
);
$xcp->queue(
'event.register',
array(array('host', 'pool', 'vm'))
);
$xcp->queue(
'event.next',
null,
array($this, 'handleXenEvents')
);
}
//--------------------------------------
$loop->run(array(
'loop' => $loop,
'server' => $this
));
}
/**
*
*/
private static function _createServer($uri)
{
list($transport, $target) = explode('://', $uri, 2);
if (($transport === 'unix')
|| ($transport === 'udg'))
{
@unlink($target);
}
$handle = @stream_socket_server(
$uri,
/* out */ $errno,
/* out */ $errstr
);
if (!$handle)
{
trigger_error(
"could not create the server socket $uri: $errno - $errstr",
E_USER_ERROR
);
}
return $handle;
}
/**
* Dependency injector.
*
* @var DI
*/
private $_di;
/**
* Mapping from user identifier to record.
*
* Each record contains:
* - “name” (string): the user name used for sign in;
* - “password” (string): the user password hashed for sign in.
*
* @var array
*/
private $_users = array();
/**
* Mapping from user name to identifier.
*
* @var array
*/
private $_usersByName = array();
/**
* Tokens that may be used to authenticate clients.
*
* Each token record is an array containing:
* - “expiration” (integer): timestamp of when this token will be
* considered invalid;
* - “uid” (string): the identifier of the user authenticated with
* this token.
*
* @var array
*/
private $_tokens = array();
/**
* @var array
*/
private $_xenPools = array();
/**
* @var array
*/
private $_xenHosts = array();
/**
* @var array
*/
private $_xenVms = array();
/**
*
*/
private static function _tS($val)
{
if (is_scalar($val))
{
return (string) $val;
}
return gettype($val);
}
/**
*
*/
private static function _permissionFromString($string)
{
$permissions = array(
'none' => self::NONE,
'read' => self::READ,
'write' => self::WRITE,
'admin' => self::ADMIN
);
return isset($permissions[$string])
? $permissions[$string]
: false;
}
/**
*
*/
private static function _permissionToString($permission)
{
$permissions = array(
self::NONE => 'none',
self::READ => 'read',
self::WRITE => 'write',
self::ADMIN => 'admin',
);
return isset($permissions[$permission])
? $permissions[$permission]
: false;
}
/**
*
*/
private function _update(&$old, $new)
{
// There was no previous record.
if ($old === null)
{
echo "new record\n";
$old = $new;
return;
}
// The record has been deleted.
if ($new === null)
{
echo "record deleted\n";
$old = null;
return;
}
$old_keys = array_keys($old);
$new_keys = array_keys($new);
foreach (array_diff($old_keys, $new_keys) as $key)
{
$_ = self::_tS($old[$key]);
echo "field removed: $key => $_\n";
}
foreach (array_diff($new_keys, $old_keys) as $key)
{
$_ = self::_tS($new[$key]);
echo "field added: $key => $_\n";
}
foreach (array_intersect($new_keys, $old_keys) as $key)
{
if ($new[$key] === $old[$key])
{
continue;
}
$_1 = self::_tS($old[$key]);
$_2 = self::_tS($new[$key]);
echo "field changed: $key => $_1$_2\n";
}
$old = $new;
}
/**
*
*/
private function _saveDatabase()
{
$data = json_encode(array(
'users' => $this->_users,
'usersByName' => $this->_usersByName,
'tokens' => $this->_tokens,
));
$bytes = @file_put_contents(
$this->_di->get('config')->get('database.json'),
$data
);
if ($bytes === false)
{
trigger_error(
'could not write the database',
E_USER_ERROR
);
}
}
/**
*
*/
private function _loadDatabase()
{
$file = $this->_di->get('config')->get('database.json');
if (!file_exists($file))
{
trigger_error(
'no such database, using default values (admin:admin)',
E_USER_WARNING
);
// @todo Factorizes this code with api_user_create().
$this->_users = array(
1 => array(
'name' => 'admin',
'password' => '$2y$10$VzBQqiwnhG5zc2.MQmmW4ORcPW6FE7SLhPr1VBV2ubn5zJoesnmli',
'permission' => self::ADMIN,
),
);
$this->_usersByName = array(
'admin' => '1',
);
return;
}
$data = @file_get_contents(
$this->_di->get('config')->get('database.json')
);
if (($data === false)
|| (($data = json_decode($data, true)) === null))
{
trigger_error(
'could not read the database',
E_USER_ERROR
);
}
foreach (array('users', 'usersByName', 'tokens') as $entry)
{
if (!isset($data[$entry]))
{
trigger_error(
"missing entry from the database: $entry",
E_USER_ERROR
);
}
$this->{'_'.$entry} = $data[$entry];
}
}
/**
*
*/
private function _checkPermission($uid, $permission, $object = null)
{
return ($this->_users[$uid]['permission'] >= $permission);
}
}

51
src/lib/Base.php Normal file
View File

@ -0,0 +1,51 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
* Ultimate base class.
*/
abstract class Base
{
function __destruct()
{}
function __get($name)
{
trigger_error(
'no such readable property '.get_class($this).'->'.$name,
E_USER_ERROR
);
}
function __set($name, $value)
{
trigger_error(
'no such writable property '.get_class($this).'->'.$name,
E_USER_ERROR
);
}
protected function __construct()
{}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
*
*/
final class BufferedWriter
{
/**
* @param string $data
*/
function __construct($data, $len = null)
{
$this->_data = $data;
$this->_len = $len ?: strlen($data);
}
/**
*
*/
function onWrite($handle)
{
$written = @fwrite(
$handle,
$this->_data,
$this->_len
);
if ($written === false)
{
// @todo Log error.
return false;
}
$this->_len -= $written;
if (!$this->_len)
{
// Write complete, stops watching this handle.
return false;
}
$this->_data = substr($this->_data, -$this->_len);
}
}

286
src/lib/Client.php Normal file
View File

@ -0,0 +1,286 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
*
*/
final class Client extends Base
{
/**
*
*/
public $uid = 0;
/**
* @param Loop $loop
* @param resource $handle
* @param callable[] $methods
*/
function __construct(Loop $loop, $handle, $methods)
{
parent::__construct();
$this->_loop = $loop;
$this->_handle = $handle;
$this->_methods = $methods;
stream_set_blocking($this->_handle, false);
$loop->addRead($this->_handle, array($this, '_onRead'));
}
/**
*
*/
function isAuthenticated()
{
return ($this->uid !== 0);
}
/**
*
*/
function notify($method, array $params)
{
$this->_send(array(
'method' => $method,
'params' => $params
));
}
/**
*
*/
function respond($id, $result)
{
if ($id === null)
{
trigger_error(
'notifications do not expect responses',
E_USER_ERROR
);
}
$this->_send(array(
'id' => $id,
'result' => $result
));
}
////////////////////////////////////////
//
////////////////////////////////////////
/**
* This function is called when data become available on the
* connection.
*
* Its name starts with a “_” because it is only supposed to be
* used internally (it is not private because of PHP limits).
*/
function _onRead()
{
// Read the message length.
if (!$this->_len)
{
$len = stream_get_line($this->_handle, 1024, "\n");
if ($len === false)
{
if (feof($this->_handle))
{
echo "client disconnected\n";
// Closes this socket and stops listening to it.
stream_socket_shutdown($this->_handle, STREAM_SHUT_RDWR);
fclose($this->_handle);
return false;
}
$this->_error('failed to read the request length');
}
if (!ctype_digit($len)
|| ($len <= 0))
{
$this->_error('invalid request length');
}
$this->_len = (int) $len;
}
$buf = @fread($this->_handle, $this->_len);
if ($buf === false)
{
$this->_error('failed to read the request');
}
$this->_buf .= $buf;
$this->_len -= strlen($buf);
if ($this->_len !== 0)
{
return;
}
$request = @json_decode($this->_buf, true);
$this->_buf = '';
if ($request === null)
{
$this->_warning(null, -32700, 'Parse error');
return;
}
if (!isset($request['jsonrpc'], $request['method'])
|| ($request['jsonrpc'] !== '2.0'))
{
$this->_warning(null, -32600, 'Invalid request');
return;
}
$id = isset($request['id']) ? $request['id'] : null;
$method = $request['method'];
$params = isset($request['params']) ? $request['params'] : array();
if (isset($this->_methods[$method]))
{
$_ = $this->_methods[$method];
$error = $_($id, $params, $this);
}
else
{
$error = array(-32601, 'Method not found', $method);
}
if (($error === null)
|| ($id === null))
{
// No errors or this was a notification (no error handling).
return;
}
$errors = array(
-32602 => array(
'Invalid params',
$params
),
);
if (is_numeric($error) && isset($errors[$error]))
{
$error = $errors[$error];
}
elseif (!is_array($error) || !isset($error[0], $error[1]))
{
trigger_error(
'invalid error',
E_USER_ERROR
);
}
$this->_warning(
$id,
$error[0],
$error[1],
isset($error[2]) ? $error[2] : null
);
}
/**
* @var Loop
*/
private $_loop;
/**
* @var resource
*/
private $_handle;
/**
* @var callable[]
*/
private $_methods;
/**
* @var integer
*/
private $_len;
/**
* @var string|null
*/
private $_buf;
/**
*
*/
private function _error($message)
{
throw new Exception($message);
}
/**
* @param array $message
*/
private function _send($message)
{
$message['jsonrpc'] = '2.0';
$message = json_encode($message);
$data = strlen($message)."\n".$message;
$len = strlen($data);
$written = @fwrite($this->_handle, $data, $len);
if ($written === false)
{
$this->_error('failed to send the message');
}
$len -= $written;
if ($len)
{
echo "$len bytes to write, using a buffered writer\n";
$this->_loop->addWrite(
$this->_handle,
array(
new BufferedWriter(substr($data, -$len), $len),
'onWrite'
)
);
}
}
/**
*
*/
private function _warning($id, $code, $message, $data = null)
{
$message = array(
'id' => $id,
'error' => array(
'code' => $code,
'message' => $message
)
);
($data !== null)
and $message['error']['data'] = $data;
$this->_send($message);
}
}

159
src/lib/Config.php Normal file
View File

@ -0,0 +1,159 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
*
*/
final class Config extends Base
{
/**
*
*/
function __construct(array $entries = null)
{
parent::__construct();
$this->_entries = isset($entries) ? $entries : array();
}
/**
* Returns an entry.
*
* @param string $path
* @param mixed $default Optional.
*
* @return array
*
* @throws Exception If there is no such entry and no default value as been
* specified.
*/
function get($path, $default = 'throws an exception')
{
$entry = $this->_entries;
$parts = explode('.', $path);
foreach ($parts as $part)
{
/*
* Nothing found.
*/
if (!isset($entry[$part])
&& !array_key_exists($part, $entry))
{
if (func_num_args() < 2)
{
throw new Exception('no such entry ('.$path.')');
}
$entry = $default;
break;
}
$entry = $entry[$part];
}
return $this->_resolve($entry);
}
/**
*
*
* @param string $path
* @param array|string $value
*/
function set($path, $value)
{
$entry = &$this->_entries;
$parts = explode('.', $path);
$i = 0;
$n = count($parts);
while (
($i < $n)
&& (
isset($entry[$part = $parts[$i]])
|| (
is_array($entry)
&& array_key_exists($part, $entry)
)
)
)
{
$entry = &$entry[$part];
++$i;
}
while ($i < $n)
{
if (!is_array($entry))
{
$entry = array();
}
$entry = &$entry[$parts[$i]];
++$i;
}
$entry = $value;
}
/**
* @var array
*/
private $_entries;
/**
*
*/
private function _replaceCallback(array $match)
{
return $this->get($match[1]);
}
/**
*
*/
private function _resolve($entry)
{
if (is_string($entry))
{
return preg_replace_callback(
'/#\{([-a-zA-Z0-9_.]+)\}/',
array($this, '_replaceCallback'),
$entry
);
}
if (is_array($entry))
{
foreach ($entry as &$item)
{
$item = $this->_resolve($item);
}
}
return $entry;
}
}

83
src/lib/Connection.php Normal file
View File

@ -0,0 +1,83 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
*
*/
abstract class Connection extends Base
{
/**
*
*/
function __construct($handle, Loop $loop)
{
parent::__construct();
$this->_handle = $handle;
$this->_loop = $loop;
}
/**
*
*/
function __destruct()
{
$this->close();
parent::__destruct();
}
/**
*
*/
final function close()
{
$this->_loop->removeRead($this->_handle);
$this->_loop->removeWrite($this->_handle);
$this->handleClose();
if (is_resource($this->_handle))
{
stream_socket_shutdown($this->_handle, STREAM_SHUT_RDWR);
fclose($this->_handle);
}
}
/**
*
*/
function handleClose()
{}
/**
* @var resource
*/
private $_handle;
/**
* @var Loop
*/
private $_loop;
}

112
src/lib/DI.php Normal file
View File

@ -0,0 +1,112 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
* Dependency injector.
*/
final class DI extends Base
{
function __construct()
{
parent::__construct();
}
function get($id)
{
if (isset($this->_entries[$id])
|| array_key_exists($id, $this->_entries))
{
return $this->_entries[$id];
}
$tmp = str_replace(array('_', '.'), array('', '_'), $id);
if (method_exists($this, '_get_'.$tmp))
{
return $this->{'_get_'.$tmp}();
}
if (method_exists($this, '_init_'.$tmp))
{
$value = $this->{'_init_'.$tmp}();
$this->set($id, $value);
return $value;
}
throw new Exception('no such entry: '.$id);
}
function set($id, $value)
{
$this->_entries[$id] = $value;
}
private $_entries = array();
////////////////////////////////////////
private function _init_application()
{
return new Application($this);
}
private function _init_errorLogger()
{
return new ErrorLogger($this->get('logger'));
}
private function _init_logger()
{
$logger = new \Monolog\Logger('main');
$config = $this->get('config');
if ($email = $config->get('log.email', false))
{
$logger->pushHandler(
new \Monolog\Handler\FingersCrossedHandler(
new \Monolog\Handler\NativeMailerHandler(
$email,
'[XO Server]',
'no-reply@vates.fr',
\Monolog\Logger::DEBUG
),
\Monolog\Logger::WARNING
)
);
}
if ($file = $config->get('log.file', false))
{
$logger->pushHandler(
new \Monolog\Handler\StreamHandler($file)
);
}
return $logger;
}
private function _init_loop()
{
return new Loop;
}
}

110
src/lib/ErrorLogger.php Normal file
View File

@ -0,0 +1,110 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
use \Monolog\Logger;
/**
*
*/
final class ErrorLogger extends Base
{
/**
*
*/
function __construct(Logger $logger)
{
parent::__construct();
$this->_logger = $logger;
}
/**
* Handles fatal errors on shutdown.
*/
function handleShutdown()
{
$e = error_get_Last();
if ((($e['type'] === E_ERROR) || ($e['type'] === E_USER_ERROR))
&& ($e !== $this->_last))
{
$this->log($e['type'], $e['message'], $e['file'], $e['line']);
}
}
/**
*
*/
function log($no, $str, $file, $line)
{
static $map = array(
E_NOTICE => Logger::NOTICE,
E_USER_NOTICE => Logger::NOTICE,
E_WARNING => Logger::WARNING,
E_CORE_WARNING => Logger::WARNING,
E_USER_WARNING => Logger::WARNING,
E_ERROR => Logger::ERROR,
E_USER_ERROR => Logger::ERROR,
E_CORE_ERROR => Logger::ERROR,
E_RECOVERABLE_ERROR => Logger::ERROR,
E_STRICT => Logger::DEBUG,
);
// Used to prevents the last error from being logged twice.
$this->_last = array(
'type' => $no,
'message' => $str,
'file' => $file,
'line' => $line
);
$priority = isset($map[$no])
? $map[$no]
: Logger::WARNING
;
// Appends the location if necessary.
if (!preg_match('/(?:at|in) [^ ]+:[0-9]+$/', $str))
{
$str .= " in $file:$line";
}
$this->_logger->addRecord($priority, $str, array(
'no' => $no,
'file' => $file,
'line' => $line,
));
return false;
}
/**
*
*/
private $_last;
/**
* @var Logger
*/
private $_logger;
}

176
src/lib/Loop.php Normal file
View File

@ -0,0 +1,176 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
*
*/
final class Loop extends Base
{
function __construct()
{
parent::__construct();
}
/**
* @param resource $handle
* @param callable $callback
*/
function addRead($handle, $callback)
{
$id = (int) $handle;
$this->_readHandles[$id] = $handle;
$this->_readCallbacks[$id] = $callback;
}
/**
*
*/
function removeRead($handle)
{
$id = (int) $handle;
unset(
$this->_readHandles[$id],
$this->_readCallbacks[$id]
);
}
/**
* @param resource $handle
* @param callable $callback
*/
function addWrite($handle, $callback)
{
$id = (int) $handle;
$this->_writeHandles[$id] = $handle;
$this->_writeCallbacks[$id] = $callback;
}
/**
*
*/
function removeWrite($handle)
{
$id = (int) $handle;
unset(
$this->_writeHandles[$id],
$this->_writeCallbacks[$id]
);
}
/**
*
*/
function remove($handle)
{
$id = (int) $handle;
unset(
$this->_readHandles[$id],
$this->_readCallbacks[$id],
$this->_writeHandles[$id],
$this->_writeCallbacks[$id]
);
}
/**
* @param mixed $user_data
*/
function run($user_data = null)
{
$this->_running = true;
do
{
$read = $this->_readHandles;
$write = $this->_writeHandles;
$except = null;
if (@stream_select($read, $write, $except, null) === false)
{
trigger_error(
'error while waiting for activity',
E_USER_ERROR
);
}
foreach ($read as $handle)
{
$callback = $this->_readCallbacks[(int) $handle];
$result = $callback($handle, $user_data);
if (!is_resource($handle))
{
$this->remove($handle);
}
elseif ($result === false)
{
$this->removeRead($handle);
}
}
foreach ($write as $handle)
{
$callback = $this->_writeCallbacks[(int) $handle];
$result = $callback($handle, $user_data);
if (!is_resource($handle))
{
$this->remove($handle);
}
elseif ($result === false)
{
$this->removeWrite($handle);
}
}
} while ($this->_running
&& ($this->_readHandles || $this->_writeHandles));
}
/**
* @var boolean
*/
private $_running;
/**
* @var resource[]
*/
private $_readHandles = array();
/**
* @var callable[]
*/
private $_readCallbacks = array();
/**
* @var resource[]
*/
private $_writeHandles = array();
/**
* @var callable[]
*/
private $_writeCallbacks = array();
}

198
src/lib/XCP.php Normal file
View File

@ -0,0 +1,198 @@
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
*
*/
final class XCP extends Base
{
/**
*
*/
function __construct(Loop $loop, $url, $user, $password)
{
parent::__construct();
$this->_loop = $loop;
$this->_url = $url;
$this->_queue[] = array(
array($this, '_setToken'),
'session.login_with_password',
array($user, $password)
);
$this->_next();
}
/**
*
*/
function queue($method, array $params = null, $callback = null)
{
($params !== null)
or $params = array();
$this->_queue[] = array($callback, $method, $params);
if (count($this->_queue) === 1)
{
$this->_next();
}
}
/**
*
*/
function onData($handle)
{
$this->_buf .= stream_get_contents($handle);
if (!feof($handle))
{
return;
}
// Reads the response.
list(, $response) = explode("\r\n\r\n", $this->_buf, 2);
$this->_buf = '';
$response = xmlrpc_decode($response);
if (!isset($response['Value']))
{
var_export($response);
trigger_error(
'Invalid response',
E_USER_ERROR
);
}
$response = $response['Value'];
// Notifies.
$request = array_shift($this->_queue);
if ($request[0] !== null)
{
$callback = $request[0];
$result = $callback($response, $this);
if ($result === true)
{
$this->_queue[] = $request;
}
}
// Sends the next request if any.
if ($this->_queue)
{
$this->_next();
}
// Stops reading this socket.
return false;
}
/**
*
*/
private function _next()
{
$_ = parse_url($this->_url);
$https = isset($_['scheme']) && !strcasecmp($_['scheme'], 'https');
$port = isset($_['port']) ? $_['port'] : ($https ? 443 : 80);
$host = isset($_['host']) ? $_['host'] : '127.0.0.1';
$query = isset($_['query']) ? '?'.$_['query'] : '';
$path = isset($_['path']) ? $_['path'] : '/';
list(, $method, $params) = reset($this->_queue);
isset($this->_tok)
and array_unshift($params, $this->_tok);
$data = xmlrpc_encode_request($method, $params, array(
'verbosity' => 'no_white_space',
'encoding' => 'UTF-8',
));
$request = implode("\r\n", array(
"POST $path HTTP/1.1",
"Host: $host:$port",
'Connection: close',
'Content-Type: text/xml; charset=UTF-8',
'Content-Length: '.strlen($data),
'',
$data
));
$hdl = @stream_socket_client(
($https ? 'tls' : 'tcp')."://$host:$port",
/* out */ $errno,
/* out */ $errstr,
ini_get('default_socket_timeout'), // Default value.
STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT
);
if (!$hdl)
{
trigger_error(
"cannot connect to {$this->_url}: $errno - $errstr",
E_USER_ERROR
);
}
stream_set_blocking($hdl, false);
fwrite($hdl, $request);
$this->_loop->addRead($hdl, array($this, 'onData'));
}
/**
*
*/
private function _setToken($token)
{
$this->_tok = $token;
}
/**
* @var string
*/
private $_buf = '';
/**
* @var Loop
*/
private $_loop;
/**
* @var string
*/
private $_url;
/**
* Session token.
*
* @var string
*/
private $_tok;
/**
* @var array
*/
private $_queue = array();
}

73
src/xo-server Executable file
View File

@ -0,0 +1,73 @@
#!/usr/bin/php
<?php
/**
* This file is a part of Xen Orchestra Server.
*
* Xen Orchestra Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Xen Orchestra Server is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xen Orchestra Server. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Julien Fontanet <julien.fontanet@vates.fr>
* @license http://www.gnu.org/licenses/gpl-3.0-standalone.html GPLv3
*
* @package Xen Orchestra Server
*/
/**
* Bootstraps and returns the application singleton.
*/
function _bootstrap()
{
static $application;
if (!isset($application))
{
// Variables definition.
$root_dir = defined('__DIR__')
? __DIR__
: dirname(__FILE__)
;
// Class autoloading is done by composer.
require($root_dir.'/../vendor/autoload.php');
// Reads configuration.
$config = new Config(array_merge_recursive(
require($root_dir.'/config/global.php'),
require($root_dir.'/config/local.php')
));
// Injects some variables.
$config->set('root_dir', $root_dir);
// Dependency injector.
$di = new DI;
$di->set('config', $config);
// Logs all errors.
$error_logger = $di->get('error_logger');
set_error_handler(array($error_logger, 'log'));
register_shutdown_function(array($error_logger, 'handleShutdown'));
// Finally, creates the application.
$application = $di->get('application');
}
return $application;
}
_bootstrap()->run();
// Local Variables:
// mode: php
// End: