Hey!
I had to build a PHP federation server for the needs of blackwallet, so I decided to share it here - this is the first version of it and some features are to be built (e.g: possibility to add data to the db)
For the moment it can only retrieve the account id associated to the stellar address nor retrieve the first stellar address associated to the given id.
I'll update the posts each time I'll update the federation server!
Federation Route (controller)
public function federation() {
header('Access-Control-Allow-Origin: *');
header('Content-type: application/json');
$this->load->model('federation_model');
// check that parameters are defined
if( !isset($_GET['q']) || !isset($_GET['type']) )
return $this->federation_model->error('missing parameters, excepted parameter '.
'q and parameter type');
$this->federation_model->handle($_GET['q'], $_GET['type']);
}
Federation Model
<?php
class Federation_model extends CI_Model {
public function __construct()
{
parent::__construct();
$this->load->database();
}
/* display error details */
public function error($detail,$code=400) {
http_response_code($code);
echo '{"detail": "'.$detail.'"}';
}
/* handle the federation lookup */
public function handle($q, $type) {
switch( $type ) {
case 'name':
$this->getRecordFromName(strtolower($q));
break;
case 'id':
$this->getRecordFromAccountId($q);
break;
case 'link':
$this->linkNameToAccountId($q);
break;
case 'linkEmail':
$this->linkEmailToAccountId($q);
break;
default:
$this->error('Unhandled type');
break;
}
}
/* display the record linked to the name $name */
private function getRecordFromName($name) {
$this->db->select('*');
$this->db->from('user');
$this->db->where('stellar_address', $name);
$query = $this->db->get();
if( $query->num_rows() == 0 ) {
$this->error('No record found', 404);
return false;
}
$this->displayRecord($query->row(0));
return $query;
}
/*
display the first record linked to the account id $accountId
an account id can have multiple records linked to it
*/
private function getRecordFromAccountId($accountId) {
$this->db->select('*');
$this->db->from('user');
$this->db->where('account_id', $accountId);
$query = $this->db->get();
if( $query->num_rows() == 0 ) {
$this->error('No record found', 404);
return false;
}
$this->displayRecord($query->row(0));
return $query;
}
/* try to link if available, a name to a stellar account id,
the data is provided this way: name:accountid */
private function linkNameToAccountId($data) {
$data = explode(':', $data);
$name = $data[0];
$accountId = $data[1];
// check that the name is a valid name (alphanumeric regex check)
if(!ctype_alnum($name)) {
$this->error('Invalid name, must contains alphanumeric characters only.');
return false;
}
// check that no name is linked yet to the accountid
$this->db->select('*');
$this->db->from('user');
$this->db->where('account_id', $accountId);
$query = $this->db->get();
if( $query->num_rows() > 0 ) {
$this->error('You already have a federated name or email linked.');
return false;
}
// check that the name is not already used by someone else
$this->db->select('*');
$this->db->from('user');
$this->db->where('stellar_address', $name.'*domain.com');
$query2 = $this->db->get();
if( $query2->num_rows() > 0 ) {
$this->error('Name already used by someone.');
return false;
}
// finally, we insert the name into the db
$this->db->insert('user',
array(
'stellar_address' => strtolower($name).'*domain.com',
'account_id' => $accountId
)
);
echo '{"detail":"Success!"}';
}
/* try to link if available, an email to a stellar account id,
the data is provided this way: email:accountid:emailsecret
where email secret is a hash of the email to prevent people from linking
emails they do not own to an account id
*/
private function linkEmailToAccountId($data) {
$data = explode(':', $data);
$email = $data[0];
$accountId = $data[1];
$emailSecret = $data[2];
// check that the emailSecret is correct
// used to prevent users from linking an email that is not their property to an account id
$email_split = str_split($email);
$_emailSecret = 974621;
foreach($email_split as $char) {
$_emailSecret += ord($char)+923;
}
if($_emailSecret != $emailSecret) {
$this->error('Invalid email hash');
return false;
}
// check that the email is not already linked to an account
$this->db->select('*');
$this->db->from('user');
$this->db->where('stellar_address', $email.'*domain.com');
$query = $this->db->get();
if( $query->num_rows() > 0 ) {
$this->error('This email is already linked to an account id.');
return false;
}
// finally, we insert the email into the db
$this->db->insert('user',
array(
'stellar_address' => strtolower($email).'*domain.com',
'account_id' => $accountId
)
);
echo '{"detail":"Email successfully linked!"}';
}
/* display a record in JSON format */
private function displayRecord($record) {
if( $record->memo != '' ) {
echo '{'
.'"stellar_address":"'.$record->stellar_address.'",'
.'"account_id":"'.$record->account_id.'",'
.'"memo":"'.$record->memo.'",'
.'"memo_type":"'.$record->memo_type.'"'
.'}';
} else {
echo '{'
.'"stellar_address":"'.$record->stellar_address.'",'
.'"account_id":"'.$record->account_id.'"'
.'}';
}
}
}
Database (user)
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
CREATE TABLE IF NOT EXISTS `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`stellar_address` varchar(255) COLLATE latin1_general_ci NOT NULL,
`account_id` varchar(255) COLLATE latin1_general_ci NOT NULL,
`memo_type` varchar(255) COLLATE latin1_general_ci NOT NULL,
`memo` varchar(255) COLLATE latin1_general_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=0 ;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
I am using codeigniter but this can be easily implemented on any website with little modifications.
It's live on blackwallet.co and can be tested with the following federated addresses:
orbit84*blackwallet.co (simply link to an account id)
test*blackwallet.co (link to an account id + memo)