Skip to content

Commit

Permalink
[base] Stricter input (backward incompatible) #40
Browse files Browse the repository at this point in the history
Whether network graph data is passed directly or fetched
from a URL or a file should be explicit in order to avoid
opening security holes in the system.

Closes #40
  • Loading branch information
nemesifier committed Aug 30, 2017
1 parent 42484a9 commit aa81003
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 95 deletions.
38 changes: 18 additions & 20 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ Calculate diff of an OLSR 0.6.x topology:
from netdiff import OlsrParser
from netdiff import diff
old = OlsrParser('./stored-olsr.json')
new = OlsrParser('http://127.0.0.1:9090')
old = OlsrParser(file='./stored-olsr.json')
new = OlsrParser(url='http://127.0.0.1:9090')
diff(old, new)
In alternative, you may also use the subtraction operator:
Expand All @@ -92,8 +92,8 @@ In alternative, you may also use the subtraction operator:
from netdiff import OlsrParser
from netdiff import diff
old = OlsrParser('./stored-olsr.json')
new = OlsrParser('http://127.0.0.1:9090')
old = OlsrParser(file='./stored-olsr.json')
new = OlsrParser(url='http://127.0.0.1:9090')
old - new
The output will be an ordered dictionary with three keys:
Expand Down Expand Up @@ -210,18 +210,17 @@ The available parsers are:
Initialization arguments
~~~~~~~~~~~~~~~~~~~~~~~~

**data**: the only required argument, different inputs are accepted:
Data can be supplied in 3 different ways, in the following order of precedence:

* JSON formatted string representing the topology
* python `dict` (or subclass of `dict`) representing the topology
* string representing a HTTP URL where the data resides
* string representing a telnet URL where the data resides
* string representing a file path where the data resides
* ``data``: ``dict`` or ``str`` representing the topology/graph
* ``url``: URL to fetch data from
* ``file``: file path to retrieve data from

**timeout**: integer representing timeout in seconds for HTTP or telnet requests, defaults to None
Other available arguments:

**verify**: boolean indicating to the `request library whether to do SSL certificate
verification or not <http://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification>`_
* **timeout**: integer representing timeout in seconds for HTTP or telnet requests, defaults to ``None``
* **verify**: boolean indicating to the `request library whether to do SSL certificate
verification or not <http://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification>`_

Initialization examples
~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -231,41 +230,40 @@ Local file example:
.. code-block:: python
from netdiff import BatmanParser
BatmanParser('./my-stored-topology.json')
BatmanParser(file='./my-stored-topology.json')
HTTP example:

.. code-block:: python
from netdiff import NetJsonParser
url = 'https://raw.githubusercontent.com/interop-dev/netjson/master/examples/network-graph.json'
NetJsonParser(url)
NetJsonParser(url=url)
Telnet example with ``timeout``:

.. code-block:: python
from netdiff import OlsrParser
OlsrParser('telnet://127.0.1:8080', timeout=5)
OlsrParser(url='telnet://127.0.1', timeout=5)
HTTPS example with self-signed SSL certificate using ``verify=False``:

.. code-block:: python
from netdiff import NetJsonParser
OlsrParser('https://myserver.mydomain.com/topology.json', verify=False)
OlsrParser(url='https://myserver.mydomain.com/topology.json', verify=False)
NetJSON output
--------------

Netdiff parsers can return a valid `NetJSON`_
``NetworkGraph`` object:
Netdiff parsers can return a valid `NetJSON`_ ``NetworkGraph`` object:

.. code-block:: python
from netdiff import OlsrParser
olsr = OlsrParser('telnet://127.0.0.1:9090')
olsr = OlsrParser(url='telnet://127.0.0.1:9090')
# will return a dict
olsr.json(dict=True)
Expand Down
52 changes: 21 additions & 31 deletions netdiff/parsers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ class BaseParser(object):
revision = None
metric = None

def __init__(self, data, version=None, revision=None, metric=None,
def __init__(self, data=None, url=None, file=None,
version=None, revision=None, metric=None,
timeout=None, verify=True): # noqa
"""
Initializes a new Parser
:param data: JSON, dict, path to file or HTTP URL of topology
:param data: ``str`` or ``dict`` containing topology data
:param url: HTTP URL to retrieve topology data
:param file: path to file containing topology data
:param version: routing protocol version
:param revision: routing protocol revision
:param metric: routing protocol metric
Expand All @@ -43,11 +46,25 @@ def __init__(self, data, version=None, revision=None, metric=None,
self.metric = metric
self.timeout = timeout
self.verify = verify
if data is None and url is not None:
data = self._get_url(url)
elif data is None and file is not None:
data = self._get_file(file)
elif data is None and url is None and file is None:
raise ValueError('no topology data supplied, on of the following arguments'
'must be supplied: data, url or file')
self.original_data = self.to_python(data)
# avoid throwing NotImplementedError in tests
if self.__class__ is not BaseParser:
self.graph = self.parse(self.original_data)

def _get_url(self, url):
url = urlparse.urlparse(url)
if url.scheme in ['http', 'https']:
return self._get_http(url)
if url.scheme == 'telnet':
return self._get_telnet(url)

def __sub__(self, other):
return diff(other, self)

Expand All @@ -61,42 +78,15 @@ def to_python(self, data):
* a JSON formatted string
* a dict representing a JSON structure
"""
data = self._retrieve_data(data)
if isinstance(data, dict):
return data
elif isinstance(data, six.string_types):
# assuming is JSON
try:
return json.loads(data)
except ValueError:
raise ConversionException('Could not recognize format', data=data)
else:
raise ConversionException('Could not recognize format', data=data)

def _retrieve_data(self, data):
"""
if recognizes a URL or a path
tries to retrieve and returns data
otherwise will return data as is
"""
if isinstance(data, six.string_types):
# if it looks like URL
if '://' in data:
url = urlparse.urlparse(data)
if url.scheme in ['http', 'https']:
return self._get_http(url)
if url.scheme == 'telnet':
return self._get_telnet(url)
# if it looks like a path
elif True in [data.startswith('./'),
data.startswith('../'),
data.startswith('/'),
data.startswith('.\\'),
data.startswith('..\\'),
data.startswith('\\'),
data[1:3].startswith(':\\')]:
return self._get_file(data)
return data
pass
raise ConversionException('Could not recognize format', data=data)

def _get_file(self, path):
try:
Expand Down
58 changes: 26 additions & 32 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,88 +26,82 @@ def _load_contents(self, file):
def test_version(self):
get_version()

def test_no_data_supplied(self):
with self.assertRaises(ValueError):
BaseParser()

def test_parse_file(self):
dir = os.path.dirname(os.path.realpath(__file__))
path = '{0}/static/olsr-2-links.json'.format(dir)
p = BaseParser(path)
p = BaseParser(file=path)
self.assertIsInstance(p.original_data, dict)
with self.assertRaises(TopologyRetrievalError):
BaseParser('../wrong.json')
BaseParser(file='../wrong.json')

@responses.activate
def test_parse_http(self):
responses.add(
responses.GET,
'http://localhost:9090',
body=self._load_contents('tests/static/olsr-2-links.json')
)
p = BaseParser('http://localhost:9090')
responses.add(responses.GET, 'http://localhost:9090',
body=self._load_contents('tests/static/olsr-2-links.json'))
p = BaseParser(url='http://localhost:9090')
self.assertIsInstance(p.original_data, dict)

@responses.activate
def test_topology_retrieval_error_http_404(self):
responses.add(
responses.GET,
'http://404.com',
body='not found',
status=404
)
responses.add(responses.GET, 'http://404.com',
body='not found', status=404)
with self.assertRaises(TopologyRetrievalError):
BaseParser('http://404.com')
BaseParser(url='http://404.com')

@responses.activate
def test_topology_retrieval_error_http(self):
def request_callback(request):
raise ConnectionError('test exception')
responses.add_callback(
responses.GET,
'http://connectionerror.com',
callback=request_callback,
)
responses.add_callback(responses.GET, 'http://connectionerror.com',
callback=request_callback)
with self.assertRaises(TopologyRetrievalError):
BaseParser('http://connectionerror.com')
BaseParser(url='http://connectionerror.com')

@mock.patch('telnetlib.Telnet')
def test_telnet_retrieval_error(self, MockClass):
telnetlib.Telnet.side_effect=ValueError('testing exception')
telnetlib.Telnet.side_effect = ValueError('testing exception')
with self.assertRaises(TopologyRetrievalError):
BaseParser('telnet://wrong.com')
BaseParser(url='telnet://wrong.com')

@mock.patch('telnetlib.Telnet')
def test_telnet_retrieval(self, MockClass):
with self.assertRaises(ConversionException):
BaseParser('telnet://127.0.0.1')
BaseParser(url='telnet://127.0.0.1')

def test_topology_retrieval_error_file(self):
with self.assertRaises(TopologyRetrievalError):
BaseParser('./tests/static/wrong.json')
BaseParser(file='./tests/static/wrong.json')

def test_parse_json_string(self):
p = BaseParser('{}')
p = BaseParser(data='{}')
self.assertIsInstance(p.original_data, dict)
p = BaseParser(u'{}')
p = BaseParser(data=u'{}')
self.assertIsInstance(p.original_data, dict)

def test_parse_dict(self):
p = BaseParser({})
p = BaseParser(data={})
self.assertIsInstance(p.original_data, dict)

def test_parse_conversion_exception(self):
with self.assertRaises(ConversionException):
BaseParser('wrong [] ; .')
BaseParser(data='wrong [] ; .')

def test_parse_error(self):
with self.assertRaises(ConversionException):
BaseParser(8)
BaseParser(data=8)

def test_parse_not_implemented(self):
class MyParser(BaseParser):
pass
with self.assertRaises(NotImplementedError):
MyParser('{}')
MyParser(data='{}')

def test_json_not_implemented(self):
p = BaseParser('{}')
p = BaseParser(data='{}')
with self.assertRaises(NotImplementedError):
p.json()

Expand Down
4 changes: 2 additions & 2 deletions tests/test_netjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@


CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
links2 = '{0}/static/netjson-2-links.json'.format(CURRENT_DIR)
links3 = '{0}/static/netjson-3-links.json'.format(CURRENT_DIR)
links2 = open('{0}/static/netjson-2-links.json'.format(CURRENT_DIR)).read()
links3 = open('{0}/static/netjson-3-links.json'.format(CURRENT_DIR)).read()


class TestNetJsonParser(TestCase):
Expand Down
10 changes: 5 additions & 5 deletions tests/test_olsr.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@


CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
links2 = '{0}/static/olsr-2-links.json'.format(CURRENT_DIR)
links2_cost = '{0}/static/olsr-2-links-cost-changed.json'.format(CURRENT_DIR)
links3 = '{0}/static/olsr-3-links.json'.format(CURRENT_DIR)
links5 = '{0}/static/olsr-5-links.json'.format(CURRENT_DIR)
links5_cost = '{0}/static/olsr-5-links-cost-changed.json'.format(CURRENT_DIR)
links2 = open('{0}/static/olsr-2-links.json'.format(CURRENT_DIR)).read()
links2_cost = open('{0}/static/olsr-2-links-cost-changed.json'.format(CURRENT_DIR)).read()
links3 = open('{0}/static/olsr-3-links.json'.format(CURRENT_DIR)).read()
links5 = open('{0}/static/olsr-5-links.json'.format(CURRENT_DIR)).read()
links5_cost = open('{0}/static/olsr-5-links-cost-changed.json'.format(CURRENT_DIR)).read()


class TestOlsrParser(TestCase):
Expand Down
8 changes: 4 additions & 4 deletions tests/test_olsr_txtinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@


CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
links2 = '{0}/static/olsr-2-links.txt'.format(CURRENT_DIR)
links2_cost = '{0}/static/olsr-2-links-cost-changed.txt'.format(CURRENT_DIR)
links3 = '{0}/static/olsr-3-links.txt'.format(CURRENT_DIR)
links5 = '{0}/static/olsr-5-links.txt'.format(CURRENT_DIR)
links2 = open('{0}/static/olsr-2-links.txt'.format(CURRENT_DIR)).read()
links2_cost = open('{0}/static/olsr-2-links-cost-changed.txt'.format(CURRENT_DIR)).read()
links3 = open('{0}/static/olsr-3-links.txt'.format(CURRENT_DIR)).read()
links5 = open('{0}/static/olsr-5-links.txt'.format(CURRENT_DIR)).read()


class TestOlsrTxtinfoParser(TestCase):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
links2 = '{0}/static/netjson-2-links.json'.format(CURRENT_DIR)
links2 = open('{0}/static/netjson-2-links.json'.format(CURRENT_DIR)).read()


class TestUtils(TestCase):
Expand Down

0 comments on commit aa81003

Please sign in to comment.