From: jonl <jonl@sippbx.hig.no>
Date: Thu, 12 Jan 2012 14:51:58 +0000 (+0100)
Subject: Hermes is a framework for managing SIP-accounts, aliases and automatic phone-provisio... 
X-Git-Url: https://git.defcon.no/?a=commitdiff_plain;h=665e8f51f50d2f09069e870a71826d3b8295ae34;p=hermes

Hermes is a framework for managing SIP-accounts, aliases and automatic phone-provisioning, based on Kamailio. This is the initial import.
---

665e8f51f50d2f09069e870a71826d3b8295ae34
diff --git a/alias.php b/alias.php
new file mode 100644
index 0000000..398c12a
--- /dev/null
+++ b/alias.php
@@ -0,0 +1,133 @@
+<?php
+require_once('config.php');
+require_once('lib/user_functions.php');
+require_once('lib/number_functions.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+require_once('lib/alias_functions.php');
+
+$config = get_config();
+
+$config['sql_link'] = @mysql_connect( 
+	$config['sql_server'],
+	$config['sql_username'],
+	$config['sql_password']
+);
+if ( !$config['sql_link'] )
+{
+	print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database connection failed.'));
+	exit;
+}
+
+//*************************************************************************************	
+	switch ( $_SERVER['PATH_INFO'] )
+	{
+		case "/list":
+			break;
+		case "/add":
+			if ( array_key_exists( 'destination', $_GET) 
+				//&& array_key_exists( 'domain', $_GET ) 
+				&& array_key_exists( 'alias_username', $_GET ) 
+				&& array_key_exists( 'alias_domain', $_GET ) )
+			{
+				$alias_username = $_GET['alias_username'];
+				$alias_domain   = $_GET['alias_domain'];
+
+				if ( !verify_sipadress($_GET['destination']) ) 
+				{
+					// TODO: Provide a better response..
+					print json_encode ( array( 'response' => 'invalid', 'cause' => 'destination' ) );
+					break;
+				}
+				if ( !verify_sipadress( $alias_username . "@" . $alias_domain) )
+				{
+					// TODO: Provide a better response..
+					print json_encode ( array( 'response' => 'invalid', 'cause' => 'alias' ) );
+					break;
+				}
+
+				list ( $dest_username, $dest_domain ) = split_sipaddress( $_GET['destination']);
+
+				if ( (!$dest_username)||(!$dest_domain))
+				{
+					// TODO: Provide a better response..
+					print json_encode ( array( 'response' => 'invalid', 'cause' => 'alias' ) );
+					break;
+				}
+				if ( !is_kamailio_domain( $alias_domain ) )
+				{
+					print json_encode ( array( 'response' => 'invalid', 'cause' => 'nxdomain' ) );
+					break;
+				}
+				if ( is_kamailio_domain( $dest_domain) && ( !is_kamailio_subscriber($dest_username, $dest_domain) ) )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'Requesting a local alias, but there is no such user'));
+					break;
+				}
+				if ( verify_e164( $alias_username ) && is_kamailio_subscriber($dest_username, $dest_domain) )
+				{
+					$t = get_e164_alias( $dest_username, $dest_domain );
+					if ( $t )
+					{
+						print json_encode ( array( 
+							'response' => 'failed', 
+							'cause' => 'exists', 
+							'detail' => 'User already has E164 number alias', 
+							'alias' => $t ));
+						break;
+					}
+				}
+				if ( alias_exists ( $alias_username, $alias_domain ) )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'exists', 'detail' => 'The requested alias is already present.'));
+					break;
+				}
+				if ( add_alias( $alias_username, $alias_domain, $dest_username, $dest_domain ) )
+				{
+					print json_encode( array ('response' => 'ok',
+						'alias' => $alias_username . "@" . $alias_domain,
+						'destination' => $dest_username . "@" . $dest_domain ));
+					break;
+				}
+				print json_encode ( array ( 'response' => 'error' ));
+				break;
+			}
+			else
+			{
+				print json_encode ( array( 'response' => 'invalid') );
+			}	
+			break;
+		case "/remove":
+			if ( array_key_exists( 'alias_username', $_GET ) 
+				&& array_key_exists( 'alias_domain', $_GET ) )
+			{
+				$alias_username = $_GET['alias_username'];
+				$alias_domain   = $_GET['alias_domain'];
+
+				if ( !verify_sipadress( $alias_username . "@" . $alias_domain) )
+				{
+					// TODO: Provide a better response..
+					print json_encode ( array( 'response' => 'invalid', 'cause' => 'address', 'detail' => 'Not a valid SIP address' ) );
+					break;
+				}
+				if ( ! alias_exists ( $alias_username, $alias_domain ) )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'The requested alias does not exist.'));
+					break;
+				}
+				if ( remove_alias( $alias_username, $alias_domain ) )
+				{
+					print json_encode( array ('response' => 'ok',
+						'alias' => $alias_username . "@" . $alias_domain));
+					break;
+				}
+				print json_encode ( array ( 'response' => 'error' ));
+				break;
+			}
+			print json_encode ( array( 'response' => 'invalid') );
+			break;
+		default:
+			print json_encode ( array( 'response' => 'invalid') );
+	}
+mysql_close( $config['sql_link'] );
+?>
diff --git a/api-nodes.txt b/api-nodes.txt
new file mode 100644
index 0000000..376beb1
--- /dev/null
+++ b/api-nodes.txt
@@ -0,0 +1,168 @@
+All API nodes currently use GET requests for all parameters.
+All API nodes return JSON data, and all results will contain 
+a 'response' element. The 'response' may be set to:
+  * 'ok' on success. Further contents depend on the node requested.
+  * 'invalid' on invalid requests. A 'cause' element should give a
+	keyword denoting what was invalid about the request...
+  * 'failed' on simple failure. A 'cause' element is supplied, with a
+	failure-keyword (e.g. 'nonexistant', 'inuse', 'dbfail').
+	A final 'detail' is commonly included, with a verbose desctiption.
+  * 'error' on critical failure. Regard this as a soft form of "500 server error"...
+	
+List of API nodes:
+---------------------
+
+user/get?username=foo&domain=bar
+	Return User information, not including hardphone association.
+	Possible extension: allow user/get?address=foo@bar
+
+user/list
+user/list?search=foo@bar
+	Return a list of users in user@domain format.
+	If the search parameter is included, a globbing search is performed,
+	and only matches are listed.
+
+user/add_local?username=foo&domain=bar&displayname=baz&email=qux@zef.tld
+	Adds a user account to both Kamailio and provisioning, 
+	if the username@domain is nonexistant, and the domain is local (handled by Kamailio).
+	The password for the user is auto-generated.
+	Returns a full user object, the same form as user/get
+
+user/add_remote?username=foo&domain=bar&password=S3cr3t&displayname=baz&registrar=sip.example.com
+  optionals: &r_port=5060&proxy=sipproxy.example.com&p_port=5060&authid=realfoo&dialplan=(.*)&linetext=lalala
+	Adds a provisioning user for a remote SIP account. This allows locally provisioned hardphones
+	to be associated with non-local SIP accounts.
+	Returns a full user object, the same form as user/get
+
+user/remove?username=foo&domain=bar
+	Removes user account from Kamailio, if present, and removes user from provisioning.
+	Will fail if user has associated hardphones, remove phones before removing user.
+	Returns 'ok' on success.
+
+user/gen_pw
+	Test-node, generates a random password on the same form as that used
+	by user/add_local. May be used with the to-be-implemented change_pw node.
+
+user/change_pw?username=foo&domain=bar&password=baz
+	Not implemented!
+
+user/update?username=foo&domain=bar
+  optionals: &displayname=baz&dialplan=(.*)&linetext=lalala
+	Not implemented! Will only be implemented for Local users.
+	Remote users will have to be removed, and re-added.
+
+phone/get?mac=f00ba2ba5c00
+phone/get?user=foo@bar.baz
+	Returns a list of associated elements. If MAC address is given as parameter,
+	a list of associated user@domain addresses is retruned. If user@domain is 
+	given as parameter, a list of associated mac addresses is returned.
+
+phone/list?search=f00ba2
+	Returns the list of registered MAC addresses. If the (optional) search
+	parameter is included, the list is the result of a globbing search,
+	a subset matching on the MAC addresses.
+
+phone/add?mac=f00ba2ba5c00&username=foo&domain=bar.bz
+phone/add?mac=f00ba2ba5c00&user=foo@bar.bz
+	Adds a phone-to-user association to Provisioning. The user may
+	be provided as 'username' and 'domain' in separate parameters,
+	or as 'user' in sip-address form. The MAC address must be valid,
+	in plain-hex or colon-separated form, and the user must
+	match a valid user registration.
+	Returns the mac+user combinations on success.
+
+phone/remove?mac=f00ba2ba5c00&username=foo&domain=bar.bz
+phone/remove?mac=f00ba2ba5c00&user=foo@bar.bz
+	Deletes a phone-to-user association. Required parameters
+	and formats are identical to the phone/add node.
+	Returns 'ok' on success.
+
+
+numbers/list
+numbers/list?search=1234
+numbers/list?random=true
+  optional: limit=n
+	Gives a list of entries in the phone number pool.
+	With no parameters, the entire (ordered) list of available numbers is returned.
+	With 'search', the result of a pre-post globbing search is returned
+	With 'random', a list of entries is returned unordered (random order)
+	For all nodes, 'limit' may be a numeric limit of desired results.
+
+numbers/add_range?start=%2B471234&end=%2B47123456
+	The start and end must have identical lead-in, and start must be less than end.
+	Both numbers must be given in E164 format, remember to urlencode the plus (+ -> %2B)
+	The numeric component of both will be extracted, and all numbers in the range
+	will be iteratively added to the pool, including the ending number.
+	Returns 'ok' on success.
+
+numbers/add?number=%2B4761123456
+	Adds the given number to the pool, after verifying that the number is in
+	valid E164 format, and that the number does not already exist in the pool.
+	Returns 'response' 'ok' and "Added <number>" in 'detail' on success.
+
+numbers/remove?number=%2B4761123456
+	Removes/pulls a number from the pool.
+	The number must be a valid e164 number, and must be present in the pool.
+	Returns 'response' as 'ok' with 'number' set to the number that was pulled from the pool.
+
+numbers/inpool?number=%2B4761123456
+	A debug-node, may disappear at any time ^_^
+	Tests if the given E164 number is in the pool.
+	Returns 'response' = 'ok' with 'number' = '<number>' If the number is in the pool
+	Returns response = 'failed' with 'cause' = 'nonexistant' if not.
+
+
+alias/list
+	Currently not implemented.
+
+alias/user?destination=foo@bar.bz
+alias/user?destination=foo@bar.bz&e164=true
+	Currently not implemented
+
+alias/add?alias_username=foo&alias_domain=bar.bz&destination=bar@qux.zx
+	Add an alias specified by alias_username and alias_domain that
+	points to the destination SIP-adress. 
+
+	On success, 'ok' is returned, with 'alias' and 'destination' set
+	to the resulting alias and destination adresses.
+
+	Returns 'invalid' with 'cause' = 'destination' or 'alias' if the
+	resulting alias-adress or destination are invalid SIP-adresses.
+	Returns 'failed' with 'cause' = 'nxdomain' if the given alias domain
+	is not a Kamailio domain.
+	Returns 'failed' with 'cause' = 'exists' if the alias is an E164 number
+	and the user already has an E164 alias registered.
+	Returns 'failed' with 'cause' = 'nonexistant' when an alias for a local
+	domain is requested, but the local subscriber does not exist.
+	Returns 'failed' with 'cause' = 'exists' for aliases that already exists, 
+	(and aliases that overlaps SIP-accounts - not implemented).
+
+
+alias/remove?alias_username=foo&alias_domain=bar.bz
+	Removes the alias given by alias_username and alias_domain.
+
+	Returns 'ok' with the removed alias adress as 'alias' on success
+	Returns 'failed' with 'cause' = 'nonexistant' it the alias does not exists.
+	Returns 'invalid' with 'cause' = 'address' if the given alias is not a valid SIP adress.
+
+TODO list:
+---------------------
+Planned, but not implemented nodes/functions:
+	user/change_pw?username=foo&domain=bar&password=baz
+	user/update?username=foo&domain=bar
+	alias/list
+	alias/user?destination=foo@bar.bz
+
+Parameter updates:
+	Update nodes where username=foo&domain=bar.bz is used, to also support user=foo@bar.bz
+		user/get
+		user/add_local
+		user/add_remote
+		user/remove
+		user/change_pw
+		user/update
+		phone/get (reverse, needs username & domain param)
+		alias/add
+		alias/remove
+
+authentication-mechanism :)
diff --git a/config.php.sample b/config.php.sample
new file mode 100644
index 0000000..5b0a82b
--- /dev/null
+++ b/config.php.sample
@@ -0,0 +1,24 @@
+<?php
+
+require_once('lib/auth.php');
+function get_config()
+{
+	return array(
+		'sql_server' => 'localhost',
+		'sql_username' => 'dbusername',
+		'sql_password' => 'dbuserpass',
+		'kamailio_db' => 'kamailio',
+		'kamailio_domain_table' => 'domain',
+		'kamailio_subscriber_table' => 'subscriber',
+		'kamailio_alias_table' => 'dbaliases',
+		'provision_db' => 'provision',
+		'provision_users_table' => 'users',
+		'provision_phones_table' => 'phones',
+		'provision_servers_table' => 'servers',
+		'numbers_table' => 'number_pool',
+		'standard_dialplan' => '(*xx*.|xxx.)',
+		'default_domain' => 'hig.no',
+		'permit_multiple_e164alias' => false,
+	);
+}
+?>
diff --git a/lib/alias_functions.php b/lib/alias_functions.php
new file mode 100644
index 0000000..4a6b3aa
--- /dev/null
+++ b/lib/alias_functions.php
@@ -0,0 +1,98 @@
+<?php
+require_once('config.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+
+
+function get_destination ( $alias_username, $alias_domain )
+{
+	global $config;
+}
+function get_aliases ( $dest_username, $dest_domain )
+{
+	global $config;
+
+	$aliases = array();
+	$query = sprintf("SELECT CONCAT( alias_username, '@', alias_domain) AS alias FROM %s" 
+		. " WHERE username = '%s' AND domain = '%s'",
+		$config['kamailio_alias_table'], $dest_username, $dest_domain);
+	$result = sql_dbquery( $config['kamailio_db'], $query);
+	// If result is empty, there was either an SQL error, or simply no results.
+	// At this point, no error checking is performed, instead the empty array is returned.
+	if ( ! $result ) return $aliases; 
+
+	while ( $row = mysql_fetch_assoc( $result ) )
+	{
+		array_push( $aliases, $row['alias'] );
+	}
+	return $aliases;
+}
+
+function get_e164_alias( $dest_username, $dest_domain )
+{
+	global $config;
+	$cur_aliases = get_aliases( $dest_username, $dest_domain );
+	foreach ( $cur_aliases as $testalias )
+	{
+		list( $tau, $foo ) = split_sipaddress( $testalias );
+		if ( verify_e164 ( $tau ) ) return $testalias;
+	}
+	return false;
+
+}
+function alias_exists( $alias_username, $alias_domain )
+{
+	global $config;
+	$query = sprintf("SELECT COUNT(*) AS num FROM %s  WHERE alias_username = '%s' AND alias_domain = '%s'",
+		$config['kamailio_alias_table'],
+		sql_clean($alias_username), sql_clean($alias_domain));
+	$result = sql_dbquery($config['kamailio_db'], $query);
+
+	if ( !$result ) return true; // This is an error. Better to fail claiming alias exists...
+	$row = mysql_fetch_row($result);
+	if ( !$row ) return true; // This is an error. Better to fail claiming alias exists...
+	$num_r = $row[0];
+	if ( $num_r == 1 ) 
+	{
+		$query = sprintf("SELECT CONCAT(username, '@', domain ) AS destination FROM %s  WHERE alias_username = '%s' AND alias_domain = '%s'",
+			$config['kamailio_alias_table'],
+			sql_clean($alias_username), sql_clean($alias_domain));
+		$result = sql_dbquery($config['kamailio_db'], $query);
+		$row = mysql_fetch_row($result);
+		if ( is_string( $row[0] ) ) return $row[0];
+		return true; // Failure mode..
+	}
+
+	return false;
+}
+
+function add_alias( $alias_username, $alias_domain, $dest_username, $dest_domain )
+{
+	global $config;
+	// The following will, in normal cases, lead to a double-test, and double the number of queries...
+	// Defensive comment: better safe than sorry :P
+	if ( alias_exists( $alias_username, $alias_domain ) )
+		return false;
+
+	$query = sprintf("INSERT INTO %s ( alias_username, alias_domain, username, domain) VALUES ('%s','%s','%s','%s')",
+		$config['kamailio_alias_table'],
+		sql_clean($alias_username), sql_clean($alias_domain),
+		sql_clean($dest_username), sql_clean($dest_domain));
+
+	return sql_dbexec( $config['kamailio_db'], $query);
+	
+}
+
+function remove_alias ( $alias_username, $alias_domain )
+{
+	global $config;
+	if (! alias_exists ( $alias_username, $alias_domain ) ) return false;
+	$query = sprintf ("DELETE FROM %s WHERE alias_username = '%s' AND alias_domain = '%s'",	
+		$config['kamailio_alias_table'],
+		sql_clean( $alias_username ),
+		sql_clean( $alias_domain ));
+	return sql_dbexec( $config['kamailio_db'], $query);
+
+}
+
+?>
diff --git a/lib/auth.php b/lib/auth.php
new file mode 100644
index 0000000..2f8f9cf
--- /dev/null
+++ b/lib/auth.php
@@ -0,0 +1,13 @@
+<?php
+
+function token_auth( )
+{
+	return true;
+}
+
+if (! token_auth() )
+{
+	print json_encode( array( 'response' => 'failed', 'cause' => 'unauthorized', 'description' => 'Not authorized') );
+	exit;
+}
+?>
diff --git a/lib/check_email.php b/lib/check_email.php
new file mode 100644
index 0000000..800f52d
--- /dev/null
+++ b/lib/check_email.php
@@ -0,0 +1,38 @@
+<?php 
+
+function check_email_address($email) {
+	// First, we check that there's one @ symbol, 
+	// and that the lengths are right.
+	if (!ereg("^[^@]{1,64}@[^@]{1,255}$", $email)) {
+		// Email invalid because wrong number of characters 
+		// in one section or wrong number of @ symbols.
+		return false;
+	}
+	// Split it into sections to make life easier
+	$email_array = explode("@", $email);
+	$local_array = explode(".", $email_array[0]);
+	for ($i = 0; $i < sizeof($local_array); $i++) {
+		if
+			(!ereg("^(([A-Za-z0-9!#$%&'*+/=?^_`{|}~-][A-Za-z0-9!#$%&'*+/=?^_`{|}~\.-]{0,63})|(\"[^(\\|\")]{0,62}\"))$",
+			       $local_array[$i])) {
+				return false;
+			}
+	}
+	// Check if domain is IP. If not, 
+	// it should be valid domain name
+	if (!ereg("^\[?[0-9\.]+\]?$", $email_array[1])) {
+		$domain_array = explode(".", $email_array[1]);
+		if (sizeof($domain_array) < 2) {
+			return false; // Not enough parts to domain
+		}
+		for ($i = 0; $i < sizeof($domain_array); $i++) {
+			if
+				(!ereg("^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]+))$",
+				       $domain_array[$i])) {
+					return false;
+				}
+		}
+	}
+	return true;
+}
+?>
diff --git a/lib/common_functions.php b/lib/common_functions.php
new file mode 100644
index 0000000..52d99e8
--- /dev/null
+++ b/lib/common_functions.php
@@ -0,0 +1,54 @@
+<?php
+require_once('config.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+require_once('lib/check_email.php');
+
+$config = get_config();
+
+function is_kamailio_domain ( $domain )
+{
+	global $config;
+	$query = sprintf("SELECT id FROM %s WHERE domain = '%s'",
+		$config['kamailio_domain_table'],
+		sql_clean( $domain )
+	);
+	$domain = sql_dbquery_single( $config['kamailio_db'], $query );
+	if ( !$domain ) return false;
+	return $domain['id'];
+
+}
+
+function get_servers( $domain )
+{
+	global $config;
+	$query = sprintf("SELECT registrar, r_port, proxy, p_port FROM %s WHERE domain = '%s'",
+		$config['provision_servers_table'],
+		sql_clean( $domain ));
+	$servers = sql_dbquery_single( $config['provision_db'], $query );
+	return $servers;
+}
+
+function verify_sipadress( $address ) //TODO: Improve/expand on the test!
+{
+	return check_email_address($address); // A SIP address follows the requirements for email addresses...
+	// Except that it permits the use of :portnumber notation!
+	// TODO: Fix this so it allows :NNNN syntax for portnumbers...
+}
+
+function split_sipaddress ( $address )
+{
+	if ( !verify_sipadress( $address ) ) return null;
+	$data = split('@', $address);
+	if ( count( $data ) != 2 ) return null;
+	return $data;
+}
+
+function clean_mac ( $input )
+{
+	$mac = strtolower(  preg_replace('/:/', '', $input) );
+	if ( ! preg_match( '/^[a-f0-9]{1,}$/', $mac ) ) return null;
+	if (strlen( $mac ) != 12 ) return null;
+	return $mac;
+}
+?>
diff --git a/lib/db_functions.php b/lib/db_functions.php
new file mode 100644
index 0000000..6122257
--- /dev/null
+++ b/lib/db_functions.php
@@ -0,0 +1,42 @@
+<?php
+require_once('config.php');
+require_once('lib/common_functions.php');
+
+function sql_dbquery( $db, $query )
+{
+	if ( ! mysql_select_db( $db ) ) return false;
+	$result = mysql_query( $query );
+	if ( !$result ) return false;
+	if (mysql_num_rows($result) == 0) return false;
+	return $result;
+}
+function sql_dbexec ( $db, $query )
+{
+	if ( ! mysql_select_db( $db ) ) return false;
+	$result = mysql_query( $query );
+	if ( !$result ) return false;
+	return $result;
+}
+function sql_dbquery_single( $db, $query )
+{
+	$result = sql_dbquery( $db, $query );
+	if ( ! $result ) return false;
+	if ( mysql_num_rows($result) != 1) return false;
+	$data = mysql_fetch_assoc( $result );
+	return $data;
+}
+function sql_dbtest_numrows ( $db, $query, $numrows )
+{
+	$result = sql_dbquery( $db, $query );
+
+	if ( ! $result ) return false;
+	if ( mysql_num_rows($result) == $numrows ) return true;
+	return false;
+}
+function sql_clean( $string )
+{
+	return addslashes( $string );
+}
+
+
+?>
diff --git a/lib/number_functions.php b/lib/number_functions.php
new file mode 100644
index 0000000..d814a99
--- /dev/null
+++ b/lib/number_functions.php
@@ -0,0 +1,155 @@
+<?php
+require_once('config.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+
+function verify_e164 ( $input )
+{
+	// A really paranoid E164 test. Starts out with a regexp,
+	// where $arr[1] will be set to the numeric part of the
+	// e164 content, if valid. The rest is paranoid behaviour,
+	// if the regexp matches, the other tests can not fail...
+
+	// e164 format is: A plus (+) followed by at least four
+	// digits, and no more than 15 digits total.
+	if  (! preg_match ( '/^\+(\d{4,15})$/', $input, $arr ) )
+		return 0;
+	$number = $arr[1];
+	if ( ! $number )
+		return 0;
+	
+	if ( preg_match( '/\+/', $number ))
+		return 0;
+	
+	if ( !is_numeric( $number ) )
+		return 0;
+	
+	return $number;
+	
+}
+
+function number_inpool( $number )
+{
+	global $config;
+	if (! verify_e164( $number ) )
+	{
+		return false;
+	}
+
+	$test = "SELECT COUNT(*) FROM " . $config['numbers_table'] . " WHERE number = '" . $number . "'";
+	$result = sql_dbquery($config['provision_db'], $test);
+	if ( !$result ) return false;
+	$row = mysql_fetch_row($result);
+	if ( !$row ) return false;
+	$num_r = $row[0];
+	if ( $num_r == 1 ) return true;
+
+	return false;
+}
+
+function get_random_numbers ( $limit = 0 )
+{
+	global $config;
+	$query = "SELECT number FROM " . $config['numbers_table'] . " ORDER BY RAND()";
+	if ( $limit && is_numeric( $limit ) )
+		$query .= " LIMIT " . $limit;
+	$result = sql_dbquery( $config['provision_db'], $query );
+	print mysql_error();
+	if ( !$result ) return null;
+	if (mysql_num_rows($result) < 1 ) return null;
+	$rows = array();
+	while ( $row = mysql_fetch_assoc( $result ) )
+	{
+		array_push( $rows, $row['number'] );
+	}
+	return $rows;
+
+}
+
+function get_numbers ( $search=null, $limit = 0 )
+{
+	global $config;
+	$query = "SELECT number FROM " . $config['numbers_table'];
+
+	if ( $search )
+		$query .= sprintf(" WHERE number LIKE '%%%s%%'", sql_clean($search));
+
+	$query .= " ORDER BY number ASC";
+
+	if ( $limit && is_numeric( $limit ) )
+		$query .= " LIMIT " . $limit;
+
+
+	$result = sql_dbquery( $config['provision_db'], $query );
+	print mysql_error();
+	if ( !$result ) return null;
+	if (mysql_num_rows($result) < 1 ) return null;
+	$rows = array();
+	while ( $row = mysql_fetch_assoc( $result ) )
+	{
+		array_push( $rows, $row['number'] );
+	}
+	return $rows;
+}
+
+function add_range( $start, $end )
+{
+	global $config;
+	$start_numeric = null;
+	$end_numeric = null;
+
+	// The start and end must be in the same e164 range. Because
+	// of this, they must hav identical length
+	if ( strlen( $start ) != strlen( $end ))
+		return "Start and end have different lengths.";
+
+	// The parameters MUST be in e164 format.
+	$start_numeric = verify_e164( $start );
+	$end_numeric = verify_e164( $end );
+	if  (! $start_numeric )
+		return "Start of range is not a valid e164 number";
+	if  (! $end_numeric )
+		return "End of range is not a valid e164 number";
+
+	// Significant, a simple sanity check.
+	if ( ! ($start < $end) )
+		return "Start of range is after end of range";
+
+	// Hard-coded paranoia: We expect ranges to be less than 10k numbers..
+	if ( ($end - $start ) > 9999 )
+		return "Range is larger than hard limit permits";
+
+	for ( $num = $start_numeric; $num <= $end_numeric; $num++ )
+	{
+		add_number( '+' . $num );
+	}
+	return 'ok';
+}
+
+function add_number( $number )
+{
+	global $config;
+	if (! verify_e164( $number ) )
+	{
+		return false;
+	}
+
+	if (number_inpool( $number ) ) return false;
+
+	$insert = "INSERT INTO " . $config['numbers_table'] . "	(number) VALUES ('" . $number . "')";
+	return sql_dbexec( $config['provision_db'], $insert);
+}
+
+function remove_number ( $number )
+{
+	global $config;
+	if (! verify_e164( $number ) )
+	{
+		return false;
+	}
+	if ( !number_inpool( $number ) ) return false;
+	$query = "DELETE FROM " . $config['numbers_table'] . " WHERE number = '" . $number . "'";
+	return sql_dbexec( $config['provision_db'], $query);
+
+}
+?>
diff --git a/lib/phone_functions.php b/lib/phone_functions.php
new file mode 100644
index 0000000..17fbe10
--- /dev/null
+++ b/lib/phone_functions.php
@@ -0,0 +1,120 @@
+<?php
+require_once('config.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+
+function get_user_phones ( $username, $domain ) 
+{
+	global $config;
+	$query = sprintf("SELECT id FROM %s WHERE username = '%s' AND domain = '%s'",
+		$config['provision_users_table'],
+		sql_clean( $username ),
+		sql_clean( $domain )
+	);
+
+	$user = sql_dbquery_single( $config['provision_db'], $query );
+	if ( ! $user ) return null;
+	$user_id = $user['id'];
+
+	$query = sprintf("SELECT mac FROM %s WHERE user_rel = %d",
+		$config['provision_phones_table'],
+		$user_id
+	);
+
+	$result = sql_dbquery( $config['provision_db'], $query );
+	if ( !$result ) return null;
+	if (mysql_num_rows($result) < 1 ) return null;
+	$rows = array();
+	while ( $row = mysql_fetch_assoc( $result ) )
+		array_push( $rows, $row['mac'] );
+	return $rows;
+}
+
+function get_phone_users ( $macaddress ) 
+{
+	global $config;
+	$ptbl = $config['provision_phones_table'];
+	$utbl = $config['provision_users_table'];
+	$query = "SELECT ".$ptbl.".mac as mac, CONCAT( ".$utbl.".username, '@', ".$utbl.".domain ) as user
+		FROM ".$ptbl."
+		INNER JOIN ".$utbl." ON ".$ptbl.".user_rel = ".$utbl.".id
+		WHERE ".$ptbl.".mac = '".sql_clean($macaddress ). "'";
+
+	$result = sql_dbquery( $config['provision_db'], $query );
+	if ( !$result ) return null;
+	if (mysql_num_rows($result) < 1 ) return null;
+	$rows = array();
+	while ( $row = mysql_fetch_assoc( $result ) )
+	{
+		array_push( $rows, $row['user'] );
+	}
+	return $rows;
+}
+
+function add_phone_user( $mac, $username, $domain )
+{
+	global $config;
+	// Get ID of user, for use with user_rel field..
+	$user_id = get_provision_userid( $username, $domain );
+	if ( !$user_id ) return false;
+
+	// Doublecheck :)
+	$mac = clean_mac($mac);
+	if (!$mac) return false;
+
+	// Triplecheck :)
+	$phones = get_user_phones ( $username, $domain);
+	if ( $phones && in_array( $mac, $phones ) )
+		return false;
+
+	// OK, so we have the User ID, a valid MAC, and no previous registration
+	// of that combination. Going to add.
+	$query = sprintf("INSERT INTO %s ( mac, user_rel ) VALUES ( '%s', %d )",
+		$config['provision_phones_table'], $mac, $user_id);
+	return sql_dbexec( $config['provision_db'], $query );
+}
+
+function delete_phone_user( $mac, $username, $domain )
+{
+	global $config;
+	// Get ID of user, for use with user_rel field..
+	$user_id = get_provision_userid( $username, $domain );
+	if ( !$user_id ) return false;
+
+	// Doublecheck :)
+	$mac = clean_mac($mac);
+	if (!$mac) return false;
+
+	// Triplecheck :)
+	$phones = get_user_phones ( $username, $domain);
+	if ( !$phones ) return false;
+	if ( ! in_array( $mac, $phones ) ) return false;
+
+	// OK, so we have the User ID, a valid MAC, and no previous registration
+	// of that combination. Going to remove.
+	$query = sprintf("DELETE FROM %s WHERE mac = '%s' AND user_rel = %d",
+		$config['provision_phones_table'], $mac, $user_id);
+	return sql_dbexec( $config['provision_db'], $query );
+}
+
+
+
+function list_phones ( $search = null )
+{
+	global $config;
+	$query = sprintf("SELECT mac FROM %s", $config['provision_phones_table']);
+	if ( $search )
+		$query .= sprintf(" WHERE mac LIKE '%s%%'", sql_clean($search));
+
+	$result = sql_dbquery( $config['provision_db'], $query );
+	if ( !$result ) return null;
+	if (mysql_num_rows($result) < 1 ) return null;
+	$rows = array();
+	while ( $row = mysql_fetch_assoc( $result ) )
+	{
+		array_push( $rows, $row['mac'] );
+	}
+	return $rows;
+}
+
+?>
diff --git a/lib/user_functions.php b/lib/user_functions.php
new file mode 100644
index 0000000..7944551
--- /dev/null
+++ b/lib/user_functions.php
@@ -0,0 +1,217 @@
+<?php
+require_once('config.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+
+$config = get_config();
+
+function generate_password( $length = 32 )
+{
+	$string = "";
+	 while ( strlen( $string ) < $length )
+		$string .= substr(md5(rand().rand()), 0, $length);
+	return substr( $string, 0, $length );
+
+}
+
+
+function is_kamailio_subscriber ( $user, $domain )
+{
+	global $config;
+	$query = sprintf("SELECT username FROM %s WHERE username = '%s' AND domain = '%s'",
+		$config['kamailio_subscriber_table'],
+		sql_clean( $user ),
+		sql_clean( $domain )
+	);
+	return sql_dbtest_numrows( $config['kamailio_db'], $query, 1);
+}
+
+function is_provision_user ( $user, $domain )
+{
+	global $config;
+	$query = sprintf("SELECT username FROM %s WHERE username = '%s' AND domain = '%s'",
+		$config['provision_users_table'],
+		sql_clean( $user ),
+		sql_clean( $domain )
+	);
+	return sql_dbtest_numrows( $config['provision_db'], $query, 1);
+}
+
+function add_kamailio_subscriber( $username, $domain, $password, $email )
+{
+
+	global $config;
+
+	$ha1  = md5( $username . ":" . $domain . ":" . $password );
+	$ha1b = md5( $username . "@" . $domain . ":" . $domain . ":" . $password );
+
+	$query = sprintf( "INSERT INTO %s (username, domain, password, email_address, ha1, ha1b) VALUES ('%s','%s','%s', '%s', '%s', '%s')",
+		$config['kamailio_subscriber_table'],
+		sql_clean($username),
+		sql_clean($domain),
+		sql_clean($password),
+		sql_clean($email),
+		$ha1,
+		$ha1b
+	);
+	if ( ! sql_dbexec( $config['kamailio_db'], $query ) ) return false;
+	return true;
+}
+
+function delete_kamailio_subscriber( $username, $domain )
+{
+	global $config;
+
+
+	$query = sprintf("SELECT id FROM %s WHERE username = '%s' AND domain = '%s'",
+		$config['kamailio_subscriber_table'],
+		sql_clean($username),
+		sql_clean($domain)
+	);
+	$row = sql_dbquery_single( $config['kamailio_db'], $query );
+	if  (!$row) return false;
+	$user_rowid = $row['id'];
+	if ( !$user_rowid ) return false;
+
+	$query = sprintf( "DELETE FROM  %s WHERE id = %d AND username = '%s' AND domain = '%s'",
+		$config['kamailio_subscriber_table'],
+		$user_rowid,
+		sql_clean($username),
+		sql_clean($domain)
+	);
+	if ( ! sql_dbexec( $config['kamailio_db'], $query ) ) return false;
+	return true;
+}
+
+function add_provision_user( $username, $password, $domain, $authid, $registrar, $r_port, $proxy, $p_port, $displayname, $dialplan, $linetext )
+{	global $config;
+
+	if ( is_provision_user( $username, $password ) ) return false;
+	$query = sprintf ("INSERT INTO %s ( username, password, displayname, domain, registrar, r_port, proxy, p_port, dialplan, authid, linetext )
+		VALUES ('%s', '%s', '%s', '%s', '%s', %d, '%s', %d, '%s', '%s', '%s')",
+		$config['provision_users_table'],
+		sql_clean($username), 
+		sql_clean($password), 
+		sql_clean($displayname), 
+		sql_clean($domain), 
+		sql_clean($registrar), 
+		$r_port, 
+		sql_clean($proxy), 
+		$p_port, 
+		sql_clean($dialplan), 
+		sql_clean($authid), 
+		sql_clean($linetext)
+	);
+	if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false;
+	return true;
+}
+function get_provision_userid ( $username, $domain )
+{
+	global $config;
+
+	$query = sprintf("SELECT id FROM %s WHERE username = '%s' AND domain = '%s'",
+		$config['provision_users_table'],
+		sql_clean($username),
+		sql_clean($domain)
+	);
+
+	$row = sql_dbquery_single( $config['provision_db'], $query );
+	if  (!$row) return false;
+	$user_rowid = $row['id'];
+	return $user_rowid;
+}
+
+function delete_provision_user( $username, $domain )
+{
+	global $config;
+
+	$user_rowid = get_provision_userid( $username, $domain );
+	if ( !$user_rowid ) return false;
+
+	$query = sprintf( "DELETE FROM  %s WHERE id = %d AND username = '%s' AND domain = '%s'",
+		$config['provision_users_table'],
+		$user_rowid,
+		sql_clean($username),
+		sql_clean($domain)
+	);
+	if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false;
+	return true;
+}
+
+function list_users ( $search = null )
+{
+	global $config;
+	$query = sprintf("SELECT CONCAT(username, '@', domain) FROM %s ORDER BY username,domain", $config['provision_users_table'] );
+
+	if ( array_key_exists ( 'search', $_GET ) )
+	{
+		$search = $_GET['search']; // TODO: Add some sanitation and input validation!
+		$query = sprintf("SELECT CONCAT(username, '@', domain) FROM %s WHERE CONCAT(username, '@', domain) LIKE '%%%s%%' ORDER BY username,domain", $config['provision_users_table'], sql_clean( $search ) );
+	}
+
+	$result = sql_dbquery( $config['provision_db'], $query );
+	if ( !$result ) return null;
+	$list = array();
+	while ( $row = mysql_fetch_row( $result ) )
+	{
+		array_push( $list, $row[0] );
+	}
+	return $list;
+	print json_encode( array( 'response' => 'ok', 'list' => $list ));
+
+
+}
+function get_userdata( $username, $domain )
+{
+	global $config;
+	if ( is_kamailio_subscriber( $username, $domain ) // User must be present in both!
+		&& is_provision_user( $username, $domain ) ) $type = 'local';
+	else if ( is_provision_user( $username, $domain ) ) $type = 'remote';
+	else return null;
+
+	$provision_data = null;
+	$kamailio_data = null;
+
+	$query_provision = sprintf ("SELECT id, username, password, displayname, domain, registrar, r_port, proxy, p_port, dialplan, authid, linetext FROM %s WHERE username = '%s' AND domain = '%s'",
+		$config['provision_users_table'],
+		sql_clean($username),
+		sql_clean($domain));
+
+	$provision_data = sql_dbquery_single( $config['provision_db'] , $query_provision );
+	if ( ! $provision_data ) return false;
+
+	if ( $type == 'local' )
+	{
+		// WARNING: Note the typo in the name of the 'permittedcalls' column!
+		$query_kamailio = sprintf ("SELECT id, username, domain, password, email_address, ha1, ha1b, rpid, permitedcalls FROM %s WHERE username = '%s' AND domain = '%s'",
+			$config['kamailio_subscriber_table'],
+			sql_clean($username),
+			sql_clean($domain));
+		$kamailio_data = sql_dbquery_single( $config['kamailio_db'] , $query_kamailio );
+		if ( ! $kamailio_data ) return false;
+	}
+	$user['type'] = $type;
+	$user['username']    = $provision_data['username'];
+	$user['password']    = $provision_data['password'];
+	$user['domain']      = $provision_data['domain'];
+	$user['authid']      = $provision_data['authid'];
+	$user['registrar']   = $provision_data['registrar'];
+	$user['r_port']      = $provision_data['r_port'];
+	$user['proxy']       = $provision_data['proxy'];
+	$user['p_port']      = $provision_data['p_port'];
+	$user['dialplan']    = $provision_data['dialplan'];
+	$user['displayname'] = $provision_data['displayname'];
+	$user['linetext']    = $provision_data['linetext'];
+	if ( $type == 'local' )
+	{
+		$user['email'] = $kamailio_data['email'];
+		$user['ha1']   = $kamailio_data['ha1'];
+		$user['ha1b']  = $kamailio_data['ha1b'];
+		$user['rpid']  = $kamailio_data['rpid'];
+		$user['permittedcalls'] = $kamailio_data['permitedcalls'];
+	}
+
+	return $user;
+}
+	
+?>
diff --git a/numbers.php b/numbers.php
new file mode 100644
index 0000000..e9fc796
--- /dev/null
+++ b/numbers.php
@@ -0,0 +1,141 @@
+<?php
+require_once('config.php');
+//require_once('lib/user_functions.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+require_once('lib/number_functions.php');
+
+$config = get_config();
+
+$config['sql_link'] = @mysql_connect( 
+	$config['sql_server'],
+	$config['sql_username'],
+	$config['sql_password']
+);
+if ( !$config['sql_link'] )
+{
+	print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database connection failed.'));
+	exit;
+}
+
+//*************************************************************************************	
+	switch ( $_SERVER['PATH_INFO'] )
+	{
+		case "/list":
+			// List all (distinct) phone MAC-adresses registered...
+			$limit = 0;
+			$random = false;
+			$search = null;
+			if ( array_key_exists('limit', $_GET ) && is_numeric( $_GET['limit']))
+				$limit = $_GET['limit'];
+
+			if ( array_key_exists('search', $_GET ) )
+				$search = $_GET['search'];
+			else if ( array_key_exists('random', $_GET ) && (strtolower( $_GET['random'] ) === 'true'))
+				$random = true;
+
+			$numbers = null;
+
+			if ( $random )
+			{
+				$numbers = get_random_numbers( $limit );
+			} 
+			else
+			{
+				$numbers = get_numbers ( $search, $limit );
+			}
+			if ( $numbers )
+			{
+				print json_encode( array( 'response' => 'ok', 'list' => $numbers ));
+				break;
+			}
+
+			print json_encode( array( 'response' => 'failed', 'cause' => 'empty', 'detail' => 'Empty result.' ));
+			break;
+		case "/add_range":
+			if ( array_key_exists('start', $_GET) && array_key_exists('end', $_GET) )
+			{
+				$start = $_GET['start'];
+				$end = sql_clean( $_GET['end'] );
+				$result = add_range( $start, $end );
+				if ( $result === 'ok' )
+				{
+					print  json_encode ( array( 'response' => 'ok') );
+				}
+				else
+				{
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'rejected', 'detail' => $result ) );
+				}
+			}
+			break;
+		case "/add":
+			// TODO: This should return better responses!
+			//  Currently, it will fail with "invalid"
+			if ( array_key_exists('number', $_GET))
+			{
+				$number = $_GET['number'];
+
+				if (! verify_e164( $number ) )
+				{
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'rejected', 'detail' => "Not a valid e164 number" ));
+					break;
+				}
+				if ( number_inpool( $number ) )
+				{
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'exists', 'detail' => "Number is already in the pool" ));
+					break;
+				}
+
+				$result = add_number( $number );
+				if ( $result )
+				{
+					print  json_encode ( array( 'response' => 'ok', 'detail' => 'Added ' . $number, 'number' => $number ) );
+					break;
+				}
+				else
+				{
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'rejected' ));
+				}
+			}
+			print json_encode ( array( 'response' => 'invalid') );
+			break;
+		case "/remove":
+			if ( array_key_exists('number', $_GET))
+			{
+				$number = $_GET['number'];
+
+				if (! verify_e164( $number ) )
+				{
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'rejected', 'detail' => "Not a valid e164 number" ));
+					break;
+				}
+				if ( !number_inpool( $number ) )
+				{
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => "Number not in pool" ));
+					break;
+				}
+				if ( !remove_number ( $number ) )
+				{
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'dbfail', 'detail' => "Failed to remove number" ));
+					break;
+				}
+				print  json_encode ( array( 'response' => 'ok', 'detail' => 'Removed ' . $number, 'number' => $number ) );
+				break;
+			}
+			print json_encode ( array( 'response' => 'invalid') );
+			break;
+		case "/inpool":
+			if ( array_key_exists('number', $_GET))
+			{
+				$number = $_GET['number'];
+				if ( number_inpool( $number ) )
+					print  json_encode ( array( 'response' => 'ok', 'number' => $number ) );
+				else
+					print  json_encode ( array( 'response' => 'failed', 'cause' => 'nonexistant') );
+				break;
+			}
+		default:
+			print json_encode ( array( 'response' => 'invalid') );
+	}
+mysql_close( $config['sql_link'] );
+?>
diff --git a/phone.php b/phone.php
new file mode 100644
index 0000000..11dd6db
--- /dev/null
+++ b/phone.php
@@ -0,0 +1,205 @@
+<?php
+require_once('config.php');
+require_once('lib/user_functions.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+require_once('lib/phone_functions.php');
+
+$config = get_config();
+
+$config['sql_link'] = @mysql_connect( 
+	$config['sql_server'],
+	$config['sql_username'],
+	$config['sql_password']
+);
+if ( !$config['sql_link'] )
+{
+	print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database connection failed.'));
+	exit;
+}
+
+//*************************************************************************************	
+	switch ( $_SERVER['PATH_INFO'] )
+	{
+		case "/get":
+			// Required GET parameters:
+			// user: authentication username, SIP-username without domain component
+			// domain: Domain/realm of the user. username + '@' + domain == SIP address.
+			if ( array_key_exists( 'mac', $_GET) )
+			{
+				$mac = $_GET['mac'];
+				$relations = get_phone_users ( $mac );
+				if ( $relations )
+				{
+					print json_encode( array( 'response' => 'ok', 'list' => $relations ));
+				}
+				else print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'No results.'));
+			}
+			else if ( array_key_exists( 'user', $_GET ) )
+			{
+				$user = split_sipaddress($_GET['user']);
+				if (! $user )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'Invalid SIP address') );
+					break;
+				}
+				list ( $username, $domain ) = $user;
+				$userdata = get_user_phones( $username, $domain );
+				if ( $userdata )
+				{
+					print json_encode( array( 'response' => 'ok', 'list' => $userdata ));
+				}
+				else print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'No results.'));
+
+			}
+			else
+				print json_encode ( array( 'response' => 'invalid') );
+			break;
+		case "/list":
+			// List all (distinct) phone MAC-adresses registered...
+			$search = null;
+			if ( array_key_exists('search', $_GET ) )
+				$search = $_GET['search'];
+
+			$phones = list_phones( $search );
+			print json_encode( array( 'response' => 'ok', 'list' => $phones ));
+			break;
+		case "/add":
+			// Add a MAC+user...
+			/*
+			Parameters:
+				mac	  The MAC-address of the phone to add an entry for
+				Either:
+				user	  A registered username on user@domain form (SIP address)
+				Or:
+				username  A registered username, combines with:
+				domain    A valid domain .. to form a registered user@domain combo :)
+
+			*/
+			if ( array_key_exists('mac', $_GET ) &&
+				( array_key_exists('user', $_GET) ||
+					( array_key_exists('username', $_GET) && array_key_exists('domain', $_GET ))))
+			{
+				$username = "";
+				$domain = "";
+				if ( array_key_exists('username', $_GET) )
+				{
+					$username = $_GET['username'];
+					$domain = $_GET['domain'];
+				}
+				else
+				{
+					$user = split_sipaddress($_GET['user']);
+					if ( !$user )
+					{
+						print json_encode ( array( 'response' => 'failed', 'cause' => 'invalid', 'detail' => 'Invalid SIP address') );
+						break;
+					}
+					list ( $username, $domain ) = $user;
+				}
+				$mac = clean_mac($_GET['mac']);
+				if ( !$mac )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'invalid', 'detail' => 'No valid MAC address given.') );
+					break;
+				}
+				
+				if ( !is_provision_user ( $username, $domain ) )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'User not registered.'));
+					break;
+				}
+				$phones = get_user_phones ( $username, $domain);
+				if ( $phones && in_array( $mac, $phones ) )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'exists', 'detail' => 'This phone and user combination is already configured..'));
+					break;
+				}
+				$res = add_phone_user ( $mac, $username, $domain );
+				if ( !$res )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' =>'dbfail', 'detail' => 'Failed to add phone to database.'));
+					break;
+				}
+				else
+				{
+					print json_encode( array ( 'response' => 'ok', 'data' => array (
+						'mac' => $mac, 'username' => $username, 'domain' => $domain) ));
+					break;
+				}
+				break;
+			}
+			else
+				print json_encode ( array( 'response' => 'invalid') );
+			break;
+
+
+		case "/remove":
+			// Del a MAC+user...
+			/*
+			Parameters:
+				mac	  The MAC-address of the phone to add an entry for
+				Either:
+				user	  A registered username on user@domain form (SIP address)
+				Or:
+				username  A registered username, combines with:
+				domain    A valid domain .. to form a registered user@domain combo :)
+
+			*/
+			if ( array_key_exists('mac', $_GET ) &&
+				( array_key_exists('user', $_GET) ||
+					( array_key_exists('username', $_GET) && array_key_exists('domain', $_GET ))))
+			{
+				$username = "";
+				$domain = "";
+				if ( array_key_exists('username', $_GET) )
+				{
+					$username = $_GET['username'];
+					$domain = $_GET['domain'];
+				}
+				else
+				{
+					$user = split_sipaddress($_GET['user']);
+					if ( !$user )
+					{
+						print json_encode ( array( 'response' => 'failed', 'cause' => 'invalid', 'detail' => 'Invalid SIP address') );
+						break;
+					}
+					list ( $username, $domain ) = $user;
+				}
+				$mac = clean_mac($_GET['mac']);
+				if ( !$mac )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'invalid', 'detail' => 'No valid MAC address given.') );
+					break;
+				}
+				
+				$phones = get_user_phones ( $username, $domain);
+				if ( ! $phones || !in_array( $mac, $phones ) )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'Unable to locate requested combination'));
+					break;
+				}
+				$res = delete_phone_user ( $mac, $username, $domain );
+				if ( !$res )
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' =>'dbfail', 'detail' => 'Failed to remove phone from database.'));
+					break;
+				}
+				else
+				{
+					print json_encode( array ( 'response' => 'ok' ));
+					break;
+				}
+				break;
+			}
+			else
+				print json_encode ( array( 'response' => 'invalid') );
+			break;
+
+
+		default:
+			print json_encode ( array( 'response' => 'invalid') );
+	}
+mysql_close( $config['sql_link'] );
+?>
diff --git a/user.php b/user.php
new file mode 100644
index 0000000..ca15955
--- /dev/null
+++ b/user.php
@@ -0,0 +1,319 @@
+<?php
+require_once('config.php');
+require_once('lib/user_functions.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+require_once('lib/phone_functions.php');
+
+$config = get_config();
+
+$config['sql_link'] = @mysql_connect( 
+	$config['sql_server'],
+	$config['sql_username'],
+	$config['sql_password']
+);
+if ( !$config['sql_link'] )
+{
+	print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database connection failed.'));
+	exit;
+}
+
+//*************************************************************************************	
+	switch ( $_SERVER['PATH_INFO'] )
+	{
+		case "/get":
+			// Required GET parameters:
+			// user: authentication username, SIP-username without domain component
+			// domain: Domain/realm of the user. username + '@' + domain == SIP address.
+			if ( array_key_exists( 'username', $_GET) && array_key_exists( 'domain', $_GET ) )
+			{
+				$username = $_GET['username'];
+				$domain = $_GET['domain'];
+				// Now, do funky stuff.
+				/*
+				Test if user exists in both 'kamailio.subscribers' and 'provision.users'
+					* Return 'response' => 'ok', 'type' => 'local', 'user' => complete user object.
+				Test if user exists in 'provision.user' only
+					* Return 'response' => 'ok', 'type' => 'remote', 'user' => complete user object.
+				If user does is neither local nor remote
+					* Return 'response' => 'failed' with 'cause' => 'nonexistant'
+				On failure, return 'response' => 'failed' with 'cause' => 'error' (may set 'detail' => 'message')
+				 
+				*/	
+				// Dummy-response:
+				$userdata = get_userdata( $username, $domain );
+				if ( $userdata )
+				{
+					print json_encode( array( 'response' => 'ok', 'user' => $userdata ));
+				}
+				else
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'Request for user ' . $username . '@' . $domain . ' failed.'));
+				}
+			}
+			else
+				print json_encode ( array( 'response' => 'invalid') );
+			break;
+		case "/list":
+			/* 
+			Simply list all users in user@domain format 
+			Perform a search operation if 'search' exists as a GET-parameter
+			 * The search should try to do a "smart search" on SIP-usernames:
+			   * Try to search with names in in username@domain format
+			   * Do the search with wildcards before and after input text.
+			   * The search must be done in the provisioning tables, to be able
+			     to match non-local users.
+			   * SQL SELECT CONCAT() WHERE CONCAT() must be used *shrug*
+			   */
+			$search = null;
+			if ( array_key_exists ( 'search', $_GET ) ) 
+				$search = $_GET['search']; // TODO: Add some sanitation and input validation!
+			$list = list_users( $search );
+			print json_encode( array( 'response' => 'ok', 'list' => $list ));
+			break;
+		case "/add_local":
+			/*
+			What to do??
+			Required parameters should be...
+				username
+				domain
+				displayname
+				email
+
+			Verify that domain is local (lookup in the 'kamailio.domain' table.
+			Verify that the username is available (nonexistant for domain in kamilio.subscribers (and provision.users?))
+				* Autocreate password
+				* Add username, domain, email and created password to the 'kamailio.subscriber' table
+				* Get the registrar+port, proxy+port from the 'provision.servers' table.
+				* standard dialplan from configuration.
+				* Add to the 'provision.users' table:
+					username -> username
+					password -> generated password
+					displayname -> displayname
+					domain -> domain
+					registrar -> provision.servers.registrar
+					r_port -> provision.servers.r_port
+					proxy -> provision.servers.proxy
+					p_port -> provision.servers.p_port
+					authid -> username
+					dialplan -> standard dialplan
+					linetext -> username
+				* Return 'response' => 'ok' with a full user object in JSON format.
+			If any of the tests fail, return 'response' => 'failed' with 'cause' => "description" on JSON format.
+
+			*/
+			// Test required parameters:
+			if ( array_key_exists( 'username', $_GET) 
+				&& array_key_exists( 'domain', $_GET ) 
+				&& array_key_exists( 'displayname', $_GET ) 
+				&& array_key_exists( 'email', $_GET ) )
+			{
+				$username = $_GET['username'];
+				$domain = $_GET['domain'];
+				$password = generate_password();
+				$displayname = $_GET['displayname'];
+				$email = $_GET['email'];
+
+				if (  !is_kamailio_domain( $domain ) )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'nxdomain', 'detail' => 'The selected domain is not local' ));
+					break;
+				}
+
+				$servers = get_servers( $domain );
+				if ( !$servers )
+				{
+					print json_encode( array( 'response' => 'failed', 'cause' => 'servfail', 'detail' => 'Servers lookup failed for domain '. $domain ) );
+					break;
+				}
+				$registrar = $servers['registrar'];
+				$r_port = $servers['r_port'];
+				$proxy = $servers['proxy'];
+				$p_port = $servers['p_port'];
+				$authid = $username;
+				$linetext = $username;
+				$dialplan = $config['standard_dialplan'];
+
+				if ( is_provision_user ( $username, $domain ) )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'exists', 'detail' => 'User already exists in provisioning configuration' ));
+					break;
+				}
+				if ( is_kamailio_subscriber ( $username, $domain ) )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'exists', 'detail' => 'User already exists as a Kamailio subscriber' ));
+					break;
+				}
+				$kam_res = add_kamailio_subscriber( $username, $domain, $password, $email );
+				if ( !$kam_res )
+				{
+					print json_encode( array( 'response' => 'failed', 'cause' => 'dbfail', 'detail' => 'Failed to add kamailio subscriber.' ) );
+					break;
+				}
+				$pro_res = add_provision_user( $username, $password, $domain, $authid, $registrar, $r_port, $proxy, $p_port, $displayname, $dialplan, $linetext );
+				if ( !$pro_res )
+				{
+					// Rollback data added to Kamailio! Try to simulate atomicity, or atleast maintain integrity...
+					delete_kamailio_subscriber( $username, $domain );
+					// Give errormessage, and quit.
+					print json_encode( array( 'response' => 'failed', 'cause' => 'dbfail', 'detail' => 'Failed to add user for provisioning. Rolled back kamailio subscriber' ) );
+					break;
+				}
+				$userdata = get_userdata( $username, $domain );
+				if ( !$userdata )
+				{
+					// Rollback data added to Kamailio! Try to simulate atomicity, or atleast maintain integrity...
+					delete_kamailio_subscriber( $username, $domain );
+					delete_provision_user( $username, $domain );
+					// Give errormessage, and quit.
+					print json_encode( array( 'response' => 'failed', 'cause' => 'dbfail', 'detail' => 'Failed to read recently added data. Operations rolled back' ) );
+	
+				}
+				print json_encode( array( 'response' => 'ok', 'user' => $userdata ));
+			}
+			else 
+				print json_encode( array( 'response' => 'invalid', 'cause' => 'parameters' ) );
+			break;
+		case "/add_remote":
+			/*
+			Required parameters should be...
+				username
+				password
+				domain
+				displayname
+				registrar
+			Optional parameters
+				r_port
+				proxy
+				p_port
+				authid
+				dialplan
+				linetext
+			
+			Verify that the username+domain is not already registered in 'provision.users'.
+				* If r_port is empty, set to 5060
+				* If proxy/port is empty, set to registrar/port
+				* If authid is empty, set to username
+				* If dialplan is empty, set to standard dialplan
+				* If linetext is empty, set to username@domain
+				* Add to the 'provision.users' table:
+					username -> username
+					password -> supplied password
+					displayname -> displayname
+					domain -> domain
+					registrar -> registrar
+					r_port -> r_port
+					proxy -> proxy
+					p_port -> p_port
+					authid -> authid
+					dialplan -> dialplan
+					linetext -> linetext
+				* Return 'response' => 'ok' with a full user object in JSON format.
+			If any of the tests fail, return 'response' => 'failed' with 'cause' => "description" in JSON format.
+			*/
+
+
+			// Test required parameters:
+			if ( array_key_exists( 'username', $_GET) 
+				&& array_key_exists( 'password', $_GET )
+				&& array_key_exists( 'displayname', $_GET )
+				&& array_key_exists( 'domain', $_GET )
+				&& array_key_exists( 'registrar', $_GET ) )
+			{
+				$username = $_GET['username'];
+				$password = $_GET['password'];
+				$domain = $_GET['domain'];
+				$displayname = $_GET['displayname'];
+				$registrar = $_GET['registrar'];
+				$r_port = ( array_key_exists('r_port', $_GET) ) ? $_GET['r_port'] : 5060;
+
+				$proxy = ( array_key_exists('proxy', $_GET) ) ? $_GET['proxy'] : $registrar;
+				$p_port = ( array_key_exists('p_port', $_GET) ) ? $_GET['p_port'] : $r_port;
+				$authid = ( array_key_exists('authid', $_GET) ) ? $_GET['authid'] : $username;
+				$dialplan = ( array_key_exists('dialplan', $_GET) ) ? $_GET['dialplan'] : $config['standard_dialplan'];
+				$linetext = ( array_key_exists('linetext', $_GET) ) ? $_GET['linetext'] : $username . '@' . $domain;
+
+				if ( is_provision_user ( $username, $domain ) )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'exists', 'detail' => 'User already exists in provisioning configuration' ));
+					break;
+				}
+				if ( is_kamailio_subscriber ( $username, $domain ) )
+				{
+					print json_encode ( array( 'response' => 'failed', 'cause' => 'exists', 'detail' => 'User already exists as a Kamailio subscriber' ));
+					break;
+				}
+
+				$pro_res = add_provision_user( $username, $password, $domain, $authid, $registrar, $r_port, $proxy, $p_port, $displayname, $dialplan, $linetext );
+				if ( !$pro_res )
+				{
+					// Give errormessage, and quit.
+					print json_encode( array( 'response' => 'failed', 'cause' => 'dbfail', 'detail' => 'Failed to add user for provisioning.' ) );
+					break;
+				}
+				$userdata = get_userdata( $username, $domain );
+				if ( !$userdata )
+				{
+					// Rollback data added!
+					delete_provision_user( $username, $domain );
+					// Give errormessage, and quit.
+					print json_encode( array( 'response' => 'failed', 'cause' => 'dbfail', 'detail' => 'Failed to read recently added data. Operations rolled back' ) );
+	
+				}
+				print json_encode( array( 'response' => 'ok', 'user' => $userdata ));
+			}
+			else 
+				print json_encode( array( 'response' => 'invalid', 'cause' => 'parameters' ) );
+			break;
+		case "/remove":
+			/*
+			Required parameters should be...
+				username
+				domain
+			
+			* Verify that no associations/relations exist in 'provision.phones'
+			* Verify that the user exists in 'provision.users'
+				* Remove from 'provision.users'
+				* Test to see of user exists in 'kamailio.subscriber'.
+					* Remove from 'kamailio.subscribers'
+					* Return response' => 'ok', 'type' => 'local'
+				* If not in 'kamailio.subscribers'
+					* Return response' => 'ok', 'type' => 'remote'
+			* If associations exist, return 'response' => 'failed', 'cause' => 'inuse'
+			* If no such user exists, return 'response' => 'failed' with 'cause' => 'nonexistant'
+			* On other failures, return 'response' => 'failed' with 'cause' => 'error' (may set 'detail' => 'message')
+			*/
+			if ( array_key_exists( 'username', $_GET) && array_key_exists( 'domain', $_GET ) )
+			{
+				$username = $_GET['username'];
+				$domain = $_GET['domain'];
+				if ( get_user_phones ( $username, $domain ) )
+				{
+					print json_encode( array( 'response' => 'failed', 'cause' => 'inuse', 'detail' => 'User has associated provisioning. Remove and retry.' ) );
+					break;
+				}
+				if ( is_provision_user( $username, $domain ) || is_kamailio_subscriber( $username, $domain ) )
+				{
+					delete_provision_user( $username, $domain );
+					delete_kamailio_subscriber( $username, $domain );
+					print json_encode( array ( 'response' => 'ok', 'detail' => 'User ' . $username . '@' . $domain . ' deleted.'));
+					break;
+				}
+				else
+				{
+					print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant', 'detail' => 'Unable to remove nonexistant user.'));
+					break;
+				}
+
+				break;
+
+			}
+			break;
+		case "/gen_pw":
+			print generate_password();
+			break;
+		default:
+			print json_encode ( array( 'response' => 'invalid') );
+	}
+mysql_close( $config['sql_link'] );
+?>