diff options
Diffstat (limited to 'libs/jsonrpc/src/JsonRPC')
25 files changed, 2715 insertions, 0 deletions
diff --git a/libs/jsonrpc/src/JsonRPC/Client.php b/libs/jsonrpc/src/JsonRPC/Client.php new file mode 100644 index 00000000..73ab1342 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Client.php @@ -0,0 +1,198 @@ +<?php + +namespace JsonRPC; + +use Exception; +use JsonRPC\Request\RequestBuilder; +use JsonRPC\Response\ResponseParser; + +/** + * JsonRPC client class + * + * @package JsonRPC + * @author Frederic Guillot + */ +class Client +{ + /** + * If the only argument passed to a function is an array + * assume it contains named arguments + * + * @access private + * @var boolean + */ + private $isNamedArguments = true; + + /** + * Do not immediately throw an exception on error. Return it instead. + * + * @access public + * @var boolean + */ + private $returnException = false; + + /** + * True for a batch request + * + * @access private + * @var boolean + */ + private $isBatch = false; + + /** + * Batch payload + * + * @access private + * @var array + */ + private $batch = array(); + + /** + * Http Client + * + * @access private + * @var HttpClient + */ + private $httpClient; + + /** + * Constructor + * + * @access public + * @param string $url Server URL + * @param bool $returnException Return exceptions + * @param HttpClient $httpClient HTTP client object + */ + public function __construct($url = '', $returnException = false, HttpClient $httpClient = null) + { + $this->httpClient = $httpClient ?: new HttpClient($url); + $this->returnException = $returnException; + } + + /** + * Arguments passed are always positional + * + * @access public + * @return $this + */ + public function withPositionalArguments() + { + $this->isNamedArguments = false; + return $this; + } + + /** + * Get HTTP Client + * + * @access public + * @return HttpClient + */ + public function getHttpClient() + { + return $this->httpClient; + } + + /** + * Set username and password + * + * @access public + * @param string $username + * @param string $password + * @return $this + */ + public function authentication($username, $password) + { + $this->httpClient + ->withUsername($username) + ->withPassword($password); + + return $this; + } + + /** + * Automatic mapping of procedures + * + * @access public + * @param string $method Procedure name + * @param array $params Procedure arguments + * @return mixed + */ + public function __call($method, array $params) + { + if ($this->isNamedArguments && count($params) === 1 && is_array($params[0])) { + $params = $params[0]; + } + + return $this->execute($method, $params); + } + + /** + * Start a batch request + * + * @access public + * @return Client + */ + public function batch() + { + $this->isBatch = true; + $this->batch = array(); + return $this; + } + + /** + * Send a batch request + * + * @access public + * @return array + */ + public function send() + { + $this->isBatch = false; + return $this->sendPayload('['.implode(', ', $this->batch).']'); + } + + /** + * Execute a procedure + * + * @access public + * @param string $procedure Procedure name + * @param array $params Procedure arguments + * @param array $reqattrs + * @param string|null $requestId Request Id + * @param string[] $headers Headers for this request + * @return mixed + */ + public function execute($procedure, array $params = array(), array $reqattrs = array(), $requestId = null, array $headers = array()) + { + $payload = RequestBuilder::create() + ->withProcedure($procedure) + ->withParams($params) + ->withRequestAttributes($reqattrs) + ->withId($requestId) + ->build(); + + if ($this->isBatch) { + $this->batch[] = $payload; + return $this; + } + + return $this->sendPayload($payload, $headers); + } + + /** + * Send payload + * + * @access private + * @throws Exception + * @param string $payload + * @param string[] $headers + * @return Exception|Client + */ + private function sendPayload($payload, array $headers = array()) + { + return ResponseParser::create() + ->withReturnException($this->returnException) + ->withPayload($this->httpClient->execute($payload, $headers)) + ->parse(); + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/AccessDeniedException.php b/libs/jsonrpc/src/JsonRPC/Exception/AccessDeniedException.php new file mode 100644 index 00000000..d1aabfbe --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/AccessDeniedException.php @@ -0,0 +1,13 @@ +<?php + +namespace JsonRPC\Exception; + +/** + * Class AccessDeniedException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class AccessDeniedException extends RpcCallFailedException +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/AuthenticationFailureException.php b/libs/jsonrpc/src/JsonRPC/Exception/AuthenticationFailureException.php new file mode 100644 index 00000000..770edeeb --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/AuthenticationFailureException.php @@ -0,0 +1,13 @@ +<?php + +namespace JsonRPC\Exception; + +/** + * Class AuthenticationFailureException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class AuthenticationFailureException extends RpcCallFailedException +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ConnectionFailureException.php b/libs/jsonrpc/src/JsonRPC/Exception/ConnectionFailureException.php new file mode 100644 index 00000000..195f8910 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/ConnectionFailureException.php @@ -0,0 +1,13 @@ +<?php + +namespace JsonRPC\Exception; + +/** + * Class ConnectionFailureException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class ConnectionFailureException extends RpcCallFailedException +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonFormatException.php b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonFormatException.php new file mode 100644 index 00000000..294bc74c --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonFormatException.php @@ -0,0 +1,13 @@ +<?php + +namespace JsonRPC\Exception; + +/** + * Class InvalidJsonFormatException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class InvalidJsonFormatException extends RpcCallFailedException +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php new file mode 100644 index 00000000..2e3ff05b --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php @@ -0,0 +1,13 @@ +<?php + +namespace JsonRPC\Exception; + +/** + * Class InvalidJsonRpcFormatException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class InvalidJsonRpcFormatException extends RpcCallFailedException +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php b/libs/jsonrpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php new file mode 100644 index 00000000..16f75910 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php @@ -0,0 +1,13 @@ +<?php + +namespace JsonRPC\Exception; + +/** + * Class ResponseEncodingFailureException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class ResponseEncodingFailureException extends RpcCallFailedException +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ResponseException.php b/libs/jsonrpc/src/JsonRPC/Exception/ResponseException.php new file mode 100644 index 00000000..e97b4e6b --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/ResponseException.php @@ -0,0 +1,62 @@ +<?php + +namespace JsonRPC\Exception; + +use Exception; + +/** + * Class ResponseException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class ResponseException extends RpcCallFailedException +{ + /** + * A value that contains additional information about the error. + * + * @access protected + * @link http://www.jsonrpc.org/specification#error_object + * @var mixed + */ + protected $data; + + /** + * Constructor + * + * @access public + * @param string $message [optional] The Exception message to throw. + * @param int $code [optional] The Exception code. + * @param Exception $previous [optional] The previous exception used for the exception chaining. Since 5.3.0 + * @param mixed $data [optional] A value that contains additional information about the error. + */ + public function __construct($message = '', $code = 0, Exception $previous = null, $data = null) + { + parent::__construct($message, $code, $previous); + $this->setData($data); + } + + /** + * Attach additional information + * + * @access public + * @param mixed $data [optional] A value that contains additional information about the error. + * @return \JsonRPC\Exception\ResponseException + */ + public function setData($data = null) + { + $this->data = $data; + return $this; + } + + /** + * Get additional information + * + * @access public + * @return mixed|null + */ + public function getData() + { + return $this->data; + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/RpcCallFailedException.php b/libs/jsonrpc/src/JsonRPC/Exception/RpcCallFailedException.php new file mode 100644 index 00000000..b3fcd84e --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/RpcCallFailedException.php @@ -0,0 +1,15 @@ +<?php + +namespace JsonRPC\Exception; + +use Exception; + +/** + * Class RpcCallFailedException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class RpcCallFailedException extends Exception +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/Exception/ServerErrorException.php b/libs/jsonrpc/src/JsonRPC/Exception/ServerErrorException.php new file mode 100644 index 00000000..29031604 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Exception/ServerErrorException.php @@ -0,0 +1,13 @@ +<?php + +namespace JsonRPC\Exception; + +/** + * Class ServerErrorException + * + * @package JsonRPC\Exception + * @author Frederic Guillot + */ +class ServerErrorException extends RpcCallFailedException +{ +} diff --git a/libs/jsonrpc/src/JsonRPC/HttpClient.php b/libs/jsonrpc/src/JsonRPC/HttpClient.php new file mode 100644 index 00000000..01d50445 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/HttpClient.php @@ -0,0 +1,449 @@ +<?php + +namespace JsonRPC; + +use Closure; +use JsonRPC\Exception\AccessDeniedException; +use JsonRPC\Exception\ConnectionFailureException; +use JsonRPC\Exception\ServerErrorException; + +/** + * Class HttpClient + * + * @package JsonRPC + * @author Frederic Guillot + */ +class HttpClient +{ + /** + * URL of the server + * + * @access protected + * @var string + */ + protected $url; + + /** + * HTTP client timeout + * + * @access protected + * @var integer + */ + protected $timeout = 5; + + /** + * Default HTTP headers to send to the server + * + * @access protected + * @var array + */ + protected $headers = array( + 'User-Agent: JSON-RPC PHP Client <https://github.com/fguillot/JsonRPC>', + 'Content-Type: application/json', + 'Accept: application/json', + 'Connection: close', + ); + + /** + * Username for authentication + * + * @access protected + * @var string + */ + protected $username; + + /** + * Password for authentication + * + * @access protected + * @var string + */ + protected $password; + + /** + * Enable debug output to the php error log + * + * @access protected + * @var boolean + */ + protected $debug = false; + + /** + * Cookies + * + * @access protected + * @var array + */ + protected $cookies = array(); + + /** + * SSL certificates verification + * + * @access protected + * @var boolean + */ + protected $verifySslCertificate = true; + + /** + * SSL client certificate + * + * @access protected + * @var string + */ + protected $sslLocalCert; + + /** + * Callback called before the doing the request + * + * @access protected + * @var Closure + */ + protected $beforeRequest; + + /** + * HttpClient constructor + * + * @access public + * @param string $url + */ + public function __construct($url = '') + { + $this->url = $url; + } + + /** + * Set URL + * + * @access public + * @param string $url + * @return $this + */ + public function withUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * Set username + * + * @access public + * @param string $username + * @return $this + */ + public function withUsername($username) + { + $this->username = $username; + return $this; + } + + /** + * Set password + * + * @access public + * @param string $password + * @return $this + */ + public function withPassword($password) + { + $this->password = $password; + return $this; + } + + /** + * Set timeout + * + * @access public + * @param integer $timeout + * @return $this + */ + public function withTimeout($timeout) + { + $this->timeout = $timeout; + return $this; + } + + /** + * Set headers + * + * @access public + * @param array $headers + * @return $this + */ + public function withHeaders(array $headers) + { + $this->headers = array_merge($this->headers, $headers); + return $this; + } + + /** + * Set cookies + * + * @access public + * @param array $cookies + * @param boolean $replace + */ + public function withCookies(array $cookies, $replace = false) + { + if ($replace) { + $this->cookies = $cookies; + } else { + $this->cookies = array_merge($this->cookies, $cookies); + } + } + + /** + * Enable debug mode + * + * @access public + * @return $this + */ + public function withDebug() + { + $this->debug = true; + return $this; + } + + /** + * Disable SSL verification + * + * @access public + * @return $this + */ + public function withoutSslVerification() + { + $this->verifySslCertificate = false; + return $this; + } + + /** + * Assign a certificate to use TLS + * + * @access public + * @return $this + */ + public function withSslLocalCert($path) + { + $this->sslLocalCert = $path; + return $this; + } + + /** + * Assign a callback before the request + * + * @access public + * @param Closure $closure + * @return $this + */ + public function withBeforeRequestCallback(Closure $closure) + { + $this->beforeRequest = $closure; + return $this; + } + + /** + * Get cookies + * + * @access public + * @return array + */ + public function getCookies() + { + return $this->cookies; + } + + /** + * Do the HTTP request + * + * @access public + * @throws ConnectionFailureException + * @param string $payload + * @param string[] $headers Headers for this request + * @return array + */ + public function execute($payload, array $headers = array()) + { + if (is_callable($this->beforeRequest)) { + call_user_func_array($this->beforeRequest, array($this, $payload, $headers)); + } + + if ($this->isCurlLoaded()) { + $ch = curl_init(); + $requestHeaders = $this->buildHeaders($headers); + $headers = array(); + curl_setopt_array($ch, array( + CURLOPT_URL => trim($this->url), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => $this->timeout, + CURLOPT_MAXREDIRS => 2, + CURLOPT_SSL_VERIFYPEER => $this->verifySslCertificate, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => $requestHeaders, + CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$headers) { + $headers[] = $header; + return strlen($header); + } + )); + if ($this->sslLocalCert !== null) { + curl_setopt($ch, CURLOPT_CAINFO, $this->sslLocalCert); + } + $response = curl_exec($ch); + curl_close($ch); + if ($response !== false) { + $response = json_decode($response, true); + } else { + throw new ConnectionFailureException('Unable to establish a connection'); + } + } else { + $stream = fopen(trim($this->url), 'r', false, $this->buildContext($payload, $headers)); + + if (! is_resource($stream)) { + throw new ConnectionFailureException('Unable to establish a connection'); + } + + $metadata = stream_get_meta_data($stream); + $headers = $metadata['wrapper_data']; + $response = json_decode(stream_get_contents($stream), true); + + fclose($stream); + } + + if ($this->debug) { + error_log('==> Request: '.PHP_EOL.(is_string($payload) ? $payload : json_encode($payload, JSON_PRETTY_PRINT))); + error_log('==> Headers: '.PHP_EOL.var_export($headers, true)); + error_log('==> Response: '.PHP_EOL.json_encode($response, JSON_PRETTY_PRINT)); + } + + $this->handleExceptions($headers); + $this->parseCookies($headers); + + return $response; + } + + /** + * Prepare stream context + * + * @access protected + * @param string $payload + * @param string[] $headers + * @return resource + */ + protected function buildContext($payload, array $headers = array()) + { + $headers = $this->buildHeaders($headers); + + $options = array( + 'http' => array( + 'method' => 'POST', + 'protocol_version' => 1.1, + 'timeout' => $this->timeout, + 'max_redirects' => 2, + 'header' => implode("\r\n", $headers), + 'content' => $payload, + 'ignore_errors' => true, + ), + 'ssl' => array( + 'verify_peer' => $this->verifySslCertificate, + 'verify_peer_name' => $this->verifySslCertificate, + ) + ); + + if ($this->sslLocalCert !== null) { + $options['ssl']['local_cert'] = $this->sslLocalCert; + } + + return stream_context_create($options); + } + + /** + * Parse cookies from response + * + * @access protected + * @param array $headers + */ + protected function parseCookies(array $headers) + { + foreach ($headers as $header) { + $pos = stripos($header, 'Set-Cookie:'); + + if ($pos !== false) { + $cookies = explode(';', substr($header, $pos + 11)); + + foreach ($cookies as $cookie) { + $item = explode('=', $cookie); + + if (count($item) === 2) { + $name = trim($item[0]); + $value = $item[1]; + $this->cookies[$name] = $value; + } + } + } + } + } + + /** + * Throw an exception according the HTTP response + * + * @access public + * @param array $headers + * @throws AccessDeniedException + * @throws ServerErrorException + */ + public function handleExceptions(array $headers) + { + $exceptions = array( + '401' => '\JsonRPC\Exception\AccessDeniedException', + '403' => '\JsonRPC\Exception\AccessDeniedException', + '404' => '\JsonRPC\Exception\ConnectionFailureException', + '500' => '\JsonRPC\Exception\ServerErrorException', + ); + + foreach ($headers as $header) { + foreach ($exceptions as $code => $exception) { + if (strpos($header, 'HTTP/1.0 '.$code) !== false || strpos($header, 'HTTP/1.1 '.$code) !== false) { + throw new $exception('Response: '.$header); + } + } + } + } + + /** + * Tests if the curl extension is loaded + * + * @access protected + * @return bool + */ + protected function isCurlLoaded() + { + return extension_loaded('curl'); + } + + /** + * Prepare Headers + * + * @access protected + * @param array $headers + * @return array + */ + protected function buildHeaders(array $headers) + { + $headers = array_merge($this->headers, $headers); + + if (!empty($this->username) && !empty($this->password)) { + $headers[] = 'Authorization: Basic ' . base64_encode($this->username . ':' . $this->password); + } + + if (!empty($this->cookies)) { + $cookies = array(); + + foreach ($this->cookies as $key => $value) { + $cookies[] = $key . '=' . $value; + } + + $headers[] = 'Cookie: ' . implode('; ', $cookies); + } + return $headers; + } +} diff --git a/libs/jsonrpc/src/JsonRPC/MiddlewareHandler.php b/libs/jsonrpc/src/JsonRPC/MiddlewareHandler.php new file mode 100644 index 00000000..61d5a2d2 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/MiddlewareHandler.php @@ -0,0 +1,114 @@ +<?php + +namespace JsonRPC; + +/** + * Class MiddlewareHandler + * + * @package JsonRPC + * @author Frederic Guillot + */ +class MiddlewareHandler +{ + /** + * Procedure Name + * + * @access protected + * @var string + */ + protected $procedureName = ''; + + /** + * Username + * + * @access protected + * @var string + */ + protected $username = ''; + + /** + * Password + * + * @access protected + * @var string + */ + protected $password = ''; + + /** + * List of middleware to execute before to call the method + * + * @access protected + * @var MiddlewareInterface[] + */ + protected $middleware = array(); + + /** + * Set username + * + * @access public + * @param string $username + * @return $this + */ + public function withUsername($username) + { + if (! empty($username)) { + $this->username = $username; + } + + return $this; + } + + /** + * Set password + * + * @access public + * @param string $password + * @return $this + */ + public function withPassword($password) + { + if (! empty($password)) { + $this->password = $password; + } + + return $this; + } + + /** + * Set procedure name + * + * @access public + * @param string $procedureName + * @return $this + */ + public function withProcedure($procedureName) + { + $this->procedureName = $procedureName; + return $this; + } + + /** + * Add a new middleware + * + * @access public + * @param MiddlewareInterface $middleware + * @return MiddlewareHandler + */ + public function withMiddleware(MiddlewareInterface $middleware) + { + $this->middleware[] = $middleware; + return $this; + } + + /** + * Execute all middleware + * + * @access public + */ + public function execute() + { + foreach ($this->middleware as $middleware) { + $middleware->execute($this->username, $this->password, $this->procedureName); + } + } +} diff --git a/libs/jsonrpc/src/JsonRPC/MiddlewareInterface.php b/libs/jsonrpc/src/JsonRPC/MiddlewareInterface.php new file mode 100644 index 00000000..ab55261d --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/MiddlewareInterface.php @@ -0,0 +1,27 @@ +<?php + +namespace JsonRPC; + +use JsonRPC\Exception\AccessDeniedException; +use JsonRPC\Exception\AuthenticationFailureException; + +/** + * Interface MiddlewareInterface + * + * @package JsonRPC + * @author Frederic Guillot + */ +interface MiddlewareInterface +{ + /** + * Execute Middleware + * + * @access public + * @param string $username + * @param string $password + * @param string $procedureName + * @throws AccessDeniedException + * @throws AuthenticationFailureException + */ + public function execute($username, $password, $procedureName); +} diff --git a/libs/jsonrpc/src/JsonRPC/ProcedureHandler.php b/libs/jsonrpc/src/JsonRPC/ProcedureHandler.php new file mode 100644 index 00000000..fe33f6b1 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/ProcedureHandler.php @@ -0,0 +1,296 @@ +<?php + +namespace JsonRPC; + +use BadFunctionCallException; +use Closure; +use InvalidArgumentException; +use ReflectionFunction; +use ReflectionMethod; + +/** + * Class ProcedureHandler + * + * @package JsonRPC + * @author Frederic Guillot + */ +class ProcedureHandler +{ + /** + * List of procedures + * + * @access protected + * @var array + */ + protected $callbacks = array(); + + /** + * List of classes + * + * @access protected + * @var array + */ + protected $classes = array(); + + /** + * List of instances + * + * @access protected + * @var array + */ + protected $instances = array(); + + /** + * Before method name to call + * + * @access protected + * @var string + */ + protected $beforeMethodName = ''; + + /** + * Register a new procedure + * + * @access public + * @param string $procedure Procedure name + * @param closure $callback Callback + * @return $this + */ + public function withCallback($procedure, Closure $callback) + { + $this->callbacks[$procedure] = $callback; + return $this; + } + + /** + * Bind a procedure to a class + * + * @access public + * @param string $procedure Procedure name + * @param mixed $class Class name or instance + * @param string $method Procedure name + * @return $this + */ + public function withClassAndMethod($procedure, $class, $method = '') + { + if ($method === '') { + $method = $procedure; + } + + $this->classes[$procedure] = array($class, $method); + return $this; + } + + /** + * Bind a class instance + * + * @access public + * @param mixed $instance + * @return $this + */ + public function withObject($instance) + { + $this->instances[] = $instance; + return $this; + } + + /** + * Set a before method to call + * + * @access public + * @param string $methodName + * @return $this + */ + public function withBeforeMethod($methodName) + { + $this->beforeMethodName = $methodName; + return $this; + } + + /** + * Register multiple procedures from array + * + * @access public + * @param array $callbacks Array with procedure names (array keys) and callbacks (array values) + * @return $this + */ + public function withCallbackArray($callbacks) + { + foreach ($callbacks as $procedure => $callback) { + $this->withCallback($procedure, $callback); + } + + return $this; + } + + /** + * Bind multiple procedures to classes from array + * + * @access public + * @param array $callbacks Array with procedure names (array keys) and class and method names (array values) + * @return $this + */ + public function withClassAndMethodArray($callbacks) + { + foreach ($callbacks as $procedure => $callback) { + $this->withClassAndMethod($procedure, $callback[0], $callback[1]); + } + + return $this; + } + + /** + * Execute the procedure + * + * @access public + * @param string $procedure Procedure name + * @param array $params Procedure params + * @return mixed + */ + public function executeProcedure($procedure, array $params = array()) + { + if (isset($this->callbacks[$procedure])) { + return $this->executeCallback($this->callbacks[$procedure], $params); + } elseif (isset($this->classes[$procedure]) && method_exists($this->classes[$procedure][0], $this->classes[$procedure][1])) { + return $this->executeMethod($this->classes[$procedure][0], $this->classes[$procedure][1], $params); + } + + foreach ($this->instances as $instance) { + if (method_exists($instance, $procedure)) { + return $this->executeMethod($instance, $procedure, $params); + } + } + + throw new BadFunctionCallException('Unable to find the procedure'); + } + + /** + * Execute a callback + * + * @access public + * @param Closure $callback Callback + * @param array $params Procedure params + * @return mixed + */ + public function executeCallback(Closure $callback, $params) + { + $reflection = new ReflectionFunction($callback); + + $arguments = $this->getArguments( + $params, + $reflection->getParameters(), + $reflection->getNumberOfRequiredParameters(), + $reflection->getNumberOfParameters() + ); + + return $reflection->invokeArgs($arguments); + } + + /** + * Execute a method + * + * @access public + * @param mixed $class Class name or instance + * @param string $method Method name + * @param array $params Procedure params + * @return mixed + */ + public function executeMethod($class, $method, $params) + { + $instance = is_string($class) ? new $class : $class; + $reflection = new ReflectionMethod($class, $method); + + $this->executeBeforeMethod($instance, $method); + + $arguments = $this->getArguments( + $params, + $reflection->getParameters(), + $reflection->getNumberOfRequiredParameters(), + $reflection->getNumberOfParameters() + ); + + return $reflection->invokeArgs($instance, $arguments); + } + + /** + * Execute before method if defined + * + * @access public + * @param mixed $object + * @param string $method + */ + public function executeBeforeMethod($object, $method) + { + if ($this->beforeMethodName !== '' && method_exists($object, $this->beforeMethodName)) { + call_user_func_array(array($object, $this->beforeMethodName), array($method)); + } + } + + /** + * Get procedure arguments + * + * @access public + * @param array $requestParams Incoming arguments + * @param array $methodParams Procedure arguments + * @param integer $nbRequiredParams Number of required parameters + * @param integer $nbMaxParams Maximum number of parameters + * @return array + */ + public function getArguments(array $requestParams, array $methodParams, $nbRequiredParams, $nbMaxParams) + { + $nbParams = count($requestParams); + + if ($nbParams < $nbRequiredParams) { + throw new InvalidArgumentException('Wrong number of arguments'); + } + + if ($nbParams > $nbMaxParams) { + throw new InvalidArgumentException('Too many arguments'); + } + + if ($this->isPositionalArguments($requestParams)) { + return $requestParams; + } + + return $this->getNamedArguments($requestParams, $methodParams); + } + + /** + * Return true if we have positional parameters + * + * @access public + * @param array $request_params Incoming arguments + * @return bool + */ + public function isPositionalArguments(array $request_params) + { + return array_keys($request_params) === range(0, count($request_params) - 1); + } + + /** + * Get named arguments + * + * @access public + * @param array $requestParams Incoming arguments + * @param array $methodParams Procedure arguments + * @return array + */ + public function getNamedArguments(array $requestParams, array $methodParams) + { + $params = array(); + + foreach ($methodParams as $p) { + $name = $p->getName(); + + if (array_key_exists($name, $requestParams)) { + $params[$name] = $requestParams[$name]; + } elseif ($p->isDefaultValueAvailable()) { + $params[$name] = $p->getDefaultValue(); + } else { + throw new InvalidArgumentException('Missing argument: '.$name); + } + } + + return $params; + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Request/BatchRequestParser.php b/libs/jsonrpc/src/JsonRPC/Request/BatchRequestParser.php new file mode 100644 index 00000000..c0fc776e --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Request/BatchRequestParser.php @@ -0,0 +1,55 @@ +<?php + +namespace JsonRPC\Request; + +/** + * Class BatchRequestParser + * + * @package JsonRPC\Request + * @author Frederic Guillot + */ +class BatchRequestParser extends RequestParser +{ + /** + * Parse incoming request + * + * @access public + * @return string + */ + public function parse() + { + $responses = array(); + + foreach ($this->payload as $payload) { + $responses[] = RequestParser::create() + ->withPayload($payload) + ->withProcedureHandler($this->procedureHandler) + ->withMiddlewareHandler($this->middlewareHandler) + ->withLocalException($this->localExceptions) + ->parse(); + } + + $responses = array_filter($responses); + return empty($responses) ? '' : '['.implode(',', $responses).']'; + } + + /** + * Return true if we have a batch request + * + * ex : [ + * 0 => '...', + * 1 => '...', + * 2 => '...', + * 3 => '...', + * ] + * + * @static + * @access public + * @param array $payload + * @return bool + */ + public static function isBatchRequest(array $payload) + { + return array_keys($payload) === range(0, count($payload) - 1); + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Request/RequestBuilder.php b/libs/jsonrpc/src/JsonRPC/Request/RequestBuilder.php new file mode 100644 index 00000000..145d21c1 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Request/RequestBuilder.php @@ -0,0 +1,129 @@ +<?php + +namespace JsonRPC\Request; + +/** + * Class RequestBuilder + * + * @package JsonRPC\Request + * @author Frederic Guillot + */ +class RequestBuilder +{ + /** + * Request ID + * + * @access private + * @var mixed + */ + private $id = null; + + /** + * Method name + * + * @access private + * @var string + */ + private $procedure = ''; + + /** + * Method arguments + * + * @access private + * @var array + */ + private $params = array(); + + /** + * Additional request attributes + * + * @access private + * @var array + */ + private $reqattrs = array(); + + /** + * Get new object instance + * + * @static + * @access public + * @return RequestBuilder + */ + public static function create() + { + return new static(); + } + + /** + * Set id + * + * @access public + * @param null $id + * @return RequestBuilder + */ + public function withId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set method + * + * @access public + * @param string $procedure + * @return RequestBuilder + */ + public function withProcedure($procedure) + { + $this->procedure = $procedure; + return $this; + } + + /** + * Set parameters + * + * @access public + * @param array $params + * @return RequestBuilder + */ + public function withParams(array $params) + { + $this->params = $params; + return $this; + } + + /** + * Set additional request attributes + * + * @access public + * @param array $reqattrs + * @return RequestBuilder + */ + public function withRequestAttributes(array $reqattrs) + { + $this->reqattrs = $reqattrs; + return $this; + } + + /** + * Build the payload + * + * @access public + * @return string + */ + public function build() + { + $payload = array_merge_recursive($this->reqattrs, array( + 'jsonrpc' => '2.0', + 'method' => $this->procedure, + 'id' => $this->id ?: mt_rand(), + )); + + if (! empty($this->params)) { + $payload['params'] = $this->params; + } + + return json_encode($payload); + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Request/RequestParser.php b/libs/jsonrpc/src/JsonRPC/Request/RequestParser.php new file mode 100644 index 00000000..ea1b7d43 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Request/RequestParser.php @@ -0,0 +1,200 @@ +<?php + +namespace JsonRPC\Request; + +use Exception; +use JsonRPC\Exception\AccessDeniedException; +use JsonRPC\Exception\AuthenticationFailureException; +use JsonRPC\Exception\InvalidJsonRpcFormatException; +use JsonRPC\MiddlewareHandler; +use JsonRPC\ProcedureHandler; +use JsonRPC\Response\ResponseBuilder; +use JsonRPC\Validator\JsonFormatValidator; +use JsonRPC\Validator\RpcFormatValidator; + +/** + * Class RequestParser + * + * @package JsonRPC + * @author Frederic Guillot + */ +class RequestParser +{ + /** + * Request payload + * + * @access protected + * @var mixed + */ + protected $payload; + + /** + * List of exceptions that should not be relayed to the client + * + * @access protected + * @var array() + */ + protected $localExceptions = array( + 'JsonRPC\Exception\AuthenticationFailureException', + 'JsonRPC\Exception\AccessDeniedException', + ); + + /** + * ProcedureHandler + * + * @access protected + * @var ProcedureHandler + */ + protected $procedureHandler; + + /** + * MiddlewareHandler + * + * @access protected + * @var MiddlewareHandler + */ + protected $middlewareHandler; + + /** + * Get new object instance + * + * @static + * @access public + * @return RequestParser + */ + public static function create() + { + return new static(); + } + + /** + * Set payload + * + * @access public + * @param mixed $payload + * @return $this + */ + public function withPayload($payload) + { + $this->payload = $payload; + return $this; + } + + /** + * Exception classes that should not be relayed to the client + * + * @access public + * @param mixed $exception + * @return $this + */ + public function withLocalException($exception) + { + if (is_array($exception)) { + $this->localExceptions = array_merge($this->localExceptions, $exception); + } else { + $this->localExceptions[] = $exception; + } + + return $this; + } + + /** + * Set procedure handler + * + * @access public + * @param ProcedureHandler $procedureHandler + * @return $this + */ + public function withProcedureHandler(ProcedureHandler $procedureHandler) + { + $this->procedureHandler = $procedureHandler; + return $this; + } + + /** + * Set middleware handler + * + * @access public + * @param MiddlewareHandler $middlewareHandler + * @return $this + */ + public function withMiddlewareHandler(MiddlewareHandler $middlewareHandler) + { + $this->middlewareHandler = $middlewareHandler; + return $this; + } + + /** + * Parse incoming request + * + * @access public + * @return string + * @throws AccessDeniedException + * @throws AuthenticationFailureException + */ + public function parse() + { + try { + + JsonFormatValidator::validate($this->payload); + RpcFormatValidator::validate($this->payload); + + $this->middlewareHandler + ->withProcedure($this->payload['method']) + ->execute(); + + $result = $this->procedureHandler->executeProcedure( + $this->payload['method'], + empty($this->payload['params']) ? array() : $this->payload['params'] + ); + + if (! $this->isNotification()) { + return ResponseBuilder::create() + ->withId($this->payload['id']) + ->withResult($result) + ->build(); + } + } catch (Exception $e) { + return $this->handleExceptions($e); + } + + return ''; + } + + /** + * Handle exceptions + * + * @access protected + * @param Exception $e + * @return string + * @throws Exception + */ + protected function handleExceptions(Exception $e) + { + foreach ($this->localExceptions as $exception) { + if ($e instanceof $exception) { + throw $e; + } + } + + if ($e instanceof InvalidJsonRpcFormatException || ! $this->isNotification()) { + return ResponseBuilder::create() + ->withId(isset($this->payload['id']) ? $this->payload['id'] : null) + ->withException($e) + ->build(); + } + + return ''; + } + + /** + * Return true if the message is a notification + * + * @access protected + * @return bool + */ + protected function isNotification() + { + return is_array($this->payload) && !isset($this->payload['id']); + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Response/ResponseBuilder.php b/libs/jsonrpc/src/JsonRPC/Response/ResponseBuilder.php new file mode 100644 index 00000000..a0348ce3 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Response/ResponseBuilder.php @@ -0,0 +1,336 @@ +<?php + +namespace JsonRPC\Response; + +use BadFunctionCallException; +use Exception; +use InvalidArgumentException; +use JsonRPC\Exception\AccessDeniedException; +use JsonRPC\Exception\AuthenticationFailureException; +use JsonRPC\Exception\InvalidJsonFormatException; +use JsonRPC\Exception\InvalidJsonRpcFormatException; +use JsonRPC\Exception\ResponseEncodingFailureException; +use JsonRPC\Exception\ResponseException; +use JsonRPC\Validator\JsonEncodingValidator; + +/** + * Class ResponseBuilder + * + * @package JsonRPC + * @author Frederic Guillot + */ +class ResponseBuilder +{ + /** + * Payload ID + * + * @access protected + * @var mixed + */ + protected $id; + + /** + * Payload ID + * + * @access protected + * @var mixed + */ + protected $result; + + /** + * Payload error code + * + * @access protected + * @var integer + */ + protected $errorCode; + + /** + * Payload error message + * + * @access private + * @var string + */ + protected $errorMessage; + + /** + * Payload error data + * + * @access protected + * @var mixed + */ + protected $errorData; + + /** + * HTTP Headers + * + * @access protected + * @var array + */ + protected $headers = array( + 'Content-Type' => 'application/json', + ); + + /** + * HTTP status + * + * @access protected + * @var string + */ + protected $status; + + /** + * Exception + * + * @access protected + * @var ResponseException + */ + protected $exception; + + /** + * Get new object instance + * + * @static + * @access public + * @return ResponseBuilder + */ + public static function create() + { + return new static(); + } + + /** + * Set id + * + * @access public + * @param mixed $id + * @return $this + */ + public function withId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set result + * + * @access public + * @param mixed $result + * @return $this + */ + public function withResult($result) + { + $this->result = $result; + return $this; + } + + /** + * Set error + * + * @access public + * @param integer $code + * @param string $message + * @param string $data + * @return $this + */ + public function withError($code, $message, $data = '') + { + $this->errorCode = $code; + $this->errorMessage = $message; + $this->errorData = $data; + return $this; + } + + /** + * Set exception + * + * @access public + * @param Exception $exception + * @return $this + */ + public function withException(Exception $exception) + { + $this->exception = $exception; + return $this; + } + + /** + * Add HTTP header + * + * @access public + * @param string $name + * @param string $value + * @return $this + */ + public function withHeader($name, $value) + { + $this->headers[$name] = $value; + return $this; + } + + /** + * Add HTTP Status + * + * @access public + * @param string $status + * @return $this + */ + public function withStatus($status) + { + $this->status = $status; + return $this; + } + + /** + * Get status + * + * @access public + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * Get headers + * + * @access public + * @return string[] + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Build response + * + * @access public + * @return string + */ + public function build() + { + $options = 0; + if (defined('JSON_UNESCAPED_SLASHES')) { + $options |= JSON_UNESCAPED_SLASHES; + } + if (defined('JSON_UNESCAPED_UNICODE')) { + $options |= JSON_UNESCAPED_UNICODE; + } + $encodedResponse = json_encode($this->buildResponse(), $options); + JsonEncodingValidator::validate(); + + return $encodedResponse; + } + + /** + * Send HTTP headers + * + * @access public + * @return $this + */ + public function sendHeaders() + { + if (! empty($this->status)) { + header($this->status); + } + + foreach ($this->headers as $name => $value) { + header($name.': '.$value); + } + + return $this; + } + + /** + * Build response payload + * + * @access protected + * @return array + */ + protected function buildResponse() + { + $response = array('jsonrpc' => '2.0'); + $this->handleExceptions(); + + if (! empty($this->errorMessage)) { + $response['error'] = $this->buildErrorResponse(); + } else { + $response['result'] = $this->result; + } + + $response['id'] = $this->id; + return $response; + } + + /** + * Build response error payload + * + * @access protected + * @return array + */ + protected function buildErrorResponse() + { + $response = array( + 'code' => $this->errorCode, + 'message' => $this->errorMessage, + ); + + if (! empty($this->errorData)) { + $response['data'] = $this->errorData; + } + + return $response; + } + + /** + * Transform exceptions to JSON-RPC errors + * + * @access protected + */ + protected function handleExceptions() + { + try { + if ($this->exception instanceof Exception) { + throw $this->exception; + } + } catch (InvalidJsonFormatException $e) { + $this->errorCode = -32700; + $this->errorMessage = 'Parse error'; + $this->id = null; + } catch (InvalidJsonRpcFormatException $e) { + $this->errorCode = -32600; + $this->errorMessage = 'Invalid Request'; + $this->id = null; + } catch (BadFunctionCallException $e) { + $this->errorCode = -32601; + $this->errorMessage = 'Method not found'; + } catch (InvalidArgumentException $e) { + $this->errorCode = -32602; + $this->errorMessage = 'Invalid params'; + $this->errorData = $this->exception->getMessage(); + } catch (ResponseEncodingFailureException $e) { + $this->errorCode = -32603; + $this->errorMessage = 'Internal error'; + $this->errorData = $this->exception->getMessage(); + } catch (AuthenticationFailureException $e) { + $this->errorCode = 401; + $this->errorMessage = 'Unauthorized'; + $this->status = 'HTTP/1.0 401 Unauthorized'; + $this->withHeader('WWW-Authenticate', 'Basic realm="JsonRPC"'); + } catch (AccessDeniedException $e) { + $this->errorCode = 403; + $this->errorMessage = 'Forbidden'; + $this->status = 'HTTP/1.0 403 Forbidden'; + } catch (ResponseException $e) { + $this->errorCode = $this->exception->getCode(); + $this->errorMessage = $this->exception->getMessage(); + $this->errorData = $this->exception->getData(); + } catch (Exception $e) { + $this->errorCode = $this->exception->getCode(); + $this->errorMessage = $this->exception->getMessage(); + } + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Response/ResponseParser.php b/libs/jsonrpc/src/JsonRPC/Response/ResponseParser.php new file mode 100644 index 00000000..02d449ba --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Response/ResponseParser.php @@ -0,0 +1,154 @@ +<?php + +namespace JsonRPC\Response; + +use BadFunctionCallException; +use InvalidArgumentException; +use Exception; +use JsonRPC\Exception\InvalidJsonFormatException; +use JsonRPC\Exception\InvalidJsonRpcFormatException; +use JsonRPC\Exception\ResponseException; +use JsonRPC\Validator\JsonFormatValidator; + +/** + * Class ResponseParser + * + * @package JsonRPC\Request + * @author Frederic Guillot + */ +class ResponseParser +{ + /** + * Payload + * + * @access private + * @var mixed + */ + private $payload; + + /** + * Do not immediately throw an exception on error. Return it instead. + * + * @var bool + */ + private $returnException = false; + + /** + * Get new object instance + * + * @static + * @access public + * @return ResponseParser + */ + public static function create() + { + return new static(); + } + + /** + * Set Return Exception Or Throw It + * + * @param $returnException + * @return ResponseParser + */ + public function withReturnException($returnException) + { + $this->returnException = $returnException; + return $this; + } + + /** + * Set payload + * + * @access public + * @param mixed $payload + * @return $this + */ + public function withPayload($payload) + { + $this->payload = $payload; + return $this; + } + + /** + * Parse response + * + * @return array|Exception|null + * @throws InvalidJsonFormatException + * @throws BadFunctionCallException + * @throws InvalidJsonRpcFormatException + * @throws InvalidArgumentException + * @throws Exception + * @throws ResponseException + */ + public function parse() + { + JsonFormatValidator::validate($this->payload); + + if ($this->isBatchResponse()) { + $results = array(); + + foreach ($this->payload as $response) { + $results[] = self::create() + ->withReturnException($this->returnException) + ->withPayload($response) + ->parse(); + } + + return $results; + } + + if (isset($this->payload['error']['code'])) { + try { + $this->handleExceptions(); + } catch (Exception $e) { + if ($this->returnException) { + return $e; + } + throw $e; + } + } + + return isset($this->payload['result']) ? $this->payload['result'] : null; + } + + /** + * Handle exceptions + * + * @access private + * @throws InvalidJsonFormatException + * @throws InvalidJsonRpcFormatException + * @throws ResponseException + */ + private function handleExceptions() + { + switch ($this->payload['error']['code']) { + case -32700: + throw new InvalidJsonFormatException('Parse error: '.$this->payload['error']['message']); + case -32600: + throw new InvalidJsonRpcFormatException('Invalid Request: '.$this->payload['error']['message']); + case -32601: + throw new BadFunctionCallException('Procedure not found: '.$this->payload['error']['message']); + case -32602: + throw new InvalidArgumentException('Invalid arguments: '.$this->payload['error']['message']); + default: + throw new ResponseException( + $this->payload['error']['message'], + $this->payload['error']['code'], + null, + isset($this->payload['error']['data']) ? $this->payload['error']['data'] : null + ); + } + } + + /** + * Return true if we have a batch response + * + * @access private + * @return boolean + */ + private function isBatchResponse() + { + return array_keys($this->payload) === range(0, count($this->payload) - 1); + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Server.php b/libs/jsonrpc/src/JsonRPC/Server.php new file mode 100644 index 00000000..1ed075a4 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Server.php @@ -0,0 +1,386 @@ +<?php + +namespace JsonRPC; + +use Closure; +use Exception; +use JsonRPC\Request\BatchRequestParser; +use JsonRPC\Request\RequestParser; +use JsonRPC\Response\ResponseBuilder; +use JsonRPC\Validator\HostValidator; +use JsonRPC\Validator\JsonFormatValidator; +use JsonRPC\Validator\UserValidator; + +/** + * JsonRPC server class + * + * @package JsonRPC + * @author Frederic Guillot + */ +class Server +{ + /** + * Allowed hosts + * + * @access protected + * @var array + */ + protected $hosts = array(); + + /** + * Data received from the client + * + * @access protected + * @var array + */ + protected $payload = array(); + + /** + * List of exceptions that should not be relayed to the client + * + * @access protected + * @var array() + */ + protected $localExceptions = array(); + + /** + * Username + * + * @access protected + * @var string + */ + protected $username = ''; + + /** + * Password + * + * @access protected + * @var string + */ + protected $password = ''; + + /** + * Allowed users + * + * @access protected + * @var array + */ + protected $users = array(); + + /** + * $_SERVER + * + * @access protected + * @var array + */ + protected $serverVariable; + + /** + * ProcedureHandler object + * + * @access protected + * @var ProcedureHandler + */ + protected $procedureHandler; + + /** + * MiddlewareHandler object + * + * @access protected + * @var MiddlewareHandler + */ + protected $middlewareHandler; + + /** + * Response builder + * + * @access protected + * @var ResponseBuilder + */ + protected $responseBuilder; + + /** + * Response builder + * + * @access protected + * @var RequestParser + */ + protected $requestParser; + + /** + * + * Batch request parser + * + * @access protected + * @var BatchRequestParser + */ + protected $batchRequestParser; + + /** + * Constructor + * + * @access public + * @param string $request + * @param array $server + * @param ResponseBuilder $responseBuilder + * @param RequestParser $requestParser + * @param BatchRequestParser $batchRequestParser + * @param ProcedureHandler $procedureHandler + * @param MiddlewareHandler $middlewareHandler + */ + public function __construct( + $request = '', + array $server = array(), + ResponseBuilder $responseBuilder = null, + RequestParser $requestParser = null, + BatchRequestParser $batchRequestParser = null, + ProcedureHandler $procedureHandler = null, + MiddlewareHandler $middlewareHandler = null + ) { + if ($request !== '') { + $this->payload = json_decode($request, true); + } else { + $this->payload = json_decode(file_get_contents('php://input'), true); + } + + $this->serverVariable = $server ?: $_SERVER; + $this->responseBuilder = $responseBuilder ?: ResponseBuilder::create(); + $this->requestParser = $requestParser ?: RequestParser::create(); + $this->batchRequestParser = $batchRequestParser ?: BatchRequestParser::create(); + $this->procedureHandler = $procedureHandler ?: new ProcedureHandler(); + $this->middlewareHandler = $middlewareHandler ?: new MiddlewareHandler(); + } + + /** + * Define alternative authentication header + * + * @access public + * @param string $header Header name + * @return $this + */ + public function setAuthenticationHeader($header) + { + if (! empty($header)) { + $header = 'HTTP_'.str_replace('-', '_', strtoupper($header)); + $value = $this->getServerVariable($header); + + if (! empty($value)) { + list($this->username, $this->password) = explode(':', base64_decode($value)); + } + } + + return $this; + } + + /** + * Get ProcedureHandler + * + * @access public + * @return ProcedureHandler + */ + public function getProcedureHandler() + { + return $this->procedureHandler; + } + + /** + * Get MiddlewareHandler + * + * @access public + * @return MiddlewareHandler + */ + public function getMiddlewareHandler() + { + return $this->middlewareHandler; + } + + /** + * Get username + * + * @access public + * @return string + */ + public function getUsername() + { + return $this->username ?: $this->getServerVariable('PHP_AUTH_USER'); + } + + /** + * Get password + * + * @access public + * @return string + */ + public function getPassword() + { + return $this->password ?: $this->getServerVariable('PHP_AUTH_PW'); + } + + /** + * IP based client restrictions + * + * @access public + * @param array $hosts List of hosts + * @return $this + */ + public function allowHosts(array $hosts) + { + $this->hosts = $hosts; + return $this; + } + + /** + * HTTP Basic authentication + * + * @access public + * @param array $users Dictionary of username/password + * @return $this + */ + public function authentication(array $users) + { + $this->users = $users; + return $this; + } + + /** + * Register a new procedure + * + * @access public + * @deprecated Use $server->getProcedureHandler()->withCallback($procedure, $callback) + * @param string $procedure Procedure name + * @param closure $callback Callback + * @return $this + */ + public function register($procedure, Closure $callback) + { + $this->procedureHandler->withCallback($procedure, $callback); + return $this; + } + + /** + * Bind a procedure to a class + * + * @access public + * @deprecated Use $server->getProcedureHandler()->withClassAndMethod($procedure, $class, $method); + * @param string $procedure Procedure name + * @param mixed $class Class name or instance + * @param string $method Procedure name + * @return $this + */ + public function bind($procedure, $class, $method = '') + { + $this->procedureHandler->withClassAndMethod($procedure, $class, $method); + return $this; + } + + /** + * Bind a class instance + * + * @access public + * @deprecated Use $server->getProcedureHandler()->withObject($instance); + * @param mixed $instance Instance name + * @return $this + */ + public function attach($instance) + { + $this->procedureHandler->withObject($instance); + return $this; + } + + /** + * Exception classes that should not be relayed to the client + * + * @access public + * @param Exception|string $exception + * @return $this + */ + public function withLocalException($exception) + { + $this->localExceptions[] = $exception; + return $this; + } + + /** + * Parse incoming requests + * + * @access public + * @return string + */ + public function execute() + { + try { + JsonFormatValidator::validate($this->payload); + HostValidator::validate($this->hosts, $this->getServerVariable('REMOTE_ADDR')); + UserValidator::validate($this->users, $this->getUsername(), $this->getPassword()); + + $this->middlewareHandler + ->withUsername($this->getUsername()) + ->withPassword($this->getPassword()) + ; + + $response = $this->parseRequest(); + + } catch (Exception $e) { + $response = $this->handleExceptions($e); + } + + $this->responseBuilder->sendHeaders(); + return $response; + } + + /** + * Handle exceptions + * + * @access protected + * @param Exception $e + * @return string + * @throws Exception + */ + protected function handleExceptions(Exception $e) + { + foreach ($this->localExceptions as $exception) { + if ($e instanceof $exception) { + throw $e; + } + } + + return $this->responseBuilder->withException($e)->build(); + } + + /** + * Parse incoming request + * + * @access protected + * @return string + */ + protected function parseRequest() + { + if (BatchRequestParser::isBatchRequest($this->payload)) { + return $this->batchRequestParser + ->withPayload($this->payload) + ->withProcedureHandler($this->procedureHandler) + ->withMiddlewareHandler($this->middlewareHandler) + ->withLocalException($this->localExceptions) + ->parse(); + } + + return $this->requestParser + ->withPayload($this->payload) + ->withProcedureHandler($this->procedureHandler) + ->withMiddlewareHandler($this->middlewareHandler) + ->withLocalException($this->localExceptions) + ->parse(); + } + + /** + * Check existence and get value of server variable + * + * @access protected + * @param string $variable + * @return string|null + */ + protected function getServerVariable($variable) + { + return isset($this->serverVariable[$variable]) ? $this->serverVariable[$variable] : null; + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Validator/HostValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/HostValidator.php new file mode 100644 index 00000000..3f9d6989 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Validator/HostValidator.php @@ -0,0 +1,73 @@ +<?php + +namespace JsonRPC\Validator; + +use JsonRPC\Exception\AccessDeniedException; + +/** + * Class HostValidator + * + * @package JsonRPC\Validator + * @author Frederic Guillot + */ +class HostValidator +{ + /** + * Validate + * + * @static + * @access public + * @param array $hosts + * @param string $remoteAddress + * @throws AccessDeniedException + */ + public static function validate(array $hosts, $remoteAddress) + { + if (!empty($hosts)) { + foreach ($hosts as $host) { + if (self::ipMatch($remoteAddress, $host)) { + return; + } + } + throw new AccessDeniedException('Access Forbidden'); + } + } + + /** + * Validate remoteAddress match host + * @param $remoteAddress + * @param $host + * @return bool + */ + public static function ipMatch($remoteAddress, $host) + { + $host = trim($host); + if (strpos($host, '/') !== false) { + list($network, $mask) = explode('/', $host); + if (self::netMatch($remoteAddress, $network, $mask)) { + return true; + } + } + + if ($host === $remoteAddress) { + return true; + } + + return false; + } + + /** + * validate the ipAddress in network + * 192.168.1.1/24 + * @param $clientIp + * @param $networkIp + * @param $mask + * + * @return bool + */ + public static function netMatch($clientIp, $networkIp, $mask) + { + $mask1 = 32 - $mask; + return ((ip2long($clientIp) >> $mask1) == (ip2long($networkIp) >> $mask1)); + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Validator/JsonEncodingValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/JsonEncodingValidator.php new file mode 100644 index 00000000..0bbc4abd --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Validator/JsonEncodingValidator.php @@ -0,0 +1,44 @@ +<?php + +namespace JsonRPC\Validator; + +use JsonRPC\Exception\ResponseEncodingFailureException; + +/** + * Class JsonEncodingValidator + * + * @package JsonRPC\Validator + * @author Frederic Guillot + */ +class JsonEncodingValidator +{ + public static function validate() + { + $jsonError = json_last_error(); + + if ($jsonError !== JSON_ERROR_NONE) { + switch ($jsonError) { + case JSON_ERROR_DEPTH: + $errorMessage = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $errorMessage = 'Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $errorMessage = 'Unexpected control character found'; + break; + case JSON_ERROR_SYNTAX: + $errorMessage = 'Syntax error, malformed JSON'; + break; + case JSON_ERROR_UTF8: + $errorMessage = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $errorMessage = 'Unknown error'; + break; + } + + throw new ResponseEncodingFailureException($errorMessage, $jsonError); + } + } +} diff --git a/libs/jsonrpc/src/JsonRPC/Validator/JsonFormatValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/JsonFormatValidator.php new file mode 100644 index 00000000..ca8e7a69 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Validator/JsonFormatValidator.php @@ -0,0 +1,30 @@ +<?php + +namespace JsonRPC\Validator; + +use JsonRPC\Exception\InvalidJsonFormatException; + +/** + * Class JsonFormatValidator + * + * @package JsonRPC\Validator + * @author Frederic Guillot + */ +class JsonFormatValidator +{ + /** + * Validate + * + * @static + * @access public + * @param mixed $payload + * @throws InvalidJsonFormatException + */ + public static function validate($payload) + { + if (! is_array($payload)) { + throw new InvalidJsonFormatException('Malformed payload'); + } + } +} + diff --git a/libs/jsonrpc/src/JsonRPC/Validator/RpcFormatValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/RpcFormatValidator.php new file mode 100644 index 00000000..f253a5a1 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Validator/RpcFormatValidator.php @@ -0,0 +1,35 @@ +<?php + +namespace JsonRPC\Validator; + +use JsonRPC\Exception\InvalidJsonRpcFormatException; + +/** + * Class RpcFormatValidator + * + * @package JsonRPC\Validator + * @author Frederic Guillot + */ +class RpcFormatValidator +{ + /** + * Validate + * + * @static + * @access public + * @param array $payload + * @throws InvalidJsonRpcFormatException + */ + public static function validate(array $payload) + { + if (! isset($payload['jsonrpc']) || + ! isset($payload['method']) || + ! is_string($payload['method']) || + $payload['jsonrpc'] !== '2.0' || + (isset($payload['params']) && ! is_array($payload['params']))) { + + throw new InvalidJsonRpcFormatException('Invalid JSON RPC payload'); + } + } +} + diff --git a/libs/jsonrpc/src/JsonRPC/Validator/UserValidator.php b/libs/jsonrpc/src/JsonRPC/Validator/UserValidator.php new file mode 100644 index 00000000..4f889719 --- /dev/null +++ b/libs/jsonrpc/src/JsonRPC/Validator/UserValidator.php @@ -0,0 +1,21 @@ +<?php + +namespace JsonRPC\Validator; + +use JsonRPC\Exception\AuthenticationFailureException; + +/** + * Class UserValidator + * + * @package JsonRPC\Validator + * @author Frederic Guillot + */ +class UserValidator +{ + public static function validate(array $users, $username, $password) + { + if (! empty($users) && (! isset($users[$username]) || $users[$username] !== $password)) { + throw new AuthenticationFailureException('Access not allowed'); + } + } +} |