├── tests
├── __init__.py
├── test_wifi.py
├── test_ntp.py
├── test_http.py
└── test_ping.py
├── docs
├── requirements.txt
├── changelog.rst
├── contributing.rst
├── .readthedocs.yaml
├── index.rst
├── installation.rst
├── Makefile
├── make.bat
├── use.rst
└── conf.py
├── setup.cfg
├── MANIFEST.in
├── tox.ini
├── ripe
├── atlas
│ ├── sagan
│ │ ├── helpers
│ │ │ ├── __init__.py
│ │ │ └── compatibility.py
│ │ ├── version.py
│ │ ├── __init__.py
│ │ ├── wifi.py
│ │ ├── http.py
│ │ ├── ping.py
│ │ ├── ntp.py
│ │ ├── traceroute.py
│ │ ├── ssl.py
│ │ ├── base.py
│ │ └── dns.py
│ └── __init__.py
└── __init__.py
├── .gitignore
├── .github
└── workflows
│ ├── test.yml
│ └── python-package.yml
├── setup.py
├── CONTRIBUTING.rst
├── CHANGES.rst
└── README.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx-rtd-theme
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = 1
3 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CHANGES.rst
2 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include CHANGES.rst
2 | include LICENSE
3 | include README.rst
4 | include MANIFEST.in
5 | recursive-include ripe *.py
6 | recursive-include tests *.py
7 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # commands=python setup.py test []
2 | # deps=nose
3 |
4 | [testenv]
5 | deps =
6 | flake8
7 | pytest
8 | commands =
9 | # flake8 --max-line-length=88 setup.py ripe/atlas/sagan/ tests/
10 | pytest {posargs}
11 |
--------------------------------------------------------------------------------
/docs/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: ubuntu-24.04
5 | tools:
6 | python: "3.12"
7 |
8 | sphinx:
9 | configuration: docs/conf.py
10 |
11 | python:
12 | install:
13 | - requirements: docs/requirements.txt
14 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/helpers/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/version.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | __version__ = "2.0.0"
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | bin/
12 | build/
13 | develop-eggs/
14 | dist/
15 | eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | *.egg-info/
22 | .installed.cfg
23 | *.egg
24 | .eggs
25 |
26 | # Installer logs
27 | pip-log.txt
28 | pip-delete-this-directory.txt
29 |
30 | # Unit test / coverage reports
31 | htmlcov/
32 | .tox/
33 | .coverage
34 | .cache
35 | nosetests.xml
36 | coverage.xml
37 |
38 | # Translations
39 | *.mo
40 |
41 | # Mr Developer
42 | .mr.developer.cfg
43 | .project
44 | .pydevproject
45 |
46 | # Rope
47 | .ropeproject
48 |
49 | # Django stuff:
50 | *.log
51 | *.pot
52 |
53 | # Sphinx documentation
54 | docs/_build/
55 |
56 | # PyCharm
57 | .idea
58 |
59 |
--------------------------------------------------------------------------------
/ripe/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | try:
17 | __import__('pkg_resources').declare_namespace(__name__)
18 | except ImportError:
19 | from pkgutil import extend_path
20 | __path__ = extend_path(__path__, __name__)
21 |
--------------------------------------------------------------------------------
/ripe/atlas/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | try:
17 | __import__('pkg_resources').declare_namespace(__name__)
18 | except ImportError:
19 | from pkgutil import extend_path
20 | __path__ = extend_path(__path__, __name__)
21 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/helpers/compatibility.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | """
17 | We put stuff here to help cope with differences between Python versions.
18 | """
19 |
20 | try:
21 | string = basestring # Python2
22 | except NameError:
23 | string = str # Python3
24 |
25 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python package
5 |
6 | on:
7 | push:
8 | branches: [ '*' ]
9 | pull_request:
10 | branches: [ $default-branch ]
11 |
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | python-version: ["3.10", "3.11", "3.12", "3.13"]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | python -m pip install tox
30 | - name: tox
31 | run: tox
32 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. RIPE Atlas Sagan documentation master file, created by
2 | sphinx-quickstart on Tue Apr 29 13:41:57 2014.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to RIPE Atlas Sagan's documentation!
7 | ********************************************
8 |
9 | A parsing library for RIPE Atlas measurement results
10 |
11 | .. _index-why-this-exists:
12 |
13 | Why This Exists
14 | ===============
15 |
16 | RIPE Atlas generates a **lot** of data, and the format of that data changes over
17 | time. Often you want to do something simple like fetch the median RTT for each
18 | measurement result between date `X` and date `Y`. Unfortunately, there are are
19 | dozens of edge cases to account for while parsing the JSON, like the format of
20 | errors and firmware upgrades that changed the format entirely.
21 |
22 | To make this easier for our users (and for ourselves), we wrote an easy to use
23 | parser that's smart enough to figure out the best course of action for each
24 | result, and return to you a useful, native Python object.
25 |
26 | Contents:
27 |
28 | .. toctree::
29 | :maxdepth: 2
30 |
31 | installation
32 | use
33 | types
34 | contributing
35 | changelog
36 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from __future__ import absolute_import
17 |
18 | from .base import Result, ResultError, ResultParseError
19 | from .dns import DnsResult
20 | from .http import HttpResult
21 | from .ping import PingResult
22 | from .ssl import SslResult
23 | from .traceroute import TracerouteResult
24 | from .ntp import NtpResult
25 |
26 | from .version import __version__
27 |
28 | __all__ = (
29 | "Result",
30 | "PingResult",
31 | "TracerouteResult",
32 | "DnsResult",
33 | "SslResult",
34 | "HttpResult",
35 | "NtpResult",
36 | )
37 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/wifi.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from .base import Result, ParsingDict
17 |
18 |
19 | class WPASupplicant(ParsingDict):
20 |
21 | def __init__(self, data, **kwargs):
22 |
23 | ParsingDict.__init__(self, **kwargs)
24 |
25 | self.address = data.get("address")
26 | self.bssid = data.get("bssid")
27 | self.connect_time = data.get("connect-time")
28 | self.group_cipher = data.get("group_cipher")
29 | self.wpa_supplicant_id = data.get("id")
30 | self.ip_address = data.get("ip_address")
31 | self.key_management = data.get("key_mgmt")
32 | self.mode = data.get("mode")
33 | self.pairwise_cipher = data.get("pairwise_cipher")
34 | self.ssid = data.get("ssid")
35 | self.wpa_state = data.get("wpa_state")
36 |
37 |
38 | class WiFiResult(Result):
39 | """
40 | WiFi measurement result class
41 | """
42 |
43 | def __init__(self, data, **kwargs):
44 |
45 | Result.__init__(self, data, **kwargs)
46 |
47 | self.wpa_supplicant = WPASupplicant(
48 | data["wpa_supplicant"], **kwargs
49 | )
50 |
51 | __all__ = (
52 | "WiFiResult",
53 | )
54 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from os.path import abspath, dirname, join
3 | from setuptools import setup
4 |
5 | __version__ = None
6 | exec(open("ripe/atlas/sagan/version.py").read())
7 |
8 | name = "ripe.atlas.sagan"
9 | install_requires = [
10 | "python-dateutil",
11 | "pytz",
12 | "cryptography",
13 | ]
14 |
15 | # Allow setup.py to be run from any path
16 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
17 |
18 | # Get proper long description for package
19 | current_dir = dirname(abspath(__file__))
20 | description = open(join(current_dir, "README.rst")).read()
21 | changes = open(join(current_dir, "CHANGES.rst")).read()
22 | long_description = "\n\n".join([description, changes])
23 |
24 | setup(
25 | name="ripe.atlas.sagan",
26 | version=__version__,
27 | packages=["ripe", "ripe.atlas", "ripe.atlas.sagan"],
28 | namespace_packages=["ripe", "ripe.atlas"],
29 | include_package_data=True,
30 | license="GPLv3",
31 | description="A parser for RIPE Atlas measurement results",
32 | long_description=long_description,
33 | url="https://github.com/RIPE-NCC/ripe.atlas.sagan",
34 | download_url="https://github.com/RIPE-NCC/ripe.atlas.sagan",
35 | author="The RIPE Atlas Team",
36 | author_email="atlas@ripe.net",
37 | maintainer="The RIPE Atlas Team",
38 | maintainer_email="atlas@ripe.net",
39 | install_requires=install_requires,
40 | extras_require={
41 | "fast": ["ujson"],
42 | "doc": ["sphinx"]
43 | },
44 | classifiers=[
45 | "Operating System :: POSIX",
46 | "Operating System :: Unix",
47 | "Programming Language :: Python",
48 | "Programming Language :: Python :: 3.10",
49 | "Programming Language :: Python :: 3.11",
50 | "Programming Language :: Python :: 3.12",
51 | "Programming Language :: Python :: 3.13",
52 | "Topic :: Internet :: WWW/HTTP",
53 | ],
54 | )
55 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. _requirements-and-installation:
2 |
3 | Requirements & Installation
4 | ***************************
5 |
6 | .. _installation-requirements:
7 |
8 | Requirements
9 | ============
10 |
11 | As you might have guessed, with all of the magic going on under the hood, there
12 | are a few dependencies:
13 |
14 | * `cryptography`_
15 | * `python-dateutil`_
16 | * `pytz`_
17 |
18 | Additionally, we recommend that you also install `ujson`_ as it will speed up
19 | the JSON-decoding step considerably, and `sphinx`_ if you intend to build the
20 | documentation files for offline use.
21 |
22 | .. _cryptography: https://pypi.python.org/pypi/cryptography
23 | .. _python-dateutil: https://pypi.python.org/pypi/python-dateutil/
24 | .. _pytz: https://pypi.python.org/pypi/pytz/
25 | .. _ujson: https://pypi.python.org/pypi/ujson/
26 | .. _sphinx: https://pypi.python.org/pypi/Sphinx/
27 |
28 |
29 | .. _installation:
30 |
31 | Installation
32 | ============
33 |
34 | Installation should be easy, though it may take a while to install all of the
35 | aforementioned requirements. Using pip is the recommended method.
36 |
37 |
38 | .. _installation-from-pip:
39 |
40 | Using pip
41 | ---------
42 |
43 | The quickest and easiest way to install Sagan is to use ``pip``::
44 |
45 | $ pip install ripe.atlas.sagan
46 |
47 |
48 | .. _installation-from-github:
49 |
50 | From GitHub
51 | -----------
52 |
53 | If you're feeling a little more daring and want to use whatever is on GitHub,
54 | you can have pip install right from there::
55 |
56 | $ pip install git+https://github.com/RIPE-NCC/ripe.atlas.sagan.git
57 |
58 |
59 | .. _installation-from-tarball:
60 |
61 | From a Tarball
62 | --------------
63 |
64 | If for some reason you want to just download the source and install it manually,
65 | you can always do that too. Simply un-tar the file and run the following in the
66 | same directory as ``setup.py``.::
67 |
68 | $ python setup.py install
69 |
--------------------------------------------------------------------------------
/.github/workflows/python-package.yml:
--------------------------------------------------------------------------------
1 | # This workflow runs whenever a new tag is pushed.
2 | # It will install Python dependencies, run tests and lint with a variety of Python versions
3 | # If tests pass, it will build the package and publish it on pypi.org
4 |
5 | name: Python package
6 |
7 | on:
8 | push:
9 | tags:
10 | - 'v*'
11 |
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | python-version: ["3.10", "3.11", "3.12", "3.13"]
19 |
20 | steps:
21 | - uses: actions/checkout@v5
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v5
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | python -m pip install tox
30 | - name: tox
31 | run: tox
32 |
33 | build:
34 | name: Build package for PyPI
35 | runs-on: ubuntu-latest
36 | needs: test
37 | steps:
38 | - uses: actions/checkout@v5
39 | - name: Set up Python ${{ matrix.python-version }}
40 | uses: actions/setup-python@v5
41 | with:
42 | python-version: '3.11'
43 | - name: Build release
44 | run: |
45 | python -m pip install --upgrade build
46 | python -m build
47 | - name: Upload dist
48 | uses: actions/upload-artifact@v4
49 | with:
50 | name: release-dists
51 | path: dist/
52 |
53 | publish:
54 | name: Publish package to PyPI
55 | runs-on: ubuntu-latest
56 | needs: build
57 | steps:
58 | - name: Retrieve dist
59 | uses: actions/download-artifact@v5
60 | with:
61 | name: release-dists
62 | path: dist/
63 | - name: Push to pypi
64 | # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
65 | uses: pypa/gh-action-pypi-publish@release/v1.13
66 | with:
67 | user: __token__
68 | password: ${{ secrets.PYPI_API_TOKEN }}
69 | verbose: true
70 |
71 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | How To Contribute
2 | =================
3 |
4 | We would love to have contributions from everyone and no contribution is too
5 | small. Please submit as many fixes for typos and grammar bloopers as you can!
6 |
7 | To make participation in this project as pleasant as possible for everyone,
8 | we adhere to the `Code of Conduct`_ by the Python Software Foundation.
9 |
10 | The following steps will help you get started:
11 |
12 | Fork, then clone the repo:
13 |
14 | .. code:: bash
15 |
16 | $ git clone git@github.com:your-username/ripe.atlas.sagan.git
17 |
18 | Make sure the tests pass beforehand:
19 |
20 | .. code:: bash
21 |
22 | $ tox
23 |
24 | or
25 |
26 | .. code:: bash
27 |
28 | $ nosetests tests/
29 |
30 | Make your changes. Include tests for your change. Make the tests pass:
31 |
32 | .. code:: bash
33 |
34 | $ tox
35 |
36 | or
37 |
38 | .. code:: bash
39 |
40 | $ nosetests tests/
41 |
42 | Push to your fork and `submit a pull request`_.
43 |
44 | Here are a few guidelines that will increase the chances of a quick merge of
45 | your pull request:
46 |
47 | - *Always* try to add tests and docs for your code. If a feature is tested and
48 | documented, it's easier for us to merge it.
49 | - Follow `PEP 8`_.
50 | - Write `good commit messages`_.
51 | - If you change something that is noteworthy, don't forget to add an entry to
52 | the `changes`_.
53 |
54 | .. note::
55 | - If you think you have a great contribution but aren’t sure whether it
56 | adheres -- or even can adhere -- to the rules: **please submit a pull
57 | request anyway**! In the best case, we can transform it into something
58 | usable, in the worst case the pull request gets politely closed. There’s
59 | absolutely nothing to fear.
60 | - If you have a great idea but you don't know how or don't have the time to
61 | implement it, please consider opening an issue and someone will pick it up
62 | as soon as possible.
63 |
64 | Thank you for considering a contribution to this project! If you have any
65 | questions or concerns, feel free to reach out the RIPE Atlas team via the
66 | `mailing list`_, `GitHub Issue Queue`_, or `messenger pigeon`_ -- if you must.
67 |
68 | .. _submit a pull request: https://github.com/RIPE-NCC/ripe.atlas.sagan/compare/
69 | .. _PEP 8: https://www.python.org/dev/peps/pep-0008/
70 | .. _good commit messages: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
71 | .. _Code of Conduct: https://www.python.org/psf/codeofconduct/
72 | .. _changes: https://github.com/RIPE-NCC/ripe.atlas.sagan/blob/master/CHANGES.rst
73 | .. _mailing list: https://www.ripe.net/mailman/listinfo/ripe-atlas
74 | .. _GitHub Issue Queue: https://github.com/RIPE-NCC/ripe.atlas.sagan/issues
75 | .. _messenger pigeon: https://tools.ietf.org/html/rfc1149
76 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/http.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from .base import Result, ParsingDict
17 |
18 |
19 | class Response(ParsingDict):
20 |
21 | def __init__(self, data, **kwargs):
22 |
23 | ParsingDict.__init__(self, **kwargs)
24 |
25 | self.raw_data = data
26 | self.af = self.ensure("af", int)
27 | self.body_size = self.ensure("bsize", int)
28 | self.head_size = self.ensure("hsize", int)
29 | self.destination_address = self.ensure("dst_addr", str)
30 | self.source_address = self.ensure("src_addr", str)
31 | self.code = self.ensure("res", int)
32 | self.response_time = self.ensure("rt", float)
33 | self.version = self.ensure("ver", str)
34 |
35 | if not self.destination_address:
36 | self.destination_address = self.ensure(
37 | "addr", str, self.destination_address)
38 |
39 | if not self.source_address:
40 | self.source_address = self.ensure(
41 | "srcaddr", str, self.source_address)
42 |
43 | if not self.code:
44 | self._handle_malformation("No response code available")
45 |
46 | error = self.ensure("err", str)
47 | if error:
48 | self._handle_error(error)
49 |
50 |
51 | class HttpResult(Result):
52 |
53 | METHOD_GET = "GET"
54 | METHOD_POST = "POST"
55 | METHOD_PUT = "PUT"
56 | METHOD_DELETE = "DELETE"
57 | METHOD_HEAD = "HEAD"
58 | METHODS = {
59 | METHOD_GET: "GET",
60 | METHOD_POST: "POST",
61 | METHOD_PUT: "PUT",
62 | METHOD_DELETE: "DELETE",
63 | METHOD_HEAD: "HEAD"
64 | }
65 |
66 | def __init__(self, data, **kwargs):
67 |
68 | Result.__init__(self, data, **kwargs)
69 |
70 | self.uri = self.ensure("uri", str)
71 | self.method = None
72 |
73 | self.responses = []
74 |
75 | if "result" not in self.raw_data:
76 | self._handle_malformation("No result value found")
77 | return
78 |
79 | if isinstance(self.raw_data["result"], list):
80 |
81 | # All modern results
82 |
83 | for response in self.raw_data["result"]:
84 | self.responses.append(Response(response, **kwargs))
85 |
86 | if self.responses:
87 | method = self.raw_data["result"][0].get(
88 | "method",
89 | self.raw_data["result"][0].get("mode") # Firmware == 4300
90 | )
91 | if method:
92 | method = method.replace("4", "").replace("6", "")
93 | if method in self.METHODS.keys():
94 | self.method = self.METHODS[method]
95 |
96 | else:
97 |
98 | # Firmware <= 1
99 |
100 | response = self.raw_data["result"].split(" ")
101 | self.method = response[0].replace("4", "").replace("6", "")
102 | self.responses.append(Response({
103 | "dst_addr": response[1],
104 | "rt": float(response[2]) * 1000,
105 | "res": int(response[3]),
106 | "hsize": int(response[4]),
107 | "bsize": int(response[5]),
108 | }))
109 |
110 |
111 | __all__ = (
112 | "HttpResult"
113 | )
114 |
--------------------------------------------------------------------------------
/tests/test_wifi.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from ripe.atlas.sagan import Result
17 | from ripe.atlas.sagan.wifi import WiFiResult
18 |
19 |
20 | def test_wifi():
21 | raw_data = {
22 | "bundle": 1463495978,
23 | "from": "2001:67c:2e8:ffe1:eade:27ff:fe69:e6f0",
24 | "fw": "4733",
25 | "group_id": 1021275,
26 | "msm_id": 1021275,
27 | "msm_name": "WiFi",
28 | "prb_id": 105,
29 | "timestamp": 1463495978,
30 | "type": "wifi",
31 | "wpa_supplicant": {
32 | "address": "ea:de:27:69:e6:f0",
33 | "bssid": "08:ea:44:3b:6d:14",
34 | "connect-time": "2",
35 | "group_cipher": "CCMP",
36 | "id": "0",
37 | "ip_address": "193.0.10.126",
38 | "key_mgmt": "WPA2-PSK",
39 | "mode": "station",
40 | "pairwise_cipher": "CCMP",
41 | "ssid": "guestnet",
42 | "wpa_state": "COMPLETED"
43 | }
44 | }
45 |
46 | result = Result.get(raw_data)
47 | assert(isinstance(result, WiFiResult))
48 | assert(result.bundle == 1463495978)
49 | assert(result.origin == "2001:67c:2e8:ffe1:eade:27ff:fe69:e6f0")
50 | assert(result.firmware == 4733)
51 | assert(result.group_id == 1021275)
52 | assert(result.wpa_supplicant.address == "ea:de:27:69:e6:f0")
53 | assert(result.wpa_supplicant.bssid == "08:ea:44:3b:6d:14")
54 | assert(result.wpa_supplicant.connect_time == "2")
55 | assert(result.wpa_supplicant.group_cipher == "CCMP")
56 | assert(result.wpa_supplicant.wpa_supplicant_id == "0")
57 | assert(result.wpa_supplicant.ip_address == "193.0.10.126")
58 | assert(result.wpa_supplicant.key_management == "WPA2-PSK")
59 | assert(result.wpa_supplicant.mode == "station")
60 | assert(result.wpa_supplicant.pairwise_cipher == "CCMP")
61 | assert(result.wpa_supplicant.ssid == "guestnet")
62 | assert(result.wpa_supplicant.wpa_state == "COMPLETED")
63 |
64 |
65 | def test_wifi_error():
66 | raw_data = {
67 | "bundle": 1463493022,
68 | "error": "wpa timeout",
69 | "from": "2001:67c:2e8:ffe1:eade:27ff:fe69:e6f0",
70 | "fw": "4733",
71 | "group_id": 1021275,
72 | "msm_id": 1021275,
73 | "msm_name": "WiFi",
74 | "prb_id": 105,
75 | "timestamp": 1463493023,
76 | "type": "wifi",
77 | "wpa_supplicant": {"connect-time": "11"}
78 | }
79 |
80 | result = Result.get(raw_data)
81 | assert(isinstance(result, WiFiResult))
82 | assert(result.bundle == 1463493022)
83 | assert(result.origin == "2001:67c:2e8:ffe1:eade:27ff:fe69:e6f0")
84 | assert(result.firmware == 4733)
85 | assert(result.group_id == 1021275)
86 | assert(result.created_timestamp == 1463493023)
87 | assert(result.wpa_supplicant.address is None)
88 | assert(result.wpa_supplicant.bssid is None)
89 | assert(result.wpa_supplicant.connect_time == "11")
90 | assert(result.wpa_supplicant.group_cipher is None)
91 | assert(result.wpa_supplicant.wpa_supplicant_id is None)
92 | assert(result.wpa_supplicant.ip_address is None)
93 | assert(result.wpa_supplicant.key_management is None)
94 | assert(result.wpa_supplicant.mode is None)
95 | assert(result.wpa_supplicant.pairwise_cipher is None)
96 | assert(result.wpa_supplicant.ssid is None)
97 | assert(result.wpa_supplicant.wpa_state is None)
98 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/ping.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from .base import Result, ResultParseError, ParsingDict
17 |
18 |
19 | class Packet(ParsingDict):
20 |
21 | def __init__(self, data, default_ttl, default_source_address, **kwargs):
22 |
23 | ParsingDict.__init__(self, **kwargs)
24 |
25 | self.rtt = None
26 | self.dup = False
27 | self.ttl = None
28 |
29 | self.source_address = data.get(
30 | "src_addr",
31 | data.get(
32 | "srcaddr", default_source_address
33 | )
34 | )
35 |
36 | if "rtt" in data:
37 | try:
38 | self.rtt = round(float(data["rtt"]), 3)
39 | except (ValueError, TypeError):
40 | raise ResultParseError(
41 | 'RTT "{rtt}" does not appear to be a float'.format(
42 | rtt=data["rtt"]
43 | )
44 | )
45 |
46 | if self.rtt:
47 | self.ttl = default_ttl
48 | if "ttl" in data:
49 | try:
50 | self.ttl = int(data["ttl"])
51 | except (ValueError, TypeError):
52 | raise ResultParseError(
53 | 'TTL "{ttl}" does not appear to be an integer'.format(
54 | ttl=data["ttl"]
55 | )
56 | )
57 |
58 | if "dup" in data:
59 | self.dup = True
60 |
61 | def __str__(self):
62 | return str(self.rtt)
63 |
64 |
65 | class PingResult(Result):
66 | """
67 | Ping measurement result class
68 | """
69 |
70 | def __init__(self, data, **kwargs):
71 |
72 | Result.__init__(self, data, **kwargs)
73 |
74 | self.af = self.ensure("af", int)
75 | self.duplicates = self.ensure("dup", int)
76 | self.rtt_average = self.ensure("avg", float)
77 | self.rtt_max = self.ensure("max", float)
78 | self.rtt_min = self.ensure("min", float)
79 | self.packets_sent = self.ensure("sent", int)
80 | self.packets_received = self.ensure("rcvd", int)
81 | self.packet_size = self.ensure("size", int)
82 | self.destination_name = self.ensure("dst_name", str)
83 | self.destination_address = self.ensure("dst_addr", str)
84 | self.step = self.ensure("step", int)
85 | self.rtt_median = None # Redefined in self._set_rtt_median()
86 | self.packets = []
87 |
88 | if self.rtt_average is None or self.rtt_average < 0:
89 | self.rtt_average = self.rtt_min = self.rtt_max = None
90 |
91 | if 0 < self.firmware < 4460:
92 | self.af = self.ensure("pf", int)
93 |
94 | self.protocol = self.clean_protocol(self.ensure("proto", str))
95 |
96 | if 0 < self.firmware < 4460:
97 | self.destination_address = self.ensure("addr", str)
98 | self.destination_name = self.ensure("name", str)
99 | self.packet_size = None
100 | elif 0 < self.firmware < 4570 and self.protocol == self.PROTOCOL_ICMP:
101 | self.packet_size -= 8
102 |
103 | if self.af is None and self.destination_address:
104 | self.af = 4
105 | if ":" in self.destination_address:
106 | self.af = 6
107 |
108 | if self.rtt_average:
109 | self.rtt_average = round(self.rtt_average, 3)
110 |
111 | self._parse_packets(**kwargs)
112 | self._set_rtt_median()
113 |
114 | def _parse_packets(self, **kwargs):
115 |
116 | source_address = self.raw_data.get(
117 | "src_addr", self.raw_data.get("srcaddr")
118 | )
119 | for packet in self.ensure("result", list, []):
120 | self.packets.append(
121 | Packet(
122 | packet,
123 | self.ensure("ttl", int),
124 | source_address,
125 | **kwargs
126 | )
127 | )
128 |
129 | def _set_rtt_median(self):
130 | packets = sorted([
131 | p.rtt for p in self.packets if p.rtt is not None and p.dup is False
132 | ])
133 | self.rtt_median = self.calculate_median(packets)
134 |
135 | __all__ = (
136 | "PingResult",
137 | )
138 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/ntp.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from datetime import datetime
17 | from dateutil.relativedelta import relativedelta
18 | from pytz import UTC
19 |
20 | from .base import Result, ResultParseError, ParsingDict
21 |
22 |
23 | class Packet(ParsingDict):
24 | """
25 | Model for data structure of each packet for a NTP result.
26 | """
27 |
28 | NTP_EPOCH = datetime(1900, 1, 1, tzinfo=UTC)
29 |
30 | def __init__(self, data, **kwargs):
31 |
32 | ParsingDict.__init__(self, **kwargs)
33 |
34 | self.raw_data = data
35 | self.rtt = None
36 | self.offset = None
37 |
38 | if "rtt" not in data:
39 | return
40 |
41 | try:
42 | self.rtt = round(float(data["rtt"]), 3)
43 | except (ValueError, TypeError):
44 | raise ResultParseError(
45 | 'RTT "{rtt}" does not appear to be a float'.format(
46 | rtt=data["rtt"])
47 | )
48 |
49 | self.offset = self.ensure("offset", float)
50 | self.final_timestamp = self.ensure("final-ts", float)
51 | self.origin_timestamp = self.ensure("origin-ts", float)
52 | self.received_timestamp = self.ensure("receive-ts", float)
53 | self.transmitted_timestamp = self.ensure("transmit-ts", float)
54 |
55 | # Caching
56 |
57 | self._final_time = None
58 | self._origin_time = None
59 | self._received_time = None
60 | self._transmitted_time = None
61 |
62 | def __str__(self):
63 | return "{rtt}|{offset}".format(rtt=self.rtt, offset=self.offset)
64 |
65 | @property
66 | def final_time(self):
67 | if not self._final_time and self.final_timestamp:
68 | self._final_time = self.NTP_EPOCH + relativedelta(
69 | seconds=self.final_timestamp)
70 | return self._final_time
71 |
72 | @property
73 | def origin_time(self):
74 | if not self._origin_time and self.origin_timestamp:
75 | self._origin_time = self.NTP_EPOCH + relativedelta(
76 | seconds=self.origin_timestamp)
77 | return self._origin_time
78 |
79 | @property
80 | def received_time(self):
81 | if not self._received_time and self.received_timestamp:
82 | self._received_time = self.NTP_EPOCH + relativedelta(
83 | seconds=self.received_timestamp)
84 | return self._received_time
85 |
86 | @property
87 | def transmitted_time(self):
88 | if not self._transmitted_time and self.transmitted_timestamp:
89 | self._transmitted_time = self.NTP_EPOCH + relativedelta(
90 | seconds=self.transmitted_timestamp)
91 | return self._transmitted_time
92 |
93 |
94 | class NtpResult(Result):
95 | """
96 | Subclass to cover ntp type measurement results.
97 | """
98 |
99 | def __init__(self, data, **kwargs):
100 |
101 | Result.__init__(self, data, **kwargs)
102 |
103 | self.rtt_median = None
104 | self.rtt_min = None
105 | self.rtt_max = None
106 | self.offset_median = None
107 | self.offset_min = None
108 | self.offset_max = None
109 |
110 | self.af = self.ensure("af", int)
111 | self.protocol = self.ensure("proto", str)
112 | self.destination_address = self.ensure("dst_addr", str)
113 | self.destination_name = self.ensure("dst_name", str)
114 | self.source_address = self.ensure("src_addr", str)
115 | self.end_time = self.ensure("endtime", "datetime")
116 | self.leap_second_indicator = self.ensure("li", str)
117 | self.mode = self.ensure("mode", str)
118 | self.poll = self.ensure("poll", int)
119 | self.precision = self.ensure("precision", float)
120 | self.reference_id = self.ensure("ref-id", str)
121 | self.reference_time = self.ensure("ref-ts", float)
122 | self.root_delay = self.ensure("root-delay", int)
123 | self.root_dispersion = self.ensure("root-dispersion", float)
124 | self.stratum = self.ensure("stratum", int)
125 | self.version = self.ensure("version", int)
126 |
127 | self.packets = []
128 |
129 | if "result" not in self.raw_data:
130 | self._handle_malformation("No result value found")
131 | return
132 |
133 | for response in self.raw_data["result"]:
134 | self.packets.append(Packet(response, **kwargs))
135 |
136 | self._set_medians_and_extremes()
137 |
138 | def _set_medians_and_extremes(self):
139 | """
140 | Sets median values for rtt and the offset of result packets.
141 | """
142 |
143 | rtts = sorted([p.rtt for p in self.packets if p.rtt is not None])
144 | if rtts:
145 | self.rtt_min = rtts[0]
146 | self.rtt_max = rtts[-1]
147 | self.rtt_median = self.calculate_median(rtts)
148 |
149 | offsets = sorted(
150 | [p.offset for p in self.packets if p.offset is not None]
151 | )
152 | if offsets:
153 | self.offset_min = offsets[0]
154 | self.offset_max = offsets[-1]
155 | self.offset_median = self.calculate_median(offsets)
156 |
157 |
158 | __all__ = (
159 | "NtpResult"
160 | )
161 |
--------------------------------------------------------------------------------
/tests/test_ntp.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from ripe.atlas.sagan import Result
17 | from ripe.atlas.sagan.ntp import NtpResult
18 |
19 | def test_ntp_valid():
20 | result = (
21 | '{"af":4,"dst_addr":"193.0.0.229","dst_name":"atlas","from":"193.0.0.78","fw":4670,'
22 | '"group_id":1020237,"li":"no","lts":-1,"mode":"server","msm_id":1020237,"msm_name":"Ntp",'
23 | '"poll":1,"prb_id":71,"precision":0.0000019074,"proto":"UDP","ref-id":"GPS",'
24 | '"ref-ts":3627199357.7446351051,"result":['
25 | '{"final-ts":3627199379.8182010651,"offset":-8.363271,"origin-ts":3627199379.7962741852,'
26 | '"receive-ts":3627199388.1704945564,"rtt":0.021899,"transmit-ts":3627199388.170522213},'
27 | '{"final-ts":3627199379.831638813,"offset":-8.36871,"origin-ts":3627199379.8214530945,'
28 | '"receive-ts":3627199388.1952428818,"rtt":0.01016,"transmit-ts":3627199388.195268631},'
29 | '{"final-ts":3627199379.8474769592,"offset":-8.372775,"origin-ts":3627199379.8454480171,'
30 | '"receive-ts":3627199388.2192249298,"rtt":0.002004,"transmit-ts":3627199388.2192502022}'
31 | '],'
32 | '"root-delay":0,"root-dispersion":0.00140381,"src_addr":"10.0.2.12","stratum":1,'
33 | '"timestamp":1418210579,"type":"ntp","version":4}'
34 | )
35 | result = Result.get(result)
36 | assert(isinstance(result, NtpResult))
37 | assert(result.af == 4)
38 | assert(result.firmware == 4670)
39 | assert(result.destination_address == "193.0.0.229")
40 | assert(result.destination_name == "atlas")
41 | assert(result.source_address == "10.0.2.12")
42 | assert(result.origin == "193.0.0.78")
43 | assert(result.leap_second_indicator == "no")
44 | assert(result.mode == "server")
45 | assert(result.poll == 1)
46 | assert(result.precision == 1.9074e-06)
47 | assert(result.protocol == "UDP")
48 | assert(result.reference_id == "GPS")
49 | assert(result.reference_time == 3627199357.7446351051)
50 | assert(result.root_delay == 0)
51 | assert(round(result.root_dispersion, 8) == 0.00140381)
52 | assert(result.stratum == 1)
53 | assert(result.version == 4)
54 | assert(result.packets[0].final_timestamp == 3627199379.8182010651)
55 | assert(result.packets[0].final_time.isoformat() == "2014-12-10T11:22:59.818201+00:00")
56 | assert(result.packets[0].offset == -8.363271)
57 | assert(result.packets[0].rtt == 0.022)
58 | assert(result.packets[0].origin_timestamp == 3627199379.7962741852)
59 | assert(result.packets[0].origin_time.isoformat() == "2014-12-10T11:22:59.796274+00:00")
60 | assert(result.packets[0].received_timestamp == 3627199388.1704945564)
61 | assert(result.packets[0].received_time.isoformat() == "2014-12-10T11:23:08.170495+00:00")
62 | assert(result.packets[0].transmitted_timestamp == 3627199388.170522213)
63 | assert(result.packets[0].transmitted_time.isoformat() == "2014-12-10T11:23:08.170522+00:00")
64 | assert(result.rtt_min == 0.002)
65 | assert(result.rtt_max == 0.022)
66 | assert(result.rtt_median == 0.01)
67 | assert(result.offset_min == -8.372775)
68 | assert(result.offset_max == -8.363271)
69 | assert(result.offset_median == -8.36871)
70 |
71 |
72 | def test_ntp_timeout():
73 | result = (
74 | '{ "msm_id":"1020235", "fw":4661, "lts":76, "timestamp":1418196642, "dst_name":"atlas", '
75 | '"prb_id":71, "dst_addr":"193.0.6.139", "src_addr":"193.0.10.127", "proto":"UDP", "af": 4,'
76 | '"from": "193.0.0.78", "type": "ntp", "result": '
77 | '[ { "x":"*" }, { "x":"*" }, { "x":"*" } ] '
78 | '}'
79 | )
80 | result = Result.get(result)
81 | assert(isinstance(result, NtpResult))
82 | assert(result.af == 4)
83 | assert(result.firmware == 4661)
84 | assert(result.destination_address == "193.0.6.139")
85 | assert(result.destination_name == "atlas")
86 | assert(result.source_address == "193.0.10.127")
87 | assert(result.origin == "193.0.0.78")
88 | assert(result.leap_second_indicator is None)
89 | assert(result.stratum is None)
90 | assert(result.rtt_median is None)
91 | assert(result.offset_median is None)
92 | assert(getattr(result.packets[0], "final_timestamp", None) is None)
93 | assert(getattr(result.packets[0], "final_times", None) is None)
94 | assert(getattr(result.packets[0], "origin_timestamp", None) is None)
95 | assert(getattr(result.packets[0], "origin_time", None) is None)
96 | assert(getattr(result.packets[0], "transmitted_timestamp", None) is None)
97 | assert(getattr(result.packets[0], "transmitted_time", None) is None)
98 | assert(getattr(result.packets[0], "received_timestamp", None) is None)
99 | assert(getattr(result.packets[0], "received_time", None) is None)
100 | assert(result.packets[0].offset is None)
101 | assert(result.packets[0].rtt is None)
102 |
103 |
104 | def test_ntp_error():
105 | result = (
106 | '{ "msm_id":"1020235", "fw":4661, "lts":76, "timestamp":1418196642, "dst_name":"atlas", '
107 | '"prb_id":71, "dst_addr":"193.0.6.139", "src_addr":"193.0.10.127", "proto":"UDP", "af": 4,'
108 | '"from": "193.0.0.78", "type": "ntp", "result": '
109 | '[ { "error":"error-example" }, { "error":"error-example" }, { "x":"*" } ] '
110 | '}'
111 | )
112 | result = Result.get(result)
113 | assert(isinstance(result, NtpResult))
114 | assert(result.af == 4)
115 | assert(result.firmware == 4661)
116 | assert(result.destination_address == "193.0.6.139")
117 | assert(result.destination_name == "atlas")
118 | assert(result.source_address == "193.0.10.127")
119 | assert(result.origin == "193.0.0.78")
120 | assert(result.leap_second_indicator is None)
121 | assert(result.stratum is None)
122 | assert(result.rtt_median is None)
123 | assert(result.offset_median is None)
124 | assert(getattr(result.packets[0], "final_timestamp", None) is None)
125 | assert(getattr(result.packets[0], "final_time", None) is None)
126 | assert(getattr(result.packets[0], "origin_timestamp", None) is None)
127 | assert(getattr(result.packets[0], "origin_time", None) is None)
128 | assert(getattr(result.packets[0], "transmitted_timestamp", None) is None)
129 | assert(getattr(result.packets[0], "transmitted_time", None) is None)
130 | assert(getattr(result.packets[0], "received_timestamp", None) is None)
131 | assert(getattr(result.packets[0], "received_time", None) is None)
132 | assert(result.packets[0].offset is None)
133 | assert(result.packets[0].rtt is None)
134 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 |
49 | clean:
50 | rm -rf $(BUILDDIR)/*
51 |
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | dirhtml:
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
59 | @echo
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
61 |
62 | singlehtml:
63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
64 | @echo
65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
66 |
67 | pickle:
68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
69 | @echo
70 | @echo "Build finished; now you can process the pickle files."
71 |
72 | json:
73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
74 | @echo
75 | @echo "Build finished; now you can process the JSON files."
76 |
77 | htmlhelp:
78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
79 | @echo
80 | @echo "Build finished; now you can run HTML Help Workshop with the" \
81 | ".hhp project file in $(BUILDDIR)/htmlhelp."
82 |
83 | qthelp:
84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
85 | @echo
86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/RIPEAtlasSagan.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/RIPEAtlasSagan.qhc"
91 |
92 | devhelp:
93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
94 | @echo
95 | @echo "Build finished."
96 | @echo "To view the help file:"
97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/RIPEAtlasSagan"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/RIPEAtlasSagan"
99 | @echo "# devhelp"
100 |
101 | epub:
102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
103 | @echo
104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
105 |
106 | latex:
107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
108 | @echo
109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
111 | "(use \`make latexpdf' here to do that automatically)."
112 |
113 | latexpdf:
114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
115 | @echo "Running LaTeX files through pdflatex..."
116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
118 |
119 | latexpdfja:
120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121 | @echo "Running LaTeX files through platex and dvipdfmx..."
122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
124 |
125 | text:
126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
127 | @echo
128 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
129 |
130 | man:
131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
132 | @echo
133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
134 |
135 | texinfo:
136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
137 | @echo
138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
139 | @echo "Run \`make' in that directory to run these through makeinfo" \
140 | "(use \`make info' here to do that automatically)."
141 |
142 | info:
143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
144 | @echo "Running Texinfo files through makeinfo..."
145 | make -C $(BUILDDIR)/texinfo info
146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
147 |
148 | gettext:
149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
150 | @echo
151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
152 |
153 | changes:
154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
155 | @echo
156 | @echo "The overview file is in $(BUILDDIR)/changes."
157 |
158 | linkcheck:
159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
160 | @echo
161 | @echo "Link check complete; look for any errors in the above output " \
162 | "or in $(BUILDDIR)/linkcheck/output.txt."
163 |
164 | doctest:
165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
166 | @echo "Testing of doctests in the sources finished, look at the " \
167 | "results in $(BUILDDIR)/doctest/output.txt."
168 |
169 | xml:
170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
171 | @echo
172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
173 |
174 | pseudoxml:
175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
176 | @echo
177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
178 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. xml to make Docutils-native XML files
37 | echo. pseudoxml to make pseudoxml-XML files for display purposes
38 | echo. linkcheck to check all external links for integrity
39 | echo. doctest to run all doctests embedded in the documentation if enabled
40 | goto end
41 | )
42 |
43 | if "%1" == "clean" (
44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
45 | del /q /s %BUILDDIR%\*
46 | goto end
47 | )
48 |
49 |
50 | %SPHINXBUILD% 2> nul
51 | if errorlevel 9009 (
52 | echo.
53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
54 | echo.installed, then set the SPHINXBUILD environment variable to point
55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
56 | echo.may add the Sphinx directory to PATH.
57 | echo.
58 | echo.If you don't have Sphinx installed, grab it from
59 | echo.http://sphinx-doc.org/
60 | exit /b 1
61 | )
62 |
63 | if "%1" == "html" (
64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
68 | goto end
69 | )
70 |
71 | if "%1" == "dirhtml" (
72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
76 | goto end
77 | )
78 |
79 | if "%1" == "singlehtml" (
80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
84 | goto end
85 | )
86 |
87 | if "%1" == "pickle" (
88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can process the pickle files.
92 | goto end
93 | )
94 |
95 | if "%1" == "json" (
96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
97 | if errorlevel 1 exit /b 1
98 | echo.
99 | echo.Build finished; now you can process the JSON files.
100 | goto end
101 | )
102 |
103 | if "%1" == "htmlhelp" (
104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
105 | if errorlevel 1 exit /b 1
106 | echo.
107 | echo.Build finished; now you can run HTML Help Workshop with the ^
108 | .hhp project file in %BUILDDIR%/htmlhelp.
109 | goto end
110 | )
111 |
112 | if "%1" == "qthelp" (
113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
114 | if errorlevel 1 exit /b 1
115 | echo.
116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
117 | .qhcp project file in %BUILDDIR%/qthelp, like this:
118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\RIPEAtlasSagan.qhcp
119 | echo.To view the help file:
120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\RIPEAtlasSagan.ghc
121 | goto end
122 | )
123 |
124 | if "%1" == "devhelp" (
125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished.
129 | goto end
130 | )
131 |
132 | if "%1" == "epub" (
133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
137 | goto end
138 | )
139 |
140 | if "%1" == "latex" (
141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
145 | goto end
146 | )
147 |
148 | if "%1" == "latexpdf" (
149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
150 | cd %BUILDDIR%/latex
151 | make all-pdf
152 | cd %BUILDDIR%/..
153 | echo.
154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
155 | goto end
156 | )
157 |
158 | if "%1" == "latexpdfja" (
159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
160 | cd %BUILDDIR%/latex
161 | make all-pdf-ja
162 | cd %BUILDDIR%/..
163 | echo.
164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
165 | goto end
166 | )
167 |
168 | if "%1" == "text" (
169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
170 | if errorlevel 1 exit /b 1
171 | echo.
172 | echo.Build finished. The text files are in %BUILDDIR%/text.
173 | goto end
174 | )
175 |
176 | if "%1" == "man" (
177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
178 | if errorlevel 1 exit /b 1
179 | echo.
180 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
181 | goto end
182 | )
183 |
184 | if "%1" == "texinfo" (
185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
186 | if errorlevel 1 exit /b 1
187 | echo.
188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
189 | goto end
190 | )
191 |
192 | if "%1" == "gettext" (
193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
194 | if errorlevel 1 exit /b 1
195 | echo.
196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
197 | goto end
198 | )
199 |
200 | if "%1" == "changes" (
201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
202 | if errorlevel 1 exit /b 1
203 | echo.
204 | echo.The overview file is in %BUILDDIR%/changes.
205 | goto end
206 | )
207 |
208 | if "%1" == "linkcheck" (
209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
210 | if errorlevel 1 exit /b 1
211 | echo.
212 | echo.Link check complete; look for any errors in the above output ^
213 | or in %BUILDDIR%/linkcheck/output.txt.
214 | goto end
215 | )
216 |
217 | if "%1" == "doctest" (
218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
219 | if errorlevel 1 exit /b 1
220 | echo.
221 | echo.Testing of doctests in the sources finished, look at the ^
222 | results in %BUILDDIR%/doctest/output.txt.
223 | goto end
224 | )
225 |
226 | if "%1" == "xml" (
227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
228 | if errorlevel 1 exit /b 1
229 | echo.
230 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
231 | goto end
232 | )
233 |
234 | if "%1" == "pseudoxml" (
235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
236 | if errorlevel 1 exit /b 1
237 | echo.
238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
239 | goto end
240 | )
241 |
242 | :end
243 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 | * 2.0.0
4 | * Official supported Python versions changed to 3.10, 3.11, 3.12 and 3.13
5 | * Removed test on invalid country codes in SSL certs, in line with the behaviour of the cryptography library
6 | * Reinstated integration with readthedocs.io
7 | * 1.3.1
8 | * Ping edge case fix
9 | * Add py37 testing
10 | * 1.3.0
11 | * abuf.py: error handling for NS records, extended rcode, cookies and client subnets
12 | * 1.2.2
13 | * Catch problems parsing SSL certificates
14 | * 1.2.1
15 | * Add support for non-DNS names in subjectAltName extensions
16 | * 1.2
17 | * Replaced pyOpenSSL with cryptography
18 | * Added parsing of subjectAltName X509 extension
19 | * 1.1.11
20 | * Added first version of WiFi results
21 | * 1.1.10
22 | * Added a `parse_all_hops` kwarg to the Traceroute class to tell Sagan to stop parsing Hops and Packets once we have all of the last hop statistics (default=True)
23 | * Remove dependency on IPy: we were using it for IPv6 canonicalization, but all IPv6 addresses in results should be in canonical form to start with.
24 | * 1.1.9
25 | * Removed the `parse_abuf` script because no one was using it and its
26 | Python3 support was suspect anyway.
27 | * 1.1.8
28 | * Handle case where a traceroute result might not have ``dst_addr`` field.
29 | * 1.1.7
30 | * Change condition of traceroute's ``last_hop_responded`` flag.
31 | * Add couple of more traceroute's properties. ``is_success`` and ``last_hop_errors``.
32 | * Add tests to the package itself.
33 | * 1.1.6
34 | * Fix for `Issue #56`_ a case where the ``qbuf`` value wasn't being properly
35 | captured.
36 | * Fixed small bug that didn't accurately capture the ``DO`` property from
37 | the qbuf.
38 | * 1.1.5
39 | * We now ignore so-called "late" packets in traceroute results. This will
40 | likely be amended later as future probe firmwares are expected to make
41 | better use of this value, but until then, Sagan will treat these packets
42 | as invalid.
43 | * 1.1.4
44 | * Added a ``type`` attribute to all ``Result`` subclasses
45 | * Added support for a lot of new DNS answer types, including ``NSEC``,
46 | ``PTR``, ``SRV``, and more. These answers do not yet have a complete
47 | string representation however.
48 | * 1.1.3
49 | * Changed the name of ``TracerouteResult.rtt_median`` to
50 | ``TracerouteResult.last_rtt_median``.
51 | * Modified the ``DnsResult`` class to allow the "bubbling up" of error
52 | statuses.
53 | * 1.1.2
54 | * We skipped this number for some reason :-/
55 | * 1.1.1
56 | * Fixed a `string representation bug`_ found by `iortiz`_
57 | * 1.1.0
58 | * **Breaking Change**: the ``Authority`` and ``Additional`` classes were
59 | removed, replaced with the appropriate answer types. For the most part,
60 | this change should be invisible, as the common properties are the same,
61 | but if you were testing code against these class types, you should
62 | consider this a breaking change.
63 | * **Breaking Change**: The ``__str__`` format for DNS ``RrsigAnswer`` to
64 | conform the output of a typical ``dig`` binary.
65 | * Added ``__str__`` definitions to DNS answer classes for use with the
66 | toolkit.
67 | * In an effort to make Sagan (along with Cousteau and the toolkit) more
68 | portable, we dropped the requirement for the ``arrow`` package.
69 | * 1.0.0
70 | * 1.0! w00t!
71 | * **Breaking Change**: the ``data`` property of the ``TxtAnswer`` class was
72 | changed from a string to a list of strings. This is a correction from
73 | our own past deviation from the RFC, so we thought it best to conform as
74 | part of the move to 1.0.0
75 | * Fixed a bug where non-ascii characters in DNS TXT answers resulted in an
76 | exception.
77 | * 0.8.2
78 | * Fixed a bug related to non-ascii characters in SSL certificate data.
79 | * Added a wrapper for json loaders to handle differences between ujson and
80 | the default json module.
81 | * 0.8.1
82 | * Minor fix to make all ``Result`` objects properly JSON serialisable.
83 | * 0.8.0
84 | * Added `iortiz`_'s patch for flags and ``flags``
85 | and ``sections`` properties on DNS ``Answer`` objects.
86 | * 0.7.1
87 | * Changed ``README.md`` to ``README.rst`` to play nice with pypi.
88 | * 0.7
89 | * Added `pierky`_'s new ``RRSigAnswer`` class to
90 | the dns parser.
91 | * 0.6.3
92 | * Fixed a bug in how Sagan deals with inappropriate firmware versions
93 | * 0.6.2
94 | * Added `pierky`_'s fix to fix AD and CD flags
95 | parsing in DNS Header
96 | * 0.6.1
97 | * Added ``rtt_min``, ``rtt_max``, ``offset_min``, and ``offset_max`` to
98 | ``NTPResult``
99 | * 0.6.0
100 | * Support for NTP measurements
101 | * Fixes for how we calculate median values
102 | * Smarter setup.py
103 | * 0.5.0
104 | * Complete Python3 support!
105 | * 0.4.0
106 | * Added better Python3 support. Tests all pass now for ping, traceroute,
107 | ssl, and http measurements.
108 | * Modified traceroute results to make use of ``destination_ip_responded``
109 | and ``last_hop_responded``, deprecating ``target_responded``. See the
110 | docs for details.
111 | * 0.3.0
112 | * Added support for making use of some of the pre-calculated values in DNS
113 | measurements so you don't have to parse the abuf if you don't need it.
114 | * Fixed a bug in the abuf parser where a variable was being referenced by
115 | never defined.
116 | * Cleaned up some of the abuf parser to better conform to pep8.
117 | * 0.2.8
118 | * Fixed a bug where DNS ``TXT`` results with class ``IN`` were missing a
119 | ``.data`` value.
120 | * Fixed a problem in the SSL unit tests where ``\n`` was being
121 | misinterpreted.
122 | * 0.2.7
123 | * Made abuf more robust in dealing with truncation.
124 | * 0.2.6
125 | * Replaced ``SslResult.get_checksum_chain()`` with the
126 | ``SslResult.checksum_chain`` property.
127 | * Added support for catching results with an ``err`` property as an actual
128 | error.
129 | * 0.2.5
130 | * Fixed a bug in how the ``on_error`` and ``on_malformation`` preferences
131 | weren't being passed down into the subcomponents of the results.
132 | * 0.2.4
133 | * Support for ``seconds_since_sync`` across all measurement types
134 | * 0.2.3
135 | * "Treat a missing Type value in a DNS result as a malformation" (Issue #36)
136 | * 0.2.2
137 | * Minor bugfixes
138 | * 0.2.1
139 | * Added a ``median_rtt`` value to traceroute ``Hop`` objects.
140 | * Smarter and more consistent error handling in traceroute and HTTP
141 | results.
142 | * Added an ``error_message`` property to all objects that is set to ``None``
143 | by default.
144 | * 0.2.0
145 | * Totally reworked error and malformation handling. We now differentiate
146 | between a result (or portion thereof) being malformed (and therefore
147 | unparsable) and simply containing an error such as a timeout. Look for
148 | an ``is_error`` property or an ``is_malformed`` property on every object
149 | to check for it, or simply pass ``on_malformation=Result.ACTION_FAIL`` if
150 | you'd prefer things to explode with an exception. See the documentation
151 | for more details
152 | * Added lazy-loading features for parsing abuf and qbuf values out of DNS
153 | results.
154 | * Removed the deprecated properties from ``dns.Response``. You must now
155 | access values like ``edns0`` from ``dns.Response.abuf.edns0``.
156 | * More edge cases have been found and accommodated.
157 | * 0.1.15
158 | * Added a bunch of abuf parsing features from
159 | `b4ldr`_ with some help from
160 | `phicoh`_.
161 | * 0.1.14
162 | * Fixed the deprecation warnings in ``DnsResult`` to point to the right
163 | place.
164 | * 0.1.13
165 | * Better handling of ``DNSResult`` errors
166 | * Rearranged the way abufs were handled in the ``DnsResult`` class to make
167 | way for ``qbuf`` values as well. The old method of accessing ``header``,
168 | ``answers``, ``questions``, etc is still available via ``Response``, but
169 | this will go away when we move to 0.2. Deprecation warnings are in place.
170 | * 0.1.12
171 | * Smarter code for checking whether the target was reached in
172 | ``TracerouteResults``.
173 | * We now handle the ``destination_option_size`` and
174 | ``hop_by_hop_option_size`` values in ``TracerouteResult``.
175 | * Extended support for ICMP header info in traceroute ``Hop`` class by
176 | introducing a new ``IcmpHeader`` class.
177 | * 0.1.8
178 | * Broader support for SSL checksums. We now make use of ``md5`` and
179 | ``sha1``, as well as the original ``sha256``.
180 |
181 | .. _Issue #56: https://github.com/RIPE-NCC/ripe.atlas.sagan/issues/56
182 | .. _string representation bug: https://github.com/RIPE-NCC/ripe-atlas-tools/issues/1
183 | .. _b4ldr: https://github.com/b4ldr
184 | .. _phicoh: https://github.com/phicoh
185 | .. _iortiz: https://github.com/iortiz
186 | .. _pierky: https://github.com/pierky
187 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/traceroute.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import logging
17 |
18 | from calendar import timegm
19 |
20 | from .base import Result, ParsingDict
21 |
22 |
23 | log = logging.getLogger(__name__)
24 |
25 |
26 | class IcmpHeader(ParsingDict):
27 | """
28 | But why did we stop here? Why not go all the way and define subclasses for
29 | each object and for `mpls`? it comes down to a question of complexity vs.
30 | usefulness. This is such a fringe case that it's probably fine to just
31 | dump the data in to `self.objects` and let people work from there. If
32 | however you feel that this needs expansion, pull requests are welcome :-)
33 |
34 | Further information regarding the structure and meaning of the data in
35 | this class can be found here: http://localhost:8000/docs/data_struct/
36 | """
37 |
38 | def __init__(self, data, **kwargs):
39 |
40 | ParsingDict.__init__(self, **kwargs)
41 |
42 | self.raw_data = data
43 |
44 | self.version = self.ensure("version", int)
45 | self.rfc4884 = self.ensure("rfc4884", bool)
46 | self.objects = self.ensure("obj", list)
47 |
48 |
49 | class Packet(ParsingDict):
50 |
51 | ERROR_CONDITIONS = {
52 | "N": "Network unreachable",
53 | "H": "Destination unreachable",
54 | "A": "Administratively prohibited",
55 | "P": "Protocol unreachable",
56 | "p": "Port unreachable",
57 | }
58 |
59 | def __init__(self, data, **kwargs):
60 |
61 | ParsingDict.__init__(self, **kwargs)
62 |
63 | self.raw_data = data
64 |
65 | self.origin = self.ensure("from", str)
66 | self.rtt = self.ensure("rtt", float)
67 | self.size = self.ensure("size", int)
68 | self.ttl = self.ensure("ttl", int)
69 | self.mtu = self.ensure("mtu", int)
70 | self.destination_option_size = self.ensure("dstoptsize", int)
71 | self.hop_by_hop_option_size = self.ensure("hbhoptsize", int)
72 | self.arrived_late_by = self.ensure("late", int, 0)
73 | self.internal_ttl = self.ensure("ittl", int, 1)
74 |
75 | if self.rtt:
76 | self.rtt = round(self.rtt, 3)
77 |
78 | error = self.ensure("err", str)
79 | if error:
80 | self._handle_error(self.ERROR_CONDITIONS.get(error, error))
81 |
82 | icmp_header = self.ensure("icmpext", dict)
83 |
84 | self.icmp_header = None
85 | if icmp_header:
86 | self.icmp_header = IcmpHeader(icmp_header, **kwargs)
87 |
88 | def __str__(self):
89 | return self.origin
90 |
91 |
92 | class Hop(ParsingDict):
93 |
94 | def __init__(self, data, **kwargs):
95 |
96 | ParsingDict.__init__(self, **kwargs)
97 |
98 | self.raw_data = data
99 |
100 | self.index = self.ensure("hop", int)
101 |
102 | error = self.ensure("error", str)
103 | if error:
104 | self._handle_error(error)
105 |
106 | self.packets = []
107 | packet_rtts = []
108 | if "result" in self.raw_data:
109 | for raw_packet in self.raw_data["result"]:
110 | if "late" not in raw_packet:
111 | packet = Packet(raw_packet, **kwargs)
112 | if packet.rtt:
113 | packet_rtts.append(packet.rtt)
114 | self.packets.append(packet)
115 | self.median_rtt = Result.calculate_median(packet_rtts)
116 |
117 | def __str__(self):
118 | return str(self.index)
119 |
120 |
121 | class TracerouteResult(Result):
122 |
123 | def __init__(self, data, **kwargs):
124 |
125 | Result.__init__(self, data, **kwargs)
126 |
127 | self.af = self.ensure("af", int)
128 | self.destination_address = self.ensure("dst_addr", str)
129 | self.destination_name = self.ensure("dst_name", str)
130 | self.source_address = self.ensure("src_addr", str)
131 | self.end_time = self.ensure("endtime", "datetime")
132 | self.paris_id = self.ensure("paris_id", int)
133 | self.size = self.ensure("size", int)
134 |
135 | if 0 < self.firmware < 4460:
136 | self.af = self.ensure("pf", int)
137 |
138 | self.protocol = self.clean_protocol(self.ensure("proto", str))
139 |
140 | self.hops = []
141 | self.total_hops = 0
142 | self.last_median_rtt = None
143 |
144 | # Used by a few response tests below
145 | self.destination_ip_responded = False
146 | self.last_hop_responded = False
147 | self.is_success = False
148 | self.last_hop_errors = []
149 |
150 | self._parse_hops(**kwargs) # Sets hops, last_median_rtt, and total_hops
151 |
152 | @property
153 | def last_rtt(self):
154 | log.warning(
155 | '"last_rtt" is deprecated and will be removed in future versions. '
156 | 'Instead, use "last_median_rtt".')
157 | return self.last_median_rtt
158 |
159 | @property
160 | def target_responded(self):
161 | log.warning(
162 | 'The "target_responded" property is deprecated and will be removed '
163 | 'in future versions. Instead, use "destination_ip_responded".'
164 | )
165 | return self.destination_ip_responded
166 |
167 | def set_destination_ip_responded(self, last_hop):
168 | """Sets the flag if destination IP responded."""
169 | if not self.destination_address:
170 | return
171 |
172 | for packet in last_hop.packets:
173 | if packet.origin and \
174 | self.destination_address == packet.origin:
175 | self.destination_ip_responded = True
176 | break
177 |
178 | def set_last_hop_responded(self, last_hop):
179 | """Sets the flag if last hop responded."""
180 | for packet in last_hop.packets:
181 | if packet.rtt:
182 | self.last_hop_responded = True
183 | break
184 |
185 | def set_is_success(self, last_hop):
186 | """Sets the flag if traceroute result is successful or not."""
187 | for packet in last_hop.packets:
188 | if packet.rtt and not packet.is_error:
189 | self.is_success = True
190 | break
191 | else:
192 | self.set_last_hop_errors(last_hop)
193 |
194 | def set_last_hop_errors(self, last_hop):
195 | """Sets the last hop's errors."""
196 | if last_hop.is_error:
197 | self.last_hop_errors.append(last_hop.error_message)
198 | return
199 |
200 | for packet in last_hop.packets:
201 | if packet.is_error:
202 | self.last_hop_errors.append(packet.error_message)
203 |
204 | @property
205 | def end_time_timestamp(self):
206 | return timegm(self.end_time.timetuple())
207 |
208 | @property
209 | def ip_path(self):
210 | """
211 | Returns just the IPs from the traceroute.
212 | """
213 | r = []
214 | for hop in self.hops:
215 | r.append([packet.origin for packet in hop.packets])
216 | return r
217 |
218 | def _parse_hops(self, parse_all_hops=True, **kwargs):
219 |
220 | try:
221 | hops = self.raw_data["result"]
222 | assert(isinstance(hops, list))
223 | except (KeyError, AssertionError):
224 | self._handle_malformation("Legacy formats not supported")
225 | return
226 |
227 | num_hops = len(hops)
228 | # Go through the hops in reverse so that if
229 | # parse_all_hops is False we can stop processing as
230 | # soon as possible.
231 | for index, raw_hop in reversed(list(enumerate(hops))):
232 |
233 | hop = Hop(raw_hop, **kwargs)
234 |
235 | # If last hop set several useful attributes
236 | if index + 1 == num_hops:
237 | self.set_destination_ip_responded(hop)
238 | self.set_last_hop_responded(hop)
239 | self.set_is_success(hop)
240 | # We always store the last hop
241 | self.hops.insert(0, hop)
242 | elif parse_all_hops:
243 | self.hops.insert(0, hop)
244 |
245 | if hop.median_rtt and not self.last_median_rtt:
246 | self.last_median_rtt = hop.median_rtt
247 | if not parse_all_hops:
248 | # Now that we have the last RTT we can stop
249 | break
250 | self.total_hops = num_hops
251 |
252 |
253 | __all__ = (
254 | "TracerouteResult",
255 | )
256 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | RIPE Atlas Sagan
2 | ===============================================
3 |
4 | |Documentation| |PYPI Version| |Python Versions|
5 |
6 | A parsing library for RIPE Atlas measurement results
7 |
8 | Why this exists
9 | ---------------
10 |
11 | RIPE Atlas generates a **lot** of data, and the format of that data changes over
12 | time. Often you want to do something simple like fetch the median RTT for each
13 | measurement result between date ``X`` and date ``Y``. Unfortunately, there are
14 | dozens of edge cases to account for while parsing the JSON, like the format of
15 | errors and firmware upgrades that changed the format entirely.
16 |
17 | To make this easier for our users (and for ourselves), we wrote an easy to use
18 | parser that's smart enough to figure out the best course of action for each
19 | result, and return to you a useful, native Python object.
20 |
21 | How to install
22 | --------------
23 |
24 | The stable version should always be in PyPi, so you can install it with ``pip``:
25 |
26 | .. code:: bash
27 |
28 | $ pip install ripe.atlas.sagan
29 |
30 | Better yet, make sure you get ujson and sphinx installed with it:
31 |
32 | .. code:: bash
33 |
34 | $ pip install ripe.atlas.sagan[fast,doc]
35 |
36 | Quickstart: How To Use It
37 | -------------------------
38 |
39 | You can parse a result in a few ways. You can just pass the JSON-encoded string:
40 |
41 | .. code:: python
42 |
43 | from ripe.atlas.sagan import PingResult
44 |
45 | my_result = PingResult("")
46 |
47 | print(my_result.rtt_median)
48 | 123.456
49 |
50 | print(my_result.af)
51 | 6
52 |
53 | You can do the JSON-decoding yourself:
54 |
55 | .. code:: python
56 |
57 | from ripe.atlas.sagan import PingResult
58 |
59 | my_result = PingResult(
60 | json.loads("")
61 | )
62 |
63 | print(my_result.rtt_median)
64 | 123.456
65 |
66 | print(my_result.af)
67 | 6
68 |
69 | You can let the parser guess the right type for you, though this incurs a small
70 | performance penalty:
71 |
72 | .. code:: python
73 |
74 | from ripe.atlas.sagan import Result
75 |
76 | my_result = Result.get("")
77 |
78 | print(my_result.rtt_median)
79 | 123.456
80 |
81 | print(my_result.af)
82 | 6
83 |
84 | What it supports
85 | ----------------
86 |
87 | Essentially, we tried to support everything. If you pass in a DNS result string,
88 | the parser will return a ``DNSResult`` object, which contains a list of
89 | ``Response``'s, each with an ``abuf`` property, as well as all of the
90 | information in that abuf: header, question, answer, etc.
91 |
92 | .. code:: python
93 |
94 | from ripe.atlas.sagan import DnsResult
95 |
96 | my_dns_result = DnsResult("")
97 | my_dns_result.responses[0].abuf # The entire string
98 | my_dns_result.responses[0].abuf.header.arcount # Decoded from the abuf
99 |
100 | We do the same sort of thing for SSL measurements, traceroutes, everything. We
101 | try to save you the effort of sorting through whatever is in the result.
102 |
103 | Which attributes are supported?
104 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
105 |
106 | Every result type has its own properties, with a few common between all types.
107 |
108 | Specifically, these attributes exist on all ``*Result`` objects:
109 |
110 | - ``created`` An datetime object of the
111 | ``timestamp`` field
112 | - ``measurement_id``
113 | - ``probe_id``
114 | - ``firmware`` An integer representing the firmware version
115 | - ``origin`` The ``from`` attribute in the result
116 | - ``is_error`` Set to ``True`` if an error was found
117 |
118 | Additionally, each of the result types have their own properties, like
119 | ``packet_size``, ``responses``, ``certificates``, etc. You can take a look at
120 | the classes themselves, or just look at the tests if you're curious. But to get
121 | you started, here are some examples:
122 |
123 | .. code:: python
124 |
125 | # Ping
126 | ping_result.packets_sent # Int
127 | ping_result.rtt_median # Float, rounded to 3 decimal places
128 | ping_result.rtt_average # Float, rounded to 3 decimal places
129 |
130 | # Traceroute
131 | traceroute_result.af # 4 or 6
132 | traceroute_result.total_hops # Int
133 | traceroute_result.destination_address # An IP address string
134 |
135 | # DNS
136 | dns_result.responses # A list of Response objects
137 | dns_result.responses[0].response_time # Float, rounded to 3 decimal places
138 | dns_result.responses[0].headers # A list of Header objects
139 | dns_result.responses[0].headers[0].nscount # The NSCOUNT value for the first header
140 | dns_result.responses[0].questions # A list of Question objects
141 | dns_result.responses[0].questions[0].type # The TYPE value for the first question
142 | dns_result.responses[0].abuf # The raw, unparsed abuf string
143 |
144 | # SSL Certificates
145 | ssl_result.af # 4 or 6
146 | ssl_result.certificates # A list of Certificate objects
147 | ssl_result.certificates[0].checksum # The checksum for the first certificate
148 |
149 | # HTTP
150 | http_result.af # 4 or 6
151 | http_result.uri # A URL string
152 | http_result.responses # A list of Response objects
153 | http_result.responses[0].body_size # The size of the body of the first response
154 |
155 | # NTP
156 | ntp_result.af # 4 or 6
157 | ntp_result.stratum # Statum id
158 | ntp_result.version # Version number
159 | ntp_result.packets[0].final_timestamp # A float representing a high-precision NTP timestamp
160 | ntp_result.rtt_median # Median value for packets sent & received
161 |
162 | What it requires
163 | ----------------
164 |
165 | As you might have guessed, with all of this magic going on under the hood, there
166 | are a few dependencies:
167 |
168 | - `cryptography`_ (Optional: see "Troubleshooting" above)
169 | - `python-dateutil`_
170 | - `pytz`_
171 | - `IPy`_
172 |
173 | Additionally, we recommend that you also install `ujson`_ as it will speed up
174 | the JSON-decoding step considerably, and `sphinx`_ if you intend to build the
175 | documentation files for offline use.
176 |
177 | Running Tests
178 | -------------
179 |
180 | There's a full battery of tests for all measurement types, so if you've made
181 | changes and would like to submit a pull request, please run them (and update
182 | them!) before sending your request:
183 |
184 | .. code:: bash
185 |
186 | $ python setup.py test
187 |
188 | You can also install ``tox`` to test everything in all of the supported Python
189 | versions:
190 |
191 | .. code:: bash
192 |
193 | $ pip install tox
194 | $ tox
195 |
196 | Further Documentation
197 | ---------------------
198 |
199 | Complete documentation can always be found on `Read the Docs`_,
200 | and if you're not online, the project itself contains a ``docs`` directory --
201 | everything you should need is in there.
202 |
203 |
204 | Who's Responsible for This?
205 | ---------------------------
206 |
207 | Sagan is actively maintained by the RIPE NCC and primarily developed by `Daniel
208 | Quinn`_, while the abuf parser is mostly the responsibility of `Philip Homburg`_
209 | with an assist from Bert Wijnen and Rene Wilhelm who contributed to the original
210 | script. `Andreas Stirkos`_ did the bulk of the work on NTP measurements and
211 | fixed a few bugs, and big thanks go to `Chris Amin`_, `John Bond`_, and
212 | `Pier Carlo Chiodi`_ for finding and fixing stuff where they've run into
213 | problems.
214 |
215 |
216 | Colophon
217 | --------
218 |
219 | But why "`Sagan`_"? The RIPE Atlas team decided to name all of its modules after
220 | explorers, and what better name for a parser than that of the man who spent
221 | decades reaching out to the public about the wonders of the cosmos?
222 |
223 | .. _python-dateutil: https://pypi.python.org/pypi/python-dateutil
224 | .. _cryptography: https://pypi.python.org/pypi/cryptography
225 | .. _pytz: https://pypi.python.org/pypi/pytz
226 | .. _IPy: https://pypi.python.org/pypi/IPy/
227 | .. _ujson: https://pypi.python.org/pypi/ujson
228 | .. _sphinx: https://pypi.python.org/pypi/Sphinx
229 | .. _Read the Docs: http://ripe-atlas-sagan.readthedocs.org/en/latest/
230 | .. _Daniel Quinn: https://github.com/danielquinn
231 | .. _Philip Homburg: https://github.com/philiphomburg
232 | .. _Andreas Stirkos: https://github.com/astrikos
233 | .. _Chris Amin: https://github.com/chrisamin
234 | .. _John Bond: https://github.com/b4ldr
235 | .. _Pier Carlo Chiodi: https://github.com/pierky
236 | .. _Sagan: https://en.wikipedia.org/wiki/Carl_Sagan
237 | .. |Documentation| image:: https://readthedocs.org/projects/ripe-atlas-sagan/badge/?version=latest
238 | :target: http://ripe-atlas-sagan.readthedocs.org/en/latest/?badge=latest
239 | :alt: Documentation Status
240 | .. |PYPI Version| image:: https://img.shields.io/pypi/v/ripe.atlas.sagan.svg
241 | :target: https://pypi.org/project/ripe.atlas.sagan/
242 | .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/ripe.atlas.sagan.svg
243 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/ssl.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import logging
17 | import pytz
18 | import codecs
19 |
20 | from datetime import datetime
21 |
22 | try:
23 | from cryptography import x509
24 | from cryptography.x509.oid import NameOID
25 | from cryptography.hazmat.backends import openssl
26 | from cryptography.hazmat.primitives import hashes
27 | except ImportError:
28 | logging.getLogger(__name__).warning(
29 | "cryptography module is not installed, without it you cannot parse SSL "
30 | "certificate measurement results"
31 | )
32 |
33 | from .base import Result, ParsingDict
34 | from .helpers.compatibility import string
35 |
36 |
37 | EXT_SAN = "subjectAltName"
38 |
39 |
40 | class Certificate(ParsingDict):
41 |
42 | def __init__(self, data, **kwargs):
43 |
44 | ParsingDict.__init__(self, **kwargs)
45 |
46 | self.raw_data = data
47 | self.subject_cn = None
48 | self.subject_o = None
49 | self.subject_c = None
50 | self.issuer_cn = None
51 | self.issuer_o = None
52 | self.issuer_c = None
53 |
54 | self.valid_from = None
55 | self.valid_until = None
56 |
57 | self.checksum_md5 = None
58 | self.checksum_sha1 = None
59 | self.checksum_sha256 = None
60 |
61 | self.has_expired = None
62 |
63 | self.extensions = {}
64 |
65 | cert = x509.load_pem_x509_certificate(data.encode("ascii"), openssl.backend)
66 |
67 | if cert:
68 | self.checksum_md5 = self._colonify(cert.fingerprint(hashes.MD5()))
69 | self.checksum_sha1 = self._colonify(cert.fingerprint(hashes.SHA1()))
70 | self.checksum_sha256 = self._colonify(cert.fingerprint(hashes.SHA256()))
71 |
72 | self.valid_from = pytz.utc.localize(cert.not_valid_before)
73 | self.valid_until = pytz.utc.localize(cert.not_valid_after)
74 |
75 | self.has_expired = self._has_expired()
76 |
77 | self._add_extensions(cert)
78 |
79 | if cert and cert.subject:
80 | self.subject_cn, self.subject_o, self.subject_c = \
81 | self._parse_x509_name(cert.subject)
82 |
83 | if cert and cert.issuer:
84 | self.issuer_cn, self.issuer_o, self.issuer_c = \
85 | self._parse_x509_name(cert.issuer)
86 |
87 | # OID name lookup of the common abbreviations
88 | # In reality probably only CN will be used
89 | _oid_names = {
90 | NameOID.COMMON_NAME: "CN",
91 | NameOID.ORGANIZATION_NAME: "O",
92 | NameOID.ORGANIZATIONAL_UNIT_NAME: "OU",
93 | NameOID.COUNTRY_NAME: "C",
94 | NameOID.STATE_OR_PROVINCE_NAME: "S",
95 | NameOID.LOCALITY_NAME: "L",
96 | }
97 |
98 | def _get_oid_name(self, oid):
99 | return self._oid_names.get(oid, oid.dotted_string)
100 |
101 | def _name_attribute_to_string(self, name):
102 | """
103 | Build a /-separated string from an x509.Name.
104 | """
105 | return "".join(
106 | "/{}={}".format(
107 | self._get_oid_name(attr.oid),
108 | attr.value,
109 | )
110 | for attr in name
111 | )
112 |
113 | def _get_subject_alternative_names(self, ext):
114 | """
115 | Return a list of Subject Alternative Name values for the given x509
116 | extension object.
117 | """
118 | values = []
119 | for san in ext.value:
120 | if isinstance(san.value, string):
121 | # Pass on simple string SAN values
122 | values.append(san.value)
123 | elif isinstance(san.value, x509.Name):
124 | # In theory there there could be >1 RDN here...
125 | values.extend(
126 | self._name_attribute_to_string(rdn) for rdn in san.value.rdns
127 | )
128 | return values
129 |
130 | def _add_extensions(self, cert):
131 | for ext in cert.extensions:
132 | if ext.oid._name == EXT_SAN:
133 | self.extensions[EXT_SAN] = self._get_subject_alternative_names(ext)
134 |
135 | @staticmethod
136 | def _colonify(bytes):
137 | hex = codecs.getencoder("hex_codec")(bytes)[0].decode("ascii").upper()
138 | return ":".join(a+b for a, b in zip(hex[::2], hex[1::2]))
139 |
140 | @staticmethod
141 | def _parse_x509_name(name):
142 | cn = None
143 | o = None
144 | c = None
145 | for attr in name:
146 | if attr.oid == NameOID.COUNTRY_NAME:
147 | c = attr.value
148 | elif attr.oid == NameOID.ORGANIZATION_NAME:
149 | o = attr.value
150 | elif attr.oid == NameOID.COMMON_NAME:
151 | cn = attr.value
152 | return cn, o, c
153 |
154 | def _has_expired(self):
155 | now = pytz.utc.localize(datetime.utcnow())
156 | return self.valid_from <= now <= self.valid_until
157 |
158 | @property
159 | def cn(self):
160 | return self.subject_cn
161 |
162 | @property
163 | def o(self):
164 | return self.subject_o
165 |
166 | @property
167 | def c(self):
168 | return self.subject_c
169 |
170 | @property
171 | def common_name(self):
172 | return self.cn
173 |
174 | @property
175 | def organisation(self):
176 | return self.o
177 |
178 | @property
179 | def country(self):
180 | return self.c
181 |
182 | @property
183 | def checksum(self):
184 | return self.checksum_sha256
185 |
186 |
187 | class Alert(ParsingDict):
188 |
189 | # Taken from https://tools.ietf.org/html/rfc5246#section-7.2
190 | DESCRIPTION_MAP = {
191 | 0: "close_notify",
192 | 10: "unexpected_message",
193 | 20: "bad_record_mac",
194 | 21: "decryption_failed_RESERVED",
195 | 22: "record_overflow",
196 | 30: "decompression_failure",
197 | 40: "handshake_failure",
198 | 41: "no_certificate_RESERVED",
199 | 42: "bad_certificate",
200 | 43: "unsupported_certificate",
201 | 44: "certificate_revoked",
202 | 45: "certificate_expired",
203 | 46: "certificate_unknown",
204 | 47: "illegal_parameter",
205 | 48: "unknown_ca",
206 | 49: "access_denied",
207 | 50: "decode_error",
208 | 51: "decrypt_error",
209 | 60: "export_restriction_RESERVED",
210 | 70: "protocol_version",
211 | 71: "insufficient_security",
212 | 80: "internal_error",
213 | 90: "user_canceled",
214 | 100: "no_renegotiation",
215 | 110: "unsupported_extension",
216 | }
217 |
218 | def __init__(self, data, **kwargs):
219 |
220 | ParsingDict.__init__(self, **kwargs)
221 |
222 | self.raw_data = data
223 |
224 | self.level = self.ensure("level", int)
225 | self.description = self.ensure("decription", int)
226 | if self.description is None:
227 | self.description = self.ensure("description", int)
228 |
229 | @property
230 | def description_string(self):
231 | return self.DESCRIPTION_MAP.get(self.description, "Unknown")
232 |
233 |
234 | class SslResult(Result):
235 |
236 | def __init__(self, data, **kwargs):
237 |
238 | Result.__init__(self, data, **kwargs)
239 |
240 | self.af = self.ensure("af", int)
241 | self.destination_address = self.ensure("dst_addr", str)
242 | self.destination_name = self.ensure("dst_name", str)
243 | self.source_address = self.ensure("src_addr", str)
244 | self.port = self.ensure("dst_port", int)
245 | self.method = self.ensure("method", str)
246 | self.version = self.ensure("ver", str)
247 | self.response_time = self.ensure("rt", float)
248 | self.time_to_connect = self.ensure("ttc", float)
249 |
250 | if "error" in self.raw_data:
251 | self._handle_error(self.raw_data["error"])
252 |
253 | # Older versions used named ports
254 | if self.port is None and self.raw_data.get("dst_port") == "https":
255 | self.port = 443
256 |
257 | self.alert = None
258 | self.certificates = []
259 | self.is_self_signed = False
260 |
261 | if "alert" in self.raw_data:
262 | self.alert = Alert(self.raw_data["alert"], **kwargs)
263 | self._handle_error(self.alert.description_string)
264 |
265 | if "cert" in self.raw_data and isinstance(self.raw_data["cert"], list):
266 |
267 | for certificate in self.raw_data["cert"]:
268 | try:
269 | self.certificates.append(Certificate(certificate, **kwargs))
270 | except Exception as exc:
271 | self._handle_error(str(exc))
272 | continue
273 |
274 | if len(self.certificates) == 1:
275 | certificate = self.certificates[0]
276 | if certificate.subject_cn == certificate.issuer_cn:
277 | self.is_self_signed = True
278 |
279 | @property
280 | def checksum_chain(self):
281 | """
282 | Returns a list of checksums joined with "::".
283 | """
284 |
285 | checksums = []
286 | for certificate in self.certificates:
287 | checksums.append(certificate.checksum)
288 |
289 | return "::".join(checksums)
290 |
291 |
292 | __all__ = (
293 | "SslResult"
294 | )
295 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/base.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import logging
17 | import pytz
18 |
19 | from calendar import timegm
20 | from datetime import datetime
21 |
22 | from .helpers.compatibility import string
23 |
24 | # Try to use ujson if it's available
25 | try:
26 | import ujson as json
27 | except ImportError:
28 | import json
29 |
30 |
31 | log = logging.getLogger(__name__)
32 |
33 |
34 | class ResultParseError(Exception):
35 | pass
36 |
37 |
38 | class ResultError(Exception):
39 | pass
40 |
41 |
42 | class Json(object):
43 | """
44 | ujson, while impressive, is not a drop-in replacement for json as it doesn't
45 | respect the various keyword arguments permitted in the default json parser.
46 | As a workaround for this, we have our own class that defines its own
47 | .loads() method, so we can check for whichever we're using and adjust the
48 | arguments accordingly.
49 | """
50 |
51 | @staticmethod
52 | def loads(*args, **kwargs):
53 | try:
54 | if json.__name__ == "ujson":
55 | return json.loads(*args, **kwargs)
56 | return json.loads(strict=False, *args, **kwargs)
57 | except ValueError:
58 | raise ResultParseError("The JSON result could not be parsed")
59 |
60 |
61 | class ParsingDict(object):
62 | """
63 | A handy container for methods we use for validation in the various result
64 | classes.
65 |
66 | Note that Python 2.x and 3.x handle the creation of dictionary-like objects
67 | differently. If we write it this way, it works for both.
68 | """
69 |
70 | ACTION_IGNORE = 1
71 | ACTION_WARN = 2
72 | ACTION_FAIL = 3
73 |
74 | PROTOCOL_ICMP = "ICMP"
75 | PROTOCOL_UDP = "UDP"
76 | PROTOCOL_TCP = "TCP"
77 | PROTOCOL_MAP = {
78 | "ICMP": PROTOCOL_ICMP,
79 | "I": PROTOCOL_ICMP,
80 | "UDP": PROTOCOL_UDP,
81 | "U": PROTOCOL_UDP,
82 | "TCP": PROTOCOL_TCP,
83 | "T": PROTOCOL_TCP,
84 | }
85 |
86 | def __init__(self, **kwargs):
87 |
88 | self._on_error = kwargs.pop("on_error", self.ACTION_WARN)
89 | self.is_error = False
90 | self.error_message = None
91 |
92 | self._on_malformation = kwargs.pop("on_malformation", self.ACTION_WARN)
93 | self.is_malformed = False
94 |
95 | def __nonzero__(self):
96 | # If we don't define this, Python ends up calling keys()
97 | # via __len__() whenever we evaluate the object as a bool.
98 | return True
99 |
100 | def __len__(self):
101 | return len(self.keys())
102 |
103 | def __iter__(self):
104 | for key in self.keys():
105 | yield getattr(self, key)
106 |
107 | def __getitem__(self, key):
108 | return getattr(self, key)
109 |
110 | def __setitem__(self, key, item):
111 | setattr(self, key, item)
112 |
113 | def keys(self):
114 | return [p for p in dir(self) if self._is_property_name(p)]
115 |
116 | def ensure(self, key, kind, default=None):
117 | try:
118 | if kind == "datetime":
119 | return datetime.fromtimestamp(
120 | self.raw_data[key], tz=pytz.UTC)
121 | return kind(self.raw_data[key])
122 | except (TypeError, ValueError, KeyError):
123 | return default
124 |
125 | def clean_protocol(self, protocol):
126 | """
127 | A lot of measurement types make use of a protocol value, so we handle
128 | that here.
129 | """
130 | if protocol is not None:
131 | try:
132 | return self.PROTOCOL_MAP[protocol]
133 | except KeyError:
134 | self._handle_malformation(
135 | '"{protocol}" is not a recognised protocol'.format(
136 | protocol=protocol
137 | )
138 | )
139 |
140 | def _handle_malformation(self, message):
141 | if self._on_malformation == self.ACTION_FAIL:
142 | raise ResultParseError(message)
143 | elif self._on_malformation == self.ACTION_WARN:
144 | log.warning(message)
145 | self.is_malformed = True
146 |
147 | def _handle_error(self, message):
148 | if self._on_error == self.ACTION_FAIL:
149 | raise ResultError(message)
150 | elif self._on_error == self.ACTION_WARN:
151 | log.warning(message)
152 | self.is_error = True
153 | self.error_message = message
154 |
155 | def _is_property_name(self, p):
156 | if not p.startswith("_"):
157 | if p not in ("keys",):
158 | if not p.upper() == p:
159 | if not callable(getattr(self, p)):
160 | return True
161 | return False
162 |
163 |
164 | class Result(ParsingDict):
165 | """
166 | The parent class for all measurement result classes. Subclass this to
167 | handle parsing a new measurement type, or use .get() to let this class
168 | figure out the type for you.
169 | """
170 |
171 | def __init__(self, data, *args, **kwargs):
172 |
173 | ParsingDict.__init__(self, **kwargs)
174 |
175 | self.raw_data = data
176 | if isinstance(data, string):
177 | self.raw_data = Json.loads(data)
178 |
179 | for key in ("timestamp", "msm_id", "prb_id", "fw", "type"):
180 | if key not in self.raw_data:
181 | raise ResultParseError(
182 | "This doesn't look like a RIPE Atlas measurement: {}".format(
183 | self.raw_data
184 | )
185 | )
186 |
187 | self.created = datetime.fromtimestamp(
188 | self.raw_data["timestamp"], tz=pytz.UTC)
189 |
190 | self.measurement_id = self.ensure("msm_id", int)
191 | self.probe_id = self.ensure("prb_id", int)
192 | self.firmware = self.ensure("fw", int)
193 | self.origin = self.ensure("from", str)
194 | self.seconds_since_sync = self.ensure("lts", int)
195 | self.group_id = self.ensure("group_id", int)
196 | self.bundle = self.ensure("bundle", int)
197 |
198 | # Handle the weird case where fw=0 and we don't know what to expect
199 | if self.firmware == 0:
200 | self._handle_malformation("Unknown firmware: {fw}".format(
201 | fw=self.firmware)
202 | )
203 |
204 | if self.seconds_since_sync is not None:
205 | if self.seconds_since_sync < 0:
206 | self.seconds_since_sync = None
207 |
208 | if "dnserr" in self.raw_data:
209 | self._handle_error(self.raw_data["dnserr"])
210 |
211 | if "err" in self.raw_data:
212 | self._handle_error(self.raw_data["err"])
213 |
214 | def __repr__(self):
215 | return "Measurement #{measurement}, Probe #{probe}".format(
216 | measurement=self.measurement_id,
217 | probe=self.probe_id
218 | )
219 |
220 | @property
221 | def created_timestamp(self):
222 | return timegm(self.created.timetuple())
223 |
224 | @classmethod
225 | def get(cls, data, **kwargs):
226 | """
227 | Call this when you have a JSON result and just want to turn it into the
228 | appropriate Result subclass. This is less performant than calling
229 | PingResult(json_string) directly however, as the JSON has to be parsed
230 | first to find the type.
231 | """
232 |
233 | raw_data = data
234 | if isinstance(data, string):
235 | raw_data = Json.loads(data)
236 |
237 | try:
238 | kind = raw_data["type"].lower()
239 | except KeyError:
240 | raise ResultParseError("No type value was found in the JSON input")
241 |
242 | if kind == "ping":
243 | from .ping import PingResult
244 | return PingResult(raw_data, **kwargs)
245 | elif kind == "traceroute":
246 | from .traceroute import TracerouteResult
247 | return TracerouteResult(raw_data, **kwargs)
248 | elif kind == "dns":
249 | from .dns import DnsResult
250 | return DnsResult(raw_data, **kwargs)
251 | elif kind == "sslcert":
252 | from .ssl import SslResult
253 | return SslResult(raw_data, **kwargs)
254 | elif kind == "http":
255 | from .http import HttpResult
256 | return HttpResult(raw_data, **kwargs)
257 | elif kind == "ntp":
258 | from .ntp import NtpResult
259 | return NtpResult(raw_data, **kwargs)
260 | elif kind == "wifi":
261 | from .wifi import WiFiResult
262 | return WiFiResult(raw_data, **kwargs)
263 |
264 | raise ResultParseError("Unknown type value was found in the JSON input")
265 |
266 | @staticmethod
267 | def calculate_median(given_list):
268 | """
269 | Returns the median of values in the given list.
270 | """
271 | median = None
272 |
273 | if not given_list:
274 | return median
275 |
276 | given_list = sorted(given_list)
277 | list_length = len(given_list)
278 |
279 | if list_length % 2:
280 | median = given_list[int(list_length / 2)]
281 | else:
282 | median = (given_list[int(list_length / 2)] + given_list[int(list_length / 2) - 1]) / 2.0
283 |
284 | return median
285 |
286 | @property
287 | def type(self):
288 | return self.__class__.__name__.replace("Result", "").lower()
289 |
290 |
291 | __all__ = (
292 | "Result",
293 | "ResultParseError",
294 | )
295 |
--------------------------------------------------------------------------------
/docs/use.rst:
--------------------------------------------------------------------------------
1 | .. _use-and-examples:
2 |
3 | Use & Examples
4 | **************
5 |
6 | The library contains a full test suite for each measurement type, so if you're
7 | looking for examples, it's a good idea to start there. For this document we'll
8 | cover basic usage and some simple examples to get you started.
9 |
10 |
11 | .. _use:
12 |
13 | How To Use This Library
14 | =======================
15 |
16 | Sagan's sole purpose is to make RIPE Atlas measurements manageable from within
17 | Python. You shouldn't have to be fiddling with JSON, or trying to find values
18 | that changed locations between firmware versions. Instead, you should always
19 | be able to pass in the JSON string and immediately get usable Python objects.
20 |
21 |
22 | .. _use-basics:
23 |
24 | Important Note
25 | --------------
26 |
27 | The one thing that tends to confuse people when first trying out Sagan is that
28 | this library operates on **single measurement results**, and not a list of
29 | results. If you have a list of results (for example, the output of the
30 | measurement results API), then you must loop over those results and pass each
31 | result to Sagan for parsing.
32 |
33 | Basics
34 | ------
35 |
36 | To that end, the interface is pretty simple. If you have a ping measurement
37 | result, then use the PingResult class to make use of the data::
38 |
39 | from ripe.atlas.sagan import PingResult
40 |
41 | my_result = PingResult('this is where your big JSON blob goes')
42 |
43 | my_result.af
44 | # Returns 6
45 |
46 | my_result.rtt_median
47 | # Returns 123.456
48 |
49 | Note that ``rtt_median`` isn't actually in the JSON data passed in. It's
50 | calculated during the parsing phase so you don't need to fiddle with looping
51 | over attributes in a list and doing the math yourself.
52 |
53 |
54 | .. _use-plain-text-not-required:
55 |
56 | Plain Text Not Required
57 | -----------------------
58 |
59 | It should be noted that while all of the examples here use a plain text string
60 | for our results, Sagan doesn't force you to pass in a string. It's just as
61 | happy with a Python dict, the result of already running your result string
62 | through ``json.loads()``::
63 |
64 | import json
65 | from ripe.atlas.sagan import PingResult
66 |
67 | my_result_dict = json.loads('this is where your big JSON blob goes')
68 | my_result = PingResult(my_result_dict)
69 |
70 | my_result.af
71 | # Returns 6
72 |
73 | my_result.rtt_median
74 | # Returns 123.456
75 |
76 |
77 | .. _use-agnostic-parsing:
78 |
79 | Agnostic Parsing
80 | ----------------
81 |
82 | There may be a case where you have code that's just expected to parse a result
83 | string, without knowing ahead of time what type of result it is. For this we
84 | make use of the parent ``Result`` class' ``get()`` method::
85 |
86 | from ripe.atlas.sagan import Result
87 |
88 | my_result = Result.get('this is where your big JSON blob goes')
89 |
90 | my_result.af
91 | # Returns 6
92 |
93 | my_result.rtt_median
94 | # Returns 123.456
95 |
96 | As you can see it works just like PingResult, but doesn't force you to know its
97 | type up front. Note that this does incur a small performance penalty however.
98 |
99 |
100 | .. _use-errors-and-malformations:
101 |
102 | Errors & Malformations
103 | ----------------------
104 |
105 | RIPE Atlas, like the Internet is never 100% what you'd expect. Sometimes your
106 | measurement will return an error such as a timout or DNS lookup problem, and
107 | sometimes the data in a result might even be malformed on account of data
108 | corruption, damaged probe storage, etc.
109 |
110 | And like the most applications on the Internet, Sagan attemps to handle these
111 | inconsistencies gracefully. You can decide just how gracefully however.
112 |
113 | Say for example you've got a result that looks alright, but the ``abuf`` value
114 | is damaged in some way rendering it unreadable. You'll find that while the
115 | ``DnsResult`` object will not have a ``is_malformed=False``, the portion that is
116 | unreadable will be set to ``True``::
117 |
118 | from ripe.atlas.sagan import DnsResult
119 | my_result = DnsResult('your JSON blob')
120 |
121 | my_result.is_error # False
122 | my_result.is_malformed # False
123 | my_result.responses[0].abuf.is_malformed # True
124 | my_result.responses[1].abuf.is_malformed # False
125 |
126 | You can control what you'd like Sagan to do in these cases by setting
127 | ``on_malformation=`` when parsing::
128 |
129 | from ripe.atlas.sagan import DnsResult
130 |
131 | # Sets is_malformed=True and issues a warning
132 | my_result = DnsResult('your JSON blob')
133 |
134 | # Sets is_malformed=True
135 | my_result = DnsResult('your JSON blob', on_malformation=DnsResult.ACTION_IGNORE)
136 |
137 | # Sets explodes with a ResultParseError
138 | my_result = DnsResult('your JSON blob', on_malformation=DnsResult.ACTION_FAIL)
139 |
140 | Similarly, you can do the same thing with ``on_error=``, which perform the same
141 | way when Sagan encounters an error like a timeout or DNS lookup problem.
142 |
143 | Error handling is not yet complete in Sagan, so if you run across a case where
144 | it behaves in a way other than what you'd expect, please send a copy of the
145 | problematic result to atlas@ripe.net and we'll use it to update this library.
146 |
147 |
148 | .. _examples:
149 |
150 | Examples
151 | ========
152 |
153 | .. _examples-file:
154 |
155 | Parsing Results out of a Local File
156 | -----------------------------------
157 |
158 | Assume for a moment that you've downloaded a bunch of results into a local file
159 | using our *fragmented JSON* format. That is, you have in your possession a file
160 | that has a separate JSON result on every line. For the purposes of our example
161 | we'll call it ``file.txt``.::
162 |
163 | from ripe.atlas.sagan import Result
164 |
165 | my_results_file = "/path/to/file.txt"
166 | with open(my_results_file) as results:
167 | for result in results.readlines():
168 | parsed_result = Result.get(result)
169 | print(parsed_result.origin)
170 |
171 | Basically you use Python to open the file (using ``with``) and then loop over
172 | each line in the file (``.readlines()``), sending each line into Sagan which
173 | returns a ``parsed_result``. With that result, you can then pull out any of
174 | the values you like, using the :ref:`attributes-methods` documentation as a
175 | reference.
176 |
177 |
178 | .. _examples-api:
179 |
180 | Pulling Directly from the API
181 | -----------------------------
182 |
183 | A common use case for the parser is to plug it into our RESTful API service.
184 | The process for this is pretty simple: fetch a bunch of results, loop over them,
185 | and for each one, apply the parser to get the value you want.
186 |
187 | Say for example you want to get the ``checksum`` value for each result from
188 | measurement `#1012449`_. To do this, we'll fetch the latest results from each
189 | probe via the ``measurement-latest`` API, and parse each one to get the
190 | checksum values::
191 |
192 | import requests
193 | from ripe.atlas.sagan import SslResult
194 |
195 | source = "https://atlas.ripe.net/api/v1/measurement-latest/1012449/"
196 | response = requests.get(source).json
197 |
198 | for probe_id, result in response.items():
199 |
200 | result = result[0] # There's only one result for each probe
201 | parsed_result = SslResult(result) # Parsing magic!
202 |
203 | # Each SslResult has n certificates
204 | for certificate in parsed_result.certificates:
205 | print(certificate.checksum) # Print the checksum for this certificate
206 |
207 | # Make use of the handy get_checksum_chain() to render the checksum of each certificate into one string if you want
208 | print(parsed_result.get_checksum_chain())
209 |
210 |
211 | .. _#1012449: https://atlas.ripe.net/measurements/1012449/
212 |
213 |
214 | .. _examples-types:
215 |
216 | Samples from Each Type
217 | ----------------------
218 |
219 |
220 | .. _examples-types-ping:
221 |
222 | Ping
223 | ....
224 |
225 | For more information regarding all properties available, you should consult the
226 | :ref:`ping` section of this documentation.::
227 |
228 | ping_result.packets_sent # Int
229 | ping_result.rtt_median # Float, rounded to 3 decimal places
230 | ping_result.rtt_average # Float, rounded to 3 decimal places
231 |
232 |
233 | .. _examples-types-traceroute:
234 |
235 | Traceroute
236 | ..........
237 |
238 | For more information regarding all properties available, you should consult the
239 | :ref:`traceroute` section of this documentation.::
240 |
241 | traceroute_result.af # 4 or 6
242 | traceroute_result.total_hops # Int
243 | traceroute_result.destination_address # An IP address string
244 |
245 |
246 | .. _examples-types-dns:
247 |
248 | DNS
249 | ....
250 |
251 | For more information regarding all properties available, you should consult the
252 | :ref:`dns` section of this documentation.::
253 |
254 | dns_result.responses # A list of Response objects
255 | dns_result.responses[0].response_time # Float, rounded to 3 decimal places
256 | dns_result.responses[0].headers # A list of Header objects
257 | dns_result.responses[0].headers[0].nscount # The NSCOUNT value for the first header
258 | dns_result.responses[0].questions # A list of Question objects
259 | dns_result.responses[0].questions[0].type # The TYPE value for the first question
260 | dns_result.responses[0].abuf # The raw, unparsed abuf string
261 |
262 |
263 | .. _examples-types-sslcert:
264 |
265 | SSL Certificates
266 | ................
267 |
268 | For more information regarding all properties available, you should consult the
269 | :ref:`sslcert` section of this documentation.::
270 |
271 | ssl_result.af # 4 or 6
272 | ssl_result.certificates # A list of Certificate objects
273 | ssl_result.certificates[0].checksum # The checksum for the first certificate
274 |
275 |
276 | .. _examples-types-http:
277 |
278 | HTTP
279 | ....
280 |
281 | For more information regarding all properties available, you should consult the
282 | :ref:`http` section of this documentation.::
283 |
284 | http_result.af # 4 or 6
285 | http_result.uri # A URL string
286 | http_result.responses # A list of Response objects
287 | http_result.responses[0].body_size # The size of the body of the first response
288 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # RIPE Atlas Sagan documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Apr 29 13:41:57 2014.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import sys
16 | import os
17 |
18 | __version__ = None
19 | exec(open("../ripe/atlas/sagan/version.py").read())
20 |
21 | # If extensions (or modules to document with autodoc) are in another directory,
22 | # add these directories to sys.path here. If the directory is relative to the
23 | # documentation root, use os.path.abspath to make it absolute, like shown here.
24 | #sys.path.insert(0, os.path.abspath('.'))
25 |
26 | # -- General configuration ------------------------------------------------
27 |
28 | # If your documentation needs a minimal Sphinx version, state it here.
29 | #needs_sphinx = '1.0'
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
33 | # ones.
34 | extensions = [
35 | 'sphinx.ext.autodoc',
36 | 'sphinx.ext.viewcode',
37 | 'sphinx_rtd_theme',
38 | ]
39 |
40 | # Add any paths that contain templates here, relative to this directory.
41 | templates_path = ['_templates']
42 |
43 | # The suffix of source filenames.
44 | source_suffix = '.rst'
45 |
46 | # The encoding of source files.
47 | #source_encoding = 'utf-8-sig'
48 |
49 | # The master toctree document.
50 | master_doc = 'index'
51 |
52 | # General information about the project.
53 | project = u'RIPE Atlas Sagan'
54 | copyright = u'2014, RIPE NCC'
55 |
56 | # The version info for the project you're documenting, acts as replacement for
57 | # |version| and |release|, also used in various other places throughout the
58 | # built documents.
59 |
60 | #
61 | # If the build process ever explodes here, it's because you've set the version
62 | # number in ripe.atlas.tools.version to a string in a format other than x.y.z
63 | #
64 |
65 | # The short X.Y version.
66 | version = ".".join(__version__.split(".")[:2])
67 | # The full version, including alpha/beta/rc tags.
68 | release = ".".join(__version__.split(".")[:3])
69 |
70 |
71 | # The language for content autogenerated by Sphinx. Refer to documentation
72 | # for a list of supported languages.
73 | language = 'python'
74 |
75 | # There are two options for replacing |today|: either, you set today to some
76 | # non-false value, then it is used:
77 | #today = ''
78 | # Else, today_fmt is used as the format for a strftime call.
79 | #today_fmt = '%B %d, %Y'
80 |
81 | # List of patterns, relative to source directory, that match files and
82 | # directories to ignore when looking for source files.
83 | exclude_patterns = ['_build']
84 |
85 | # The reST default role (used for this markup: `text`) to use for all
86 | # documents.
87 | #default_role = None
88 |
89 | # If true, '()' will be appended to :func: etc. cross-reference text.
90 | #add_function_parentheses = True
91 |
92 | # If true, the current module name will be prepended to all description
93 | # unit titles (such as .. function::).
94 | #add_module_names = True
95 |
96 | # If true, sectionauthor and moduleauthor directives will be shown in the
97 | # output. They are ignored by default.
98 | #show_authors = False
99 |
100 | # The name of the Pygments (syntax highlighting) style to use.
101 | pygments_style = 'sphinx'
102 |
103 | # A list of ignored prefixes for module index sorting.
104 | #modindex_common_prefix = []
105 |
106 | # If true, keep warnings as "system message" paragraphs in the built documents.
107 | #keep_warnings = False
108 |
109 |
110 | # -- Options for HTML output ----------------------------------------------
111 |
112 | # The theme to use for HTML and HTML Help pages. See the documentation for
113 | # a list of builtin themes.
114 | html_theme = 'sphinx_rtd_theme'
115 |
116 | # Theme options are theme-specific and customize the look and feel of a theme
117 | # further. For a list of options available for each theme, see the
118 | # documentation.
119 | #html_theme_options = {}
120 |
121 | # Add any paths that contain custom themes here, relative to this directory.
122 | #html_theme_path = []
123 |
124 | # The name for this set of Sphinx documents. If None, it defaults to
125 | # " v documentation".
126 | #html_title = None
127 |
128 | # A shorter title for the navigation bar. Default is the same as html_title.
129 | #html_short_title = None
130 |
131 | # The name of an image file (relative to this directory) to place at the top
132 | # of the sidebar.
133 | #html_logo = None
134 |
135 | # The name of an image file (within the static path) to use as favicon of the
136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
137 | # pixels large.
138 | #html_favicon = None
139 |
140 | # Add any paths that contain custom static files (such as style sheets) here,
141 | # relative to this directory. They are copied after the builtin static files,
142 | # so a file named "default.css" will overwrite the builtin "default.css".
143 | html_static_path = ['_static']
144 |
145 | # Add any extra paths that contain custom files (such as robots.txt or
146 | # .htaccess) here, relative to this directory. These files are copied
147 | # directly to the root of the documentation.
148 | #html_extra_path = []
149 |
150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
151 | # using the given strftime format.
152 | #html_last_updated_fmt = '%b %d, %Y'
153 |
154 | # If true, SmartyPants will be used to convert quotes and dashes to
155 | # typographically correct entities.
156 | #html_use_smartypants = True
157 |
158 | # Custom sidebar templates, maps document names to template names.
159 | #html_sidebars = {}
160 |
161 | # Additional templates that should be rendered to pages, maps page names to
162 | # template names.
163 | #html_additional_pages = {}
164 |
165 | # If false, no module index is generated.
166 | #html_domain_indices = True
167 |
168 | # If false, no index is generated.
169 | #html_use_index = True
170 |
171 | # If true, the index is split into individual pages for each letter.
172 | #html_split_index = False
173 |
174 | # If true, links to the reST sources are added to the pages.
175 | #html_show_sourcelink = True
176 |
177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
178 | #html_show_sphinx = True
179 |
180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
181 | #html_show_copyright = True
182 |
183 | # If true, an OpenSearch description file will be output, and all pages will
184 | # contain a tag referring to it. The value of this option must be the
185 | # base URL from which the finished HTML is served.
186 | #html_use_opensearch = ''
187 |
188 | # This is the file name suffix for HTML files (e.g. ".xhtml").
189 | #html_file_suffix = None
190 |
191 | # Output file base name for HTML help builder.
192 | htmlhelp_basename = 'RIPEAtlasSagandoc'
193 |
194 |
195 | #
196 | # Attempt to use the ReadTheDocs theme. If it's not installed, fallback to
197 | # the default.
198 | #
199 |
200 | try:
201 | import sphinx_rtd_theme
202 | html_theme = "sphinx_rtd_theme"
203 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
204 | except ImportError:
205 | pass
206 |
207 |
208 | # -- Options for LaTeX output ---------------------------------------------
209 |
210 | latex_elements = {
211 | # The paper size ('letterpaper' or 'a4paper').
212 | #'papersize': 'letterpaper',
213 |
214 | # The font size ('10pt', '11pt' or '12pt').
215 | #'pointsize': '10pt',
216 |
217 | # Additional stuff for the LaTeX preamble.
218 | #'preamble': '',
219 | }
220 |
221 | # Grouping the document tree into LaTeX files. List of tuples
222 | # (source start file, target name, title,
223 | # author, documentclass [howto, manual, or own class]).
224 | latex_documents = [
225 | ('index', 'RIPEAtlasSagan.tex', u'RIPE Atlas Sagan Documentation',
226 | u'Daniel Quinn', 'manual'),
227 | ]
228 |
229 | # The name of an image file (relative to this directory) to place at the top of
230 | # the title page.
231 | #latex_logo = None
232 |
233 | # For "manual" documents, if this is true, then toplevel headings are parts,
234 | # not chapters.
235 | #latex_use_parts = False
236 |
237 | # If true, show page references after internal links.
238 | #latex_show_pagerefs = False
239 |
240 | # If true, show URL addresses after external links.
241 | #latex_show_urls = False
242 |
243 | # Documents to append as an appendix to all manuals.
244 | #latex_appendices = []
245 |
246 | # If false, no module index is generated.
247 | #latex_domain_indices = True
248 |
249 |
250 | # -- Options for manual page output ---------------------------------------
251 |
252 | # One entry per manual page. List of tuples
253 | # (source start file, name, description, authors, manual section).
254 | man_pages = [
255 | ('index', 'ripeatlassagan', u'RIPE Atlas Sagan Documentation',
256 | [u'Daniel Quinn'], 1)
257 | ]
258 |
259 | # If true, show URL addresses after external links.
260 | #man_show_urls = False
261 |
262 |
263 | # -- Options for Texinfo output -------------------------------------------
264 |
265 | # Grouping the document tree into Texinfo files. List of tuples
266 | # (source start file, target name, title, author,
267 | # dir menu entry, description, category)
268 | texinfo_documents = [
269 | ('index', 'RIPEAtlasSagan', u'RIPE Atlas Sagan Documentation',
270 | u'Daniel Quinn', 'RIPEAtlasSagan', 'One line description of project.',
271 | 'Miscellaneous'),
272 | ]
273 |
274 | # Documents to append as an appendix to all manuals.
275 | #texinfo_appendices = []
276 |
277 | # If false, no module index is generated.
278 | #texinfo_domain_indices = True
279 |
280 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
281 | #texinfo_show_urls = 'footnote'
282 |
283 | # If true, do not generate a @detailmenu in the "Top" node's menu.
284 | #texinfo_no_detailmenu = False
285 |
286 |
287 | # -- Options for Epub output ----------------------------------------------
288 |
289 | # Bibliographic Dublin Core info.
290 | epub_title = u'RIPE Atlas Sagan'
291 | epub_author = u'Daniel Quinn'
292 | epub_publisher = u'Daniel Quinn'
293 | epub_copyright = u'2014, Daniel Quinn'
294 |
295 | # The basename for the epub file. It defaults to the project name.
296 | #epub_basename = u'RIPE Atlas Sagan'
297 |
298 | # The HTML theme for the epub output. Since the default themes are not optimized
299 | # for small screen space, using the same theme for HTML and epub output is
300 | # usually not wise. This defaults to 'epub', a theme designed to save visual
301 | # space.
302 | #epub_theme = 'epub'
303 |
304 | # The language of the text. It defaults to the language option
305 | # or en if the language is not set.
306 | #epub_language = ''
307 |
308 | # The scheme of the identifier. Typical schemes are ISBN or URL.
309 | #epub_scheme = ''
310 |
311 | # The unique identifier of the text. This can be a ISBN number
312 | # or the project homepage.
313 | #epub_identifier = ''
314 |
315 | # A unique identification for the text.
316 | #epub_uid = ''
317 |
318 | # A tuple containing the cover image and cover page html template filenames.
319 | #epub_cover = ()
320 |
321 | # A sequence of (type, uri, title) tuples for the guide element of content.opf.
322 | #epub_guide = ()
323 |
324 | # HTML files that should be inserted before the pages created by sphinx.
325 | # The format is a list of tuples containing the path and title.
326 | #epub_pre_files = []
327 |
328 | # HTML files shat should be inserted after the pages created by sphinx.
329 | # The format is a list of tuples containing the path and title.
330 | #epub_post_files = []
331 |
332 | # A list of files that should not be packed into the epub file.
333 | epub_exclude_files = ['search.html']
334 |
335 | # The depth of the table of contents in toc.ncx.
336 | #epub_tocdepth = 3
337 |
338 | # Allow duplicate toc entries.
339 | #epub_tocdup = True
340 |
341 | # Choose between 'default' and 'includehidden'.
342 | #epub_tocscope = 'default'
343 |
344 | # Fix unsupported image types using the PIL.
345 | #epub_fix_images = False
346 |
347 | # Scale large images.
348 | #epub_max_image_width = 0
349 |
350 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
351 | #epub_show_urls = 'inline'
352 |
353 | # If false, no index is generated.
354 | #epub_use_index = True
355 |
--------------------------------------------------------------------------------
/tests/test_http.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from ripe.atlas.sagan import Result, ResultError, ResultParseError
17 | from ripe.atlas.sagan.http import HttpResult
18 |
19 | def test_http_0():
20 | data = '{"fw":0,"msm_id":12023,"prb_id":1,"src_addr":"GET4 193.0.6.139 0.042268 200 263 1406","timestamp":1319704299,"type":"http"}'
21 | result = Result.get(data)
22 | assert(result.is_malformed is True)
23 | try:
24 | Result.get(data, on_malformation=Result.ACTION_FAIL)
25 | assert False
26 | except ResultParseError:
27 | pass
28 |
29 | def test_http_1_error():
30 | data = '{"fw":1,"msm_id":12023,"prb_id":1,"src_addr":"connect error 4","timestamp":1323118908,"type":"http"}'
31 | result = Result.get(data)
32 | assert(result.is_malformed is True)
33 | try:
34 | Result.get(data, on_malformation=Result.ACTION_FAIL)
35 | assert False
36 | except ResultParseError:
37 | pass
38 |
39 | def test_http_1():
40 | result = Result.get('{"from":"62.194.83.50","fw":1,"msm_id":12023,"prb_id":1,"result":"GET4 193.0.6.139 0.030229 200 263 1406","timestamp":1333387900,"type":"http"}')
41 | assert(isinstance(result, HttpResult))
42 | assert(result.origin == "62.194.83.50")
43 | assert(result.firmware == 1)
44 | assert(result.measurement_id == 12023)
45 | assert(result.probe_id == 1)
46 | assert(result.created.isoformat() == "2012-04-02T17:31:40+00:00")
47 | assert(result.uri is None)
48 | assert(result.method == HttpResult.METHOD_GET)
49 | assert(isinstance(result.responses, list))
50 | assert(len(result.responses) == 1)
51 | assert(result.responses[0].af is None)
52 | assert(result.responses[0].body_size == 1406)
53 | assert(result.responses[0].head_size == 263)
54 | assert(result.responses[0].destination_address == "193.0.6.139")
55 | assert(result.responses[0].code == 200)
56 | assert(result.responses[0].response_time == 30.229)
57 | assert(result.responses[0].source_address is None)
58 | assert(result.responses[0].version is None)
59 |
60 | def test_http_4430():
61 | result = Result.get('{"from":"62.194.83.50","fw":4430,"msm_id":12023,"prb_id":1,"result":[{"addr":"193.0.6.139","bsize":1406,"hsize":263,"mode":"GET4","res":200,"rt":27.276,"srcaddr":"192.168.99.183","ver":"1.1"}],"timestamp":1336418202,"type":"http"}')
62 | assert(isinstance(result, HttpResult))
63 | assert(result.origin == "62.194.83.50")
64 | assert(result.firmware == 4430)
65 | assert(result.measurement_id == 12023)
66 | assert(result.probe_id == 1)
67 | assert(result.created.isoformat() == "2012-05-07T19:16:42+00:00")
68 | assert(result.uri is None)
69 | assert(result.method == HttpResult.METHOD_GET)
70 | assert(isinstance(result.responses, list))
71 | assert(len(result.responses) == 1)
72 | assert(result.responses[0].af is None)
73 | assert(result.responses[0].body_size == 1406)
74 | assert(result.responses[0].head_size == 263)
75 | assert(result.responses[0].destination_address == "193.0.6.139")
76 | assert(result.responses[0].code == 200)
77 | assert(result.responses[0].response_time == 27.276)
78 | assert(result.responses[0].source_address == "192.168.99.183")
79 | assert(result.responses[0].version == "1.1")
80 |
81 | def test_http_4460():
82 | result = Result.get('{"from":"2001:780:100:6:220:4aff:fee0:2479","fw":4460,"msm_id":1003930,"prb_id":2707,"result":[{"af":6,"bsize":22383,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":71.146000000000001,"src_addr":"2001:780:100:6:220:4aff:fee0:2479","ver":"1.1"}],"timestamp":1350471605,"type":"http","uri":"http://www.ripe.net/"}')
83 | assert(isinstance(result, HttpResult))
84 | assert(result.origin == "2001:780:100:6:220:4aff:fee0:2479")
85 | assert(result.firmware == 4460)
86 | assert(result.measurement_id == 1003930)
87 | assert(result.probe_id == 2707)
88 | assert(result.created.isoformat() == "2012-10-17T11:00:05+00:00")
89 | assert(result.uri == "http://www.ripe.net/")
90 | assert(result.method == HttpResult.METHOD_GET)
91 | assert(isinstance(result.responses, list))
92 | assert(len(result.responses) == 1)
93 | assert(result.responses[0].af == 6)
94 | assert(result.responses[0].body_size == 22383)
95 | assert(result.responses[0].head_size == 279)
96 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
97 | assert(result.responses[0].code == 200)
98 | assert(result.responses[0].response_time == 71.146)
99 | assert(result.responses[0].source_address == result.origin)
100 | assert(result.responses[0].version == "1.1")
101 |
102 |
103 | def test_http_4470():
104 | result = Result.get('{"from":"2001:4538:100:0:220:4aff:fec8:232b","fw":4470,"msm_id":1003930,"prb_id":303,"result":[{"af":6,"bsize":22243,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":765.25400000000002,"src_addr":"2001:4538:100:0:220:4aff:fec8:232b","ver":"1.1"}],"timestamp":1352869218,"type":"http","uri":"http://www.ripe.net/"}')
105 | assert(isinstance(result, HttpResult))
106 | assert(result.origin == "2001:4538:100:0:220:4aff:fec8:232b")
107 | assert(result.firmware == 4470)
108 | assert(result.measurement_id == 1003930)
109 | assert(result.probe_id == 303)
110 | assert(result.created.isoformat() == "2012-11-14T05:00:18+00:00")
111 | assert(result.uri == "http://www.ripe.net/")
112 | assert(result.method == HttpResult.METHOD_GET)
113 | assert(isinstance(result.responses, list))
114 | assert(len(result.responses) == 1)
115 | assert(result.responses[0].af == 6)
116 | assert(result.responses[0].body_size == 22243)
117 | assert(result.responses[0].head_size == 279)
118 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
119 | assert(result.responses[0].code == 200)
120 | assert(result.responses[0].response_time == 765.254)
121 | assert(result.responses[0].source_address == result.origin)
122 | assert(result.responses[0].version == "1.1")
123 |
124 |
125 | def test_http_4480():
126 | result = Result.get('{"fw":4480,"msm_id":1003930,"prb_id":2184,"result":[{"af":6,"bsize":39777,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":660.40999999999997,"src_addr":"2a02:6c80:5:0:220:4aff:fee0:2774","ver":"1.1"}],"src_addr":"2a02:6c80:5:0:220:4aff:fee0:2774","timestamp":1372582858,"type":"http","uri":"http://www.ripe.net/"}')
127 | assert(isinstance(result, HttpResult))
128 | assert(result.origin is None)
129 | assert(result.firmware == 4480)
130 | assert(result.measurement_id == 1003930)
131 | assert(result.probe_id == 2184)
132 | assert(result.created.isoformat() == "2013-06-30T09:00:58+00:00")
133 | assert(result.uri == "http://www.ripe.net/")
134 | assert(result.method == HttpResult.METHOD_GET)
135 | assert(isinstance(result.responses, list))
136 | assert(len(result.responses) == 1)
137 | assert(result.responses[0].af == 6)
138 | assert(result.responses[0].body_size == 39777)
139 | assert(result.responses[0].head_size == 279)
140 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
141 | assert(result.responses[0].code == 200)
142 | assert(result.responses[0].response_time == 660.41)
143 | assert(result.responses[0].source_address == "2a02:6c80:5:0:220:4aff:fee0:2774")
144 | assert(result.responses[0].version == "1.1")
145 |
146 |
147 | def test_http_4500():
148 | result = Result.get('{"from":"2a02:8304:1:4:220:4aff:fee0:228d","fw":4500,"msm_id":1003930,"prb_id":2954,"result":[{"af":6,"bsize":40103,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":234.048,"src_addr":"2a02:8304:1:4:220:4aff:fee0:228d","ver":"1.1"}],"timestamp":1367233244,"type":"http","uri":"http://www.ripe.net/"}')
149 | assert(isinstance(result, HttpResult))
150 | assert(result.origin == "2a02:8304:1:4:220:4aff:fee0:228d")
151 | assert(result.firmware == 4500)
152 | assert(result.measurement_id == 1003930)
153 | assert(result.probe_id == 2954)
154 | assert(result.created.isoformat() == "2013-04-29T11:00:44+00:00")
155 | assert(result.uri == "http://www.ripe.net/")
156 | assert(result.method == HttpResult.METHOD_GET)
157 | assert(isinstance(result.responses, list))
158 | assert(len(result.responses) == 1)
159 | assert(result.responses[0].af == 6)
160 | assert(result.responses[0].body_size == 40103)
161 | assert(result.responses[0].head_size == 279)
162 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
163 | assert(result.responses[0].code == 200)
164 | assert(result.responses[0].response_time == 234.048)
165 | assert(result.responses[0].source_address == result.origin)
166 | assert(result.responses[0].version == "1.1")
167 |
168 |
169 | def test_http_4520():
170 | result = Result.get('{"from":"2a02:27d0:100:115:6000::250","fw":4520,"msm_id":1003930,"msm_name":"HTTPGet","prb_id":2802,"result":[{"af":6,"bsize":40567,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":102.825,"src_addr":"2a02:27d0:100:115:6000::250","ver":"1.1"}],"timestamp":1379594363,"type":"http","uri":"http://www.ripe.net/"}')
171 | assert(isinstance(result, HttpResult))
172 | assert(result.origin == "2a02:27d0:100:115:6000::250")
173 | assert(result.firmware == 4520)
174 | assert(result.measurement_id == 1003930)
175 | assert(result.probe_id == 2802)
176 | assert(result.created.isoformat() == "2013-09-19T12:39:23+00:00")
177 | assert(result.uri == "http://www.ripe.net/")
178 | assert(result.method == HttpResult.METHOD_GET)
179 | assert(isinstance(result.responses, list))
180 | assert(len(result.responses) == 1)
181 | assert(result.responses[0].af == 6)
182 | assert(result.responses[0].body_size == 40567)
183 | assert(result.responses[0].head_size == 279)
184 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
185 | assert(result.responses[0].code == 200)
186 | assert(result.responses[0].response_time == 102.825)
187 | assert(result.responses[0].source_address == result.origin)
188 | assert(result.responses[0].version == "1.1")
189 |
190 |
191 | def test_http_4540():
192 | result = Result.get('{"from":"2001:980:36af:1:220:4aff:fec8:226d","fw":4540,"msm_id":1003930,"msm_name":"HTTPGet","prb_id":26,"result":[{"af":6,"bsize":40485,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":158.994,"src_addr":"2001:980:36af:1:220:4aff:fec8:226d","ver":"1.1"}],"timestamp":1377695803,"type":"http","uri":"http://www.ripe.net/"}')
193 | assert(isinstance(result, HttpResult))
194 | assert(result.origin == "2001:980:36af:1:220:4aff:fec8:226d")
195 | assert(result.firmware == 4540)
196 | assert(result.measurement_id == 1003930)
197 | assert(result.probe_id == 26)
198 | assert(result.created.isoformat() == "2013-08-28T13:16:43+00:00")
199 | assert(result.uri == "http://www.ripe.net/")
200 | assert(result.method == HttpResult.METHOD_GET)
201 | assert(isinstance(result.responses, list))
202 | assert(len(result.responses) == 1)
203 | assert(result.responses[0].af == 6)
204 | assert(result.responses[0].body_size == 40485)
205 | assert(result.responses[0].head_size == 279)
206 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
207 | assert(result.responses[0].code == 200)
208 | assert(result.responses[0].response_time == 158.994)
209 | assert(result.responses[0].source_address == "2001:980:36af:1:220:4aff:fec8:226d")
210 | assert(result.responses[0].version == "1.1")
211 |
212 |
213 | def test_http_4550():
214 | result = Result.get('{"from":"2001:4538:100:0:220:4aff:fec8:232b","fw":4550,"msm_id":1003930,"msm_name":"HTTPGet","prb_id":303,"result":[{"af":6,"bsize":40118,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":2092.447,"src_addr":"2001:4538:100:0:220:4aff:fec8:232b","ver":"1.1"}],"timestamp":1380901810,"type":"http","uri":"http://www.ripe.net/"}')
215 | assert(isinstance(result, HttpResult))
216 | assert(result.origin == "2001:4538:100:0:220:4aff:fec8:232b")
217 | assert(result.firmware == 4550)
218 | assert(result.measurement_id == 1003930)
219 | assert(result.probe_id == 303)
220 | assert(result.created.isoformat() == "2013-10-04T15:50:10+00:00")
221 | assert(result.uri == "http://www.ripe.net/")
222 | assert(result.method == HttpResult.METHOD_GET)
223 | assert(isinstance(result.responses, list))
224 | assert(len(result.responses) == 1)
225 | assert(result.responses[0].af == 6)
226 | assert(result.responses[0].body_size == 40118)
227 | assert(result.responses[0].head_size == 279)
228 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
229 | assert(result.responses[0].code == 200)
230 | assert(result.responses[0].response_time == 2092.447)
231 | assert(result.responses[0].source_address == "2001:4538:100:0:220:4aff:fec8:232b")
232 | assert(result.responses[0].version == "1.1")
233 |
234 |
235 | def test_http_4560():
236 | result = Result.get('{"from":"2620:0:2ed0:aaaa::210","fw":4560,"msm_id":1003930,"msm_name":"HTTPGet","prb_id":1164,"result":[{"af":6,"bsize":39340,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":739.26999999999998,"src_addr":"2620:0:2ed0:aaaa::210","ver":"1.1"}],"timestamp":1385895661,"type":"http","uri":"http://www.ripe.net/"}')
237 | assert(isinstance(result, HttpResult))
238 | assert(result.origin == "2620:0:2ed0:aaaa::210")
239 | assert(result.firmware == 4560)
240 | assert(result.measurement_id == 1003930)
241 | assert(result.probe_id == 1164)
242 | assert(result.created.isoformat() == "2013-12-01T11:01:01+00:00")
243 | assert(result.uri == "http://www.ripe.net/")
244 | assert(result.method == HttpResult.METHOD_GET)
245 | assert(isinstance(result.responses, list))
246 | assert(len(result.responses) == 1)
247 | assert(result.responses[0].af == 6)
248 | assert(result.responses[0].body_size == 39340)
249 | assert(result.responses[0].head_size == 279)
250 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
251 | assert(result.responses[0].code == 200)
252 | assert(result.responses[0].response_time == 739.27)
253 | assert(result.responses[0].source_address == "2620:0:2ed0:aaaa::210")
254 | assert(result.responses[0].version == "1.1")
255 |
256 |
257 | def test_http_4570():
258 | result = Result.get('{"from":"2001:8b0:34f:5b8:220:4aff:fee0:21fc","fw":4570,"msm_id":1003930,"msm_name":"HTTPGet","prb_id":2030,"result":[{"af":6,"bsize":41021,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":187.02199999999999,"src_addr":"2001:8b0:34f:5b8:220:4aff:fee0:21fc","ver":"1.1"}],"timestamp":1395324614,"type":"http","uri":"http://www.ripe.net/"}')
259 | assert(isinstance(result, HttpResult))
260 | assert(result.origin == "2001:8b0:34f:5b8:220:4aff:fee0:21fc")
261 | assert(result.firmware == 4570)
262 | assert(result.measurement_id == 1003930)
263 | assert(result.probe_id == 2030)
264 | assert(result.created.isoformat() == "2014-03-20T14:10:14+00:00")
265 | assert(result.uri == "http://www.ripe.net/")
266 | assert(result.method == HttpResult.METHOD_GET)
267 | assert(isinstance(result.responses, list))
268 | assert(len(result.responses) == 1)
269 | assert(result.responses[0].af == 6)
270 | assert(result.responses[0].body_size == 41021)
271 | assert(result.responses[0].head_size == 279)
272 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
273 | assert(result.responses[0].code == 200)
274 | assert(result.responses[0].response_time == 187.022)
275 | assert(result.responses[0].source_address == "2001:8b0:34f:5b8:220:4aff:fee0:21fc")
276 | assert(result.responses[0].version == "1.1")
277 |
278 |
279 | def test_http_4600():
280 | result = Result.get('{"from":"2a01:6a8:0:f:220:4aff:fec5:5b5a","fw":4600,"msm_id":1003930,"msm_name":"HTTPGet","prb_id":720,"result":[{"af":6,"bsize":41020,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":195.84399999999999,"src_addr":"2a01:6a8:0:f:220:4aff:fec5:5b5a","ver":"1.1"}],"timestamp":1396163194,"type":"http","uri":"http://www.ripe.net/"}')
281 | assert(isinstance(result, HttpResult))
282 | assert(result.origin == "2a01:6a8:0:f:220:4aff:fec5:5b5a")
283 | assert(result.firmware == 4600)
284 | assert(result.measurement_id == 1003930)
285 | assert(result.probe_id == 720)
286 | assert(result.created.isoformat() == "2014-03-30T07:06:34+00:00")
287 | assert(result.uri == "http://www.ripe.net/")
288 | assert(result.method == HttpResult.METHOD_GET)
289 | assert(isinstance(result.responses, list))
290 | assert(len(result.responses) == 1)
291 | assert(result.responses[0].af == 6)
292 | assert(result.responses[0].body_size == 41020)
293 | assert(result.responses[0].head_size == 279)
294 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
295 | assert(result.responses[0].code == 200)
296 | assert(result.responses[0].response_time == 195.844)
297 | assert(result.responses[0].source_address == "2a01:6a8:0:f:220:4aff:fec5:5b5a")
298 | assert(result.responses[0].version == "1.1")
299 |
300 |
301 | def test_http_4610():
302 | result = Result.get('{"from":"2a01:9e00:a217:d00:220:4aff:fec6:cb5b","fw":4610,"group_id":1003930,"msm_id":1003930,"msm_name":"HTTPGet","prb_id":780,"result":[{"af":6,"bsize":41020,"dst_addr":"2001:67c:2e8:22::c100:68b","hsize":279,"method":"GET","res":200,"rt":154.755,"src_addr":"2a01:9e00:a217:d00:220:4aff:fec6:cb5b","ver":"1.1"}],"timestamp":1396359320,"type":"http","uri":"http://www.ripe.net/"}')
303 | assert(isinstance(result, HttpResult))
304 | assert(result.origin == "2a01:9e00:a217:d00:220:4aff:fec6:cb5b")
305 | assert(result.firmware == 4610)
306 | assert(result.measurement_id == 1003930)
307 | assert(result.probe_id == 780)
308 | assert(result.created.isoformat() == "2014-04-01T13:35:20+00:00")
309 | assert(result.uri == "http://www.ripe.net/")
310 | assert(result.method == HttpResult.METHOD_GET)
311 | assert(isinstance(result.responses, list))
312 | assert(len(result.responses) == 1)
313 | assert(result.responses[0].af == 6)
314 | assert(result.responses[0].body_size == 41020)
315 | assert(result.responses[0].head_size == 279)
316 | assert(result.responses[0].destination_address == "2001:67c:2e8:22::c100:68b")
317 | assert(result.responses[0].code == 200)
318 | assert(result.responses[0].response_time == 154.755)
319 | assert(result.responses[0].source_address == "2a01:9e00:a217:d00:220:4aff:fec6:cb5b")
320 | assert(result.responses[0].version == "1.1")
321 |
322 |
323 | def test_http_4610_fail():
324 | result = Result.get('{"from":"2001:630:301:1080:220:4aff:fee0:20a0","fw":4610,"msm_id":1003932,"msm_name":"HTTPGet","prb_id":2493,"result":[{"af":6,"dst_addr":"2001:42d0:0:200::6","err":"timeout reading chunk","method":"GET","src_addr":"2001:630:301:1080:220:4aff:fee0:20a0"}],"timestamp":1398184661,"type":"http","uri":"http://www.afrinic.net/"}')
325 | assert(isinstance(result, HttpResult))
326 | assert(result.origin == "2001:630:301:1080:220:4aff:fee0:20a0")
327 | assert(result.firmware == 4610)
328 | assert(result.measurement_id == 1003932)
329 | assert(result.probe_id == 2493)
330 | assert(result.created.isoformat() == "2014-04-22T16:37:41+00:00")
331 | assert(result.uri == "http://www.afrinic.net/")
332 | assert(result.method == HttpResult.METHOD_GET)
333 | assert(isinstance(result.responses, list))
334 | assert(len(result.responses) == 1)
335 | assert(result.responses[0].af == 6)
336 | assert(result.responses[0].body_size is None)
337 | assert(result.responses[0].head_size is None)
338 | assert(result.responses[0].destination_address == "2001:42d0:0:200::6")
339 | assert(result.responses[0].code is None)
340 | assert(result.responses[0].response_time is None)
341 | assert(result.responses[0].source_address == "2001:630:301:1080:220:4aff:fee0:20a0")
342 | assert(result.responses[0].version is None)
343 | assert(result.responses[0].is_error is True)
344 | assert(result.responses[0].error_message == "timeout reading chunk")
345 |
346 | def test_http_lts():
347 | result = Result.get('{"lts":275,"from":"","msm_id":1003932,"fw":4650,"timestamp":1406558081,"uri":"http:\/\/www.afrinic.net\/","prb_id":902,"result":[{"method":"GET","dst_addr":"2001:42d0:0:200::6","err":"connect: Network is unreachable","af":6}],"type":"http","msm_name":"HTTPGet"}')
348 | assert(result.seconds_since_sync == 275)
349 |
--------------------------------------------------------------------------------
/ripe/atlas/sagan/dns.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from __future__ import absolute_import
17 |
18 | import base64
19 | from collections import namedtuple
20 | from datetime import datetime
21 | from pytz import UTC
22 |
23 | from .base import Result, ParsingDict
24 | from .helpers import abuf
25 | from .helpers import compatibility
26 |
27 |
28 | class Header(ParsingDict):
29 |
30 | def __init__(self, data, **kwargs):
31 |
32 | ParsingDict.__init__(self, **kwargs)
33 |
34 | self.raw_data = data
35 | self.aa = self.ensure("AA", bool)
36 | self.qr = self.ensure("QR", bool)
37 | self.nscount = self.ensure("NSCOUNT", int)
38 | self.qdcount = self.ensure("QDCOUNT", int)
39 | self.ancount = self.ensure("ANCOUNT", int)
40 | self.tc = self.ensure("TC", bool)
41 | self.rd = self.ensure("RD", bool)
42 | self.arcount = self.ensure("ARCOUNT", int)
43 | self.return_code = self.ensure("ReturnCode", str)
44 | self.opcode = self.ensure("OpCode", str)
45 | self.ra = self.ensure("RA", bool)
46 | self.z = self.ensure("Z", int)
47 | self.ad = self.ensure("AD", bool)
48 | self.cd = self.ensure("CD", bool)
49 | self.id = self.ensure("ID", int)
50 |
51 | def __str__(self):
52 | return "Header: " + self.return_code
53 |
54 | @property
55 | def flags(self):
56 | flags = namedtuple(
57 | "Flags", ("qr", "aa", "tc", "rd", "ra", "z", "ad", "cd"))
58 | return flags(qr=self.qr, aa=self.aa, tc=self.tc, rd=self.rd,
59 | ra=self.ra, z=self.z, ad=self.ad, cd=self.cd)
60 |
61 | @property
62 | def sections(self):
63 | sections = namedtuple(
64 | "Sections", ("QDCOUNT", "ANCOUNT", "NSCOUNT", "ARCOUNT"))
65 | return sections(QDCOUNT=self.qdcount, ANCOUNT=self.ancount,
66 | NSCOUNT=self.nscount, ARCOUNT=self.arcount)
67 |
68 | @property
69 | def is_authoritative(self):
70 | return self.aa
71 |
72 | @property
73 | def is_query(self):
74 | if self.qr is None:
75 | return None
76 | return not self.qr
77 |
78 | @property
79 | def nameserver_count(self):
80 | """
81 | Otherwise known as the NSCOUNT or the authority_count.
82 | """
83 | return self.nscount
84 |
85 | @property
86 | def question_count(self):
87 | return self.qdcount
88 |
89 | @property
90 | def answer_count(self):
91 | return self.ancount
92 |
93 | @property
94 | def is_truncated(self):
95 | return self.tc
96 |
97 | @property
98 | def recursion_desired(self):
99 | return self.rd
100 |
101 | @property
102 | def additional_count(self):
103 | return self.arcount
104 |
105 | @property
106 | def recursion_available(self):
107 | return self.ra
108 |
109 | @property
110 | def zero(self):
111 | return self.z
112 |
113 | @property
114 | def checking_disabled(self):
115 | return self.cd
116 |
117 | @property
118 | def authenticated_data(self):
119 | return self.aa
120 |
121 |
122 | class Option(ParsingDict):
123 |
124 | def __init__(self, data, **kwargs):
125 |
126 | ParsingDict.__init__(self, **kwargs)
127 |
128 | self.raw_data = data
129 | self.nsid = self.ensure("NSID", str)
130 | self.code = self.ensure("OptionCode", int)
131 | self.length = self.ensure("OptionLength", int)
132 | self.name = self.ensure("OptionName", str)
133 |
134 |
135 | class Edns0(ParsingDict):
136 |
137 | def __init__(self, data, **kwargs):
138 |
139 | ParsingDict.__init__(self, **kwargs)
140 |
141 | self.raw_data = data
142 | self.extended_return_code = self.ensure("ExtendedReturnCode", int)
143 | self.name = self.ensure("Name", str)
144 | self.type = self.ensure("Type", str)
145 | self.udp_size = self.ensure("UDPsize", int)
146 | self.version = self.ensure("Version", int)
147 | self.z = self.ensure("Z", int)
148 | self.do = bool(self.ensure("DO", bool))
149 |
150 | self.options = []
151 | if "Option" in self.raw_data:
152 | if isinstance(self.raw_data["Option"], list):
153 | for option in self.raw_data["Option"]:
154 | self.options.append(Option(option))
155 |
156 |
157 | class Question(ParsingDict):
158 |
159 | def __init__(self, data, **kwargs):
160 |
161 | ParsingDict.__init__(self, **kwargs)
162 |
163 | self.raw_data = data
164 | self.klass = self.ensure("Qclass", str)
165 | self.type = self.ensure("Qtype", str)
166 | self.name = self.ensure("Qname", str)
167 |
168 | def __str__(self):
169 | return ";{:30} {:<5} {:5}".format(self.name, self.klass, self.type)
170 |
171 |
172 | class Answer(ParsingDict):
173 |
174 | def __init__(self, data, **kwargs):
175 |
176 | ParsingDict.__init__(self, **kwargs)
177 |
178 | self.raw_data = data
179 | self.name = self.ensure("Name", str)
180 | self.ttl = self.ensure("TTL", int)
181 | self.type = self.ensure("Type", str)
182 | self.klass = self.ensure("Class", str)
183 | self.rd_length = self.ensure("RDlength", int)
184 |
185 | # Where data goes when the abuf parser can't understand things
186 | self.rdata = self.ensure("Rdata", str)
187 |
188 | @property
189 | def resource_data_length(self):
190 | return self.rd_length
191 |
192 | def __str__(self):
193 | return "{:22} {:<7} {:5} {:5}".format(
194 | self.name,
195 | self.ttl,
196 | self.klass,
197 | self.type
198 | )
199 |
200 |
201 | class AAnswer(Answer):
202 |
203 | def __init__(self, data, **kwargs):
204 | Answer.__init__(self, data, **kwargs)
205 | self.address = self.ensure("Address", str)
206 |
207 | def __str__(self):
208 | return "{0} {1}".format(Answer.__str__(self), self.address)
209 |
210 |
211 | class AaaaAnswer(AAnswer):
212 | pass
213 |
214 |
215 | class NsAnswer(Answer):
216 |
217 | def __init__(self, data, **kwargs):
218 | Answer.__init__(self, data, **kwargs)
219 | self.target = self.ensure("Target", str)
220 |
221 | def __str__(self):
222 | return "{0} {1}".format(Answer.__str__(self), self.target)
223 |
224 |
225 | class CnameAnswer(NsAnswer):
226 | pass
227 |
228 |
229 | class MxAnswer(Answer):
230 |
231 | def __init__(self, data, **kwargs):
232 | Answer.__init__(self, data, **kwargs)
233 | self.preference = self.ensure("Preference", int)
234 | self.mail_exchanger = self.ensure("MailExchanger", str)
235 |
236 | def __str__(self):
237 | return "{0} {1} {2}".format(
238 | Answer.__str__(self),
239 | self.preference,
240 | self.mail_exchanger
241 | )
242 |
243 |
244 | class SoaAnswer(Answer):
245 |
246 | def __init__(self, data, **kwargs):
247 | Answer.__init__(self, data, **kwargs)
248 | self.mname = self.ensure("MasterServerName", str)
249 | self.rname = self.ensure("MaintainerName", str)
250 | self.serial = self.ensure("Serial", int)
251 | self.refresh = self.ensure("Refresh", int)
252 | self.retry = self.ensure("Retry", int)
253 | self.expire = self.ensure("Expire", int)
254 | self.minimum = self.ensure("NegativeTtl", int)
255 |
256 | def __str__(self):
257 | return "{0} {1} {2} {3} {4} {5} {6} {7}".format(
258 | Answer.__str__(self),
259 | self.mname,
260 | self.rname,
261 | self.serial,
262 | self.refresh,
263 | self.retry,
264 | self.expire,
265 | self.minimum
266 | )
267 |
268 | @property
269 | def master_server_name(self):
270 | return self.mname
271 |
272 | @property
273 | def maintainer_name(self):
274 | return self.rname
275 |
276 | @property
277 | def negative_ttl(self):
278 | return self.minimum
279 |
280 | @property
281 | def nxdomain(self):
282 | return self.minimum
283 |
284 |
285 | class DsAnswer(Answer):
286 |
287 | def __init__(self, data, **kwargs):
288 | Answer.__init__(self, data, **kwargs)
289 | self.tag = self.ensure("Tag", int)
290 | self.algorithm = self.ensure("Algorithm", int)
291 | self.digest_type = self.ensure("DigestType", int)
292 | self.delegation_key = self.ensure("DelegationKey", str)
293 |
294 | def __str__(self):
295 | return "{0} {1} {2} {3} {4}".format(
296 | Answer.__str__(self),
297 | self.tag,
298 | self.algorithm,
299 | self.digest_type,
300 | self.delegation_key
301 | )
302 |
303 |
304 | class DnskeyAnswer(Answer):
305 |
306 | def __init__(self, data, **kwargs):
307 | Answer.__init__(self, data, **kwargs)
308 | self.flags = self.ensure("Flags", int)
309 | self.algorithm = self.ensure("Algorithm", int)
310 | self.protocol = self.ensure("Protocol", int)
311 | self.key = self.ensure("Key", str)
312 |
313 | def __str__(self):
314 | return "{0} {1} {2} {3} {4}".format(
315 | Answer.__str__(self),
316 | self.flags,
317 | self.algorithm,
318 | self.protocol,
319 | self.key
320 | )
321 |
322 |
323 | class TxtAnswer(Answer):
324 |
325 | def __init__(self, data, **kwargs):
326 |
327 | Answer.__init__(self, data, **kwargs)
328 |
329 | self.data = []
330 | if "Data" in self.raw_data:
331 | if isinstance(self.raw_data["Data"], list):
332 | self.data = []
333 | for s in self.raw_data["Data"]:
334 | if isinstance(s, compatibility.string):
335 | self.data.append(s)
336 |
337 | def __str__(self):
338 | return "{0} {1}".format(Answer.__str__(self), self.data_string)
339 |
340 | @property
341 | def data_string(self):
342 | return " ".join(self.data)
343 |
344 |
345 | class RRSigAnswer(Answer):
346 |
347 | def __init__(self, data, **kwargs):
348 | Answer.__init__(self, data, **kwargs)
349 | self.type_covered = self.ensure("TypeCovered", str)
350 | self.algorithm = self.ensure("Algorithm", int)
351 | self.labels = self.ensure("Labels", int)
352 | self.original_ttl = self.ensure("OriginalTTL", int)
353 | self.signature_expiration = self.ensure("SignatureExpiration", int)
354 | self.signature_inception = self.ensure("SignatureInception", int)
355 | self.key_tag = self.ensure("KeyTag", int)
356 | self.signer_name = self.ensure("SignerName", str)
357 | self.signature = self.ensure("Signature", str)
358 |
359 | def __str__(self):
360 |
361 | formatter = "%Y%m%d%H%M%S"
362 |
363 | expiration = datetime.fromtimestamp(
364 | self.signature_expiration, tz=UTC).strftime(formatter)
365 |
366 | inception = datetime.fromtimestamp(
367 | self.signature_inception, tz=UTC).strftime(formatter)
368 |
369 | return "{0} {1} {2} {3} {4} {5} {6} {7} {8} {9}".format(
370 | Answer.__str__(self),
371 | self.type_covered,
372 | self.algorithm,
373 | self.labels,
374 | self.original_ttl,
375 | expiration,
376 | inception,
377 | self.key_tag,
378 | self.signer_name,
379 | self.signature
380 | )
381 |
382 |
383 | class NotFullySupportedAnswer(Answer):
384 | """
385 | We're still working on getting the proper text representations of some
386 | Answer classes, so such classes will inherit from this one.
387 | """
388 |
389 | def __str__(self):
390 | return "{0} ---- Not fully supported ----".format(Answer.__str__(self))
391 |
392 |
393 | class NsecAnswer(NotFullySupportedAnswer):
394 |
395 | def __init__(self, data, **kwargs):
396 | Answer.__init__(self, data, **kwargs)
397 | self.next_domain_name = self.ensure("NextDomainName", str)
398 | self.types = self.ensure("Types", list)
399 |
400 |
401 | class Nsec3Answer(NotFullySupportedAnswer):
402 |
403 | def __init__(self, data, **kwargs):
404 | Answer.__init__(self, data, **kwargs)
405 | self.hash_algorithm = self.ensure("HashAlg", int)
406 | self.flags = self.ensure("Flags", int)
407 | self.iterations = self.ensure("Iterations", int)
408 | self.salt = self.ensure("Salt", str)
409 | self.hash = self.ensure("Hash", str)
410 | self.types = self.ensure("Types", list)
411 |
412 |
413 | class Nsec3ParamAnswer(NotFullySupportedAnswer):
414 |
415 | def __init__(self, data, **kwargs):
416 | Answer.__init__(self, data, **kwargs)
417 | self.algorithm = self.ensure("Algorithm", int)
418 | self.flags = self.ensure("Flags", int)
419 | self.iterations = self.ensure("Iterations", int)
420 | self.salt = self.ensure("Salt", str)
421 |
422 |
423 | class PtrAnswer(NotFullySupportedAnswer):
424 |
425 | def __init__(self, data, **kwargs):
426 | Answer.__init__(self, data, **kwargs)
427 | self.target = self.ensure("Target", str)
428 |
429 |
430 | class SrvAnswer(NotFullySupportedAnswer):
431 |
432 | def __init__(self, data, **kwargs):
433 | Answer.__init__(self, data, **kwargs)
434 | self.priority = self.ensure("Priority", int)
435 | self.weight = self.ensure("Weight", int)
436 | self.port = self.ensure("Port", int)
437 | self.target = self.ensure("Target", str)
438 |
439 |
440 | class SshfpAnswer(NotFullySupportedAnswer):
441 |
442 | def __init__(self, data, **kwargs):
443 | Answer.__init__(self, data, **kwargs)
444 | self.algorithm = self.ensure("Algorithm", int)
445 | self.digest_type = self.ensure("DigestType", int)
446 | self.fingerprint = self.ensure("Fingerprint", str)
447 |
448 |
449 | class TlsaAnswer(NotFullySupportedAnswer):
450 |
451 | def __init__(self, data, **kwargs):
452 | Answer.__init__(self, data, **kwargs)
453 | self.certificate_usage = self.ensure("CertUsage", int)
454 | self.selector = self.ensure("Selector", int)
455 | self.matching_type = self.ensure("MatchingType", int)
456 | self.certificate_associated_data = self.ensure("CertAssData", str)
457 |
458 |
459 | class HinfoAnswer(NotFullySupportedAnswer):
460 |
461 | def __init__(self, data, **kwargs):
462 | Answer.__init__(self, data, **kwargs)
463 | self.cpu = self.ensure("Cpu", str)
464 | self.os = self.ensure("Os", str)
465 |
466 |
467 | class Message(ParsingDict):
468 |
469 | ANSWER_CLASSES = {
470 | "A": AAnswer,
471 | "AAAA": AaaaAnswer,
472 | "NS": NsAnswer,
473 | "CNAME": CnameAnswer,
474 | "MX": MxAnswer,
475 | "SOA": SoaAnswer,
476 | "DS": DsAnswer,
477 | "DNSKEY": DnskeyAnswer,
478 | "TXT": TxtAnswer,
479 | "RRSIG": RRSigAnswer,
480 | "NSEC": NsecAnswer,
481 | "NSEC3": Nsec3Answer,
482 | "NSEC3PARAM": Nsec3ParamAnswer,
483 | "PTR": PtrAnswer,
484 | "SRV": SrvAnswer,
485 | "SSHFP": SshfpAnswer,
486 | "TLSA": TlsaAnswer,
487 | "HINFO": HinfoAnswer
488 | }
489 |
490 | def __init__(self, message, response_data, parse_buf=True, **kwargs):
491 |
492 | ParsingDict.__init__(self, **kwargs)
493 |
494 | self._string_representation = message
495 | self.raw_data = {}
496 |
497 | if parse_buf:
498 | self._parse_buf(message)
499 | else:
500 | self._backfill_raw_data_from_result(response_data)
501 |
502 | self.header = None
503 | if "HEADER" in self.raw_data:
504 | self.header = Header(self.raw_data["HEADER"], **kwargs)
505 |
506 | # This is a tricky one, since you can't know that the response is an
507 | # error until *after* the abuf is parsed, and it won't be parsed
508 | # until you attempt to access it.
509 | code = self.header.return_code
510 | if not code or code.upper() != "NOERROR":
511 | self._handle_error('The response did not contain "NOERROR"')
512 |
513 | self.edns0 = None
514 | self.questions = []
515 | self.answers = []
516 | self.authorities = []
517 | self.additionals = []
518 |
519 | if "EDNS0" in self.raw_data:
520 | self.edns0 = Edns0(self.raw_data["EDNS0"], **kwargs)
521 |
522 | for question in self.raw_data.get("QuestionSection", []):
523 | self.questions.append(Question(question, **kwargs))
524 |
525 | for answer in self.raw_data.get("AnswerSection", []):
526 | self._append_answer(answer, "answers", **kwargs)
527 | for authority in self.raw_data.get("AuthoritySection", []):
528 | self._append_answer(authority, "authorities", **kwargs)
529 | for additional in self.raw_data.get("AdditionalSection", []):
530 | self._append_answer(additional, "additionals", **kwargs)
531 |
532 | def __str__(self):
533 | return self._string_representation
534 |
535 | def __repr__(self):
536 | return str(self)
537 |
538 | def _append_answer(self, answer, section, **kwargs):
539 | answer_type = answer.get("Type")
540 | if answer_type is None:
541 | self._handle_malformation(
542 | "Answer has no parseable Type: {answer}".format(
543 | answer=answer
544 | )
545 | )
546 | answer_class = self.ANSWER_CLASSES.get(answer_type, Answer)
547 | getattr(self, section).append(answer_class(answer, **kwargs))
548 |
549 | def _parse_buf(self, message):
550 |
551 | try:
552 | self.raw_data = abuf.AbufParser.parse(base64.b64decode(message))
553 | except Exception as e:
554 | self.raw_data = {}
555 | self._handle_malformation(
556 | "{exception}: Unable to parse buffer: {buffer}".format(
557 | exception=e,
558 | buffer=self._string_representation
559 | )
560 | )
561 | else:
562 | if "ERROR" in self.raw_data:
563 | self._handle_error(self.raw_data["ERROR"])
564 |
565 | def _backfill_raw_data_from_result(self, response_data):
566 |
567 | # Header
568 | self.raw_data["Header"] = {}
569 | for key in ("NSCOUNT", "QDCOUNT", "ID", "ARCOUNT", "ANCOUNT"):
570 | if key in response_data:
571 | self.raw_data["Header"][key] = response_data[key]
572 |
573 | # Answers
574 | if "answers" in response_data and response_data["answers"]:
575 |
576 | # The names used in the result don't align to those used in the abuf
577 | # parser
578 | name_map = {
579 | "TTL": "TTL",
580 | "TYPE": "Type",
581 | "NAME": "Name",
582 | "RDATA": "Data",
583 | "MNAME": "MasterServerName",
584 | "RNAME": "MaintainerName",
585 | "SERIAL": "Serial",
586 | "RDLENGTH": "RDlength",
587 | }
588 |
589 | self.raw_data["AnswerSection"] = []
590 | for answer in response_data["answers"]:
591 |
592 | temporary = {}
593 |
594 | for k, v in name_map.items():
595 | if k in answer:
596 | temporary[v] = answer[k]
597 |
598 | # Special case where some older txt entires are strings and not
599 | # a list
600 | if temporary.get("Type") == "TXT":
601 | if isinstance(temporary.get("Data"), compatibility.string):
602 | temporary["Data"] = [temporary["Data"]]
603 |
604 | if temporary:
605 | self.raw_data["AnswerSection"].append(temporary)
606 |
607 |
608 | class Response(ParsingDict):
609 |
610 | def __init__(self, data, af=None, destination=None, source=None,
611 | protocol=None, part_of_set=True, parse_buf=True, **kwargs):
612 |
613 | ParsingDict.__init__(self, **kwargs)
614 |
615 | self.raw_data = data
616 |
617 | self.af = self.ensure("af", int, af)
618 | self.destination_address = self.ensure("dst_addr", str, destination)
619 | self.source_address = self.ensure("src_addr", str, source)
620 | self.protocol = self.ensure("proto", str, protocol)
621 |
622 | self.response_id = None
623 |
624 | # Preparing for lazy stuff
625 | self._abuf = None
626 | self._qbuf = None
627 | self._parse_buf = parse_buf
628 |
629 | try:
630 | self.response_time = round(float(self.raw_data["result"]["rt"]), 3)
631 | except KeyError:
632 | try:
633 | self.response_time = round(self.ensure("rt", float), 3)
634 | except TypeError:
635 | self.response_time = None
636 |
637 | try:
638 | self.response_size = self.raw_data["result"]["size"]
639 | except KeyError:
640 | self.response_size = self.ensure("size", int)
641 |
642 | if part_of_set:
643 | self.response_id = self.ensure("subid", int)
644 |
645 | if self.protocol and isinstance(self.protocol, str):
646 | self.protocol = self.clean_protocol(self.protocol)
647 |
648 | @property
649 | def abuf(self):
650 | return self._get_buf("a")
651 |
652 | @property
653 | def qbuf(self):
654 | return self._get_buf("q")
655 |
656 | def _get_buf(self, prefix):
657 | """
658 | Lazy read-only accessor for the (a|q)buf.
659 | The qbuf Message object is cached for subsequent requests.
660 | """
661 |
662 | kind = "{prefix}buf".format(prefix=prefix)
663 | private_name = "_" + kind
664 | buf = getattr(self, private_name)
665 |
666 | if buf:
667 | return buf
668 |
669 | try:
670 | buf_string = self.raw_data["result"][kind]
671 | except KeyError:
672 | buf_string = self.ensure(kind, str)
673 | if buf_string:
674 | message = Message(
675 | buf_string,
676 | self.raw_data,
677 | parse_buf=self._parse_buf,
678 | on_error=self._on_error,
679 | on_malformation=self._on_malformation
680 | )
681 | if message.is_error:
682 | self._handle_error(message.error_message)
683 | setattr(self, private_name, message)
684 | return getattr(self, private_name)
685 |
686 |
687 | class DnsResult(Result):
688 |
689 | def __init__(self, data, parse_buf=True, **kwargs):
690 | """
691 | Note that we're not setting `self.af` here, but rather we have it as a
692 | property of `Response` as it's possible that one result can contain
693 | multiple responses, each with either af=4 or af=6.
694 | """
695 |
696 | Result.__init__(self, data, **kwargs)
697 |
698 | self.responses = []
699 | self.responses_total = None
700 |
701 | af = self.ensure("af", int)
702 | protocol = self.ensure("proto", str)
703 | source_address = self.ensure("src_addr", str)
704 | destination_address = self.ensure("dst_addr", str)
705 |
706 | if 0 < self.firmware < 4460:
707 | af = self.ensure("pf", int)
708 |
709 | part_of_set, responses = self.build_responses()
710 |
711 | for response in responses:
712 | self.responses.append(Response(
713 | response,
714 | af=af,
715 | destination=destination_address,
716 | source=source_address,
717 | protocol=protocol,
718 | part_of_set=part_of_set,
719 | parse_buf=parse_buf,
720 | **kwargs
721 | ))
722 |
723 | if "error" in self.raw_data:
724 | if isinstance(self.raw_data["error"], dict):
725 | if "timeout" in self.raw_data["error"]:
726 | self._handle_error("Timeout: {timeout}".format(
727 | timeout=self.raw_data["error"]["timeout"]
728 | ))
729 | elif "getaddrinfo" in self.raw_data["error"]:
730 | self._handle_error("Name resolution error: {msg}".format(
731 | msg=self.raw_data["error"]["getaddrinfo"]
732 | ))
733 | else:
734 | self._handle_error("Unknown error: {msg}".format(
735 | msg=self.raw_data["error"]
736 | ))
737 | else:
738 | self._handle_error("Unknown error: {msg}".format(
739 | msg=self.raw_data["error"]
740 | ))
741 |
742 | def build_responses(self):
743 | """
744 | DNS measurement results are a little wacky. Sometimes you get a single
745 | response, other times you get a set of responses (result set). In order
746 | to establish a unified interface, we conform all results to the same
747 | format: a list of response objects.
748 |
749 | Additionally, the qbuf property is weird too. In the case of multiple
750 | responses, there's one qbuf for every response, but for single results,
751 | it's not stored in the result, but rather the outer result data. Again,
752 | for the purposes of uniformity, we shoehorn the qbuf into the first (and
753 | only) response in the latter case.
754 | """
755 |
756 | responses = []
757 | part_of_set = True
758 |
759 | # Account for single results
760 | if "result" in self.raw_data:
761 | if "qbuf" in self.raw_data:
762 | if "qbuf" not in self.raw_data["result"]:
763 | self.raw_data["result"]["qbuf"] = self.raw_data.pop("qbuf")
764 | responses.append(self.raw_data["result"])
765 | part_of_set = False
766 |
767 | try:
768 | self.responses_total = int(self.raw_data["result"]["submax"])
769 | except (KeyError, ValueError):
770 | pass # The value wasn't there, not much we can do about it
771 |
772 | try:
773 | responses += self.raw_data["resultset"]
774 | except KeyError:
775 | pass # self.responses remains the same
776 |
777 | return part_of_set, responses
778 |
779 | __all__ = (
780 | "DnsResult",
781 | )
782 |
--------------------------------------------------------------------------------
/tests/test_ping.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 RIPE NCC
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from ripe.atlas.sagan import Result
17 | from ripe.atlas.sagan.ping import PingResult, Packet
18 |
19 |
20 | def test_ping_0():
21 | result = Result.get('{"avg":"58.042","dst_addr":"62.2.16.12","dup":"0","fw":0,"max":"58.272","min":"57.876","msm_id":1000192,"prb_id":677,"rcvd":"3","sent":"3","src_addr":"78.128.9.202","timestamp":1328019792,"type":"ping"}')
22 | assert(result.af == 4)
23 | assert(result.rtt_average == 58.042)
24 | assert(result.rtt_median is None)
25 | assert(result.destination_address == "62.2.16.12")
26 | assert(result.destination_name is None)
27 | assert(result.duplicates == 0)
28 | assert(result.origin is None)
29 | assert(result.firmware == 0)
30 | assert(result.seconds_since_sync is None)
31 | assert(result.rtt_max == 58.272)
32 | assert(result.rtt_min == 57.876)
33 | assert(result.measurement_id == 1000192)
34 | assert(result.probe_id == 677)
35 | assert(result.protocol is None)
36 | assert(result.packets_received == 3)
37 | assert(result.packets_sent == 3)
38 | assert(result.packet_size is None)
39 | assert(result.step is None)
40 | assert(result.created.isoformat() == "2012-01-31T14:23:12+00:00")
41 | assert(result.packets == [])
42 |
43 |
44 | def test_ping_1():
45 | result = Result.get('{"addr":"62.2.16.12","avg":124.572,"dup":0,"from":"194.85.27.7","fw":1,"max":125.44499999999999,"min":123.89400000000001,"msm_id":1000192,"name":"hsi.cablecom.ch","prb_id":165,"rcvd":3,"sent":3,"size":56,"timestamp":1340523908,"type":"ping"}')
46 | assert(result.af == 4)
47 | assert(result.rtt_average == 124.572)
48 | assert(result.rtt_median is None)
49 | assert(result.destination_address == "62.2.16.12")
50 | assert(result.destination_name == "hsi.cablecom.ch")
51 | assert(result.duplicates == 0)
52 | assert(result.origin == "194.85.27.7")
53 | assert(result.firmware == 1)
54 | assert(result.seconds_since_sync is None)
55 | assert(result.rtt_max == 125.445)
56 | assert(result.rtt_min == 123.894)
57 | assert(result.measurement_id == 1000192)
58 | assert(result.probe_id == 165)
59 | assert(result.protocol is None)
60 | assert(result.packets_received == 3)
61 | assert(result.packets_sent == 3)
62 | assert(result.packet_size is None)
63 | assert(result.step is None)
64 | assert(result.created.isoformat() == "2012-06-24T07:45:08+00:00")
65 | assert(result.packets == [])
66 |
67 |
68 | def test_ping_4460():
69 | result = Result.get('{"addr":"62.2.16.12","af":4,"avg":48.388333333333328,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"188.194.234.136","fw":4460,"max":56.948999999999998,"min":43.869999999999997,"msm_id":1000192,"name":"hsi.cablecom.ch","prb_id":270,"proto":"ICMP","rcvd":3,"result":[{"rtt":43.869999999999997},{"rtt":56.948999999999998},{"rtt":44.345999999999997}],"sent":3,"size":20,"src_addr":"192.168.178.21","timestamp":1340524626,"ttl":52,"type":"ping"}')
70 | assert(result.af == 4)
71 | assert(result.rtt_average == 48.388)
72 | assert(result.rtt_median == 44.346)
73 | assert(result.destination_address == "62.2.16.12")
74 | assert(result.destination_name == "hsi.cablecom.ch")
75 | assert(result.duplicates == 0)
76 | assert(result.origin == "188.194.234.136")
77 | assert(result.firmware == 4460)
78 | assert(result.seconds_since_sync is None)
79 | assert(result.rtt_max == 56.949)
80 | assert(result.rtt_min == 43.87)
81 | assert(result.measurement_id == 1000192)
82 | assert(result.probe_id == 270)
83 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
84 | assert(result.packets_received == 3)
85 | assert(result.packets_sent == 3)
86 | assert(result.packet_size == 12)
87 | assert(result.step is None)
88 | assert(result.created.isoformat() == "2012-06-24T07:57:06+00:00")
89 | assert(result.packets[0].rtt == 43.87)
90 | assert(result.packets[1].rtt == 56.949)
91 | assert(result.packets[2].rtt == 44.346)
92 | assert(result.packets[0].ttl == 52)
93 | assert(result.packets[1].ttl == 52)
94 | assert(result.packets[2].ttl == 52)
95 | assert(result.packets[0].dup is False)
96 | assert(result.packets[1].dup is False)
97 | assert(result.packets[2].dup is False)
98 | assert(result.packets[0].source_address == "192.168.178.21")
99 | assert(result.packets[1].source_address == "192.168.178.21")
100 | assert(result.packets[2].source_address == "192.168.178.21")
101 |
102 |
103 | def test_ping_4470():
104 | result = Result.get('{"addr":"62.2.16.12","af":4,"avg":195.649,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"194.85.27.7","fw":4470,"max":197.79300000000001,"min":193.059,"msm_id":1000192,"name":"hsi.cablecom.ch","prb_id":165,"proto":"ICMP","rcvd":3,"result":[{"rtt":196.095},{"rtt":197.79300000000001},{"rtt":193.059}],"sent":3,"size":20,"src_addr":"192.168.3.8","timestamp":1344514151,"ttl":46,"type":"ping"}')
105 | assert(result.af == 4)
106 | assert(result.rtt_average == 195.649)
107 | assert(result.rtt_median == 196.095)
108 | assert(result.destination_address == "62.2.16.12")
109 | assert(result.destination_name == "hsi.cablecom.ch")
110 | assert(result.duplicates == 0)
111 | assert(result.origin == "194.85.27.7")
112 | assert(result.firmware == 4470)
113 | assert(result.seconds_since_sync is None)
114 | assert(result.rtt_max == 197.793)
115 | assert(result.rtt_min == 193.059)
116 | assert(result.measurement_id == 1000192)
117 | assert(result.probe_id == 165)
118 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
119 | assert(result.packets_received == 3)
120 | assert(result.packets_sent == 3)
121 | assert(result.packet_size == 12)
122 | assert(result.step is None)
123 | assert(result.created.isoformat() == "2012-08-09T12:09:11+00:00")
124 | assert(result.packets[0].rtt == 196.095)
125 | assert(result.packets[1].rtt == 197.793)
126 | assert(result.packets[2].rtt == 193.059)
127 | assert(result.packets[0].ttl == 46)
128 | assert(result.packets[1].ttl == 46)
129 | assert(result.packets[2].ttl == 46)
130 | assert(result.packets[0].dup is False)
131 | assert(result.packets[1].dup is False)
132 | assert(result.packets[2].dup is False)
133 | assert(result.packets[0].source_address == "192.168.3.8")
134 | assert(result.packets[1].source_address == "192.168.3.8")
135 | assert(result.packets[2].source_address == "192.168.3.8")
136 |
137 |
138 | def test_ping_4480():
139 | result = Result.get('{"addr":"62.2.16.12","af":4,"avg":95.756666666666661,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"194.85.27.7","fw":4480,"max":96.147999999999996,"min":95.388999999999996,"msm_id":1000192,"name":"hsi.cablecom.ch","prb_id":165,"proto":"ICMP","rcvd":3,"result":[{"rtt":95.733000000000004},{"rtt":96.147999999999996},{"rtt":95.388999999999996}],"sent":3,"size":20,"src_addr":"192.168.3.8","timestamp":1349776268,"ttl":46,"type":"ping"}')
140 | assert(result.af == 4)
141 | assert(result.rtt_average == 95.757)
142 | assert(result.rtt_median == 95.733)
143 | assert(result.destination_address == "62.2.16.12")
144 | assert(result.destination_name == "hsi.cablecom.ch")
145 | assert(result.duplicates == 0)
146 | assert(result.origin == "194.85.27.7")
147 | assert(result.firmware == 4480)
148 | assert(result.seconds_since_sync is None)
149 | assert(result.rtt_max == 96.148)
150 | assert(result.rtt_min == 95.389)
151 | assert(result.measurement_id == 1000192)
152 | assert(result.probe_id == 165)
153 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
154 | assert(result.packets_received == 3)
155 | assert(result.packets_sent == 3)
156 | assert(result.packet_size == 12)
157 | assert(result.step is None)
158 | assert(result.created.isoformat() == "2012-10-09T09:51:08+00:00")
159 | assert(result.packets[0].rtt == 95.733)
160 | assert(result.packets[1].rtt == 96.148)
161 | assert(result.packets[2].rtt == 95.389)
162 | assert(result.packets[0].ttl == 46)
163 | assert(result.packets[1].ttl == 46)
164 | assert(result.packets[2].ttl == 46)
165 | assert(result.packets[0].dup is False)
166 | assert(result.packets[1].dup is False)
167 | assert(result.packets[2].dup is False)
168 | assert(result.packets[0].source_address == "192.168.3.8")
169 | assert(result.packets[1].source_address == "192.168.3.8")
170 | assert(result.packets[2].source_address == "192.168.3.8")
171 |
172 |
173 | def test_ping_4500():
174 | result = Result.get('{"addr":"62.2.16.12","af":4,"avg":30.114666666666665,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"80.56.151.3","fw":4500,"max":30.344999999999999,"min":29.960999999999999,"msm_id":1000192,"name":"hsi.cablecom.ch","prb_id":202,"proto":"ICMP","rcvd":3,"result":[{"rtt":30.038},{"rtt":29.960999999999999},{"rtt":30.344999999999999}],"sent":3,"size":20,"src_addr":"192.168.1.229","timestamp":1361244431,"ttl":55,"type":"ping"}')
175 | assert(result.af == 4)
176 | assert(result.rtt_average == 30.115)
177 | assert(result.rtt_median == 30.038)
178 | assert(result.destination_address == "62.2.16.12")
179 | assert(result.destination_name == "hsi.cablecom.ch")
180 | assert(result.duplicates == 0)
181 | assert(result.origin == "80.56.151.3")
182 | assert(result.firmware == 4500)
183 | assert(result.seconds_since_sync is None)
184 | assert(result.rtt_max == 30.345)
185 | assert(result.rtt_min == 29.961)
186 | assert(result.measurement_id == 1000192)
187 | assert(result.probe_id == 202)
188 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
189 | assert(result.packets_received == 3)
190 | assert(result.packets_sent == 3)
191 | assert(result.packet_size == 12)
192 | assert(result.step is None)
193 | assert(result.created.isoformat() == "2013-02-19T03:27:11+00:00")
194 | assert(result.packets[0].rtt == 30.038)
195 | assert(result.packets[1].rtt == 29.961)
196 | assert(result.packets[2].rtt == 30.345)
197 | assert(result.packets[0].ttl == 55)
198 | assert(result.packets[1].ttl == 55)
199 | assert(result.packets[2].ttl == 55)
200 | assert(result.packets[0].dup is False)
201 | assert(result.packets[1].dup is False)
202 | assert(result.packets[2].dup is False)
203 | assert(result.packets[0].source_address == "192.168.1.229")
204 | assert(result.packets[1].source_address == "192.168.1.229")
205 | assert(result.packets[2].source_address == "192.168.1.229")
206 |
207 |
208 | def test_ping_4520():
209 | result = Result.get('{"addr":"62.2.16.12","af":4,"avg":67.420999999999992,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"194.85.27.7","fw":4520,"max":70.230999999999995,"min":65.974999999999994,"msm_id":1000192,"name":"hsi.cablecom.ch","prb_id":165,"proto":"ICMP","rcvd":3,"result":[{"rtt":70.230999999999995},{"rtt":65.974999999999994},{"rtt":66.057000000000002}],"sent":3,"size":20,"src_addr":"192.168.3.8","timestamp":1365379380,"ttl":47,"type":"ping"}')
210 | assert(result.af == 4)
211 | assert(result.rtt_average == 67.421)
212 | assert(result.rtt_median == 66.057)
213 | assert(result.destination_address == "62.2.16.12")
214 | assert(result.destination_name == "hsi.cablecom.ch")
215 | assert(result.duplicates == 0)
216 | assert(result.origin == "194.85.27.7")
217 | assert(result.firmware == 4520)
218 | assert(result.seconds_since_sync is None)
219 | assert(result.rtt_max == 70.231)
220 | assert(result.rtt_min == 65.975)
221 | assert(result.measurement_id == 1000192)
222 | assert(result.probe_id == 165)
223 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
224 | assert(result.packets_received == 3)
225 | assert(result.packets_sent == 3)
226 | assert(result.packet_size == 12)
227 | assert(result.step is None)
228 | assert(result.created.isoformat() == "2013-04-08T00:03:00+00:00")
229 | assert(result.packets[0].rtt == 70.231)
230 | assert(result.packets[1].rtt == 65.975)
231 | assert(result.packets[2].rtt == 66.057)
232 | assert(result.packets[0].ttl == 47)
233 | assert(result.packets[1].ttl == 47)
234 | assert(result.packets[2].ttl == 47)
235 | assert(result.packets[0].dup is False)
236 | assert(result.packets[1].dup is False)
237 | assert(result.packets[2].dup is False)
238 | assert(result.packets[0].source_address == "192.168.3.8")
239 | assert(result.packets[1].source_address == "192.168.3.8")
240 | assert(result.packets[2].source_address == "192.168.3.8")
241 |
242 |
243 | def test_ping_4550():
244 | result = Result.get('{"af":4,"avg":27.300999999999998,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"80.56.151.3","fw":4550,"lts":365,"max":27.300999999999998,"min":27.300999999999998,"msm_id":1000192,"msm_name":"Ping","prb_id":202,"proto":"ICMP","rcvd":1,"result":[{"srcaddr":"192.168.1.229","x":"*"},{"x":"*"},{"rtt":27.300999999999998,"ttl":54}],"sent":3,"size":20,"src_addr":"192.168.1.229","step":360,"timestamp":1378271710,"ttl":54,"type":"ping"}')
245 | assert(result.af == 4)
246 | assert(result.rtt_average == 27.301)
247 | assert(result.rtt_median == 27.301)
248 | assert(result.destination_address == "62.2.16.12")
249 | assert(result.destination_name == "hsi.cablecom.ch")
250 | assert(result.duplicates == 0)
251 | assert(result.origin == "80.56.151.3")
252 | assert(result.firmware == 4550)
253 | assert(result.seconds_since_sync == 365)
254 | assert(result.rtt_max == 27.301)
255 | assert(result.rtt_min == 27.301)
256 | assert(result.measurement_id == 1000192)
257 | assert(result.probe_id == 202)
258 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
259 | assert(result.packets_received == 1)
260 | assert(result.packets_sent == 3)
261 | assert(result.packet_size == 12)
262 | assert(result.step == 360)
263 | assert(result.created.isoformat() == "2013-09-04T05:15:10+00:00")
264 | assert(result.packets[0].rtt is None)
265 | assert(result.packets[1].rtt is None)
266 | assert(result.packets[2].rtt == 27.301)
267 | assert(result.packets[0].ttl is None)
268 | assert(result.packets[1].ttl is None)
269 | assert(result.packets[2].ttl == 54)
270 | assert(result.packets[0].dup is False)
271 | assert(result.packets[1].dup is False)
272 | assert(result.packets[2].dup is False)
273 | assert(result.packets[0].source_address == "192.168.1.229")
274 | assert(result.packets[1].source_address == "192.168.1.229")
275 | assert(result.packets[2].source_address == "192.168.1.229")
276 |
277 |
278 | def test_ping_4560():
279 | result = Result.get('{"af":4,"avg":36.887,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"62.195.143.53","fw":4560,"lts":36,"max":40.234000000000002,"min":34.747999999999998,"msm_id":1000192,"msm_name":"Ping","prb_id":202,"proto":"ICMP","rcvd":3,"result":[{"rtt":40.234000000000002},{"rtt":34.747999999999998,"srcaddr":"192.168.1.229"},{"rtt":35.679000000000002,"srcaddr":"192.168.1.229"}],"sent":3,"size":20,"src_addr":"192.168.1.229","step":360,"timestamp":1380586151,"ttl":54,"type":"ping"}')
280 | assert(result.af == 4)
281 | assert(result.rtt_average == 36.887)
282 | assert(result.rtt_median == 35.679)
283 | assert(result.destination_address == "62.2.16.12")
284 | assert(result.destination_name == "hsi.cablecom.ch")
285 | assert(result.duplicates == 0)
286 | assert(result.origin == "62.195.143.53")
287 | assert(result.firmware == 4560)
288 | assert(result.seconds_since_sync is 36)
289 | assert(result.rtt_max == 40.234)
290 | assert(result.rtt_min == 34.748)
291 | assert(result.measurement_id == 1000192)
292 | assert(result.probe_id == 202)
293 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
294 | assert(result.packets_received == 3)
295 | assert(result.packets_sent == 3)
296 | assert(result.packet_size == 12)
297 | assert(result.step == 360)
298 | assert(result.created.isoformat() == "2013-10-01T00:09:11+00:00")
299 | assert(result.packets[0].rtt == 40.234)
300 | assert(result.packets[1].rtt == 34.748)
301 | assert(result.packets[2].rtt == 35.679)
302 | assert(result.packets[0].ttl == 54)
303 | assert(result.packets[1].ttl == 54)
304 | assert(result.packets[2].ttl == 54)
305 | assert(result.packets[0].dup is False)
306 | assert(result.packets[1].dup is False)
307 | assert(result.packets[2].dup is False)
308 | assert(result.packets[0].source_address == "192.168.1.229")
309 | assert(result.packets[1].source_address == "192.168.1.229")
310 | assert(result.packets[2].source_address == "192.168.1.229")
311 |
312 |
313 | def test_ping_4570():
314 | result = Result.get('{"af":4,"avg":36.608333333333327,"dst_addr":"62.2.16.12","dst_name":"hsi.cablecom.ch","dup":0,"from":"62.195.143.53","fw":4570,"lts":-1,"max":36.741,"min":36.423999999999999,"msm_id":1000192,"msm_name":"Ping","prb_id":202,"proto":"ICMP","rcvd":3,"result":[{"rtt":36.741},{"rtt":36.659999999999997},{"rtt":36.423999999999999}],"sent":3,"size":12,"step":360,"timestamp":1384500425, "type":"ping"}')
315 | assert(result.af == 4)
316 | assert(result.rtt_average == 36.608)
317 | assert(result.rtt_median == 36.66)
318 | assert(result.destination_address == "62.2.16.12")
319 | assert(result.destination_name == "hsi.cablecom.ch")
320 | assert(result.duplicates == 0)
321 | assert(result.origin == "62.195.143.53")
322 | assert(result.firmware == 4570)
323 | assert(result.seconds_since_sync is None)
324 | assert(result.rtt_max == 36.741)
325 | assert(result.rtt_min == 36.424)
326 | assert(result.measurement_id == 1000192)
327 | assert(result.probe_id == 202)
328 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
329 | assert(result.packets_received == 3)
330 | assert(result.packets_sent == 3)
331 | assert(result.packet_size == 12)
332 | assert(result.step == 360)
333 | assert(result.created.isoformat() == "2013-11-15T07:27:05+00:00")
334 | assert(result.packets[0].rtt == 36.741)
335 | assert(result.packets[1].rtt == 36.66)
336 | assert(result.packets[2].rtt == 36.424)
337 | assert(result.packets[0].ttl is None)
338 | assert(result.packets[1].ttl is None)
339 | assert(result.packets[2].ttl is None)
340 | assert(result.packets[0].dup is False)
341 | assert(result.packets[1].dup is False)
342 | assert(result.packets[2].dup is False)
343 | assert(result.packets[0].source_address is None)
344 | assert(result.packets[1].source_address is None)
345 | assert(result.packets[2].source_address is None)
346 |
347 |
348 | def test_ping_4600():
349 | result = Result.get('{"af":4,"avg":47.951999999999998,"dst_addr":"62.2.16.24","dst_name":"hsi.cablecom.ch","dup":0,"from":"188.195.183.141","fw":4600,"group_id":1000192,"lts":222,"max":48.990000000000002,"min":45.939,"msm_id":1000192,"msm_name":"Ping","prb_id":270,"proto":"ICMP","rcvd":3,"result":[{"rtt":45.939},{"rtt":48.927},{"rtt":48.990000000000002}],"sent":3,"size":12,"src_addr":"192.168.178.21","step":360,"timestamp":1392321470,"ttl":50,"type":"ping"}')
350 | assert(result.af == 4)
351 | assert(result.rtt_average == 47.952)
352 | assert(result.rtt_median == 48.927)
353 | assert(result.destination_address == "62.2.16.24")
354 | assert(result.destination_name == "hsi.cablecom.ch")
355 | assert(result.duplicates == 0)
356 | assert(result.origin == "188.195.183.141")
357 | assert(result.firmware == 4600)
358 | assert(result.seconds_since_sync == 222)
359 | assert(result.rtt_max == 48.99)
360 | assert(result.rtt_min == 45.939)
361 | assert(result.measurement_id == 1000192)
362 | assert(result.probe_id == 270)
363 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
364 | assert(result.packets_received == 3)
365 | assert(result.packets_sent == 3)
366 | assert(result.packet_size == 12)
367 | assert(result.step == 360)
368 | assert(result.created.isoformat() == "2014-02-13T19:57:50+00:00")
369 | assert(result.packets[0].rtt == 45.939)
370 | assert(result.packets[1].rtt == 48.927)
371 | assert(result.packets[2].rtt == 48.99)
372 | assert(result.packets[0].ttl == 50)
373 | assert(result.packets[1].ttl == 50)
374 | assert(result.packets[2].ttl == 50)
375 | assert(result.packets[0].dup is False)
376 | assert(result.packets[1].dup is False)
377 | assert(result.packets[2].dup is False)
378 | assert(result.packets[0].source_address == "192.168.178.21")
379 | assert(result.packets[1].source_address == "192.168.178.21")
380 | assert(result.packets[2].source_address == "192.168.178.21")
381 |
382 |
383 | def test_ping_4610():
384 | result = Result.get('{"af":4,"avg":57.140666666666668,"dst_addr":"62.2.16.24","dst_name":"hsi.cablecom.ch","dup":0,"from":"188.195.181.120","fw":4610,"group_id":1000192,"lts":93,"max":63.213000000000001,"min":47.941000000000003,"msm_id":1000192,"msm_name":"Ping","prb_id":270,"proto":"ICMP","rcvd":3,"result":[{"rtt":63.213000000000001},{"rtt":47.941000000000003,"ttl":51},{"rtt":60.268000000000001,"ttl":50}],"sent":3,"size":12,"src_addr":"192.168.178.21","step":360,"timestamp":1395416383,"ttl":50,"type":"ping"}')
385 | assert(isinstance(result, Result))
386 | assert(isinstance(result, PingResult))
387 | assert(isinstance(result.packets[0], Packet))
388 | assert(isinstance(result.packets[1], Packet))
389 | assert(isinstance(result.packets[2], Packet))
390 | assert(result.af == 4)
391 | assert(result.rtt_average == 57.141)
392 | assert(result.rtt_median == 60.268)
393 | assert(result.destination_address == "62.2.16.24")
394 | assert(result.destination_name == "hsi.cablecom.ch")
395 | assert(result.duplicates == 0)
396 | assert(result.origin == "188.195.181.120")
397 | assert(result.firmware == 4610)
398 | assert(result.seconds_since_sync == 93)
399 | assert(result.rtt_max == 63.213)
400 | assert(result.rtt_min == 47.941)
401 | assert(result.measurement_id == 1000192)
402 | assert(result.probe_id == 270)
403 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
404 | assert(result.packets_received == 3)
405 | assert(result.packets_sent == 3)
406 | assert(result.packet_size == 12)
407 | assert(result.step == 360)
408 | assert(result.created.isoformat() == "2014-03-21T15:39:43+00:00")
409 | assert(result.packets[0].rtt == 63.213)
410 | assert(result.packets[1].rtt == 47.941)
411 | assert(result.packets[2].rtt == 60.268)
412 | assert(result.packets[0].ttl == 50)
413 | assert(result.packets[1].ttl == 51)
414 | assert(result.packets[2].ttl == 50)
415 | assert(result.packets[0].dup is False)
416 | assert(result.packets[1].dup is False)
417 | assert(result.packets[2].dup is False)
418 | assert(result.packets[0].source_address == "192.168.178.21")
419 | assert(result.packets[1].source_address == "192.168.178.21")
420 | assert(result.packets[2].source_address == "192.168.178.21")
421 |
422 |
423 | def test_ping_duplicate():
424 | result = Result.get('{"af":4,"avg":27.768000000000001,"dst_addr":"62.2.16.24","dst_name":"hsi.cablecom.ch","dup":2,"from":"109.190.83.40","fw":4610,"lts":38,"max":27.768000000000001,"min":27.768000000000001,"msm_id":1000192,"msm_name":"Ping","prb_id":1216,"proto":"ICMP","rcvd":1,"result":[{"srcaddr":"192.168.103.130","x":"*"},{"dup":1,"rtt":36.454000000000001,"ttl":54},{"dup":1,"rtt":37.756},{"rtt":27.768000000000001}],"sent":2,"size":12,"src_addr":"192.168.103.130","step":360,"timestamp":1395277728,"ttl":54,"type":"ping"}')
425 | assert(result.af == 4)
426 | assert(result.rtt_average == 27.768)
427 | assert(result.rtt_median == 27.768)
428 | assert(result.destination_address == "62.2.16.24")
429 | assert(result.destination_name == "hsi.cablecom.ch")
430 | assert(result.duplicates == 2)
431 | assert(result.origin == "109.190.83.40")
432 | assert(result.firmware == 4610)
433 | assert(result.seconds_since_sync == 38)
434 | assert(result.rtt_max == 27.768)
435 | assert(result.rtt_min == 27.768)
436 | assert(result.measurement_id == 1000192)
437 | assert(result.probe_id == 1216)
438 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
439 | assert(result.packets_received == 1)
440 | assert(result.packets_sent == 2)
441 | assert(result.packet_size == 12)
442 | assert(result.step == 360)
443 | assert(result.created.isoformat() == "2014-03-20T01:08:48+00:00")
444 | assert(result.packets[0].rtt is None)
445 | assert(result.packets[1].rtt == 36.454)
446 | assert(result.packets[2].rtt == 37.756)
447 | assert(result.packets[3].rtt == 27.768)
448 | assert(result.packets[0].ttl is None)
449 | assert(result.packets[1].ttl == 54)
450 | assert(result.packets[2].ttl == 54)
451 | assert(result.packets[3].ttl == 54)
452 | assert(result.packets[0].dup is False)
453 | assert(result.packets[1].dup is True)
454 | assert(result.packets[2].dup is True)
455 | assert(result.packets[3].dup is False)
456 | assert(result.packets[0].source_address == "192.168.103.130")
457 | assert(result.packets[1].source_address == "192.168.103.130")
458 | assert(result.packets[2].source_address == "192.168.103.130")
459 | assert(result.packets[3].source_address == "192.168.103.130")
460 |
461 |
462 | def test_ping_buggy():
463 | result = Result.get('{"af":4,"avg":-1,"dst_addr":"62.2.16.24","dst_name":"hsi.cablecom.ch","dup":2,"from":"62.195.143.53","fw":4600,"lts":130,"max":-1,"min":-1,"msm_id":1000192,"msm_name":"Ping","prb_id":202,"proto":"ICMP","rcvd":0,"result":[{"srcaddr":"192.168.1.229","x":"*"},{"dup":1,"rtt":1635.423,"ttl":55},{"dup":1,"rtt":1800.4939999999999}],"sent":1,"size":12,"src_addr":"192.168.1.229","step":360,"timestamp":1394745831,"ttl":55,"type":"ping"}')
464 | assert(result.af == 4)
465 | assert(result.rtt_average is None)
466 | assert(result.rtt_median is None)
467 | assert(result.destination_address == "62.2.16.24")
468 | assert(result.destination_name == "hsi.cablecom.ch")
469 | assert(result.duplicates == 2)
470 | assert(result.origin == "62.195.143.53")
471 | assert(result.firmware == 4600)
472 | assert(result.seconds_since_sync == 130)
473 | assert(result.rtt_max is None)
474 | assert(result.rtt_min is None)
475 | assert(result.measurement_id == 1000192)
476 | assert(result.probe_id == 202)
477 | assert(result.protocol == PingResult.PROTOCOL_ICMP)
478 | assert(result.packets_received == 0)
479 | assert(result.packets_sent == 1)
480 | assert(result.packet_size == 12)
481 | assert(result.step == 360)
482 | print(result.created, type(result.created))
483 | print(result.created.isoformat())
484 | assert(result.created.isoformat() == "2014-03-13T21:23:51+00:00")
485 | assert(result.packets[0].rtt is None)
486 | assert(result.packets[1].rtt == 1635.423)
487 | assert(result.packets[2].rtt == 1800.494)
488 | assert(result.packets[0].ttl is None)
489 | assert(result.packets[1].ttl == 55)
490 | assert(result.packets[2].ttl == 55)
491 | assert(result.packets[0].dup is False)
492 | assert(result.packets[1].dup is True)
493 | assert(result.packets[2].dup is True)
494 | assert(result.packets[0].source_address == "192.168.1.229")
495 | assert(result.packets[1].source_address == "192.168.1.229")
496 | assert(result.packets[2].source_address == "192.168.1.229")
497 |
498 |
499 | def test_ping_lts():
500 | result = Result.get('{"af":4,"prb_id":270,"result":[{"rtt":70.265},{"rtt":54.584,"ttl":51},{"rtt":52.875}],"ttl":51,"avg":59.2413333333,"size":12,"from":"188.193.157.75","proto":"ICMP","timestamp":1406561624,"dup":0,"type":"ping","sent":3,"msm_id":1000192,"fw":4650,"max":70.265,"step":360,"src_addr":"192.168.178.21","rcvd":3,"msm_name":"Ping","lts":76,"dst_name":"hsi.cablecom.ch","min":52.875,"group_id":1000192,"dst_addr":"62.2.16.24"}')
501 | assert(result.seconds_since_sync == 76)
502 |
--------------------------------------------------------------------------------