Skip to content

Add GET & OPTIONS request functionality to JSON-RPC servers (#712) #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ All notable changes to this project are documented in this file.
- Fix various issues related to signing multi-signature transactions
- Move some warnings and 'expected' errors to `DEBUG` level to avoid logging to console by default
- Empty VerificationScripts for deployed contracts now work as intended
- Fix RPC's ``getaccountstate`` response schema to match ``neo-cli`` `#714 <https://github.com/CityOfZion/neo-python/issues/714>`
- Fix RPC's ``getaccountstate`` response schema to match ``neo-cli`` `#714 <https://github.com/CityOfZion/neo-python/issues/714>`_
- Add fix to ensure tx is saved to wallet when sent using RPC
- Add bad peers to the ``getpeers`` RPC method `#715 <https://github.com/CityOfZion/neo-python/pull/715>`
- Add bad peers to the ``getpeers`` RPC method `#715 <https://github.com/CityOfZion/neo-python/pull/715>`_
- Introduce Django inspired component loading for REST and RPC server
- Allow a raw tx to be build without an active blockchain db in the environment
- Fix unnecessary default bootstrap warning for mainnet showing.
- Add GET and OPTIONS request functionality for JSON-RPC servers


[0.8.2] 2018-10-31
Expand Down
18 changes: 18 additions & 0 deletions neo/api/JSONRPC/ExtendedJsonRpcApi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from klein import Klein
from neo.Core.Blockchain import Blockchain
from neo.api.utils import json_response, cors_header
from neo.api.JSONRPC.JsonRpcApi import JsonRpcApi, JsonRpcError
from neo.Implementations.Wallets.peewee.UserWallet import UserWallet
from neocore.UInt256 import UInt256
Expand All @@ -9,12 +11,28 @@ class ExtendedJsonRpcApi(JsonRpcApi):
"""
Extended JSON-RPC API Methods
"""
app = Klein()
port = None

def __init__(self, port, wallet=None):
self.start_height = Blockchain.Default().Height
self.start_dt = datetime.datetime.utcnow()
super(ExtendedJsonRpcApi, self).__init__(port, wallet)

#
# JSON-RPC Extended API Route
#
@app.route('/')
@json_response
@cors_header
def home(self, request):

if "OPTIONS" == request.method.decode("utf-8"):
return {'supported HTTP methods': ("GET", "POST"),
'JSON-RPC server type': "extended-rpc"}

return super(ExtendedJsonRpcApi, self).home(request)

def json_rpc_method_handler(self, method, params):

if method == "getnodestate":
Expand Down
60 changes: 48 additions & 12 deletions neo/api/JSONRPC/JsonRpcApi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
from neo.Implementations.Wallets.peewee.Models import Account
from neo.Prompt.Utils import get_asset_id
from neo.Wallets.Wallet import Wallet
from furl import furl
import ast


class JsonRpcError(Exception):
Expand Down Expand Up @@ -120,27 +122,61 @@ def get_data(self, body: dict):
@json_response
@cors_header
def home(self, request):
# POST Examples:
# {"jsonrpc": "2.0", "id": 5, "method": "getblockcount", "params": []}
# or multiple requests in 1 transaction
# [{"jsonrpc": "2.0", "id": 1, "method": "getblock", "params": [10], {"jsonrpc": "2.0", "id": 2, "method": "getblock", "params": [10,1]}
#
# GET Example:
# /?jsonrpc=2.0&id=5&method=getblockcount&params=[]
# NOTE: GET requests do not support multiple requests in 1 transaction
request_id = None

try:
content = json.loads(request.content.read().decode("utf-8"))
if "POST" == request.method.decode("utf-8"):
try:
content = json.loads(request.content.read().decode("utf-8"))

# test if it's a multi-request message
if isinstance(content, list):
result = []
for body in content:
result.append(self.get_data(body))
return result

# otherwise it's a single request
return self.get_data(content)

except JSONDecodeError as e:
error = JsonRpcError.parseError()
return self.get_custom_error_payload(request_id, error.code, error.message)

# test if it's a multi-request message
if isinstance(content, list):
result = []
for body in content:
result.append(self.get_data(body))
return result
elif "GET" == request.method.decode("utf-8"):
content = furl(request.uri).args

# remove hanging ' or " from last value if value is not None to avoid SyntaxError
l_value = list(content.values())[-1]
if l_value is not None:
n_value = l_value[:-1]
l_key = list(content.keys())[-1]
content[l_key] = n_value

if len(content.keys()) > 3:
try:
params = content['params']
l_params = ast.literal_eval(params)
content['params'] = [l_params]
except KeyError:
error = JsonRpcError(-32602, "Invalid params")
return self.get_custom_error_payload(request_id, error.code, error.message)

# otherwise it's a single request
return self.get_data(content)

except JSONDecodeError as e:
error = JsonRpcError.parseError()
return self.get_custom_error_payload(request_id, error.code, error.message)
elif "OPTIONS" == request.method.decode("utf-8"):
return {'supported HTTP methods': ("GET", "POST"),
'JSON-RPC server type': "default"}

error = JsonRpcError.invalidRequest("%s is not a supported HTTP method" % request.method.decode("utf-8"))
return self.get_custom_error_payload(request_id, error.code, error.message)

def json_rpc_method_handler(self, method, params):

Expand Down
116 changes: 97 additions & 19 deletions neo/api/JSONRPC/test_extended_json_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import os
import shutil
from klein.test.test_resource import requestMock
from twisted.web import server
from twisted.web.test.test_web import DummyChannel
from neo.api.JSONRPC.ExtendedJsonRpcApi import ExtendedJsonRpcApi
from neo.Utils.BlockchainFixtureTestCase import BlockchainFixtureTestCase
from neo.Implementations.Wallets.peewee.UserWallet import UserWallet
Expand All @@ -16,8 +18,16 @@
from neo.Utils.WalletFixtureTestCase import WalletFixtureTestCase


def mock_request(body):
return requestMock(path=b'/', method="POST", body=body)
def mock_post_request(body):
return requestMock(path=b'/', method=b"POST", body=body)


def mock_get_request(path, method=b"GET"):
request = server.Request(DummyChannel(), False)
request.uri = path
request.method = method
request.clientproto = b'HTTP/1.1'
return request


class ExtendedJsonRpcApiTestCase(BlockchainFixtureTestCase):
Expand All @@ -30,16 +40,41 @@ def leveldb_testpath(self):
def setUp(self):
self.app = ExtendedJsonRpcApi(20332)

def test_HTTP_OPTIONS_request(self):
mock_req = mock_get_request(b'/?test', b"OPTIONS")
res = json.loads(self.app.home(mock_req))

self.assertTrue("GET" in res['supported HTTP methods'])
self.assertTrue("POST" in res['supported HTTP methods'])
self.assertTrue("extended-rpc" in res['JSON-RPC server type'])

def test_invalid_request_method(self):
# test HEAD method
mock_req = mock_get_request(b'/?test', b"HEAD")
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)
self.assertEqual(res["error"]["message"], 'HEAD is not a supported HTTP method')

def test_invalid_json_payload(self):
mock_req = mock_request(b"{ invalid")
# test POST requests
mock_req = mock_post_request(b"{ invalid")
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32700)

mock_req = mock_request(json.dumps({"some": "stuff"}).encode("utf-8"))
mock_req = mock_post_request(json.dumps({"some": "stuff"}).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)

def _gen_rpc_req(self, method, params=None, request_id="2"):
# test GET requests
mock_req = mock_get_request(b"/?%20invalid") # equivalent to "/? invalid"
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)

mock_req = mock_get_request(b"/?some=stuff")
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)

def _gen_post_rpc_req(self, method, params=None, request_id="2"):
ret = {
"jsonrpc": "2.0",
"id": request_id,
Expand All @@ -49,47 +84,90 @@ def _gen_rpc_req(self, method, params=None, request_id="2"):
ret["params"] = params
return ret

def _gen_get_rpc_req(self, method, params=None, request="2"):
ret = "/?jsonrpc=2.0&method=%s&params=[]&id=%s" % (method, request)
if params:
ret = "/?jsonrpc=2.0&method=%s&params=%s&id=%s" % (method, params, request)
return ret.encode('utf-8')

def test_initial_setup(self):
self.assertTrue(GetBlockchain().GetBlock(0).Hash.To0xString(), '0x996e37358dc369912041f966f8c5d8d3a8255ba5dcbd3447f8a82b55db869099')

def test_missing_fields(self):
req = self._gen_rpc_req("foo")
# test POST requests
req = self._gen_post_rpc_req("foo")
del req["jsonrpc"]
mock_req = mock_request(json.dumps(req).encode("utf-8"))
mock_req = mock_post_request(json.dumps(req).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)
self.assertEqual(res["error"]["message"], "Invalid value for 'jsonrpc'")

req = self._gen_rpc_req("foo")
req = self._gen_post_rpc_req("foo")
del req["id"]
mock_req = mock_request(json.dumps(req).encode("utf-8"))
mock_req = mock_post_request(json.dumps(req).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)
self.assertEqual(res["error"]["message"], "Field 'id' is missing")

req = self._gen_rpc_req("foo")
req = self._gen_post_rpc_req("foo")
del req["method"]
mock_req = mock_request(json.dumps(req).encode("utf-8"))
mock_req = mock_post_request(json.dumps(req).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)
self.assertEqual(res["error"]["message"], "Field 'method' is missing")

# test GET requests
mock_req = mock_get_request(b"/?method=foo&id=2")
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)
self.assertEqual(res["error"]["message"], "Invalid value for 'jsonrpc'")

mock_req = mock_get_request(b"/?jsonrpc=2.0&method=foo")
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)
self.assertEqual(res["error"]["message"], "Field 'id' is missing")

mock_req = mock_get_request(b"/?jsonrpc=2.0&id=2")
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32600)
self.assertEqual(res["error"]["message"], "Field 'method' is missing")

def test_invalid_method(self):
req = self._gen_rpc_req("invalid", request_id="42")
mock_req = mock_request(json.dumps(req).encode("utf-8"))
# test POST requests
req = self._gen_post_rpc_req("invalid", request_id="42")
mock_req = mock_post_request(json.dumps(req).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["id"], "42")
self.assertEqual(res["error"]["code"], -32601)
self.assertEqual(res["error"]["message"], "Method not found")

# test GET requests
req = self._gen_get_rpc_req("invalid")
mock_req = mock_get_request(req)
res = json.loads(self.app.home(mock_req))
self.assertEqual(res["error"]["code"], -32601)
self.assertEqual(res["error"]["message"], "Method not found")

def test_get_node_state(self):
req = self._gen_rpc_req("getnodestate")
mock_req = mock_request(json.dumps(req).encode("utf-8"))
# test POST requests
req = self._gen_post_rpc_req("getnodestate")
mock_req = mock_post_request(json.dumps(req).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
self.assertGreater(res['result']['Progress'][0], 0)
self.assertGreater(res['result']['Progress'][2], 0)
self.assertGreater(res['result']['Time elapsed (minutes)'], 0)

# test GET requests
req = self._gen_get_rpc_req("getnodestate")
mock_req = mock_get_request(req)
res = json.loads(self.app.home(mock_req))
self.assertGreater(res['result']['Progress'][0], 0)
self.assertGreater(res['result']['Progress'][2], 0)
self.assertGreater(res['result']['Time elapsed (minutes)'], 0)

def test_gettxhistory_no_wallet(self):
req = self._gen_rpc_req("gettxhistory")
mock_req = mock_request(json.dumps(req).encode("utf-8"))
req = self._gen_post_rpc_req("gettxhistory")
mock_req = mock_post_request(json.dumps(req).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
error = res.get('error', {})
self.assertEqual(error.get('code', None), -400)
Expand All @@ -104,8 +182,8 @@ def test_gettxhistory(self):
test_wallet_path,
to_aes_key(WalletFixtureTestCase.wallet_1_pass())
)
req = self._gen_rpc_req("gettxhistory")
mock_req = mock_request(json.dumps(req).encode("utf-8"))
req = self._gen_post_rpc_req("gettxhistory")
mock_req = mock_post_request(json.dumps(req).encode("utf-8"))
res = json.loads(self.app.home(mock_req))
for tx in res['result']:
self.assertIn('txid', tx.keys())
Expand Down
Loading