Skip to content

Instantly share code, notes, and snippets.

@bastman
Created November 10, 2011 12:57
Show Gist options
  • Save bastman/1354794 to your computer and use it in GitHub Desktop.
Save bastman/1354794 to your computer and use it in GitHub Desktop.
advanced json-rpc server (php)
JS Client using jquery ajax and json2.js
========================================
App.invokeRpc = function(method, params, callback)
{
var rpc = {
method:method,
params:params
};
$.ajax({
url:'rpc.php',
processData:false,
data:JSON.stringify(rpc),
type:'POST',
success:function(response)
{
var resp = response;
try
{
if(typeof(response) == 'string')
{
resp = JSON.parse(response);
}
if(typeof(resp) != 'object')
{
throw new Error("Invalid rpc response");
}
}
catch(e)
{
if(typeof(callback) == 'function')
{
callback({
result:null,
error:{
message:'Invalid rpc response!'
}
});
}
return;
}
if(typeof(callback) == 'function')
{
callback(resp);
}
},
error:function()
{
callback({
result:null,
error:{
message:'XHR ERROR'
}
});
}
});
};
the dispatcher
===============
<?php
class Dispatcher
{
/*
- advanced jsonrpc server (php): https://gist.github.com/gists/1354794
- a simple jsonrpc server (php): https://gist.github.com/1344759
- rpc client (js): https://gist.github.com/gists/1354794
- rpc client (as3): https://gist.github.com/1371010
EXAMPLE
=======
var rpc = {
"method":"MyService.foobar",
"params":["foo","bar","baz"]
}
*/
const ERROR_DISPATCHER_FAILED = "RPC DISPATCHER FAILED";
// ++++++++++++++++++++ config +++++++++++++++++++++++++++++
/**
* register your service enpoints here:
*
* e.g. User.Events -> App_Rpc_Service_User_Events
*
* means
* rpc.method: User.Events.foo()
* php: App_Rpc_Service_User_Events::foo()
* @var array
*/
protected $_services = array(
array(
"endpoint" => "User.Events",
"class" => "App_Rpc_Service_User_Events",
/*
"methods" => array(
"allow" => array(
"*"
),
"deny" => array(
"*myPrivateMethod"
),
),
*/
),
array(
"endpoint" => "Events.Artists",
"class" => "App_Rpc_Service_Events_Artists",
/*
"methods" => array(
"allow" => array(
"*"
),
"deny" => array(
"*myPrivateMethod"
),
),
*/
),
);
/**
* @var array
*/
protected $_responseHeaders = array(
"Content-Type: application/json; charset=utf-8",
);
// ++++++++++++++++++ run ++++++++++++++++++++++++++++++++
/**
* @throws Exception
* @return void
*/
public function run()
{
$this->_init();
$this->_run();
}
/**
* @param $className
* @return void
*/
public function phpAutoLoader($className)
{
$classPath = dirname(__FILE__);
$filename = str_replace(
"_",
"/",
$className
).".php";
$location = $classPath."/".$filename;
require_once($location);
if(!class_exists($className)) {
throw new Exception("classloader failed");
}
}
/**
* for php<5.3
* @throws ErrorException
* @param $errno
* @param $errstr
* @param $errfile
* @param $errline
* @return void
*/
public function phpErrorHandler($errno, $errstr, $errfile, $errline)
{
// convert php notice to exception
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
/**
* @return Hardcoded shutdown handler to detect critical PHP errors.
*/
public function phpShutdownHandler()
{
$error = error_get_last();
if ($error === null) {
// no error, we have a "normal" shut down (script is finished).
return;
}
$responseData = array(
"error" => array(
"class" => str_replace("_", ".", get_class($this)),
"message" => "rpc shutdown error"
),
"result" => null,
);
echo json_encode($responseData);
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++
/**
* @throws ErrorException
* @return void
*/
protected function _init()
{
/*
RECOMMENDED SETUP
=================
*/
ini_set("display_errors", true);
error_reporting(E_ALL|E_STRICT & ~E_NOTICE);
// turn on error exceptions
// php 5.3+
/*
set_error_handler(function($errno, $errstr, $errfile, $errline){
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
*/
// php < 5.3
set_error_handler(array($this, "phpErrorHandler"));
// try catch fatal errors
register_shutdown_function(array($this, 'phpShutdownHandler'));
spl_autoload_register(array($this, 'phpAutoLoader'));
}
/**
* @param ReflectionClass $reflectionClass
* @param ReflectionMethod $reflectionMethod
* @param object $serviceInstance
* @param array $params
* @return void
*/
protected function _onRpcBeforeInvoke(
ReflectionClass $reflectionClass,
ReflectionMethod $reflectionMethod,
$serviceInstance,
array $params
)
{
// your hooks here
}
/**
* @param Exception $exception
* @return void
*/
protected function _onRpcError(Exception $exception)
{
// your hooks here
// you may want to log sth?
// you may want to sanitize error data before deliver to client?
$error = array(
"class" => str_replace("_", ".", get_class($exception)),
"message" => $exception->getMessage(),
);
$responseData = array(
"result" => null,
"error" => $error,
);
$this->_sendResponse($responseData);
}
/**
* @param mixed $result
* @return void
*/
protected function _onRpcResult($result)
{
// your hooks here
$responseData = array(
"result" => $result,
"error" => null
);
$this->_sendResponse($responseData);
}
/**
* @param Exception $e
* @return
*/
protected function _onDispatcherError(Exception $e)
{
// YOU HAVE FUCKING FATAL ERROR IN YOUR HANDLERS!
// FIX IT MONKEY!
$responseData = array(
"error" => array(
"class" => str_replace("_", ".", get_class($e)),
"message" => self::ERROR_DISPATCHER_FAILED,
),
"result" => null,
);
echo json_encode($responseData);
return;
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
protected function _run()
{
$serviceResult = null;
$serviceError = null;
try {
$requestText = "" . $this->_fetchRequestText();
$rpc = json_decode($requestText, true);
if (!is_array($rpc)) {
throw new Exception("Invalid rpc request");
}
$rpcMethod = null;
if (array_key_exists("method", $rpc)) {
$rpcMethod = $rpc["method"];
}
$rpcParams = $rpc["params"];
if (array_key_exists("params", $rpc)) {
$rpcParams = $rpc["params"];
if ($rpcParams === null) {
$rpcParams = array();
}
}
if (!is_string($rpcMethod)) {
throw new Exception("Invalid rpc method");
}
if (!is_array($rpcParams)) {
throw new Exception("Invalid rpc params");
}
$rpcMethodParts = (array)explode(".", $rpcMethod);
$rpcMethodName = "".array_pop($rpcMethodParts);
$rpcMethodClass = implode(".", (array)$rpcMethodParts);
// locate class by convention?
// $rpcMethodClass = str_replace(".", "_", "".$rpcMethodClass);
$rpcMethodName = "".strtolower(
trim("".$rpcMethodName)
);
$rpcMethodClass = "".strtolower(
trim("".$rpcMethodClass)
);
$serviceClassName = null;
$serviceClassInfo = null;
$serviceMethodName = $rpcMethodName;
$servicesAvailable = (array)$this->_services;
foreach($servicesAvailable as $serviceInfo) {
$serviceEndpoint = "".strtolower(
trim("".$serviceInfo["endpoint"])
);
if(strlen($serviceEndpoint)<1) {
throw new Exception("Invalid rpc server config");
}
if($serviceEndpoint === $rpcMethodClass) {
$serviceClassName = "".strtolower(
trim("".$serviceInfo["class"])
);
if(strlen($serviceClassName)<1) {
throw new Exception("Invalid rpc server config");
}
$serviceClassInfo = $serviceInfo;
break;
}
}
try {
if(!class_exists($serviceClassName)) {
throw new Exception("Invalid rpc service class");
}
$reflectionClass = new ReflectionClass($serviceClassName);
} catch (Exception $e) {
throw new Exception("Invalid rpc service class");
}
if (!$reflectionClass->hasMethod($serviceMethodName)) {
throw new Exception("rpc method does not exist!");
}
$reflectionMethod = $reflectionClass->getMethod(
$serviceMethodName
);
$this->_validateReflectionMethod($reflectionMethod);
$this->_validateServiceMethodName(
(array)$serviceClassInfo, $reflectionMethod
);
$serviceResult = $this->_invokeService(
$reflectionClass,
$reflectionMethod,
$serviceClassName,
(array)$rpcParams
);
} catch (Exception $e) {
$serviceError = $e;
}
try {
if ($serviceError instanceof Exception) {
$this->_onRpcError($serviceError);
} else {
$this->_onRpcResult($serviceResult);
}
} catch (Exception $e) {
// YOU HAVE FUCKING FATAL ERROR IN YOUR HANDLERS! FIX IT MONKEY!
$this->_onDispatcherError($e);
return;
}
return;
}
/**
* @param ReflectionClass $reflectionClass
* @param ReflectionMethod $reflectionMethod
* @param $serviceClassName
* @param array $params
* @return mixed
*/
protected function _invokeService(
ReflectionClass $reflectionClass,
ReflectionMethod $reflectionMethod,
$serviceClassName,
array $params
)
{
$service = new $serviceClassName();
$this->_onRpcBeforeInvoke(
$reflectionClass,
$reflectionMethod,
$service,
(array)$params
);
$serviceResult = $reflectionMethod->invokeArgs(
$service, (array)$params
);
return $serviceResult;
}
/**
* @return string
*/
protected function _fetchRequestText()
{
$requestText = file_get_contents('php://input');
return $requestText;
}
/**
* @param array $responseData
* @return void
*/
protected function _sendResponse($responseData)
{
if(!is_array($responseData)) {
$responseData = array(
"result" => null,
"error" => array(
"class" => str_replace("_", ".", get_class($this)),
"message" => "invalid rpc response data"
),
);
}
// json encode response
$responseText = null;
try {
$responseText = json_encode($responseData);
} catch(Exception $e) {
//NOP
}
if(!is_string($responseText)) {
$responseData = array(
"result" => "null",
"error" => array(
"class" => str_replace("_", ".", get_class($this)),
"message" => "rpc encode response failed",
),
);
$responseText = json_encode($responseData);
}
// send response headers
$this->_sendResponseHeaders();
// send response text
echo "".$responseText;
}
/**
* @return void
*/
protected function _sendResponseHeaders()
{
// send response headers
$headers = $this->_responseHeaders;
if(!is_array($headers)) {
$headers = array();
}
foreach($headers as $header) {
if (is_string($header)) {
try {
header($header);
} catch(Exception $e) {
//NOP
// ignore warnings "headers already sent"
}
}
}
}
/**
* @throws Exception
* @param ReflectionMethod $reflectionMethod
* @return void
*/
protected function _validateReflectionMethod(
ReflectionMethod $reflectionMethod
)
{
$reflectionMethodName = $reflectionMethod->getName();
if ($reflectionMethodName[0] === "_") {
throw new Exception("rpc method is not invokable!");
}
if (!$reflectionMethod->isPublic()) {
throw new Exception("rpc method is not invokable!");
}
if ($reflectionMethod->isStatic()) {
throw new Exception("rpc method is not invokable!");
}
if ($reflectionMethod->isAbstract()) {
throw new Exception("rpc method is not invokable!");
}
if ($reflectionMethod->isInternal()) {
throw new Exception("rpc method is not invokable!");
}
if(method_exists($reflectionMethod, "isConstructor")) {
if ($reflectionMethod->isConstructor()) {
throw new Exception("rpc method is not invokable!");
}
}
if(method_exists($reflectionMethod, "isDestructor")) {
if ($reflectionMethod->isDestructor()) {
throw new Exception("rpc method is not invokable!");
}
}
if(method_exists($reflectionMethod, "isClosure")) {
if ($reflectionMethod->isClosure()) {
throw new Exception("rpc method is not invokable!");
}
}
if(method_exists($reflectionMethod, "isDeprecated")) {
if ($reflectionMethod->isDeprecated()) {
throw new Exception("rpc method is not invokable!");
}
}
}
/**
* @throws Exception
* @param array $serviceInfo
* @param ReflectionMethod $reflectionMethod
* @return null
*/
protected function _validateServiceMethodName(
array $serviceInfo, ReflectionMethod $reflectionMethod
) {
$result = null;
$methodName = $reflectionMethod->getName();
if(!array_key_exists("methods", $serviceInfo)) {
return $result;
}
$methodsWhiteListed = null;
$methodsBlackListed = null;
if(array_key_exists("allow", $serviceInfo["methods"])) {
$methodsWhiteListed = $serviceInfo["methods"]["allow"];
}
if(array_key_exists("deny", $serviceInfo["methods"])) {
$methodsBlackListed = $serviceInfo["methods"]["deny"];
}
if($methodsWhiteListed===null) {
$methodsWhiteListed = array("*");
}
if($methodsBlackListed===null) {
$methodsBlackListed = array();
}
if(!is_array($methodsWhiteListed)) {
throw new Exception("Invalid rpc serviceConfig.methods.allow");
}
if(!is_array($methodsBlackListed)) {
throw new Exception("Invalid rpc serviceConfig.methods.deny");
}
$isWhiteListed = false;
foreach($methodsWhiteListed as $pattern) {
$isMatched = fnmatch($pattern, $methodName, FNM_CASEFOLD);
if($isMatched) {
$isWhiteListed = true;
break;
}
}
$isBlackListed = false;
foreach($methodsBlackListed as $pattern) {
$isMatched = fnmatch($pattern, $methodName, FNM_CASEFOLD);
if($isMatched) {
$isBlackListed = true;
break;
}
}
$isAllowed = (
($isWhiteListed)
&& (!$isBlackListed)
);
if(!$isAllowed) {
throw new Exception("rpc method not allowed");
}
return $result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment