Repository: mbr/tinyrpc

Issue:
@lnoor, I now see a problem. `JSONRPCProtocol` class doesn't verify response IDs:
@lnoor, I now see a problem. `JSONRPCProtocol` class doesn't verify response IDs:
https://github.com/mbr/tinyrpc/blob/fb7490ef098d3ec42cb756ae97887300ddbccc65/tinyrpc/protocols/jsonrpc.py#L572

Theoretically it should reject responses with unexpected IDs.

_Originally posted by @AndreiPashkin in https://github.com/mbr/tinyrpc/issues/69#issuecomment-472361973_

Discussion:

Patch:
diff --git a/tinyrpc/exc.py b/tinyrpc/exc.py
index d03d42f..d051896 100644
--- a/tinyrpc/exc.py
+++ b/tinyrpc/exc.py
@@ -34,6 +34,10 @@ class InvalidReplyError(BadReplyError, ABC):
     could not be parsed into a response."""
 
 
+class UnexpectedIDError (InvalidReplyError, ABC):
+    """A reply received contained an invalid unique identifier."""
+
+
 class MethodNotFoundError(RPCError, ABC):
     """The desired method was not found."""
 
diff --git a/tinyrpc/protocols/jsonrpc.py b/tinyrpc/protocols/jsonrpc.py
index 8e68054..609a69a 100644
--- a/tinyrpc/protocols/jsonrpc.py
+++ b/tinyrpc/protocols/jsonrpc.py
@@ -12,6 +12,7 @@ Tinyrpc will detect the presence of jsonext and use it automatically.
 
 import json
 import sys
+from tinyrpc.exc import UnexpectedIDError
 from typing import Dict, Any, Union, Optional, List, Tuple, Callable, Generator
 
 from . import default_id_generator
@@ -514,6 +515,7 @@ class JSONRPCProtocol(RPCBatchProtocol):
     ) -> None:
         super(JSONRPCProtocol, self).__init__(*args, **kwargs)
         self._id_generator = id_generator
+        self._pending_replies = []
 
     def _get_unique_id(self) -> object:
         return next(self._id_generator)
@@ -573,6 +575,7 @@ class JSONRPCProtocol(RPCBatchProtocol):
 
         if not one_way:
             request.unique_id = self._get_unique_id()
+            self._pending_replies.append(request.unique_id)
 
         request.method = method
         if args is not None:
@@ -634,6 +637,12 @@ class JSONRPCProtocol(RPCBatchProtocol):
             response.result = rep.get('result', None)
 
         response.unique_id = rep['id']
+        if response.unique_id not in self._pending_replies:
+            raise UnexpectedIDError(
+                'Reply id does not correspond to any sent requests.'
+            )
+        else:
+            self._pending_replies.remove(response.unique_id)
 
         return response
 


Test patch:
diff --git a/tests/test_jsonrpc.py b/tests/test_jsonrpc.py
index 2a3fb8b..dd53882 100644
--- a/tests/test_jsonrpc.py
+++ b/tests/test_jsonrpc.py
@@ -107,12 +107,25 @@ def test_parsing_invalid_arguments(prot):
     ),
 ])
 def test_good_reply_samples(prot, data, id, result):
+    # assume the protocol is awaiting a response for
+    # a request with `id`
+    prot._pending_replies = [id]
+    
     reply = prot.parse_reply(data)
 
     assert reply.unique_id == id
     assert reply.result == result
 
 
+@pytest.mark.parametrize(('data'), [
+    """{"jsonrpc": "2.0", "result": 19, "id": 9001}"""
+])
+def test_unsolicited_reply_raises_error(prot, data):
+    prot._pending_replies = [4]
+    with pytest.raises(InvalidReplyError):
+        reply = prot.parse_reply(data)
+
+
 @pytest.mark.parametrize(('exc', 'code', 'message'), [
     (JSONRPCParseError, -32700, 'Parse error'),
     (JSONRPCInvalidRequestError, -32600, 'Invalid Request'),