diff options
Diffstat (limited to 'vendor/gipfl/protocol-jsonrpc')
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/LICENSE | 21 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/composer.json | 34 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Connection.php | 310 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Error.php | 199 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php | 28 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php | 23 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php | 217 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php | 241 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Notification.php | 98 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Packet.php | 226 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php | 11 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Request.php | 59 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/Response.php | 128 | ||||
-rw-r--r-- | vendor/gipfl/protocol-jsonrpc/src/TestCase.php | 44 |
14 files changed, 1639 insertions, 0 deletions
diff --git a/vendor/gipfl/protocol-jsonrpc/LICENSE b/vendor/gipfl/protocol-jsonrpc/LICENSE new file mode 100644 index 0000000..dd88e09 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Thomas Gelf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gipfl/protocol-jsonrpc/composer.json b/vendor/gipfl/protocol-jsonrpc/composer.json new file mode 100644 index 0000000..4a202eb --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/composer.json @@ -0,0 +1,34 @@ +{ + "name": "gipfl/protocol-jsonrpc", + "description": "JsonRPC Connection implementation", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Thomas Gelf", + "email": "thomas@gelf.net" + } + ], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "gipfl\\Protocol\\JsonRpc\\": "src" + } + }, + "require": { + "php": ">=5.6.0", + "ext-json": "*", + "gipfl/json": ">=0.1", + "gipfl/openrpc": "^0.2.1", + "gipfl/protocol": ">=0.2", + "psr/log": ">=1.1", + "react/promise": ">=2.7", + "react/stream": ">=1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^7.5 || ^6.5 || ^5.7", + "squizlabs/php_codesniffer": "^3.6" + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Connection.php b/vendor/gipfl/protocol-jsonrpc/src/Connection.php new file mode 100644 index 0000000..be4b33f --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Connection.php @@ -0,0 +1,310 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use Evenement\EventEmitterTrait; +use Exception; +use gipfl\Json\JsonEncodeException; +use InvalidArgumentException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use React\Promise\Deferred; +use React\Promise\Promise; +use React\Stream\DuplexStreamInterface; +use React\Stream\Util; +use RuntimeException; +use function call_user_func_array; +use function is_object; +use function mt_rand; +use function preg_quote; +use function preg_split; +use function React\Promise\reject; +use function sprintf; + +/** + * @deprecated Please use JsonRpcConection + */ +class Connection implements LoggerAwareInterface +{ + use EventEmitterTrait; + use LoggerAwareTrait; + + /** @var DuplexStreamInterface */ + protected $connection; + + /** @var array */ + protected $handlers = []; + + /** @var Deferred[] */ + protected $pending = []; + + protected $nsSeparator = '.'; + + protected $nsRegex = '/\./'; + + protected $unknownErrorCount = 0; + + public function handle(DuplexStreamInterface $connection) + { + $this->connection = $connection; + $this->connection->on('data', function ($data) { + try { + $this->handlePacket(Packet::decode($data)); + } catch (Exception $error) { + echo $error->getMessage() . "\n"; + $this->unknownErrorCount++; + if ($this->unknownErrorCount === 3) { + $this->close(); + } + $response = new Response(); + $response->setError(Error::forException($error)); + $this->connection->write($response->toString()); + } + }); + $connection->on('close', function () { + $this->rejectAllPendingRequests('Connection closed'); + }); + // TODO: figure out whether and how to deal with the pipe event + Util::forwardEvents($connection, $this, ['end', 'error', 'close', 'drain']); + } + + public function setNamespaceSeparator($separator) + { + $this->nsSeparator = $separator; + $this->nsRegex = '/' . preg_quote($separator, '/') . '/'; + + return $this; + } + + /** + * @param Packet $packet + */ + protected function handlePacket(Packet $packet) + { + if ($packet instanceof Response) { + $this->handleResponse($packet); + } elseif ($packet instanceof Request) { + $this->handleRequest($packet); + } elseif ($packet instanceof Notification) { + $this->handleNotification($packet); + } else { + // Will not happen as long as there is no bug in Packet + throw new RuntimeException('Packet was neither Request/Notification nor Response'); + } + } + + protected function handleResponse(Response $response) + { + $id = $response->getId(); + if (isset($this->pending[$id])) { + $promise = $this->pending[$id]; + unset($this->pending[$id]); + $promise->resolve($response); + } else { + $this->handleUnmatchedResponse($response); + } + } + + protected function handleUnmatchedResponse(Response $response) + { + // Ignore. Log? + } + + protected function handleRequest(Request $request) + { + $result = $this->handleNotification($request); + $this->sendResultForRequest($request, $result); + } + + protected function sendResultForRequest(Request $request, $result) + { + if ($result instanceof Error) { + $response = Response::forRequest($request); + $response->setError($result); + + $this->connection->write($response->toString()); + } elseif ($result instanceof Promise) { + $result->then(function ($result) use ($request) { + $this->sendResultForRequest($request, $result); + })->otherwise(function ($error) use ($request) { + $response = Response::forRequest($request); + if ($error instanceof Exception) { + $response->setError(Error::forException($error)); + } else { + $response->setError(new Error(Error::INTERNAL_ERROR, $error)); + } + // TODO: Double-check, this used to loop + $this->connection->write($response->toString()); + }); + } else { + $response = Response::forRequest($request); + $response->setResult($result); + $this->connection->write($response->toString()); + } + } + + /** + * @param Notification $notification + * @return Error|mixed + */ + protected function handleNotification(Notification $notification) + { + $method = $notification->getMethod(); + if (\strpos($method, $this->nsSeparator) === false) { + $namespace = null; + } else { + list($namespace, $method) = preg_split($this->nsRegex, $method, 2); + } + + try { + $response = $this->call($namespace, $method, $notification); + + return $response; + } catch (Exception $exception) { + return Error::forException($exception); + } + } + + /** + * @param Request $request + * @return \React\Promise\PromiseInterface + */ + public function sendRequest(Request $request) + { + $id = $request->getId(); + if ($id === null) { + $id = $this->getNextRequestId(); + $request->setId($id); + } + if (isset($this->pending[$id])) { + throw new InvalidArgumentException( + "A request with id '$id' is already pending" + ); + } + if (!$this->connection->isWritable()) { + return reject(new Exception('Cannot write to socket')); + } + try { + $this->connection->write($request->toString()); + } catch (JsonEncodeException $e) { + return reject($e->getMessage()); + } + $deferred = new Deferred(); + $this->pending[$id] = $deferred; + + return $deferred->promise()->then(function (Response $response) use ($deferred) { + if ($response->isError()) { + $deferred->reject(new RuntimeException($response->getError()->getMessage())); + } else { + $deferred->resolve($response->getResult()); + } + }, function (Exception $e) use ($deferred) { + $deferred->reject($e); + }); + } + + public function request($method, $params = null) + { + return $this->sendRequest(new Request($method, $this->getNextRequestId(), $params)); + } + + protected function getNextRequestId() + { + for ($i = 0; $i < 100; $i++) { + $id = mt_rand(1, 1000000000); + if (!isset($this->pending[$id])) { + return $id; + } + } + + throw new RuntimeException('Unable to generate a free random request ID, gave up after 100 attempts'); + } + + /** + * @param Request|mixed $request + */ + public function forgetRequest($request) + { + if ($request instanceof Request) { + unset($this->pending[$request->getId()]); + } else { + unset($this->pending[$request]); + } + } + + /** + * @param Notification $packet + */ + public function sendNotification(Notification $packet) + { + $this->connection->write($packet->toString()); + } + + /** + * @param string $method + * @param null $params + */ + public function notification($method, $params = null) + { + $notification = new Notification($method, $params); + $this->sendNotification($notification); + } + + /** + * @param $namespace + * @param $handler + * @return Connection + */ + public function setHandler($handler, $namespace = null) + { + $this->handlers[$namespace] = $handler; + + return $this; + } + + protected function call($namespace, $method, Notification $packet) + { + if (isset($this->handlers[$namespace])) { + $handler = $this->handlers[$namespace]; + if ($handler instanceof PacketHandler) { + return $handler->handle($packet); + } + + // Legacy handlers, deprecated: + $params = $packet->getParams(); + if (is_object($params)) { + return $handler->$method($params); + } + + return call_user_func_array([$handler, $method], $params); + } + + $error = new Error(Error::METHOD_NOT_FOUND); + $error->setMessage(sprintf( + '%s: %s%s%s', + $error->getMessage(), + $namespace, + $this->nsSeparator, + $method + )); + + return $error; + } + + protected function rejectAllPendingRequests($message) + { + foreach ($this->pending as $pending) { + $pending->reject(new Exception()); + } + $this->pending = []; + } + + public function close() + { + if ($this->connection) { + $this->connection->close(); + $this->handlers = []; + $this->connection = null; + } + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Error.php b/vendor/gipfl/protocol-jsonrpc/src/Error.php new file mode 100644 index 0000000..dc1d639 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Error.php @@ -0,0 +1,199 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use Exception; +use JsonSerializable; +use TypeError; + +class Error implements JsonSerializable +{ + const PARSE_ERROR = -32700; + + const INVALID_REQUEST = -32600; + + const METHOD_NOT_FOUND = -32601; + + const INVALID_PARAMS = 32602; + + const INTERNAL_ERROR = 32603; + + // Reserved for implementation-defined server-errors: + const MIN_CUSTOM_ERROR = -32000; + + const MAX_CUSTOM_ERROR = -32099; + + protected static $wellKnownErrorCodes = [ + self::PARSE_ERROR, + self::INVALID_REQUEST, + self::METHOD_NOT_FOUND, + self::INVALID_PARAMS, + self::INTERNAL_ERROR, + ]; + + protected static $errorMessages = [ + self::PARSE_ERROR => 'Invalid JSON was received by the server.', + self::INVALID_REQUEST => 'The JSON sent is not a valid Request object', + self::METHOD_NOT_FOUND => 'The method does not exist / is not available', + self::INVALID_PARAMS => 'Invalid method parameter(s)', + self::INTERNAL_ERROR => 'Internal JSON-RPC error', + ]; + + protected static $defaultCustomMessage = 'Server error. Reserved for implementation-defined server-errors.'; + + /** @var int */ + protected $code; + + /** @var string */ + protected $message; + + /** @var mixed|null */ + protected $data; + + /** + * Error constructor. + * @param int $code + * @param string $message + * @param mixed $data + */ + public function __construct($code, $message = null, $data = null) + { + if ($message === null) { + if ($this->isCustomErrorCode($code)) { + $message = self::$defaultCustomMessage; + } elseif (static::isWellKnownErrorCode($code)) { + $message = self::$errorMessages[$code]; + } else { + $message = 'Unknown error'; + } + } + $this->code = $code; + $this->message = $message; + $this->data = $data; + } + + public static function forException(Exception $exception) + { + $code = $exception->getCode(); + if (! static::isCustomErrorCode($code) + && ! static::isWellKnownErrorCode($code) + ) { + $code = self::INTERNAL_ERROR; + } + if (static::isWellKnownErrorCode($code) && $code !== self::INTERNAL_ERROR) { + $data = null; + } else { + $data = $exception->getTraceAsString(); + } + if (function_exists('iconv')) { + $data = iconv('UTF-8', 'UTF-8//IGNORE', $data); + } + + return new Error($code, sprintf( + '%s in %s(%d)', + $exception->getMessage(), + $exception->getFile(), + $exception->getLine() + ), $data); + } + + public static function forTypeError(TypeError $error) + { + $code = self::INVALID_PARAMS; + + return new Error($code, sprintf( + '%s in %s(%d)', + $error->getMessage(), + $error->getFile(), + $error->getLine() + )); + } + + /** + * @return int + */ + public function getCode() + { + return $this->code; + } + + /** + * @param int $code + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + return $this; + } + + /** + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * @param string $message + * @return $this + */ + public function setMessage($message) + { + $this->message = $message; + return $this; + } + + /** + * @return mixed|null + */ + public function getData() + { + return $this->data; + } + + /** + * @param mixed|null $data + * @return $this + */ + public function setData($data) + { + $this->data = $data; + return $this; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $result = [ + 'code' => $this->code, + 'message' => $this->message, + ]; + + if ($this->data !== null) { + $result['data'] = $this->data; + } + + return (object) $result; + } + + public static function isWellKnownErrorCode($code) + { + return isset(self::$errorMessages[$code]); + } + + public static function isCustomErrorCode($code) + { + return $code >= self::MIN_CUSTOM_ERROR && $code <= self::MAX_CUSTOM_ERROR; + } + + /** + * @deprecated please use jsonSerialize() + * @return mixed + */ + public function toPlainObject() + { + return $this->jsonSerialize(); + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php new file mode 100644 index 0000000..0c04ac7 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php @@ -0,0 +1,28 @@ +<?php + +namespace gipfl\Protocol\JsonRpc\Handler; + +use gipfl\Protocol\JsonRpc\Error; +use gipfl\Protocol\JsonRpc\Notification; +use gipfl\Protocol\JsonRpc\Request; + +class FailingPacketHandler implements JsonRpcHandler +{ + /** @var Error */ + protected $error; + + public function __construct(Error $error) + { + $this->error = $error; + } + + public function processNotification(Notification $notification) + { + // We silently ignore them + } + + public function processRequest(Request $request) + { + return $this->error; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php new file mode 100644 index 0000000..f64bc68 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php @@ -0,0 +1,23 @@ +<?php + +namespace gipfl\Protocol\JsonRpc\Handler; + +use gipfl\Protocol\JsonRpc\Error; +use gipfl\Protocol\JsonRpc\Notification; +use gipfl\Protocol\JsonRpc\Request; +use React\Promise\PromiseInterface; + +interface JsonRpcHandler +{ + /** + * @param Request $request + * @return Error|PromiseInterface|mixed + */ + public function processRequest(Request $request); + + /** + * @param Notification $notification + * @return void + */ + public function processNotification(Notification $notification); +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php new file mode 100644 index 0000000..6e0655b --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php @@ -0,0 +1,217 @@ +<?php + +namespace gipfl\Protocol\JsonRpc\Handler; + +use Exception; +use gipfl\Json\JsonSerialization; +use gipfl\OpenRpc\Reflection\MetaDataClass; +use gipfl\OpenRpc\Reflection\MetaDataMethod; +use gipfl\Protocol\JsonRpc\Error; +use gipfl\Protocol\JsonRpc\Notification; +use gipfl\Protocol\JsonRpc\Request; +use RuntimeException; +use TypeError; +use function call_user_func_array; +use function method_exists; +use function preg_split; +use function sprintf; +use function strpos; + +class NamespacedPacketHandler implements JsonRpcHandler +{ + protected $nsSeparator = '.'; + + protected $nsRegex = '/\./'; + + protected $handlers = []; + + /** + * @var MetaDataMethod[] + */ + protected $knownMethods = []; + + public function processNotification(Notification $notification) + { + list($namespace, $method) = $this->splitMethod($notification->getMethod()); + try { + $this->call($namespace, $method, $notification); + } catch (Exception $exception) { + // Well... we might want to log this + } catch (TypeError $exception) { + // Well... we might want to log this + } + } + + public function processRequest(Request $request) + { + list($namespace, $method) = $this->splitMethod($request->getMethod()); + + try { + return $this->call($namespace, $method, $request); + } catch (Exception $exception) { + return Error::forException($exception); + } catch (TypeError $error) { + return Error::forTypeError($error); + } + } + + /** + * @param string $namespace + * @param object $handler + */ + public function registerNamespace($namespace, $handler) + { + if (isset($this->handlers[$namespace])) { + throw new RuntimeException("Cannot register a namespace twice: '$namespace'"); + } + $this->handlers[$namespace] = $handler; + $this->analyzeNamespace($namespace, $handler); + } + + protected function analyzeNamespace($namespace, $handler) + { + $meta = MetaDataClass::analyze(get_class($handler)); + foreach ($meta->getMethods() as $method) { + $this->knownMethods[$namespace . $this->nsSeparator . $method->getName()] = $method; + } + } + + /** + * @param string $namespace + */ + public function removeNamespace($namespace) + { + unset($this->handlers[$namespace]); + } + + public function setNamespaceSeparator($separator) + { + $this->nsSeparator = $separator; + $this->nsRegex = '/' . preg_quote($separator, '/') . '/'; + + return $this; + } + + protected function call($namespace, $method, Notification $notification) + { + if (! isset($this->handlers[$namespace])) { + return $this->notFound($notification, ', no handler for ' . $namespace); + } + + $handler = $this->handlers[$namespace]; + if ($handler instanceof JsonRpcHandler) { + if ($notification instanceof Request) { + return $handler->processRequest($notification); + } else { + $handler->processNotification($notification); + } + } + + $params = $notification->getParams(); + if (! is_array($params)) { + try { + $params = $this->prepareParams($notification->getMethod(), $params); + } catch (Exception $e) { + return Error::forException($e); + } + } + if ($notification instanceof Request) { + $rpcMethod = $method . 'Request'; + if (is_callable([$handler, $rpcMethod])) { + return call_user_func_array([$handler, $rpcMethod], $params); + } + + return $this->notFound($notification, ', no ' . $rpcMethod); + } else { + $rpcMethod = $method . 'Notification'; + if (is_callable([$handler, $rpcMethod])) { + call_user_func_array([$handler, $rpcMethod], $params); + } + + return null; + } + } + + protected function prepareParams($method, $params) + { + if (! isset($this->knownMethods[$method])) { + throw new Exception('Cannot map params for unknown method'); + } + + $meta = $this->knownMethods[$method]; + $result = []; + foreach ($meta->getParameters() as $parameter) { + $name = $parameter->getName(); + if (property_exists($params, $name)) { + $value = $params->$name; + if ($value === null) { + // TODO: check if required + $result[] = $value; + continue; + } + switch ($parameter->getType()) { + case 'int': + $result[] = (int) $value; + break; + case 'float': + $result[] = (float) $value; + break; + case 'string': + $result[] = (string) $value; + break; + case 'array': + $result[] = (array) $value; + break; + case 'bool': + case 'boolean': + $result[] = (bool) $value; + break; + case 'object': + $result[] = (object) $value; + break; + default: + $type = $parameter->getType(); + if (class_exists($type)) { + foreach (class_implements($type) as $implement) { + if ($implement === JsonSerialization::class) { + $result[] = $type::fromSerialization($value); + break 2; + } + } + } + throw new Exception(sprintf( + 'Unsupported parameter type for %s: %s', + $method, + $parameter->getType() + )); + } + } else { + // TODO: isRequired? Set null + throw new Exception("Missing parameter for $method: $name"); + } + } + + return $result; + } + + protected function splitMethod($method) + { + if (strpos($method, $this->nsSeparator) === false) { + return [null, $method]; + } + + return preg_split($this->nsRegex, $method, 2); + } + + protected function notFound(Notification $notification, $suffix = '') + { + $error = new Error(Error::METHOD_NOT_FOUND); + $error->setMessage(sprintf( + '%s: %s' . $suffix, + $error->getMessage(), + $notification->getMethod() + )); + + return $error; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php b/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php new file mode 100644 index 0000000..88c6f5b --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php @@ -0,0 +1,241 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use Evenement\EventEmitterTrait; +use Exception; +use gipfl\Json\JsonEncodeException; +use gipfl\Protocol\JsonRpc\Handler\JsonRpcHandler; +use InvalidArgumentException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; +use React\Promise\Deferred; +use React\Promise\Promise; +use React\Stream\DuplexStreamInterface; +use React\Stream\Util; +use RuntimeException; +use function mt_rand; +use function React\Promise\reject; +use function React\Promise\resolve; + +class JsonRpcConnection implements LoggerAwareInterface +{ + use EventEmitterTrait; + use LoggerAwareTrait; + + /** @var DuplexStreamInterface */ + protected $connection; + + /** @var ?JsonRpcHandler */ + protected $handler; + + /** @var Deferred[] */ + protected $pending = []; + + protected $unknownErrorCount = 0; + + public function __construct(DuplexStreamInterface $connection, JsonRpcHandler $handler = null) + { + $this->setLogger(new NullLogger()); + $this->connection = $connection; + $this->setHandler($handler); + $this->connection->on('data', function ($data) { + try { + $this->handlePacket(Packet::decode($data)); + } catch (\Exception $error) { + $this->logger->error($error->getMessage()); + $this->unknownErrorCount++; + if ($this->unknownErrorCount === 3) { + // e.g.: decoding errors + // TODO: should we really close? Or just send error responses for every Exception? + $this->close(); + } + $response = new Response(); + $response->setError(Error::forException($error)); + $this->connection->write($response->toString()); + } + }); + $connection->on('close', function () { + $this->rejectAllPendingRequests('Connection closed'); + }); + // Hint: Util::pipe takes care of the pipe event + Util::forwardEvents($connection, $this, ['end', 'error', 'close', 'drain']); + } + + /** + * @param Packet $packet + */ + protected function handlePacket(Packet $packet) + { + if ($packet instanceof Response) { + $this->handleResponse($packet); + } elseif ($packet instanceof Request) { + if ($this->handler) { + $result = $this->handler->processRequest($packet); + } else { + $result = new Error(Error::METHOD_NOT_FOUND); + $result->setMessage($result->getMessage() . ': ' . $packet->getMethod()); + } + $this->sendResultForRequest($packet, $result); + } elseif ($packet instanceof Notification) { + if ($this->handler) { + $this->handler->processNotification($packet); + } + } else { + // Will not happen as long as there is no bug in Packet + throw new RuntimeException('Packet was neither Request/Notification nor Response'); + } + } + + protected function handleResponse(Response $response) + { + $id = $response->getId(); + if (isset($this->pending[$id])) { + $promise = $this->pending[$id]; + unset($this->pending[$id]); + $promise->resolve($response); + } else { + $this->handleUnmatchedResponse($response); + } + } + + protected function handleUnmatchedResponse(Response $response) + { + $this->logger->error('Unmatched Response: ' . $response->toString()); + } + + protected function sendResultForRequest(Request $request, $result) + { + if ($result instanceof Error) { + $response = Response::forRequest($request); + $response->setError($result); + if ($this->connection && $this->connection->isWritable()) { + $this->connection->write($response->toString()); + } else { + $this->logger->error('Failed to send response, have no writable connection'); + } + } elseif ($result instanceof Promise) { + $result->then(function ($result) use ($request) { + $this->sendResultForRequest($request, $result); + }, function ($error) use ($request) { + $response = Response::forRequest($request); + if ($error instanceof Exception || $error instanceof \Throwable) { + $response->setError(Error::forException($error)); + } else { + $response->setError(new Error(Error::INTERNAL_ERROR, $error)); + } + // TODO: Double-check, this used to loop + $this->connection->write($response->toString()); + })->done(); + } else { + $response = Response::forRequest($request); + $response->setResult($result); + if ($this->connection && $this->connection->isWritable()) { + $this->connection->write($response->toString()); + } else { + $this->logger->error('Failed to send response, have no writable connection'); + } + } + } + + /** + * @param Request $request + * @return \React\Promise\PromiseInterface + */ + public function sendRequest(Request $request) + { + $id = $request->getId(); + if ($id === null) { + $id = $this->getNextRequestId(); + $request->setId($id); + } + if (isset($this->pending[$id])) { + throw new InvalidArgumentException( + "A request with id '$id' is already pending" + ); + } + if (!$this->connection->isWritable()) { + return reject(new Exception('Cannot write to socket')); + } + try { + $this->connection->write($request->toString()); + } catch (JsonEncodeException $e) { + return reject($e->getMessage()); + } + $deferred = new Deferred(function () use ($id) { + unset($this->pending[$id]); + }); + $this->pending[$id] = $deferred; + + return $deferred->promise()->then(function (Response $response) { + if ($response->isError()) { + return reject(new RuntimeException($response->getError()->getMessage())); + } + + return resolve($response->getResult()); + }); + } + + public function request($method, $params = null) + { + return $this->sendRequest(new Request($method, $this->getNextRequestId(), $params)); + } + + protected function getNextRequestId() + { + for ($i = 0; $i < 100; $i++) { + $id = mt_rand(1, 1000000000); + if (!isset($this->pending[$id])) { + return $id; + } + } + + throw new RuntimeException('Unable to generate a free random request ID, gave up after 100 attempts'); + } + + /** + * @param Notification $packet + */ + public function sendNotification(Notification $packet) + { + $this->connection->write($packet->toString()); + } + + /** + * @param string $method + * @param null $params + */ + public function notification($method, $params = null) + { + $notification = new Notification($method, $params); + $this->sendNotification($notification); + } + + /** + * @param PacketHandler $handler + * @return $this + */ + public function setHandler(JsonRpcHandler $handler = null) + { + $this->handler = $handler; + return $this; + } + + protected function rejectAllPendingRequests($message) + { + foreach ($this->pending as $pending) { + $pending->reject(new Exception($message)); + } + $this->pending = []; + } + + public function close() + { + if ($this->connection) { + $this->connection->close(); + $this->handler = null; + $this->connection = null; + } + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Notification.php b/vendor/gipfl/protocol-jsonrpc/src/Notification.php new file mode 100644 index 0000000..338efc1 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Notification.php @@ -0,0 +1,98 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +class Notification extends Packet +{ + /** @var string */ + protected $method; + + /** @var \stdClass|array */ + protected $params; + + public function __construct($method, $params) + { + $this->setMethod($method); + $this->setParams($params); + } + + /** + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * @param string $method + */ + public function setMethod($method) + { + $this->method = $method; + } + + /** + * @return object|array + */ + public function getParams() + { + return $this->params; + } + + /** + * @param object|array $params + */ + public function setParams($params) + { + $this->params = $params; + } + + /** + * @param string $name + * @param mixed $default + * @return mixed|null + */ + public function getParam($name, $default = null) + { + $p = & $this->params; + if (\is_object($p) && \property_exists($p, $name)) { + return $p->$name; + } elseif (\is_array($p) && \array_key_exists($name, $p)) { + return $p[$name]; + } + + return $default; + } + + /** + * @return object + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $plain = [ + 'jsonrpc' => '2.0', + 'method' => $this->method, + 'params' => $this->params, + ]; + + if ($this->hasExtraProperties()) { + $plain += (array) $this->getExtraProperties(); + } + + return (object) $plain; + } + + /** + * @param $method + * @param $params + * @return static + */ + public static function create($method, $params) + { + $packet = new Notification($method, $params); + + return $packet; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Packet.php b/vendor/gipfl/protocol-jsonrpc/src/Packet.php new file mode 100644 index 0000000..8dca44e --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Packet.php @@ -0,0 +1,226 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use gipfl\Json\JsonException; +use gipfl\Json\JsonSerialization; +use gipfl\Json\JsonString; +use gipfl\Protocol\Exception\ProtocolError; +use function property_exists; + +abstract class Packet implements JsonSerialization +{ + /** @var \stdClass|null */ + protected $extraProperties; + + /** + * @return string + * @throws \gipfl\Json\JsonEncodeException + */ + public function toString() + { + return JsonString::encode($this->jsonSerialize()); + } + + /** + * @return string + * @throws \gipfl\Json\JsonEncodeException + */ + public function toPrettyString() + { + return JsonString::encode($this->jsonSerialize(), JSON_PRETTY_PRINT); + } + + /** + * @return bool + */ + public function hasExtraProperties() + { + return $this->extraProperties !== null; + } + + /** + * @return \stdClass|null + */ + public function getExtraProperties() + { + return $this->extraProperties; + } + + /** + * @param \stdClass|null $extraProperties + * @return $this + * @throws ProtocolError + */ + public function setExtraProperties($extraProperties) + { + foreach (['id', 'error', 'result', 'jsonrpc', 'method', 'params'] as $key) { + if (property_exists($extraProperties, $key)) { + throw new ProtocolError("Cannot accept '$key' as an extra property"); + } + } + $this->extraProperties = $extraProperties; + + return $this; + } + + /** + * @param string $name + * @param mixed|null $default + * @return mixed|null + */ + public function getExtraProperty($name, $default = null) + { + if (isset($this->extraProperties->$name)) { + return $this->extraProperties->$name; + } else { + return $default; + } + } + + + /** + * @param string $name + * @param mixed $value + * @return $this + */ + public function setExtraProperty($name, $value) + { + if ($this->extraProperties === null) { + $this->extraProperties = (object) [$name => $value]; + } else { + $this->extraProperties->$name = $value; + } + + return $this; + } + + /** + * @param $string + * @return Notification|Request|Response + * @throws ProtocolError + */ + public static function decode($string) + { + try { + return self::fromSerialization(JsonString::decode($string)); + } catch (JsonException $e) { + throw new ProtocolError(sprintf( + 'JSON decode failed: %s', + $e->getMessage() + ), Error::PARSE_ERROR); + } + } + + public static function fromSerialization($any) + { + $version = static::stripRequiredProperty($any, 'jsonrpc'); + if ($version !== '2.0') { + throw new ProtocolError( + "Only JSON-RPC 2.0 is supported, got $version", + Error::INVALID_REQUEST + ); + } + + // Hint: we MUST use property_exists here, as a NULL id is allowed + // in error responsed in case it wasn't possible to determine a + // request id + $hasId = property_exists($any, 'id'); + $id = static::stripOptionalProperty($any, 'id'); + $error = static::stripOptionalProperty($any, 'error'); + if (property_exists($any, 'method')) { + $method = static::stripRequiredProperty($any, 'method'); + $params = static::stripRequiredProperty($any, 'params'); + + if ($id === null) { + $packet = new Notification($method, $params); + } else { + $packet = new Request($method, $id, $params); + } + } elseif (! $hasId) { + throw new ProtocolError( + "Given string is not a valid JSON-RPC 2.0 response: id is missing", + Error::INVALID_REQUEST + ); + } else { + $packet = new Response($id); + if ($error) { + $packet->setError(new Error( + static::stripOptionalProperty($error, 'code'), + static::stripOptionalProperty($error, 'message'), + static::stripOptionalProperty($error, 'data') + )); + } else { + $result = static::stripRequiredProperty($any, 'result'); + $packet->setResult($result); + } + } + if (count((array) $any) > 0) { + $packet->setExtraProperties($any); + } + + return $packet; + } + + /** + * @param object $object + * @param string $property + * @throws ProtocolError + */ + protected static function assertPropertyExists($object, $property) + { + if (! property_exists($object, $property)) { + throw new ProtocolError( + "Expected valid JSON-RPC, got no '$property' property", + Error::INVALID_REQUEST + ); + } + } + + /** + * @param \stdClass $object + * @param string $property + * @return mixed|null + */ + protected static function stripOptionalProperty($object, $property) + { + if (property_exists($object, $property)) { + $value = $object->$property; + unset($object->$property); + + return $value; + } + + return null; + } + + /** + * @param \stdClass $object + * @param string $property + * @return mixed + * @throws ProtocolError + */ + protected static function stripRequiredProperty($object, $property) + { + if (! property_exists($object, $property)) { + throw new ProtocolError( + "Expected valid JSON-RPC, got no '$property' property", + Error::INVALID_REQUEST + ); + } + + $value = $object->$property; + unset($object->$property); + + return $value; + } + + /** + * @deprecated please use jsonSerialize() + * @return string + */ + public function toPlainObject() + { + return $this->jsonSerialize(); + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php new file mode 100644 index 0000000..e3f23c2 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php @@ -0,0 +1,11 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +/** + * @deprecated + */ +interface PacketHandler +{ + public function handle(Notification $notification); +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Request.php b/vendor/gipfl/protocol-jsonrpc/src/Request.php new file mode 100644 index 0000000..2061a41 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Request.php @@ -0,0 +1,59 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use gipfl\Protocol\Exception\ProtocolError; + +class Request extends Notification +{ + /** @var mixed */ + protected $id; + + /** + * Request constructor. + * @param string $method + * @param mixed $id + * @param null $params + */ + public function __construct($method, $id = null, $params = null) + { + parent::__construct($method, $params); + + $this->id = $id; + } + + /** + * @return object + * @throws ProtocolError + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + if ($this->id === null) { + throw new ProtocolError( + 'A request without an ID is not valid' + ); + } + + $plain = parent::jsonSerialize(); + $plain->id = $this->id; + + return $plain; + } + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * @param mixed $id + */ + public function setId($id) + { + $this->id = $id; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/Response.php b/vendor/gipfl/protocol-jsonrpc/src/Response.php new file mode 100644 index 0000000..3a5ad90 --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/Response.php @@ -0,0 +1,128 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +class Response extends Packet +{ + /** @var mixed|null This could be null when sending a parse error */ + protected $id; + + /** @var mixed */ + protected $result; + + /** @var Error|null */ + protected $error; + + /** @var string */ + protected $message; + + public function __construct($id = null) + { + $this->id = $id; + } + + /** + * @param Request $request + * @return Response + */ + public static function forRequest(Request $request) + { + return new Response($request->getId()); + } + + /** + * @return object + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $plain = [ + 'jsonrpc' => '2.0', + ]; + if ($this->hasExtraProperties()) { + $plain += (array) $this->getExtraProperties(); + } + + if ($this->id !== null) { + $plain['id'] = $this->id; + } + + if ($this->error === null) { + $plain['result'] = $this->result; + } else { + if (! isset($plain['id'])) { + $plain['id'] = null; + } + $plain['error'] = $this->error; + } + + return (object) $plain; + } + + /** + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + /** + * @param $result + * @return $this + */ + public function setResult($result) + { + $this->result = $result; + + return $this; + } + + /** + * @return bool + */ + public function hasId() + { + return null !== $this->id; + } + + /** + * @return null|int|string + */ + public function getId() + { + return $this->id; + } + + /** + * @param $id + */ + public function setId($id) + { + $this->id = $id; + } + + public function isError() + { + return $this->error !== null; + } + + /** + * @return Error|null + */ + public function getError() + { + return $this->error; + } + + /** + * @param $error + * @return $this; + */ + public function setError(Error $error) + { + $this->error = $error; + + return $this; + } +} diff --git a/vendor/gipfl/protocol-jsonrpc/src/TestCase.php b/vendor/gipfl/protocol-jsonrpc/src/TestCase.php new file mode 100644 index 0000000..05f54ba --- /dev/null +++ b/vendor/gipfl/protocol-jsonrpc/src/TestCase.php @@ -0,0 +1,44 @@ +<?php + +namespace gipfl\Protocol\JsonRpc; + +use PHPUnit\Framework\TestCase as BaseTestCase; +use React\EventLoop\LoopInterface; + +class TestCase extends BaseTestCase +{ + protected $examples = []; + + protected function parseExample($key) + { + return Packet::decode($this->examples[$key]); + } + + protected function failAfterSeconds($seconds, LoopInterface $loop) + { + $loop->addTimer($seconds, function () use ($seconds) { + throw new \RuntimeException("Timed out after $seconds seconds"); + }); + } + + protected function collectErrorsForNotices(&$errors) + { + \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$errors) { + if (\error_reporting() === 0) { // @-operator in use + return false; + } + $errors[] = new \ErrorException($errstr, 0, $errno, $errfile, $errline); + + return false; // Always continue with normal error processing + }, E_ALL | E_STRICT); + + \error_reporting(E_ALL | E_STRICT); + } + + protected function throwEventualErrors(array $errors) + { + foreach ($errors as $error) { + throw $error; + } + } +} |