├── README.markdown ├── src └── JsonRPC │ ├── Client.php │ └── Server.php └── tests └── ServerTest.php /README.markdown: -------------------------------------------------------------------------------- 1 | JsonRPC - PHP Client and Server 2 | =============================== 3 | 4 | A simple Json-RPC client/server that just works. 5 | There is only 2 files. 6 | 7 | Features 8 | -------- 9 | 10 | - JSON-RPC 2.0 protocol only 11 | - The server support batch requests and notifications 12 | - Authentication and IP based client restrictions 13 | - License: Unlicense http://unlicense.org/ 14 | 15 | Requirements 16 | ------------ 17 | 18 | - The only dependency is curl and Reflection classes 19 | - Works only with PHP >= 5.3 20 | 21 | Examples 22 | -------- 23 | 24 | ### Server 25 | 26 | register('addition', function ($a, $b) { 37 | 38 | return $a + $b; 39 | }); 40 | 41 | $server->register('random', function ($start, $end) { 42 | 43 | return mt_rand($start, $end); 44 | }); 45 | 46 | // Return the response to the client 47 | echo $server->execute(); 48 | 49 | ### Client 50 | 51 | Example with positional parameters: 52 | 53 | execute('addition', array(3, 5)); 61 | 62 | var_dump($result); 63 | 64 | Example with named arguments: 65 | 66 | execute('random', array('end' => 10, 'start' => 1)); 74 | 75 | var_dump($result); 76 | 77 | Arguments are called in the right order. 78 | If there is an error, the `execute()` method return `NULL`. 79 | 80 | ### IP based client restrictions 81 | 82 | The server can allow only some IP adresses: 83 | 84 | allowHosts(array('192.168.0.1', '127.0.0.1')); 94 | 95 | // Procedures registration 96 | 97 | [...] 98 | 99 | // Return the response to the client 100 | echo $server->execute(); 101 | 102 | If the client is blocked, you got a 403 Forbidden HTTP response. 103 | 104 | ### HTTP Basic Authentication 105 | 106 | If you use HTTPS, you can allow client by using a username/password. 107 | 108 | authentication(array('jsonrpc' => 'toto')); 118 | 119 | // Procedures registration 120 | 121 | [...] 122 | 123 | // Return the response to the client 124 | echo $server->execute(); 125 | 126 | On the client, set the credentials like that: 127 | 128 | authentication('jsonrpc', 'toto'); 138 | 139 | $result = $client->execute('addition', array('a' => 2, 'b' => 2)); -------------------------------------------------------------------------------- /src/JsonRPC/Client.php: -------------------------------------------------------------------------------- 1 | url = $url; 23 | $this->timeout = $timeout; 24 | $this->debug = $debug; 25 | $this->headers = array_merge($this->headers, $headers); 26 | } 27 | 28 | 29 | public function authentication($username, $password) 30 | { 31 | $this->username = $username; 32 | $this->password = $password; 33 | } 34 | 35 | 36 | public function execute($procedure, array $params = array()) 37 | { 38 | $id = mt_rand(); 39 | 40 | $payload = array( 41 | 'jsonrpc' => '2.0', 42 | 'method' => $procedure, 43 | 'id' => $id 44 | ); 45 | 46 | if (! empty($params)) { 47 | 48 | $payload['params'] = $params; 49 | } 50 | 51 | $result = $this->doRequest($payload); 52 | 53 | if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) { 54 | 55 | return $result['result']; 56 | } 57 | else if ($this->debug && isset($result['error'])) { 58 | 59 | print_r($result['error']); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | 66 | public function doRequest($payload) 67 | { 68 | $ch = curl_init(); 69 | 70 | curl_setopt($ch, CURLOPT_URL, $this->url); 71 | curl_setopt($ch, CURLOPT_HEADER, false); 72 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 73 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); 74 | curl_setopt($ch, CURLOPT_USERAGENT, 'JSON-RPC PHP Client'); 75 | curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); 76 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); 77 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); 78 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); 79 | 80 | if ($this->username && $this->password) { 81 | 82 | curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password); 83 | } 84 | 85 | $result = curl_exec($ch); 86 | $response = json_decode($result, true); 87 | 88 | curl_close($ch); 89 | 90 | return is_array($response) ? $response : array(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/JsonRPC/Server.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 14 | } 15 | 16 | 17 | public function allowHosts(array $hosts) { 18 | 19 | if (! in_array($_SERVER['REMOTE_ADDR'], $hosts)) { 20 | 21 | header('Content-Type: application/json'); 22 | header('HTTP/1.0 403 Forbidden'); 23 | echo '["Access Forbidden"]'; 24 | exit; 25 | } 26 | } 27 | 28 | 29 | public function authentication(array $users) 30 | { 31 | // OVH workaround 32 | if (isset($_SERVER['REMOTE_USER'])) { 33 | 34 | list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', base64_decode(substr($_SERVER['REMOTE_USER'], 6))); 35 | } 36 | 37 | if (! isset($_SERVER['PHP_AUTH_USER']) || 38 | ! isset($users[$_SERVER['PHP_AUTH_USER']]) || 39 | $users[$_SERVER['PHP_AUTH_USER']] !== $_SERVER['PHP_AUTH_PW']) { 40 | 41 | header('WWW-Authenticate: Basic realm="JsonRPC"'); 42 | header('Content-Type: application/json'); 43 | header('HTTP/1.0 401 Unauthorized'); 44 | echo '["Authentication failed"]'; 45 | exit; 46 | } 47 | } 48 | 49 | 50 | public function register($name, \Closure $callback) 51 | { 52 | self::$procedures[$name] = $callback; 53 | } 54 | 55 | 56 | public function unregister($name) 57 | { 58 | if (isset(self::$procedures[$name])) { 59 | 60 | unset(self::$procedures[$name]); 61 | } 62 | } 63 | 64 | 65 | public function unregisterAll() 66 | { 67 | self::$procedures = array(); 68 | } 69 | 70 | 71 | public function getResponse(array $data, array $payload = array()) 72 | { 73 | if (! array_key_exists('id', $payload)) { 74 | 75 | return ''; 76 | } 77 | 78 | $response = array( 79 | 'jsonrpc' => '2.0', 80 | 'id' => $payload['id'] 81 | ); 82 | 83 | $response = array_merge($response, $data); 84 | 85 | header('Content-Type: application/json'); 86 | return json_encode($response); 87 | } 88 | 89 | 90 | public function mapParameters(array $request_params, array $method_params, array &$params) 91 | { 92 | // Positional parameters 93 | if (array_keys($request_params) === range(0, count($request_params) - 1)) { 94 | 95 | if (count($request_params) !== count($method_params)) return false; 96 | $params = $request_params; 97 | 98 | return true; 99 | } 100 | 101 | // Named parameters 102 | foreach ($method_params as $p) { 103 | 104 | $name = $p->getName(); 105 | 106 | if (isset($request_params[$name])) { 107 | 108 | $params[$name] = $request_params[$name]; 109 | } 110 | else { 111 | 112 | return false; 113 | } 114 | } 115 | 116 | return true; 117 | } 118 | 119 | 120 | public function execute() 121 | { 122 | // Parse payload 123 | if (empty($this->payload)) { 124 | 125 | $this->payload = file_get_contents('php://input'); 126 | } 127 | 128 | if (is_string($this->payload)) { 129 | 130 | $this->payload = json_decode($this->payload, true); 131 | } 132 | 133 | // Check JSON format 134 | if (! is_array($this->payload)) { 135 | 136 | return $this->getResponse(array( 137 | 'error' => array( 138 | 'code' => -32700, 139 | 'message' => 'Parse error' 140 | )), 141 | array('id' => null) 142 | ); 143 | } 144 | 145 | // Handle batch request 146 | if (array_keys($this->payload) === range(0, count($this->payload) - 1)) { 147 | 148 | $responses = array(); 149 | 150 | foreach ($this->payload as $payload) { 151 | 152 | if (! is_array($payload)) { 153 | 154 | $responses[] = $this->getResponse(array( 155 | 'error' => array( 156 | 'code' => -32600, 157 | 'message' => 'Invalid Request' 158 | )), 159 | array('id' => null) 160 | ); 161 | } 162 | else { 163 | 164 | $server = new Server($payload); 165 | $response = $server->execute(); 166 | 167 | if ($response) $responses[] = $response; 168 | } 169 | } 170 | 171 | return empty($responses) ? '' : '['.implode(',', $responses).']'; 172 | } 173 | 174 | // Check JSON-RPC format 175 | if (! isset($this->payload['jsonrpc']) || 176 | ! isset($this->payload['method']) || 177 | ! is_string($this->payload['method']) || 178 | $this->payload['jsonrpc'] !== '2.0' || 179 | (isset($this->payload['params']) && ! is_array($this->payload['params']))) { 180 | 181 | return $this->getResponse(array( 182 | 'error' => array( 183 | 'code' => -32600, 184 | 'message' => 'Invalid Request' 185 | )), 186 | array('id' => null) 187 | ); 188 | } 189 | 190 | // Procedure not found 191 | if (! isset(self::$procedures[$this->payload['method']])) { 192 | 193 | return $this->getResponse(array( 194 | 'error' => array( 195 | 'code' => -32601, 196 | 'message' => 'Method not found' 197 | )), 198 | $this->payload 199 | ); 200 | } 201 | 202 | $callback = self::$procedures[$this->payload['method']]; 203 | $params = array(); 204 | 205 | $reflection = new \ReflectionFunction($callback); 206 | 207 | if (isset($this->payload['params'])) { 208 | 209 | $parameters = $reflection->getParameters(); 210 | 211 | if (! $this->mapParameters($this->payload['params'], $parameters, $params)) { 212 | 213 | return $this->getResponse(array( 214 | 'error' => array( 215 | 'code' => -32602, 216 | 'message' => 'Invalid params' 217 | )), 218 | $this->payload 219 | ); 220 | } 221 | } 222 | 223 | $result = $reflection->invokeArgs($params); 224 | 225 | return $this->getResponse(array('result' => $result), $this->payload); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tests/ServerTest.php: -------------------------------------------------------------------------------- 1 | register('subtract', $subtract); 19 | 20 | $this->assertEquals( 21 | json_decode('{"jsonrpc": "2.0", "result": 19, "id": 1}', true), 22 | json_decode($server->execute(), true) 23 | ); 24 | 25 | 26 | $server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 1}'); 27 | $server->register('subtract', $subtract); 28 | 29 | $this->assertEquals( 30 | json_decode('{"jsonrpc": "2.0", "result": -19, "id": 1}', true), 31 | json_decode($server->execute(), true) 32 | ); 33 | } 34 | 35 | 36 | public function testNamedParameters() 37 | { 38 | $subtract = function($minuend, $subtrahend) { 39 | 40 | return $minuend - $subtrahend; 41 | }; 42 | 43 | 44 | $server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}'); 45 | $server->unregisterAll(); 46 | $server->register('subtract', $subtract); 47 | 48 | $this->assertEquals( 49 | json_decode('{"jsonrpc": "2.0", "result": 19, "id": 3}', true), 50 | json_decode($server->execute(), true) 51 | ); 52 | 53 | 54 | $server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}'); 55 | $server->unregisterAll(); 56 | $server->register('subtract', $subtract); 57 | 58 | $this->assertEquals( 59 | json_decode('{"jsonrpc": "2.0", "result": 19, "id": 4}', true), 60 | json_decode($server->execute(), true) 61 | ); 62 | } 63 | 64 | 65 | public function testNotification() 66 | { 67 | $update = function($p1, $p2, $p3, $p4, $p5) {}; 68 | $foobar = function() {}; 69 | 70 | 71 | $server = new Server('{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}'); 72 | $server->unregisterAll(); 73 | $server->register('update', $update); 74 | $server->register('foobar', $foobar); 75 | 76 | $this->assertEquals('', $server->execute()); 77 | 78 | 79 | $server = new Server('{"jsonrpc": "2.0", "method": "foobar"}'); 80 | $server->unregisterAll(); 81 | $server->register('update', $update); 82 | $server->register('foobar', $foobar); 83 | 84 | $this->assertEquals('', $server->execute()); 85 | } 86 | 87 | 88 | public function testNoMethod() 89 | { 90 | $server = new Server('{"jsonrpc": "2.0", "method": "foobar", "id": "1"}'); 91 | $server->unregisterAll(); 92 | 93 | $this->assertEquals( 94 | json_decode('{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}', true), 95 | json_decode($server->execute(), true) 96 | ); 97 | } 98 | 99 | 100 | public function testInvalidJson() 101 | { 102 | $server = new Server('{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]'); 103 | $server->unregisterAll(); 104 | 105 | $this->assertEquals( 106 | json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true), 107 | json_decode($server->execute(), true) 108 | ); 109 | } 110 | 111 | 112 | public function testInvalidRequest() 113 | { 114 | $server = new Server('{"jsonrpc": "2.0", "method": 1, "params": "bar"}'); 115 | $server->unregisterAll(); 116 | 117 | $this->assertEquals( 118 | json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true), 119 | json_decode($server->execute(), true) 120 | ); 121 | } 122 | 123 | 124 | public function testBatchInvalidJson() 125 | { 126 | $server = new Server('[ 127 | {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, 128 | {"jsonrpc": "2.0", "method" 129 | ]'); 130 | 131 | $server->unregisterAll(); 132 | 133 | $this->assertEquals( 134 | json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true), 135 | json_decode($server->execute(), true) 136 | ); 137 | } 138 | 139 | 140 | public function testBatchEmptyArray() 141 | { 142 | $server = new Server('[]'); 143 | $server->unregisterAll(); 144 | 145 | $this->assertEquals( 146 | json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true), 147 | json_decode($server->execute(), true) 148 | ); 149 | } 150 | 151 | 152 | public function testBatchNotEmptyButInvalid() 153 | { 154 | $server = new Server('[1]'); 155 | $server->unregisterAll(); 156 | 157 | $this->assertEquals( 158 | json_decode('[{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}]', true), 159 | json_decode($server->execute(), true) 160 | ); 161 | } 162 | 163 | 164 | public function testBatchInvalid() 165 | { 166 | $server = new Server('[1,2,3]'); 167 | $server->unregisterAll(); 168 | 169 | $this->assertEquals( 170 | json_decode('[ 171 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, 172 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, 173 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null} 174 | ]', true), 175 | json_decode($server->execute(), true) 176 | ); 177 | } 178 | 179 | 180 | public function testBatchOk() 181 | { 182 | $server = new Server('[ 183 | {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, 184 | {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, 185 | {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"}, 186 | {"foo": "boo"}, 187 | {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, 188 | {"jsonrpc": "2.0", "method": "get_data", "id": "9"} 189 | ]'); 190 | 191 | $server->unregisterAll(); 192 | 193 | $server->register('sum', function($a, $b, $c) { 194 | 195 | return $a + $b + $c; 196 | }); 197 | 198 | $server->register('subtract', function($minuend, $subtrahend) { 199 | 200 | return $minuend - $subtrahend; 201 | }); 202 | 203 | $server->register('get_data', function() { 204 | 205 | return array('hello', 5); 206 | }); 207 | 208 | $response = $server->execute(); 209 | 210 | //var_dump($response); 211 | //print_r(json_decode($response, true)); 212 | 213 | $this->assertEquals( 214 | json_decode('[ 215 | {"jsonrpc": "2.0", "result": 7, "id": "1"}, 216 | {"jsonrpc": "2.0", "result": 19, "id": "2"}, 217 | {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, 218 | {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"}, 219 | {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} 220 | ]', true), 221 | json_decode($response, true) 222 | ); 223 | } 224 | 225 | 226 | public function testBatchNotifications() 227 | { 228 | $server = new Server('[ 229 | {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]}, 230 | {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]} 231 | ]'); 232 | 233 | $server->unregisterAll(); 234 | 235 | $server->register('notify_sum', function($a, $b, $c) { 236 | 237 | }); 238 | 239 | $server->register('notify_hello', function($id) { 240 | 241 | }); 242 | 243 | $this->assertEquals('', $server->execute()); 244 | } 245 | } --------------------------------------------------------------------------------