La scorsa settimana mi sono occupato di introdurre il concetto di plugin in modo teorico e successivamente mostrare un'implementazione molto semplice per fornire alla propria applicazione la capacità di accettare plugin per estendere il framework. Scrivere applicazioni plugin-capable è ormai un'operazione che si può considerare di routine, quasi un vero e proprio obbligo se si desidera rendere pubblico e facilmente personalizzabile il proprio prodotto. In questo articolo cercherò invece di introdurvi alle metodologie utili per creare applicazioni in grado di esporre ad utenti esterni le proprie funzionalità.
Il concetto di API pubblica non è affatto una novità: il mondo dei Web service è nato e si è sviluppato al fine di promuovere e migliorare questo concetto; ormai è da considerarsi normale che un'applicazione di successo fornisca delle API per permettere ai propri utenti (o addirittura a chiunque) di sfruttare servizi più o meno complessi che una volta erano privati e fruibili solamente attraverso l'applicazione stessa. Basti pensare a Google o Flickr, che fanno delle API pubbliche uno degli strumenti più interessanti per chi desidera sfruttare questi immensi database di informazioni per i propri scopi, ludici o economici.
I protocolli di comunicazione
Con il tempo e con l'introduzione dei Web service sono andati ad affermarsi diversi protocolli di comunicazione che potremmo definire standard: tra questi possiamo ricordare SOAP, XML-RPC, XML e JSON; ognuno di questi standard ha i suoi pro ed i suoi contro, ed un servizio che punta ad essere il più cross platform possibile dovrebbe riuscire a comunicare sfruttando il maggior numero di protocolli implementabili. Spesso però non si hanno di queste esigenze, oppure si opta per soluzioni più semplici ma allo stesso tempo vincenti: JSON è un protocollo di comunicazione che è nato (in realtà si è affermato) successivamente alla prorompente entrata nel mondo dello sviluppo web della tecnologia AJAX, e pare essere una buona soluzione se si desidera far fruire le API ad applicazioni client; SOAP ed XML-RPC sono invece i due protocolli più utilizzati nell'implementazione dei webservice: a fronte di una maggior complessità, il primo dei due risulta molto più versatile e sta via via soppiantando il secondo; infine abbiamo tutta quella serie di soluzioni proprietarie che sfruttano chiamate XML semplici, header GET/POST o altre soluzioni simili.
Per l'esempio che andrò ad illustrare ho optato per l'utilizzo di XML semplice sia per le richieste sia per le risposte.
Il server per le API pubbliche
Come primo esempio illustrerò come implementare un semplice server capace di esporre delle API accessibili da chiunque voglia utilizzare le funzionalità esposte. Il protocollo di comunicazione utilizzato sarà XML sia per le richieste che per le risposte.
<?php
abstract class Service
{
abstract public function getServicesSignatures();
}
final class Type
{
const INT = 'int';
const STRING = 'string';
}
class ServiceServer
{
public $service_dir;
public function __construct($service_dir)
{
$this->service_dir = $service_dir;
}
public function run()
{
$data = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : file_get_contents("php://input");
if(!$data || strlen($data) == 0)
{
$this->dumpServices();
return false;
}
try
{
$dom = new DomDocument();
$dom->loadXML($data);
$method = $dom->firstChild->nodeName;
$arguments = array();
foreach($dom->firstChild->childNodes as $child)
{
$arguments[$child->nodeName] = $child->textContent;
}
list($service, $method) = explode('.', $method);
$service = $this->loadService($service);
$signatures = $service->getServicesSignatures();
if(!isset($signatures[$method]))
$this->reportError("Metodo ".$method." non implementato dal servizio ".$service);
$signature = $signatures[$method];
foreach($arguments as $key => $value)
{
switch($signature[$key])
{
case Type::INT:
$value = intval($value);
break;
}
$arguments[$key] = $value;
}
$result = call_user_func_array(array($service, $method), $arguments);
$this->reportResult("<Std.Result>".$result."</Std.Result>");
}catch(Exception $e)
{
$this->reportError($e->__toString());
}
return true;
}
private function dumpServices()
{
echo "<h2>Lista dei servizi disponibili</h2><ul>";
foreach($this->listServices() as $service => $signatures)
{
foreach($signatures as $method => $signature)
{
$params = array();
foreach($signature as $name => $type)
$params[] = $type." ".$name;
echo "<li>".$service.".".$method."(".implode(", ", $params).")</li>";
}
}
echo "</ul>";
}
private function reportError($e)
{
$xml = array("Std.Error", $e);
$this->reportResult($xml);
}
private function reportResult($result)
{
header('Content-Type', 'text/xml');
header('Content-Length', strlen($result));
echo $result;
}
public function listServices()
{
$services = array();
foreach(glob($this->service_dir."/*.php") as $service_file)
{
$service_name = basename($service_file, ".php");
$service = $this->loadService($service_name);
$services[$service_name] = $service->getServicesSignatures();
}
return $services;
}
public function loadService($name)
{
$service_file = $this->service_dir."/".$name.".php";
if(!file_exists($service_file))
throw Exception("Il servizio ".$name." non può essere caricato!");
require_once $service_file;
if(!class_exists($name))
throw Exception("Il servizio caricato (".$name.") non può essere instanziato!");
return new $name;
}
}
$server = new ServiceServer(dirname(__FILE__).'/services');
$server->run();
?>
Abbiamo definito tre classi che abbiamo evidenziato in rosso nel codice:
- La classe astratta Service che serve per rappresentare un servizio; ogni servizio implementato dovrà esporre il metodo
getServicesSignatures
che si occuperà di restituire una array multidimensionale contenente la definizione dei metodi esposti dal servizio; - La classe Type che definisce due tipi di dato accettabili dai metodi;
- La classe ServiceServer che si occupa di accettare le chiamate in arrivo, effettuare il parsing dell'XML inviato, caricare il servizio richiesto ed eventualmente eseguirlo. In caso di errore o eccezioni verrà restituito il nodo XML
Std.Error
contenente la stringa di errore; nel caso di risultato corretto restituiremoStd.Result
;
Il sistema scelto è molto semplice ma flessibile; non sono stati implementati tutti i controlli possibili e le ottimizzazioni, ma il sistema è ben strutturato ed facilmente utilizzabile.
I servizi implementati vanno posizionati nella cartella services
e non sono altro che classi che estendono la classe astratta Service ed espongono dei metodi. Vediamo un esempio:
<?php
final class TestService extends Service
{
public function getServicesSignatures()
{
return array(
"sum" => array(
"first_value" => Type::INT,
"second_value" => Type::INT),
"hello_world" => array(
"name" => Type::STRING)
);
}
public function sum($first_value, $second_value)
{
return $first_value + $second_value;
}
public function hello_world($name)
{
return $name." dice: Hello World";
}
}
?>
Il client e l'utilizzo delle API
Passiamo ora brevemente al client ed alla sua implementazione in PHP. Il client si connetterà al server, invierà la richiesta in XML ed intercetterà la risposta:
<?php
class Service
{
private $client;
private $name;
public function __construct($client, $name)
{
$this->client = $client;
$this->name = $name;
}
public function __call($name, $args)
{
$data = '<'.$this->name.".".$name.'>';
foreach($args[0] as $key => $value)
$data .= '<'.$key.'>'.$value.'</'.$key.'>';
$data .= '</'.$this->name.".".$name.'>';
$in = "POST ".$this->client->path." HTTP/1.0rn";
$in .= "User-Agent: phpRPCrn";
$in .= "Host: ".$this->client->host."rn";
$in .= "Content-Type: text/xmlrn";
$in .= "Connection: closern";
$in .= "Content-length: ".strlen($data)."rnrn";
$in .= $data."rn";
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if($socket < 0)
throw new Exception("Impossibile aprire il socket");
$result = socket_connect($socket, gethostbyname($this->client->host), 80);
if($result < 0)
throw new Exception("Impossibile connettere il socket");
socket_write($socket, $in, strlen ($in));
$buffer = "";
while ($out = socket_read($socket, 2048))
$buffer .= $out;
list($headers, $body) = explode("rnrn", $buffer, 2);
$dom = new DomDocument();
$dom->loadXML($body);
switch($dom->firstChild->nodeName)
{
case 'Std.Result':
return $dom->firstChild->textContent;
break;
case 'Std.Error':
throw new Exception($dom->firstChild->textContent);
break;
}
}
}
class ServiceClient
{
public $host;
public $path;
public function __construct($host, $path)
{
$this->host = $host;
$this->path = $path;
}
public function getService($name)
{
return new Service($this, $name);
}
}
?>
Abbiamo implementato due classi:
- La classe Service rappresenta un servizio dal punto di vista del client e si occupa praticamente di aprire un socket verso il server, inviargli una richiesta HTTP ed interpretare la risposta;
- La classe ServiceClient invece è un sistema molto semplice per caricare un servizio;
Per utilizzare il client con il server ed il servizio di prova precedentemente specificato, utilizziamo il seguente codice:
<?php
$client = new ServiceClient('localhost', '/~gabriele/articoli/20/api_pubblica_xml/server.php');
$test_service = $client->getService('TestService');
echo $test_service->sum(array(
'first_value' => 10,
'second_value' => 40));
?>
Conclusioni
Siamo arrivati alla fine dell'articolo. La settimana prossima ci occuperemo di descrivere ancora un po' le soluzioni possibile e vedremo come aggiungere un sistema di autenticazione alle API in modo da renderle private solamente per gli utenti che dispongono di una determinata chiave.
Nella prima parte dell'articolo abbiamo introdotto un semplice sistema client server basato su un protocollo XML che permette ad un'applicazione web o ad un sito Internet di esporre una serie di API pubbliche utilizzabili da chiunque abbia la necessità di sfruttare i servizi implementati. Abbiamo evidenziato l'importanza e la similitudine dei Web service ed abbiamo visto come con un semplice protocollo si possano ottenere buoni risultati senza eccessiva fatica. Oggi estenderemo il lavoro fatto la settimana scorsa aggiungendo il supporto per API private, cioè utilizzabili solamente previa autenticazione sul server che le espone.
Le API private
Il concetto di API privata è nato e si è evoluto di pari passo ai Web service; la maggior parte dei servizi più blasonati ed utilizzati di questo periodo (come le API di Google o di Flickr) sono di tipo privato cioè possono essere utilizzate solamente previa autorizzazione. L'autorizzazione si ottiene registrandosi sul sito che fornisce le API ed implementa il server in modo da ottenere una chiave univoca ed eventualmente dei dati di autenticazione che identifichino chi sta utilizzando il sistema.
Le chiavi generate sono solitamente stringhe necessariamente univoche che possono essere passate come parametro della chiamata HTTP all'API per effettuare l'autenticazione e procedere con l'utilizzo del servizio. Nel caso in cui siano utilizzate da codice eseguito lato server abbiamo la certezza che la chiave non verrà resa pubblica e che non potrà essere utilizzata da altri utenti al di fuori di chi ha accesso al codice lato server che implementa il client.
Esistono anche sistemi più complessi per l'autenticazione, ma in questo articolo mostreremo solamente il concetto dal punto di vista teorico fornendo una semplice implementazione che non si preoccupa di essere sicura e flessibile in toto. Nel caso in cui si necessiti di utilizzare qualche sistema più completo il server ed il client potranno essere estesi senza alcun problema per rispondere alle esigenze.
Il server per le API private
Il server per le API private si basa su quello per le API pubbliche implementato nelle prime pagine di questo articolo. Estenderemo la classe ServiceServer in modo da aggiungere il controllo sulla validità di una chiave univoca passata via URL. Questa chiave verrà confrontata con i dati presenti all'interno di un database XML e nel caso in cui il valore fosse corretto, il server si occuperà di esporre solamente i servizi per i quali l'utente autenticato abbia dei permessi.
Il database XML utilizzato sarà estremamente semplice (il consiglio è di passare subito ad una struttura SQL per supportare più agilmente grosse quantità di utenti):
<?xml version="1.0" encoding="UTF-8" ?>
<database>
<user id="5f423b7772a80f77438407c8b78ff305">
<metadata>
<name>Gabriele Farina</name>
<expires>10-12-2007</expires>
</metadata>
<permissions>
<service name="TestService">
<method name="sum" />
</service>
</permissions>
</user>
</database>
Utilizzando XPath recupereremo il nodo user che si riferisce all'utente autenticato, ed utilizzeremo il contenuto del nodo permissions per validare le chiamate effettuate.
Vediamo ora il codice del server che esporrà le API private:
<?php
require_once "server.php";
final class UsersDatabase
{
private $path;
private $dom;
public function __construct($path)
{
if(!file_exists($path))
throw new Exception("Non posso caricare il database degli utenti");
$dom = new DomDocument();
$dom->load($path);
$this->path = $path;
$this->dom = $dom;
}
public function userExists($key)
{
$xp = new domxpath($this->dom);
$users = $xp->query("/database/user[@id='".$key."']");
return $users->length;
}
public function createServiceProxy($service, $key)
{
$xp = new domxpath($this->dom);
$permissions = $xp->query("/database/user[@id='".$key."']/permissions/service");
$map = array();
foreach($permissions as $permission)
{
$name = $permission->getAttribute('name');
if($name != get_class($service))
continue;
foreach($permission->childNodes as $method)
{
if($method->nodeName != "method")
continue;
$map[] = $method->getAttribute('name');
}
}
return new ServiceProxy($service, $map);
}
}
class ServiceProxy
{
private $service;
private $map;
public function __construct($service, $map)
{
$this->service = $service;
$this->map = $map;
}
public function __call($name, $args)
{
if($name != "getServicesSignatures" && !in_array($name, $this->map))
throw new Exception("Il metodo non può essere chiamato");
return call_user_func_array(array($this->service, $name), $args);
}
}
class PrivateServiceServer extends ServiceServer
{
private $users_db;
public function __construct($path)
{
parent::__construct($path);
try
{
$this->users_db = new UsersDatabase("users.xml");
}catch(Exception $e)
{
$this->reportError($e->__toString());
}
}
public function run()
{
$key = isset($_GET['key']) ? $_GET['key'] : '';
if(!$this->users_db->userExists($key))
$this->reportError("Impossibile autenticarsi");
else
parent::run();
}
public function loadService($name)
{
$service = parent::loadService($name);
return $this->users_db->createServiceProxy($service, $_GET['key']);
}
}
$server = new PrivateServiceServer(dirname(__FILE__).'/services');
$server->run();
?>
Per implementare il server abbiamo definito due nuove classi ed esteso, dunque, il server della volta precedente:
- La classe UsersDatabase serve a rappresentare il database degli utenti salvato all'interno di un file XML; il database in questione viene interrogato per controllare che un utente esista realmente e recuperare la lista dei servizi ai quali ha accesso.
- La classe ServiceProxy serve come proxy per la classe Service: ogni chiamata a metodo viene filtrata in modo che si sia sicuri che il client stia accedendo ad un metodo permesso dalla chiave con la quale si è autenticato. Un ServiceProxy viene creato dalla classe UsersDatabase in base alla chiave di autenticazione specificata nell'URL di connessione.
- La classe PrivateServiceServer che estende
ServiceServer
e reimplementa il metodoloadService
in modo che resituisca un proxy anzichè un'istanza della classe originale. Anche il metodorun
viene sovrascritto per effettuare l'autenticazione prima di eseguire il codice.
Il client e l'utilizzo delle API private
Grazie all'utilizzo dell'autenticazione basata su URL, le modifiche da apportare al client descritto nelle pagine precedenti affinchè il tutto funzioni correttamente sono minimali, quasi nulle:
<?php
require_once "client.php";
class PrivateClient extends ServiceClient
{
public function __construct($host, $path, $key)
{
parent::__construct($host, $path."?key=".$key);
}
}
?>
Quello di cui ci occupiamo è semplicemente accodare la chiave all'URL di destinazione affinchè l'autenticazione possa avvenire correttamente sul server.
Apportata questa semplice modifica possiamo utilizzare il nuovo client ed il nuovo server con il vecchio servizio di prova utilizzando il seguente codice:
<?php
$client = new PrivateClient('localhost', '/~gabriele/articoli/21/api_privata_xml/private_server.php', '5f423b7772a80f77438407c8b78ff305');
$test_service = $client->getService('TestService');
echo $test_service->sum(array(
'first_value' => 10,
'second_value' => 40));
?>
Come possiamo notare la chiamata avviene correttamente ed il metodo viene eseguito senza errori.
Conclusioni
Siamo giunti alla fine di questa breve introduzione al mondo delle API pubbliche e private. Il sistema esposto oggi è molto semplice e non integrato alla perfezione con il vecchio server, ma a discapito della semplicità risulta completo dal punto di vista delle funzionalità esponibili. Ovviamente un sistema più completo avrà un grado di sicurezza maggiore ed una modularità più ampia che gli permetterà di essere esteso seguendo le esigenze e le scelte implementative fatte durante lo sviluppo e l'evoluzione dell'applicazione di base.