Skip to content
This repository has been archived by the owner on Nov 15, 2021. It is now read-only.

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

Merged
merged 41 commits into from
Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
13e979b
Merge pull request #38 from CityOfZion/development
jseagrave21 Oct 10, 2018
e41e950
Merge pull request #40 from CityOfZion/development
jseagrave21 Oct 11, 2018
7db54f4
Merge pull request #41 from CityOfZion/development
jseagrave21 Oct 11, 2018
54a4f3f
Fix ExtendedJsonRpcApi (#662)
jseagrave21 Oct 11, 2018
0f96dc3
Mute expected test stacktrace and clearly identify why an exception i…
ixje Oct 12, 2018
1e5b4dc
Add guideline for adding tests to the neo-privnet-unittest image (#661)
dauTT Oct 12, 2018
8febcf1
Update CHANGELOG.rst
jseagrave21 Oct 18, 2018
c7a790f
Update CHANGELOG.rst
jseagrave21 Oct 18, 2018
c51390e
Merge CoZ Development into jseagrave21 Development (#49)
jseagrave21 Oct 18, 2018
32a16e3
Merge pull request #50 from CityOfZion/development
jseagrave21 Oct 18, 2018
3c8232d
Merge pull request #53 from CityOfZion/development
jseagrave21 Oct 25, 2018
1b00db8
Merge pull request #54 from CityOfZion/development
jseagrave21 Oct 26, 2018
eb495bf
Merge pull request #58 from CityOfZion/development
jseagrave21 Oct 28, 2018
4df642e
Merge pull request #63 from CityOfZion/development
jseagrave21 Nov 1, 2018
e12e746
Update test_extended_json_rpc_api.py
jseagrave21 Nov 1, 2018
0f55903
Update test_extended_json_rpc_api.py
jseagrave21 Nov 1, 2018
7eb4a1e
Merge pull request #64 from CityOfZion/development
jseagrave21 Nov 8, 2018
ec42908
Update requirements.txt
jseagrave21 Nov 8, 2018
bfbc2cd
Update JsonRpcApi.py
jseagrave21 Nov 8, 2018
42f5160
Update test_json_rpc_api.py
jseagrave21 Nov 8, 2018
02aa2b8
Update test_extended_json_rpc_api.py
jseagrave21 Nov 8, 2018
ef8db46
Update test_json_invoke_rpc_api.py
jseagrave21 Nov 8, 2018
2cda293
Update CHANGELOG.rst
jseagrave21 Nov 8, 2018
1386c89
Update test_json_rpc_api.py
jseagrave21 Nov 8, 2018
1264b25
Update JsonRpcApi.py
jseagrave21 Nov 9, 2018
d487d1a
Update test_json_rpc_api.py
jseagrave21 Nov 9, 2018
29df360
Update CHANGELOG.rst
jseagrave21 Nov 9, 2018
26cba32
Merge pull request #65 from CityOfZion/development
jseagrave21 Nov 9, 2018
122347f
Update CHANGELOG.rst
jseagrave21 Nov 9, 2018
1aac551
Update JsonRpcApi.py
jseagrave21 Nov 10, 2018
c4f68ba
Update test_json_rpc_api.py
jseagrave21 Nov 10, 2018
034957b
Update ExtendedJsonRpcApi.py
jseagrave21 Nov 10, 2018
f6c2d22
Update test_extended_json_rpc_api.py
jseagrave21 Nov 10, 2018
cbbdd09
Update JsonRpcApi.py
jseagrave21 Nov 10, 2018
0385de6
Update test_json_rpc_api.py
jseagrave21 Nov 10, 2018
d526b6c
Update test_extended_json_rpc_api.py
jseagrave21 Nov 10, 2018
0222e80
Update test_json_invoke_rpc_api.py
jseagrave21 Nov 10, 2018
1079f5b
Update CHANGELOG.rst
jseagrave21 Nov 19, 2018
0b3f450
Update CHANGELOG.rst
jseagrave21 Nov 19, 2018
2b1ba19
Merge pull request #71 from CityOfZion/development
jseagrave21 Nov 19, 2018
a3fe3aa
Update CHANGELOG.rst
jseagrave21 Nov 19, 2018
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