Browse Source
- improved handling of JSON 2.0 RPC protocol - permits batched requests (not yet supported by Electrum client)master
Neil Booth
8 years ago
3 changed files with 291 additions and 109 deletions
@ -0,0 +1,249 @@ |
|||
# Copyright (c) 2016, Neil Booth |
|||
# |
|||
# All rights reserved. |
|||
# |
|||
# See the file "LICENCE" for information about the copyright |
|||
# and warranty status of this software. |
|||
|
|||
'''Class for handling JSON RPC 2.0 connections, server or client.''' |
|||
|
|||
import asyncio |
|||
import json |
|||
import numbers |
|||
import time |
|||
|
|||
from lib.util import LoggedClass |
|||
|
|||
|
|||
def json_result_payload(result, id_): |
|||
# We should not respond to notifications |
|||
assert id_ is not None |
|||
return {'jsonrpc': '2.0', 'error': None, 'result': result, 'id': id_} |
|||
|
|||
def json_error_payload(message, code, id_=None): |
|||
error = {'message': message, 'code': code} |
|||
return {'jsonrpc': '2.0', 'error': error, 'result': None, 'id': id_} |
|||
|
|||
def json_notification_payload(method, params): |
|||
return {'jsonrpc': '2.0', 'id': None, 'method': method, 'params': params} |
|||
|
|||
|
|||
class JSONRPC(asyncio.Protocol, LoggedClass): |
|||
'''Manages a JSONRPC connection. |
|||
|
|||
Assumes JSON messages are newline-separated and that newlines |
|||
cannot appear in the JSON other than to separate lines. |
|||
|
|||
Derived classes need to implement the synchronous functions |
|||
on_json_request() and method_handler(). They probably also want |
|||
to override connection_made() and connection_lost() but should be |
|||
sure to call the implementation in this base class first. |
|||
|
|||
on_json_request() is passed a JSON request as a python object |
|||
after decoding. It should arrange to pass on to the asynchronous |
|||
handle_json_request() method. |
|||
|
|||
method_handler() takes a method string and should return a function |
|||
that can be passed a parameters array, or None for an unknown method. |
|||
|
|||
Handlers should raise an RPCError on error. |
|||
''' |
|||
|
|||
# See http://www.jsonrpc.org/specification |
|||
PARSE_ERROR = -32700 |
|||
INVALID_REQUEST = -32600 |
|||
METHOD_NOT_FOUND = -32601 |
|||
INVALID_PARAMS = -32602 |
|||
INTERAL_ERROR = -32603 |
|||
|
|||
ID_TYPES = (type(None), str, numbers.Number) |
|||
|
|||
class RPCError(Exception): |
|||
'''RPC handlers raise this error.''' |
|||
def __init__(self, msg, code=-1, **kw_args): |
|||
super().__init__(**kw_args) |
|||
self.msg = msg |
|||
self.code |
|||
|
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.start = time.time() |
|||
self.transport = None |
|||
# Parts of an incomplete JSON line. We buffer them until |
|||
# getting a newline. |
|||
self.parts = [] |
|||
# recv_count is JSON messages not calls to data_received() |
|||
self.recv_count = 0 |
|||
self.recv_size = 0 |
|||
self.send_count = 0 |
|||
self.send_size = 0 |
|||
self.error_count = 0 |
|||
|
|||
def connection_made(self, transport): |
|||
'''Handle an incoming client connection.''' |
|||
self.transport = transport |
|||
|
|||
def peer_info(self): |
|||
'''Return peer info.''' |
|||
if self.transport: |
|||
return self.transport.get_extra_info('peername') |
|||
return None |
|||
|
|||
def connection_lost(self, exc): |
|||
'''Handle client disconnection.''' |
|||
pass |
|||
|
|||
def data_received(self, data): |
|||
'''Handle incoming data (synchronously). |
|||
|
|||
Requests end in newline characters. Pass complete requests to |
|||
decode_message for handling. |
|||
''' |
|||
self.recv_size += len(data) |
|||
while True: |
|||
npos = data.find(ord('\n')) |
|||
if npos == -1: |
|||
self.parts.append(data) |
|||
break |
|||
self.recv_count += 1 |
|||
tail, data = data[:npos], data[npos + 1:] |
|||
parts, self.parts = self.parts, [] |
|||
parts.append(tail) |
|||
self.decode_message(b''.join(parts)) |
|||
|
|||
def decode_message(self, message): |
|||
'''Decode a binary message and queue it for asynchronous handling. |
|||
|
|||
Messages that cannot be decoded are logged and dropped. |
|||
''' |
|||
try: |
|||
message = message.decode() |
|||
except UnicodeDecodeError as e: |
|||
msg = 'cannot decode binary bytes: {}'.format(e) |
|||
self.logger.warning(msg) |
|||
self.send_json_error(msg, self.PARSE_ERROR) |
|||
return |
|||
|
|||
try: |
|||
message = json.loads(message) |
|||
except json.JSONDecodeError as e: |
|||
msg = 'cannot decode JSON: {}'.format(e) |
|||
self.logger.warning(msg) |
|||
self.send_json_error(msg, self.PARSE_ERROR) |
|||
return |
|||
|
|||
self.on_json_request(message) |
|||
|
|||
def send_json_notification(self, method, params): |
|||
'''Create a json notification.''' |
|||
return self.send_json(json_notification_payload(method, params)) |
|||
|
|||
def send_json_result(self, result, id_): |
|||
'''Send a JSON result.''' |
|||
return self.send_json(json_result_payload(result, id_)) |
|||
|
|||
def send_json_error(self, message, code, id_=None): |
|||
'''Send a JSON error.''' |
|||
self.error_count += 1 |
|||
return self.send_json(json_error_payload(message, code, id_)) |
|||
|
|||
def send_json(self, payload): |
|||
'''Send a JSON payload.''' |
|||
if self.transport.is_closing(): |
|||
self.logger.info('send_json: connection closing, not sending') |
|||
return False |
|||
|
|||
try: |
|||
data = (json.dumps(payload) + '\n').encode() |
|||
except TypeError: |
|||
msg = 'JSON encoding failure: {}'.format(payload) |
|||
self.logger.error(msg) |
|||
return self.send_json_error(msg, self.INTERNAL_ERROR, |
|||
payload.get('id')) |
|||
|
|||
self.send_count += 1 |
|||
self.send_size += len(data) |
|||
self.transport.write(data) |
|||
return True |
|||
|
|||
async def handle_json_request(self, request): |
|||
'''Asynchronously handle a JSON request. |
|||
|
|||
Handles batch requests. Returns True if the request response |
|||
was sent (or if nothing was sent because the request was a |
|||
notification). Returns False if the send was aborted because |
|||
the connection is closing. |
|||
''' |
|||
if isinstance(request, list): |
|||
payload = self.batch_request_payload(request) |
|||
else: |
|||
payload = await self.single_request_payload(request) |
|||
|
|||
if not payload: |
|||
return True |
|||
return self.send_json(payload) |
|||
|
|||
async def batch_request_payload(self, batch): |
|||
'''Return the JSON payload corresponding to a batch JSON request.''' |
|||
# Batches must have at least one request. |
|||
if not batch: |
|||
return json_error_payload('empty request list', |
|||
self.INVALID_REQUEST) |
|||
|
|||
# PYTHON 3.6: use asynchronous comprehensions when supported |
|||
payload = [] |
|||
for item in request: |
|||
item_payload = await self.single_request_payload(item) |
|||
if item_payload: |
|||
payload.append(item_payload) |
|||
return payload |
|||
|
|||
async def single_request_payload(self, request): |
|||
'''Return the JSON payload corresponding to a single JSON request. |
|||
|
|||
Return None if the request is a notification. |
|||
''' |
|||
if not isinstance(request, dict): |
|||
return json_error_payload('request must be a dict', |
|||
self.INVALID_REQUEST) |
|||
|
|||
id_ = request.get('id') |
|||
if not isinstance(id_, self.ID_TYPES): |
|||
return json_error_payload('invalid id: {}'.format(id_), |
|||
self.INVALID_REQUEST) |
|||
|
|||
try: |
|||
result = await self.method_result(request.get('method'), |
|||
request.get('params', [])) |
|||
if id_ is None: |
|||
return None |
|||
return json_result_payload(result, id_) |
|||
except self.RPCError as e: |
|||
if id_ is None: |
|||
return None |
|||
return json_error_payload(e.msg, e.code, id_) |
|||
|
|||
async def method_result(self, method, params): |
|||
if not isinstance(method, str): |
|||
raise self.RPCError('invalid method: {}'.format(method), |
|||
self.INVALID_REQUEST) |
|||
|
|||
if not isinstance(params, list): |
|||
raise self.RPCError('params should be an array', |
|||
self.INVALID_REQUEST) |
|||
|
|||
handler = self.method_handler(method) |
|||
if not handler: |
|||
raise self.RPCError('unknown method: {}'.format(method), |
|||
self.METHOD_NOT_FOUND) |
|||
|
|||
return await handler(params) |
|||
|
|||
def on_json_request(self, request): |
|||
raise NotImplementedError('on_json_request in class {}'. |
|||
format(self.__class__.__name__)) |
|||
|
|||
def method_handler(self, method): |
|||
raise NotImplementedError('method_handler in class {}'. |
|||
format(self.__class__.__name__)) |
Loading…
Reference in new issue