├── .coverage ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── python-publish.yml │ └── python-test.yml ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── UML-Diagram.png ├── pythonping ├── __init__.py ├── executor.py ├── icmp.py ├── network.py ├── payload_provider.py └── utils.py ├── requirements.txt ├── setup.py └── test ├── __init__.py ├── test_executor.py ├── test_icmp.py ├── test_network.py ├── test_payload_provider.py ├── test_ping.py └── test_utils.py /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alessandromaggio/pythonping/afa1e9588002bb2cade23451326bdbea06e7496e/.coverage -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: Python test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: '3.x' 15 | - name: Test with pytest 16 | run: | 17 | sudo pip install pytest pytest-cov 18 | sudo -E pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | ### Intellij ### 43 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 44 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 45 | 46 | .idea/ 47 | # User-specific stuff: 48 | .idea/workspace.xml 49 | .idea/tasks.xml 50 | .idea/dictionaries 51 | .idea/vcs.xml 52 | .idea/jsLibraryMappings.xml 53 | 54 | # Sensitive or high-churn files: 55 | .idea/dataSources.ids 56 | .idea/dataSources.xml 57 | .idea/dataSources.local.xml 58 | .idea/sqlDataSources.xml 59 | .idea/dynamic.xml 60 | .idea/uiDesigner.xml 61 | 62 | # Gradle: 63 | .idea/gradle.xml 64 | .idea/libraries 65 | 66 | # Mongo Explorer plugin: 67 | .idea/mongoSettings.xml 68 | 69 | ## File-based project format: 70 | *.iws 71 | 72 | ## Plugin-specific files: 73 | 74 | # IntelliJ 75 | /out/ 76 | 77 | # mpeltonen/sbt-idea plugin 78 | .idea_modules/ 79 | 80 | # JIRA plugin 81 | atlassian-ide-plugin.xml 82 | 83 | # Crashlytics plugin (for Android Studio and IntelliJ) 84 | com_crashlytics_export_strings.xml 85 | crashlytics.properties 86 | crashlytics-build.properties 87 | fabric.properties 88 | 89 | 90 | # Byte-compiled / optimized / DLL files 91 | __pycache__/ 92 | *.py[cod] 93 | 94 | # C extensions 95 | *.so 96 | 97 | # Distribution / packaging 98 | bin/ 99 | build/ 100 | develop-eggs/ 101 | dist/ 102 | eggs/ 103 | lib/ 104 | lib64/ 105 | parts/ 106 | sdist/ 107 | var/ 108 | *.egg-info/ 109 | .installed.cfg 110 | *.egg 111 | 112 | 113 | # Directories potentially created on remote AFP share 114 | .AppleDB 115 | .AppleDesktop 116 | Network Trash Folder 117 | Temporary Items 118 | .apdisk 119 | temp.py 120 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "test" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team by raising an issue on GitHub. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue 4 | here on GitHub. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | ** All PRs must merge into the dev branch, and not into master**. The dev branch is the only one that can merge into master directly. 10 | 11 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 12 | build. 13 | 2. Make sure all corresponding test cases pass. 14 | 3. Update the README.md with details of changes to the interface, this includes new environment 15 | variables, exposed ports, useful file locations and container parameters. 16 | 4. Version number will be increased only when merging dev into master, so that a version update may carry multiple PRs from various developers. The versioning scheme we use is [SemVer](http://semver.org/). 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alessandro Maggio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pythonping 2 | PythonPing is simple way to ping in Python. With it, you can send ICMP Probes to remote devices like 3 | you would do from the terminal. PythonPing is modular, so that you can run it in a script as a 4 | standalone function, or integrate its components in a fully-fledged application. 5 | 6 | ## Basic Usage 7 | The simplest usage of PythonPing is in a script. You can use the `ping` function to ping a target. 8 | If you want to see the output immediately, emulating what happens on the terminal, use the 9 | `verbose` flag as below. 10 | 11 | ```python 12 | from pythonping import ping 13 | 14 | ping('127.0.0.1', verbose=True) 15 | ``` 16 | This will yeld the following result. 17 | ``` 18 | Reply from 127.0.0.1, 9 bytes in 0.17ms 19 | Reply from 127.0.0.1, 9 bytes in 0.14ms 20 | Reply from 127.0.0.1, 9 bytes in 0.12ms 21 | Reply from 127.0.0.1, 9 bytes in 0.12ms 22 | ``` 23 | 24 | Regardless of the verbose mode, the `ping` function will always return a `ResponseList` object. 25 | This is a special iterable object, containing a list of `Response` items. In each response, you can 26 | find the packet received and some meta information, like the time it took to receive the response 27 | and any error message. 28 | 29 | You can also tune your ping by using some of its additional parameters: 30 | * `size` is an integer that allows you to specify the size of the ICMP payload you desire 31 | * `timeout` is the number of seconds you wish to wait for a response, before assuming the target 32 | is unreachable 33 | * `payload` allows you to use a specific payload (bytes) 34 | * `count` specify allows you to define how many ICMP packets to send 35 | * `interval` the time to wait between pings, in seconds 36 | * `sweep_start` and `sweep_end` allows you to perform a ping sweep, starting from payload size 37 | defined in `sweep_start` and growing up to size defined in `sweep_end`. Here, we repeat the payload 38 | you provided to match the desired size, or we generate a random one if no payload was provided. 39 | Note that if you defined `size`, these two fields will be ignored 40 | * `df` is a flag that, if set to True, will enable the *Don't Fragment* flag in the IP header 41 | * `verbose` enables the verbose mode, printing output to a stream (see `out`) 42 | * `out` is the target stream of verbose mode. If you enable the verbose mode and do not provide 43 | `out`, verbose output will be send to the `sys.stdout` stream. You may want to use a file here. 44 | * `match` is a flag that, if set to True, will enable payload matching between a ping request 45 | and reply (default behaviour follows that of Windows which counts a successful reply by a 46 | matched packet identifier only; Linux behaviour counts a non equivalent payload with a matched 47 | packet identifier in reply as fail, such as when pinging 8.8.8.8 with 1000 bytes and the reply 48 | is truncated to only the first 74 of request payload with a matching packet identifier) 49 | 50 | ## FAQ 51 | ### Do I need privileged mode or root? 52 | Yes, you need to be root to use pythonping. 53 | 54 | ### Why do I need to be root to use pythonping? 55 | All operating systems allow programs to create TCP or UDP sockets without requiring particular 56 | permissions. However, ping runs in ICMP (which is neither TCP or UDP). This means we have to create 57 | raw IP packets, and sniff the traffic on the network card. 58 | **Operating systems are designed to require root for such operations**. This is because having 59 | unrestricted access to the NIC can expose the user to risks if the application running has bad 60 | intentions. This is not the case with pythonping of course, but nonetheless we need this capability 61 | to create custom IP packets. Unfortunately, there is simply no other way to create ICMP packets. 62 | 63 | ## Advanced Usage 64 | If you wish to extend PythonPing, or integrate it in your application, we recommend to use the 65 | classes that are part of Python Ping instead of the `ping` function. `executor.Communicator` 66 | handles the communication with the target device, it takes care of sending ICMP requests and 67 | processing responses (note that for it to be thread safe you must then handle making a unique 68 | seed ID for each thread instance, see ping.__init\__ for an example of this). It ultimately 69 | produces the `executor.ResponseList` object. The `Communicator` needs to know a target and 70 | which payloads to send to the remote device. For that, we have several classes in the 71 | `payload_provider` module. You may want to create your own provider by extending 72 | `payload_provider.PayloadProvider`. If you are interested in that, you should check the 73 | documentation of both `executor` and `payload_provider` module. 74 | 75 | ## Code Structure 76 | 77 | ### Top Level Directory Layout 78 | Our project directory structure contains all src files in the pythonping folder, test cases in another folder, and helping documentation in on the top level directory. 79 | 80 | ``` 81 | . 82 | ├── pythonping # Source files 83 | ├── test # Automated Testcases for the package 84 | ├── CODE_OF_CONDUCT # An md file containing code of conduct 85 | ├── CONTRIBUTING # Contributing Guidlins 86 | ├── LICENSE # MIT License 87 | ├── README.md # An md file 88 | └── setup.py # Instalation 89 | ``` 90 | 91 | A UML Diagram of the code structure is below: 92 | 93 | ![ER1](https://raw.githubusercontent.com/alessandromaggio/pythonping/master/docs/UML-Diagram.png) 94 | 95 | As per the uml diagram above five distinct classes outside of init exist in this package: Executor, Icmp, Payload Provider, and Utils. Each of them rely on attributes which have been listed as sub-classes for brevities sake. An overview of each class is as follows. 96 | 97 | ### Utils 98 | Simply generates random text. See function random_text. 99 | 100 | ### Network 101 | Opens a socket to send and recive data. See functions send, recv, and del. 102 | 103 | ### Payload Provider 104 | Generates ICMP Payloads with no Headers. It's functionaly a interface. It has three 105 | functions init, iter, and next, which are all implmented by subclasses List, Repeat, and Sweep which store payloads in diffrent lists. 106 | 107 | ### ICMP 108 | Generates the ICMP heaser through subclass ICMPType, and various helper functions. 109 | 110 | ### Executor 111 | Has various subclasses including Message, Response, Success, and Communicator used for sending icmp packets and collecting data. 112 | 113 | ### Init 114 | Uses network, executor, payload_provider and utils.random_text to construct and send ICMP packets to ping a network. 115 | 116 | ## Tests 117 | A test package exists under the folder test, and contains a serise of unit tests. Before commiting changes make sure to run the test bench and make sure all corrisponding cases pass. For new functionality new test cases must be added and documented. 118 | 119 | To run testcases we can simply use the ```unitest discover``` utility by running the following command: 120 | 121 | ``` 122 | python -m unittest discover 123 | ``` 124 | 125 | To run the test cases in a specific file FILE we must run the following command: 126 | 127 | ``` 128 | python -m unittest discover -s -p FILE 129 | ``` 130 | 131 | Another option is to run the following from the top level directory: 132 | 133 | ``` 134 | pytest test 135 | ``` 136 | 137 | To test for coverage simply run: 138 | 139 | ``` 140 | coverage run -m pytest test 141 | ``` 142 | 143 | ## Contributing 144 | Before contributing read through the contribution guidlines found the CONTRIBUTING file. 145 | 146 | ### Code Style 147 | A few key points when contributing to this repo are as follows: 148 | 1. Use tabs over spaces. 149 | 2. Format doc strings as such: 150 | ``` 151 | DESCRIPTION 152 | 153 | :param X: DESCRIPTION 154 | :type X: Type 155 | :param Y: DESCRIPTION 156 | :type Y: Type 157 | ``` 158 | Please add doc strings to all functions added. 159 | 3. Do not add spaces between docstring and first function line. 160 | 4. Do not go over 200 characters per line. 161 | 5. When closing multiline items under brackets('()', '[]', ... etc) put the closing bracket on it's own line. -------------------------------------------------------------------------------- /docs/UML-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alessandromaggio/pythonping/afa1e9588002bb2cade23451326bdbea06e7496e/docs/UML-Diagram.png -------------------------------------------------------------------------------- /pythonping/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from random import randint 3 | from . import network, executor, payload_provider 4 | from .utils import random_text 5 | 6 | 7 | # this needs to be available across all thread usages and will hold ints 8 | SEED_IDs = [] 9 | 10 | 11 | def ping(target, 12 | timeout=2, 13 | count=4, 14 | size=1, 15 | interval=0, 16 | payload=None, 17 | sweep_start=None, 18 | sweep_end=None, 19 | df=False, 20 | verbose=False, 21 | out=sys.stdout, 22 | match=False, 23 | source=None, 24 | out_format='legacy'): 25 | """Pings a remote host and handles the responses 26 | 27 | :param target: The remote hostname or IP address to ping 28 | :type target: str 29 | :param timeout: Time in seconds before considering each non-arrived reply permanently lost. 30 | :type timeout: Union[int, float] 31 | :param count: How many times to attempt the ping 32 | :type count: int 33 | :param size: Size of the entire packet to send 34 | :type size: int 35 | :param interval: Interval to wait between pings 36 | :type interval: int 37 | :param payload: Payload content, leave None if size is set to use random text 38 | :type payload: Union[str, bytes] 39 | :param sweep_start: If size is not set, initial size in a sweep of sizes 40 | :type sweep_start: int 41 | :param sweep_end: If size is not set, final size in a sweep of sizes 42 | :type sweep_end: int 43 | :param df: Don't Fragment flag value for IP Header 44 | :type df: bool 45 | :param verbose: Print output while performing operations 46 | :type verbose: bool 47 | :param out: Stream to which redirect the verbose output 48 | :type out: stream 49 | :param match: Do payload matching between request and reply (default behaviour follows that of Windows which is 50 | by packet identifier only, Linux behaviour counts a non equivalent payload in reply as fail, such as when pinging 51 | 8.8.8.8 with 1000 bytes and reply is truncated to only the first 74 of request payload with packet identifiers 52 | the same in request and reply) 53 | :type match: bool 54 | :param repr_format: How to __repr__ the response. Allowed: legacy, None 55 | :type repr_format: str 56 | :return: List with the result of each ping 57 | :rtype: executor.ResponseList""" 58 | provider = payload_provider.Repeat(b'', 0) 59 | if sweep_start and sweep_end and sweep_end >= sweep_start: 60 | if not payload: 61 | payload = random_text(sweep_start) 62 | provider = payload_provider.Sweep(payload, sweep_start, sweep_end) 63 | elif size and size > 0: 64 | if not payload: 65 | payload = random_text(size) 66 | provider = payload_provider.Repeat(payload, count) 67 | options = () 68 | if df: 69 | options = network.Socket.DONT_FRAGMENT 70 | 71 | # Fix to allow for pythonping multithreaded usage; 72 | # no need to protect this loop as no one will ever surpass 0xFFFF amount of threads 73 | while True: 74 | # seed_id needs to be less than or equal to 65535 (as original code was seed_id = getpid() & 0xFFFF) 75 | seed_id = randint(0x1, 0xFFFF) 76 | if seed_id not in SEED_IDs: 77 | SEED_IDs.append(seed_id) 78 | break 79 | 80 | 81 | comm = executor.Communicator(target, provider, timeout, interval, socket_options=options, verbose=verbose, output=out, 82 | seed_id=seed_id, source=source, repr_format=out_format) 83 | 84 | comm.run(match_payloads=match) 85 | 86 | SEED_IDs.remove(seed_id) 87 | 88 | return comm.responses 89 | -------------------------------------------------------------------------------- /pythonping/executor.py: -------------------------------------------------------------------------------- 1 | """Module that actually performs the ping, sending and receiving packets""" 2 | 3 | import os 4 | import sys 5 | import time 6 | from . import icmp 7 | from . import network 8 | 9 | # Python 3.5 compatibility 10 | if sys.version_info[1] == 5: 11 | from enum import IntEnum, Enum 12 | 13 | class AutoNumber(Enum): 14 | def __new__(cls): 15 | value = len(cls.__members__) + 1 16 | obj = object.__new__(cls) 17 | obj._value = value 18 | return obj 19 | 20 | class SuccessOn(AutoNumber): 21 | One = () 22 | Most = () 23 | All = () 24 | else: 25 | from enum import IntEnum, auto 26 | 27 | class SuccessOn(IntEnum): 28 | One = auto() 29 | Most = auto() 30 | All = auto() 31 | 32 | 33 | class Message: 34 | """Represents an ICMP message with destination socket""" 35 | def __init__(self, target, packet, source): 36 | """Creates a message that may be sent, or used to represent a response 37 | 38 | :param target: Target IP or hostname of the message 39 | :type target: str 40 | :param packet: ICMP packet composing the message 41 | :type packet: icmp.ICMP 42 | :param source: Source IP or hostname of the message 43 | :type source: str""" 44 | self.target = target 45 | self.packet = packet 46 | self.source = source 47 | 48 | def send(self, source_socket): 49 | """Places the message on a socket 50 | 51 | :param source_socket: The socket to place the message on 52 | :type source_socket: network.Socket""" 53 | source_socket.send(self.packet.packet) 54 | 55 | def __repr__(self): 56 | return repr(self.packet) 57 | 58 | 59 | def represent_seconds_in_ms(seconds): 60 | """Converts seconds into human-readable milliseconds with 2 digits decimal precision 61 | 62 | :param seconds: Seconds to convert 63 | :type seconds: Union[int, float] 64 | :return: The same time expressed in milliseconds, with 2 digits of decimal precision 65 | :rtype: float""" 66 | return round(seconds * 1000, 2) 67 | 68 | 69 | class Response: 70 | """Represents a response to an ICMP message, with metadata like timing""" 71 | def __init__(self, message, time_elapsed, source_request=None, repr_format=None): 72 | """Creates a representation of ICMP message received in response 73 | 74 | :param message: The message received 75 | :type message: Union[None, Message] 76 | :param time_elapsed: Time elapsed since the original request was sent, in seconds 77 | :type time_elapsed: float 78 | :param source_request: ICMP packet represeting the request that originated this response 79 | :type source_request: ICMP 80 | :param repr_format: How to __repr__ the response. Allowed: legacy, None 81 | :type repr_format: str""" 82 | self.message = message 83 | self.time_elapsed = time_elapsed 84 | self.source_request = source_request 85 | self.repr_format = repr_format 86 | 87 | @property 88 | def success(self): 89 | return self.error_message is None 90 | 91 | @property 92 | def error_message(self): 93 | if self.message is None: 94 | return 'No response' 95 | if self.message.packet.message_type == 0 and self.message.packet.message_code == 0: 96 | # Echo Reply, response OK - no error 97 | return None 98 | if self.message.packet.message_type == 3: 99 | # Destination unreachable, returning more details based on message code 100 | unreachable_messages = [ 101 | 'Network Unreachable', 102 | 'Host Unreachable', 103 | 'Protocol Unreachable', 104 | 'Port Unreachable', 105 | 'Fragmentation Required', 106 | 'Source Route Failed', 107 | 'Network Unknown', 108 | 'Host Unknown', 109 | 'Source Host Isolated', 110 | 'Communication with Destination Network is Administratively Prohibited', 111 | 'Communication with Destination Host is Administratively Prohibited', 112 | 'Network Unreachable for ToS', 113 | 'Host Unreachable for ToS', 114 | 'Communication Administratively Prohibited', 115 | 'Host Precedence Violation', 116 | 'Precedence Cutoff in Effect' 117 | ] 118 | try: 119 | return unreachable_messages[self.message.packet.message_code] 120 | except IndexError: 121 | # Should never generate IndexError, this serves as additional protection 122 | return 'Unreachable' 123 | # Error was not identified 124 | return 'Network Error' 125 | 126 | @property 127 | def time_elapsed_ms(self): 128 | return represent_seconds_in_ms(self.time_elapsed) 129 | 130 | def legacy_repr(self): 131 | if self.message is None: 132 | return 'Request timed out' 133 | if self.success: 134 | return 'Reply from {0}, {1} bytes in {2}ms'.format(self.message.source, 135 | len(self.message.packet.raw), 136 | self.time_elapsed_ms) 137 | # Not successful, but with some code (e.g. destination unreachable) 138 | return '{0} from {1} in {2}ms'.format(self.error_message, self.message.source, self.time_elapsed_ms) 139 | 140 | def __repr__(self): 141 | if self.repr_format == 'legacy': 142 | return self.legacy_repr() 143 | if self.message is None: 144 | return 'Timed out' 145 | if self.success: 146 | return 'status=OK\tfrom={0}\tms={1}\t\tbytes\tsnt={2}\trcv={3}'.format( 147 | self.message.source, 148 | self.time_elapsed_ms, 149 | len(self.source_request.raw)+20, 150 | len(self.message.packet.raw) 151 | ) 152 | return 'status=ERR\tfrom={1}\terror="{0}"'.format(self.message.source, self.error_message) 153 | 154 | class ResponseList: 155 | """Represents a series of ICMP responses""" 156 | def __init__(self, initial_set=[], verbose=False, output=sys.stdout): 157 | """Creates a ResponseList with initial data if available 158 | 159 | :param initial_set: Already existing responses 160 | :type initial_set: list 161 | :param verbose: Flag to enable verbose mode, defaults to False 162 | :type verbose: bool 163 | :param output: File where to write verbose output, defaults to stdout 164 | :type output: file""" 165 | self._responses = [] 166 | self.clear() 167 | self.verbose = verbose 168 | self.output = output 169 | self.rtt_avg = 0 170 | self.rtt_min = 0 171 | self.rtt_max = 0 172 | self.stats_packets_sent = 0 173 | self.stats_packets_returned = 0 174 | for response in initial_set: 175 | self.append(response) 176 | 177 | def success(self, option=SuccessOn.One): 178 | """Check success state of the request. 179 | 180 | :param option: Sets a threshold for success sign. ( 1 - SuccessOn.One, 2 - SuccessOn.Most, 3 - SuccessOn.All ) 181 | :type option: int 182 | :return: Whether this set of responses is successful 183 | :rtype: bool 184 | """ 185 | result = False 186 | success_list = [resp.success for resp in self._responses] 187 | if option == SuccessOn.One: 188 | result = True in success_list 189 | elif option == SuccessOn.Most: 190 | result = success_list.count(True) / len(success_list) > 0.5 191 | elif option == SuccessOn.All: 192 | result = False not in success_list 193 | return result 194 | 195 | @property 196 | def packet_loss(self): 197 | return self.packets_lost 198 | 199 | @property 200 | def rtt_min_ms(self): 201 | return represent_seconds_in_ms(self.rtt_min) 202 | 203 | @property 204 | def rtt_max_ms(self): 205 | return represent_seconds_in_ms(self.rtt_max) 206 | 207 | @property 208 | def rtt_avg_ms(self): 209 | return represent_seconds_in_ms(self.rtt_avg) 210 | 211 | def clear(self): 212 | self._responses = [] 213 | self.stats_packets_sent = 0 214 | self.stats_packets_returned = 0 215 | 216 | 217 | def append(self, value): 218 | self._responses.append(value) 219 | self.stats_packets_sent += 1 220 | if len(self) == 1: 221 | self.rtt_avg = value.time_elapsed 222 | self.rtt_max = value.time_elapsed 223 | self.rtt_min = value.time_elapsed 224 | else: 225 | # Calculate the total of time, add the new value and divide for the new number 226 | self.rtt_avg = ((self.rtt_avg * (len(self)-1)) + value.time_elapsed) / len(self) 227 | if value.time_elapsed > self.rtt_max: 228 | self.rtt_max = value.time_elapsed 229 | if value.time_elapsed < self.rtt_min: 230 | self.rtt_min = value.time_elapsed 231 | if value.success: 232 | self.stats_packets_returned += 1 233 | 234 | if self.verbose: 235 | print(value, file=self.output) 236 | 237 | @property 238 | def stats_packets_lost(self): 239 | return self.stats_packets_sent - self.stats_packets_returned 240 | 241 | @property 242 | def stats_success_ratio(self): 243 | return self.stats_packets_returned / self.stats_packets_sent 244 | 245 | @property 246 | def stats_lost_ratio(self): 247 | return 1 - self.stats_success_ratio 248 | 249 | @property 250 | def packets_lost(self): 251 | return self.stats_lost_ratio 252 | 253 | def __len__(self): 254 | return len(self._responses) 255 | 256 | def __repr__(self): 257 | ret = '' 258 | for response in self._responses: 259 | ret += '{0}\r\n'.format(response) 260 | ret += '\r\n' 261 | ret += 'Round Trip Times min/avg/max is {0}/{1}/{2} ms'.format(self.rtt_min_ms, self.rtt_avg_ms, self.rtt_max_ms) 262 | return ret 263 | 264 | def __iter__(self): 265 | for response in self._responses: 266 | yield response 267 | 268 | 269 | class Communicator: 270 | """Instance actually communicating over the network, sending messages and handling responses""" 271 | def __init__(self, target, payload_provider, timeout, interval, socket_options=(), seed_id=None, 272 | verbose=False, output=sys.stdout, source=None, repr_format=None): 273 | """Creates an instance that can handle communication with the target device 274 | 275 | :param target: IP or hostname of the remote device 276 | :type target: str 277 | :param payload_provider: An iterable list of payloads to send 278 | :type payload_provider: PayloadProvider 279 | :param timeout: Timeout that will apply to all ping messages, in seconds 280 | :type timeout: Union[int, float] 281 | :param interval: Interval to wait between pings, in seconds 282 | :type interval: int 283 | :param socket_options: Options to specify for the network.Socket 284 | :type socket_options: tuple 285 | :param seed_id: The first ICMP packet ID to use 286 | :type seed_id: Union[None, int] 287 | :param verbose: Flag to enable verbose mode, defaults to False 288 | :type verbose: bool 289 | :param output: File where to write verbose output, defaults to stdout 290 | :type output: file 291 | :param repr_format: How to __repr__ the response. Allowed: legacy, None 292 | :type repr_format: str""" 293 | self.socket = network.Socket(target, 'icmp', options=socket_options, source=source) 294 | self.provider = payload_provider 295 | self.timeout = timeout 296 | self.interval = interval 297 | self.responses = ResponseList(verbose=verbose, output=output) 298 | self.seed_id = seed_id 299 | self.repr_format = repr_format 300 | # note that to make Communicator instances thread safe, the seed ID must be unique per thread 301 | if self.seed_id is None: 302 | self.seed_id = os.getpid() & 0xFFFF 303 | 304 | def __del__(self): 305 | pass 306 | 307 | def send_ping(self, packet_id, sequence_number, payload): 308 | """Sends one ICMP Echo Request on the socket 309 | 310 | :param packet_id: The ID to use for the packet 311 | :type packet_id: int 312 | :param sequence_number: The sequence number to use for the packet 313 | :type sequence_number: int 314 | :param payload: The payload of the ICMP message 315 | :type payload: Union[str, bytes] 316 | :rtype: ICMP""" 317 | i = icmp.ICMP( 318 | icmp.Types.EchoRequest, 319 | payload=payload, 320 | identifier=packet_id, sequence_number=sequence_number) 321 | self.socket.send(i.packet) 322 | return i 323 | 324 | def listen_for(self, packet_id, timeout, payload_pattern=None, source_request=None): 325 | """Listens for a packet of a given id for a given timeout 326 | 327 | :param packet_id: The ID of the packet to listen for, the same for request and response 328 | :type packet_id: int 329 | :param timeout: How long to listen for the specified packet, in seconds 330 | :type timeout: float 331 | :param payload_pattern: Payload reply pattern to match to request, if set to None, match by ID only 332 | :type payload_pattern: Union[None, bytes] 333 | :return: The response to the request with the specified packet_id 334 | :rtype: Response""" 335 | time_left = timeout 336 | response = icmp.ICMP() 337 | while time_left > 0: 338 | # Keep listening until a packet arrives 339 | raw_packet, source_socket, time_left = self.socket.receive(time_left) 340 | # If we actually received something 341 | if raw_packet != b'': 342 | response.unpack(raw_packet) 343 | 344 | # Ensure we have not unpacked the packet we sent (RHEL will also listen to outgoing packets) 345 | if response.id == packet_id and response.message_type != icmp.Types.EchoRequest.type_id: 346 | if payload_pattern is None: 347 | # To allow Windows-like behaviour (no payload inspection, but only match packet identifiers), 348 | # simply allow for it to be an always true in the legacy usage case 349 | payload_matched = True 350 | else: 351 | payload_matched = (payload_pattern == response.payload) 352 | 353 | if payload_matched: 354 | return Response(Message('', response, source_socket[0]), timeout - time_left, source_request, repr_format=self.repr_format) 355 | return Response(None, timeout, source_request, repr_format=self.repr_format) 356 | 357 | @staticmethod 358 | def increase_seq(sequence_number): 359 | """Increases an ICMP sequence number and reset if it gets bigger than 2 bytes 360 | 361 | :param sequence_number: The sequence number to increase 362 | :type sequence_number: int 363 | :return: The increased sequence number of 1, in case an increase was not possible 364 | :rtype: int""" 365 | sequence_number += 1 366 | if sequence_number > 0xFFFF: 367 | sequence_number = 1 368 | return sequence_number 369 | 370 | def run(self, match_payloads=False): 371 | """Performs all the pings and stores the responses 372 | 373 | :param match_payloads: optional to set to True to make sure requests and replies have equivalent payloads 374 | :type match_payloads: bool""" 375 | self.responses.clear() 376 | identifier = self.seed_id 377 | seq = 1 378 | for payload in self.provider: 379 | icmp_out = self.send_ping(identifier, seq, payload) 380 | if not match_payloads: 381 | self.responses.append(self.listen_for(identifier, self.timeout, None, icmp_out)) 382 | else: 383 | self.responses.append(self.listen_for(identifier, self.timeout, icmp_out.payload, icmp_out)) 384 | 385 | seq = self.increase_seq(seq) 386 | 387 | if self.interval: 388 | time.sleep(self.interval) 389 | -------------------------------------------------------------------------------- /pythonping/icmp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import struct 3 | 4 | def checksum(data): 5 | """Creates the ICMP checksum as in RFC 1071 6 | 7 | :param data: Data to calculate the checksum ofs 8 | :type data: bytes 9 | :return: Calculated checksum 10 | :rtype: int 11 | 12 | Divides the data in 16-bits chunks, then make their 1's complement sum""" 13 | subtotal = 0 14 | for i in range(0, len(data)-1, 2): 15 | subtotal += ((data[i] << 8) + data[i+1]) # Sum 16 bits chunks together 16 | if len(data) % 2: # If length is odd 17 | subtotal += (data[len(data)-1] << 8) # Sum the last byte plus one empty byte of padding 18 | while subtotal >> 16: # Add carry on the right until fits in 16 bits 19 | subtotal = (subtotal & 0xFFFF) + (subtotal >> 16) 20 | check = ~subtotal # Performs the one complement 21 | return ((check << 8) & 0xFF00) | ((check >> 8) & 0x00FF) # Swap bytes 22 | 23 | 24 | class ICMPType: 25 | """Represents an ICMP type, as combination of type and code 26 | 27 | ICMP Types should inherit from this class so that the code can identify them easily. 28 | This is a static class, not meant to be instantiated""" 29 | def __init__(self): 30 | raise TypeError('ICMPType may not be instantiated') 31 | 32 | 33 | class Types(ICMPType): 34 | class EchoReply(ICMPType): 35 | type_id = 0 36 | ECHO_REPLY = (type_id, 0,) 37 | 38 | class DestinationUnreachable(ICMPType): 39 | type_id = 3 40 | NETWORK_UNREACHABLE = (type_id, 0,) 41 | HOST_UNREACHABLE = (type_id, 1,) 42 | PROTOCOL_UNREACHABLE = (type_id, 2,) 43 | PORT_UNREACHABLE = (type_id, 3,) 44 | FRAGMENTATION_REQUIRED = (type_id, 4,) 45 | SOURCE_ROUTE_FAILED = (type_id, 5,) 46 | NETWORK_UNKNOWN = (type_id, 6,) 47 | HOST_UNKNOWN = (type_id, 7,) 48 | SOURCE_HOST_ISOLATED = (type_id, 8,) 49 | NETWORK_ADMINISTRATIVELY_PROHIBITED = (type_id, 9,) 50 | HOST_ADMINISTRATIVELY_PROHIBITED = (type_id, 10,) 51 | NETWORK_UNREACHABLE_TOS = (type_id, 11,) 52 | HOST_UNREACHABLE_TOS = (type_id, 12,) 53 | COMMUNICATION_ADMINISTRATIVELY_PROHIBITED = (type_id, 13,) 54 | HOST_PRECEDENCE_VIOLATION = (type_id, 14,) 55 | PRECEDENCE_CUTOFF = (type_id, 15,) 56 | 57 | class SourceQuench(ICMPType): 58 | type_id = 4 59 | SOURCE_QUENCH = (type_id, 0,) 60 | 61 | class Redirect(ICMPType): 62 | type_id = 5 63 | FOR_NETWORK = (type_id, 0,) 64 | FOR_HOST = (type_id, 1,) 65 | FOR_TOS_AND_NETWORK = (type_id, 2,) 66 | FOR_TOS_AND_HOST = (type_id, 3,) 67 | 68 | class EchoRequest(ICMPType): 69 | type_id = 8 70 | ECHO_REQUEST = (type_id, 0,) 71 | 72 | class RouterAdvertisement(ICMPType): 73 | type_id = 9 74 | ROUTER_ADVERTISEMENT = (type_id, 0,) 75 | 76 | class RouterSolicitation(ICMPType): 77 | type_id = 10 78 | ROUTER_SOLICITATION = (type_id, 0) 79 | # Aliases 80 | ROUTER_DISCOVERY = ROUTER_SOLICITATION 81 | ROUTER_SELECTION = ROUTER_SOLICITATION 82 | 83 | class TimeExceeded(ICMPType): 84 | type_id = 11 85 | TTL_EXPIRED_IN_TRANSIT = (type_id, 0) 86 | FRAGMENT_REASSEMBLY_TIME_EXCEEDED = (type_id, 1) 87 | 88 | class BadIPHeader(ICMPType): 89 | type_id = 12 90 | POINTER_INDICATES_ERROR = (type_id, 0) 91 | MISSING_REQUIRED_OPTION = (type_id, 1) 92 | BAD_LENGTH = (type_id, 2) 93 | 94 | class Timestamp(ICMPType): 95 | type_id = 13 96 | TIMESTAMP = (type_id, 0) 97 | 98 | class TimestampReply(ICMPType): 99 | type_id = 14 100 | TIMESTAMP_REPLY = (type_id, 0) 101 | 102 | class InformationRequest(ICMPType): 103 | type_id = 15 104 | INFORMATION_REQUEST = (type_id, 0) 105 | 106 | class InformationReply(ICMPType): 107 | type_id = 16 108 | INFORMATION_REPLY = (type_id, 0) 109 | 110 | class AddressMaskRequest(ICMPType): 111 | type_id = 17 112 | ADDRESS_MASK_REQUEST = (type_id, 0) 113 | 114 | class AddressMaskReply(ICMPType): 115 | type_id = 18 116 | ADDRESS_MASK_REPLY = (type_id, 0) 117 | 118 | class Traceroute(ICMPType): 119 | type_id = 30 120 | INFORMATION_REQUEST = (type_id, 30) 121 | 122 | 123 | class ICMP: 124 | LEN_TO_PAYLOAD = 41 # Ethernet, IP and ICMP header lengths combined 125 | 126 | def __init__(self, message_type=Types.EchoReply, payload=None, identifier=None, sequence_number=1): 127 | """Creates an ICMP packet 128 | 129 | :param message_type: Type of ICMP message to send 130 | :type message_type: Union[ICMPType, (int, int), int] 131 | :param payload: utf8 string or bytes payload 132 | :type payload: Union[str, bytes] 133 | :param identifier: ID of this ICMP packet 134 | :type identifier: int""" 135 | self.message_code = 0 136 | if issubclass(message_type, ICMPType): 137 | self.message_type = message_type.type_id 138 | elif isinstance(message_type, tuple): 139 | self.message_type = message_type[0] 140 | self.message_code = message_type[1] 141 | elif isinstance(message_type, int): 142 | self.message_type = message_type 143 | if payload is None: 144 | payload = bytes('1', 'utf8') 145 | elif isinstance(payload, str): 146 | payload = bytes(payload, 'utf8') 147 | self.payload = payload 148 | if identifier is None: 149 | identifier = os.getpid() 150 | self.id = identifier & 0xFFFF # Prevent identifiers bigger than 16 bits 151 | self.sequence_number = sequence_number 152 | self.received_checksum = None 153 | self.raw = None 154 | 155 | @property 156 | def packet(self): 157 | """The raw packet with header, ready to be sent from a socket""" 158 | p = self._header(check=self.expected_checksum) + self.payload 159 | if self.raw is None: 160 | self.raw = p 161 | return p 162 | 163 | def _header(self, check=0): 164 | """The raw ICMP header 165 | 166 | :param check: Checksum value 167 | :type check: int 168 | :return: The packed header 169 | :rtype: bytes""" 170 | # TODO implement sequence number 171 | return struct.pack("BBHHH", 172 | self.message_type, 173 | self.message_code, 174 | check, 175 | self.id, 176 | self.sequence_number) 177 | 178 | def __repr__(self): 179 | return ' '.join('{:02x}'.format(b) for b in self.raw) 180 | 181 | @property 182 | def is_valid(self): 183 | """True if the received checksum is valid, otherwise False""" 184 | if self.received_checksum is None: 185 | return True 186 | return self.expected_checksum == self.received_checksum 187 | 188 | @property 189 | def expected_checksum(self): 190 | """The checksum expected for this packet, calculated with checksum field set to 0""" 191 | return checksum(self._header() + self.payload) 192 | 193 | @property 194 | def header_length(self): 195 | """Length of the ICMP header""" 196 | return len(self._header()) 197 | 198 | @staticmethod 199 | def generate_from_raw(raw): 200 | """Creates a new ICMP representation from the raw bytes 201 | 202 | :param raw: The raw packet including payload 203 | :type raw: bytes 204 | :return: An ICMP instance representing the packet 205 | :rtype: ICMP""" 206 | packet = ICMP() 207 | packet.unpack(raw) 208 | return packet 209 | 210 | def unpack(self, raw): 211 | """Unpacks a raw packet and stores it in this object 212 | 213 | :param raw: The raw packet, including payload 214 | :type raw: bytes""" 215 | self.raw = raw 216 | self.message_type, \ 217 | self.message_code, \ 218 | self.received_checksum, \ 219 | self.id, \ 220 | self.sequence_number = struct.unpack("BBHHH", raw[20:28]) 221 | self.payload = raw[28:] 222 | -------------------------------------------------------------------------------- /pythonping/network.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import select 3 | import time 4 | 5 | 6 | class Socket: 7 | DONT_FRAGMENT = (socket.SOL_IP, 10, 1) # Option value for raw socket 8 | PROTO_LOOKUP = {"icmp": socket.IPPROTO_ICMP, "tcp": socket.IPPROTO_TCP, "udp": socket.IPPROTO_UDP, 9 | "ip": socket.IPPROTO_IP, "raw": socket.IPPROTO_RAW} 10 | 11 | def __init__(self, destination, protocol, options=(), buffer_size=2048, source=None): 12 | """Creates a network socket to exchange messages 13 | 14 | :param destination: Destination IP address 15 | :type destination: str 16 | :param protocol: Name of the protocol to use 17 | :type protocol: str 18 | :param options: Options to set on the socket 19 | :type options: tuple 20 | :param source: Source IP to use - implemented in future releases 21 | :type source: Union[None, str] 22 | :param buffer_size: Size in bytes of the listening buffer for incoming packets (replies) 23 | :type buffer_size: int""" 24 | try: 25 | self.destination = socket.gethostbyname(destination) 26 | except socket.gaierror as e: 27 | raise RuntimeError('Cannot resolve address "' + destination + '", try verify your DNS or host file') 28 | 29 | self.protocol = Socket.getprotobyname(protocol) 30 | self.buffer_size = buffer_size 31 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, self.protocol) 32 | self.source = source 33 | if options: 34 | self.socket.setsockopt(*options) 35 | 36 | # Implementing a version of socket.getprotobyname for this library since built-in is not thread safe 37 | # for python 3.5, 3.6, and 3.7: 38 | # https://bugs.python.org/issue30482 39 | # This bug was causing failures as it would occasionally return a 0 (incorrect) instead of a 1 (correct) 40 | # for the 'icmp' string, causing a OSError for "Protocol not supported" in multi-threaded usage: 41 | # https://github.com/alessandromaggio/pythonping/issues/40 42 | @staticmethod 43 | def getprotobyname(name): 44 | try: 45 | return Socket.PROTO_LOOKUP[name.lower()] 46 | except KeyError: 47 | raise KeyError("'" + str(name) + "' is not in the list of supported proto types: " 48 | + str(list(Socket.PROTO_LOOKUP.keys()))) 49 | 50 | def send(self, packet): 51 | """Sends a raw packet on the stream 52 | 53 | :param packet: The raw packet to send 54 | :type packet: bytes""" 55 | if self.source: 56 | self.socket.bind((self.source, 0)) 57 | self.socket.sendto(packet, (self.destination, 0)) 58 | 59 | def receive(self, timeout=2): 60 | """Listen for incoming packets until timeout 61 | 62 | :param timeout: Time after which stop listening 63 | :type timeout: Union[int, float] 64 | :return: The packet, the remote socket, and the time left before timeout 65 | :rtype: (bytes, tuple, float)""" 66 | time_left = timeout 67 | while time_left > 0: 68 | start_select = time.perf_counter() 69 | data_ready = select.select([self.socket], [], [], time_left) 70 | elapsed_in_select = time.perf_counter() - start_select 71 | time_left -= elapsed_in_select 72 | if not data_ready[0]: 73 | # Timeout 74 | return b'', '', time_left 75 | packet, source = self.socket.recvfrom(self.buffer_size) 76 | return packet, source, time_left 77 | 78 | def __del__(self): 79 | try: 80 | if hasattr(self, "socket") and self.socket: 81 | self.socket.close() 82 | except AttributeError: 83 | raise AttributeError("Attribute error because of failed socket init. Make sure you have the root privilege." 84 | " This error may also be caused from DNS resolution problems.") 85 | -------------------------------------------------------------------------------- /pythonping/payload_provider.py: -------------------------------------------------------------------------------- 1 | """Module generating ICMP payloads (with no header)""" 2 | 3 | 4 | class PayloadProvider: 5 | def __init__(self): 6 | raise NotImplementedError('Cannot create instances of PayloadProvider') 7 | 8 | def __iter__(self): 9 | raise NotImplementedError() 10 | 11 | def __next__(self): 12 | raise NotImplementedError() 13 | 14 | 15 | class List(PayloadProvider): 16 | def __init__(self, payload_list): 17 | """Creates a provider of payloads from an existing list of payloads 18 | 19 | :param payload_list: An existing list of payloads 20 | :type payload_list: list""" 21 | self._payloads = payload_list 22 | self._counter = 0 23 | 24 | def __iter__(self): 25 | self._counter = 0 26 | return self 27 | 28 | def __next__(self): 29 | if self._counter < len(self._payloads): 30 | ret = self._payloads[self._counter] 31 | self._counter += 1 32 | return ret 33 | raise StopIteration 34 | 35 | 36 | class Repeat(PayloadProvider): 37 | def __init__(self, pattern, count): 38 | """Creates a provider of many identical payloads 39 | 40 | :param pattern: The existing payload 41 | :type pattern: Union[str, bytes] 42 | :param count: How many payloads to generate 43 | :type count: int""" 44 | self.pattern = pattern 45 | self.count = count 46 | self._counter = 0 47 | 48 | def __iter__(self): 49 | self._counter = 0 50 | return self 51 | 52 | def __next__(self): 53 | if self._counter < self.count: 54 | self._counter += 1 55 | return self.pattern 56 | raise StopIteration 57 | 58 | 59 | class Sweep(PayloadProvider): 60 | def __init__(self, pattern, start_size, end_size): 61 | """Creates a provider of payloads of increasing size 62 | 63 | :param pattern: The existing payload, may be cut or replicated to fit the size 64 | :type pattern: Union[str, bytes] 65 | :param start_size: The first payload size to start with, included 66 | :type start_size: int 67 | :param end_size: The payload size to end with, included 68 | :type end_size: int""" 69 | if start_size > end_size: 70 | raise ValueError('end_size must be greater or equal than start_size') 71 | if len(pattern) == 0: 72 | raise ValueError('pattern cannot be empty') 73 | self.pattern = pattern 74 | self.start_size = start_size 75 | self.end_size = end_size 76 | # Extend the length of the pattern if needed 77 | while not len(self.pattern) >= end_size: 78 | self.pattern += pattern 79 | self._current_size = self.start_size 80 | 81 | def __iter__(self): 82 | self._current_size = self.start_size 83 | return self 84 | 85 | def __next__(self): 86 | if self._current_size <= self.end_size: 87 | ret = self.pattern[0:self._current_size] 88 | self._current_size += 1 89 | return ret 90 | raise StopIteration 91 | -------------------------------------------------------------------------------- /pythonping/utils.py: -------------------------------------------------------------------------------- 1 | """Module containing service classes and functions""" 2 | 3 | import string 4 | import random 5 | 6 | 7 | def random_text(size): 8 | """Returns a random text of the specified size 9 | 10 | :param size: Size of the random string, must be greater than 0 11 | :type size int 12 | :return: Random string 13 | :rtype: str""" 14 | return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(size)) 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pexpect==4.0.8 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.md', 'r', encoding='utf-8') as file: 4 | long_description = file.read() 5 | 6 | setup(name='pythonping', 7 | version='1.1.4', 8 | description='A simple way to ping in Python', 9 | url='https://github.com/alessandromaggio/pythonping', 10 | author='Alessandro Maggio', 11 | author_email='me@alessandromaggio.com', 12 | license='MIT', 13 | packages=['pythonping'], 14 | keywords=['ping', 'icmp', 'network'], 15 | classifiers=[ 16 | 'Development Status :: 5 - Production/Stable', 17 | 'Intended Audience :: System Administrators', 18 | 'Natural Language :: English' 19 | ], 20 | long_description=long_description, 21 | long_description_content_type='text/markdown', 22 | zip_safe=False) 23 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alessandromaggio/pythonping/afa1e9588002bb2cade23451326bdbea06e7496e/test/__init__.py -------------------------------------------------------------------------------- /test/test_executor.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import unittest 3 | from pythonping import executor 4 | from pythonping import icmp 5 | 6 | 7 | class SuccessfulResponseMock(executor.Response): 8 | """Mock a successful response to a ping""" 9 | @property 10 | def success(self): 11 | return True 12 | 13 | 14 | class FailingResponseMock(executor.Response): 15 | """Mock a failed response to a ping""" 16 | @property 17 | def success(self): 18 | return False 19 | 20 | 21 | class ExecutorUtilsTestCase(unittest.TestCase): 22 | """Tests for standalone functions in pythonping.executor""" 23 | 24 | def test_represent_seconds_in_ms(self): 25 | """Verifies conversion from seconds to milliseconds works correctly""" 26 | self.assertEqual(executor.represent_seconds_in_ms(4), 4000, 'Failed to convert seconds to milliseconds') 27 | self.assertEqual(executor.represent_seconds_in_ms(0), 0, 'Failed to convert seconds to milliseconds') 28 | self.assertEqual(executor.represent_seconds_in_ms(0.001), 1, 'Failed to convert seconds to milliseconds') 29 | self.assertEqual(executor.represent_seconds_in_ms(0.0001), 0.1, 'Failed to convert seconds to milliseconds') 30 | self.assertEqual(executor.represent_seconds_in_ms(0.00001), 0.01, 'Failed to convert seconds to milliseconds') 31 | 32 | 33 | class ResponseTestCase(unittest.TestCase): 34 | """Tests to verify that a Response object renders information correctly""" 35 | 36 | @staticmethod 37 | def craft_response_of_type(response_type): 38 | """Generates an executor.Response from an icmp.Types 39 | 40 | :param response_type: Type of response 41 | :type response_type: Union[icmp.Type, tuple] 42 | :return: The crafted response 43 | :rtype: executor.Response""" 44 | return executor.Response(executor.Message('', icmp.ICMP(response_type), '127.0.0.1'), 0.1) 45 | 46 | def test_success(self): 47 | """Verifies the if the Response can indicate a success to a request correctly""" 48 | self.assertTrue(self.craft_response_of_type(icmp.Types.EchoReply).success, 49 | 'Unable to validate a successful response') 50 | self.assertFalse(self.craft_response_of_type(icmp.Types.DestinationUnreachable).success, 51 | 'Unable to validate Destination Unreachable') 52 | self.assertFalse(self.craft_response_of_type(icmp.Types.BadIPHeader).success, 53 | 'Unable to validate Bad IP Header') 54 | self.assertFalse(executor.Response(None, 0.1).success, 'Unable to validate timeout (no payload)') 55 | 56 | def test_error_message(self): 57 | """Verifies error messages are presented correctly""" 58 | self.assertEqual(self.craft_response_of_type(icmp.Types.EchoReply).error_message, None, 59 | 'Generated error message when response was correct') 60 | self.assertEqual(self.craft_response_of_type(icmp.Types.DestinationUnreachable).error_message, 61 | 'Network Unreachable', 62 | 'Unable to classify a Network Unreachable error correctly') 63 | self.assertEqual(executor.Response(None, 0.1).error_message, 'No response', 64 | 'Unable to generate correct message when response is not received') 65 | pass 66 | 67 | def time_elapsed(self): 68 | """Verifies the time elapsed is presented correctly""" 69 | self.assertEqual(executor.Response(None, 1).time_elapsed_ms, 1000, 'Bad ms representation for 1 second') 70 | self.assertEqual(executor.Response(None, 0.001).time_elapsed_ms, 1, 'Bad ms representation for 1 ms') 71 | self.assertEqual(executor.Response(None, 0.01).time_elapsed_ms, 10, 'Bad ms representation for 10 ms') 72 | 73 | 74 | class ResponseListTestCase(unittest.TestCase): 75 | """Tests for ResponseList""" 76 | 77 | @staticmethod 78 | def responses_from_times(times): 79 | """Generates a list of empty responses from a list of time elapsed 80 | 81 | :param times: List of time elapsed for each response 82 | :type times: list 83 | :return: List of responses 84 | :rtype: executor.ResponseList""" 85 | return executor.ResponseList([executor.Response(None, _) for _ in times]) 86 | 87 | def test_rtt_min_ms(self): 88 | """Verifies the minimum RTT is found correctly""" 89 | self.assertEqual( 90 | self.responses_from_times([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).rtt_min, 91 | 0, 92 | 'Unable to identify minimum RTT of 0' 93 | ) 94 | self.assertEqual( 95 | self.responses_from_times([38, 11, 93, 100, 38, 11, 0.1]).rtt_min, 96 | 0.1, 97 | 'Unable to identify minimum RTT of 0.1' 98 | ) 99 | self.assertEqual( 100 | self.responses_from_times([10, 10, 10, 10]).rtt_min, 101 | 10, 102 | 'Unable to identify minimum RTT of 10 on a series of only 10s' 103 | ) 104 | 105 | def test_rtt_max_ms(self): 106 | """Verifies the maximum RTT is found correctly""" 107 | self.assertEqual( 108 | self.responses_from_times([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).rtt_max, 109 | 9, 110 | 'Unable to identify maximum RTT of 9' 111 | ) 112 | self.assertEqual( 113 | self.responses_from_times([38, 11, 93, 100, 38, 11, 0.1]).rtt_max, 114 | 100, 115 | 'Unable to identify maximum RTT of 100' 116 | ) 117 | self.assertEqual( 118 | self.responses_from_times([10, 10, 10, 10]).rtt_max, 119 | 10, 120 | 'Unable to identify maximum RTT of 10 on a series of only 10s' 121 | ) 122 | 123 | def test_rtt_avg_ms(self): 124 | """Verifies the average RTT is found correctly""" 125 | self.assertEqual( 126 | self.responses_from_times([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).rtt_avg, 127 | 4.5, 128 | 'Unable to identify average RTT of 4.5' 129 | ) 130 | self.assertEqual( 131 | self.responses_from_times([38, 11, 93, 100, 38, 11, 0.1]).rtt_avg, 132 | 41.58571428571429, 133 | 'Unable to identify average RTT of 41.58571428571429' 134 | ) 135 | self.assertEqual( 136 | self.responses_from_times([10, 10, 10, 10]).rtt_avg, 137 | 10, 138 | 'Unable to identify average RTT of 10 on a series of only 10s' 139 | ) 140 | 141 | def test_len(self): 142 | """Verifies the length is returned correctly""" 143 | self.assertEqual( 144 | len(self.responses_from_times(list(range(10)))), 145 | 10, 146 | 'Unable identify the length of 10' 147 | ) 148 | self.assertEqual( 149 | len(self.responses_from_times(list(range(0)))), 150 | 0, 151 | 'Unable identify the length of 0' 152 | ) 153 | self.assertEqual( 154 | len(self.responses_from_times(list(range(23)))), 155 | 23, 156 | 'Unable identify the length of 23' 157 | ) 158 | 159 | def test_iterable(self): 160 | """Verifies it is iterable""" 161 | self.assertTrue( 162 | isinstance(self.responses_from_times([0, 1, 2, 3]), collections.abc.Iterable), 163 | 'Unable to iterate over ResponseList object' 164 | ) 165 | 166 | def test_success_all_success(self): 167 | """Verify success is calculated correctly if all responses are successful""" 168 | rs = executor.ResponseList([ 169 | SuccessfulResponseMock(None, 1), 170 | SuccessfulResponseMock(None, 1), 171 | SuccessfulResponseMock(None, 1), 172 | SuccessfulResponseMock(None, 1) 173 | ]) 174 | self.assertTrue( 175 | rs.success(executor.SuccessOn.One), 176 | 'Unable to calculate success on one correctly with all responses successful' 177 | ) 178 | self.assertTrue( 179 | rs.success(executor.SuccessOn.Most), 180 | 'Unable to calculate success on most with all responses successful' 181 | ) 182 | self.assertTrue( 183 | rs.success(executor.SuccessOn.All), 184 | 'Unable to calculate success on all with all responses successful' 185 | ) 186 | 187 | def test_success_one_success(self): 188 | """Verify success is calculated correctly if one response is successful""" 189 | rs = executor.ResponseList([ 190 | SuccessfulResponseMock(None, 1), 191 | FailingResponseMock(None, 1), 192 | FailingResponseMock(None, 1), 193 | FailingResponseMock(None, 1) 194 | ]) 195 | self.assertTrue( 196 | rs.success(executor.SuccessOn.One), 197 | 'Unable to calculate success on one correctly with one response successful' 198 | ) 199 | self.assertFalse( 200 | rs.success(executor.SuccessOn.Most), 201 | 'Unable to calculate success on most with one response successful' 202 | ) 203 | self.assertFalse( 204 | rs.success(executor.SuccessOn.All), 205 | 'Unable to calculate success on all with one response successful' 206 | ) 207 | 208 | def test_success_most_success(self): 209 | """Verify success is calculated correctly if most responses are successful""" 210 | rs = executor.ResponseList([ 211 | SuccessfulResponseMock(None, 1), 212 | SuccessfulResponseMock(None, 1), 213 | SuccessfulResponseMock(None, 1), 214 | FailingResponseMock(None, 1) 215 | ]) 216 | self.assertTrue( 217 | rs.success(executor.SuccessOn.One), 218 | 'Unable to calculate success on one correctly with most responses successful' 219 | ) 220 | self.assertTrue( 221 | rs.success(executor.SuccessOn.Most), 222 | 'Unable to calculate success on most with most responses successful' 223 | ) 224 | self.assertFalse( 225 | rs.success(executor.SuccessOn.All), 226 | 'Unable to calculate success on all with most responses successful' 227 | ) 228 | 229 | def test_success_half_success(self): 230 | """Verify success is calculated correctly if half responses are successful""" 231 | rs = executor.ResponseList([ 232 | SuccessfulResponseMock(None, 1), 233 | SuccessfulResponseMock(None, 1), 234 | FailingResponseMock(None, 1), 235 | FailingResponseMock(None, 1) 236 | ]) 237 | self.assertTrue( 238 | rs.success(executor.SuccessOn.One), 239 | 'Unable to calculate success on one correctly with half responses successful' 240 | ) 241 | self.assertFalse( 242 | rs.success(executor.SuccessOn.Most), 243 | 'Unable to calculate success on most with half responses successful' 244 | ) 245 | self.assertFalse( 246 | rs.success(executor.SuccessOn.All), 247 | 'Unable to calculate success on all with half responses successful' 248 | ) 249 | 250 | def test_no_packets_lost(self): 251 | rs = executor.ResponseList([ 252 | SuccessfulResponseMock(None, 1), 253 | SuccessfulResponseMock(None, 1), 254 | SuccessfulResponseMock(None, 1), 255 | SuccessfulResponseMock(None, 1) 256 | ]) 257 | 258 | self.assertEqual(rs.stats_packets_sent, rs.stats_packets_returned, 'unable to correctly count sent and returned packets when all responses successful') 259 | self.assertEqual( 260 | rs.stats_packets_lost, 261 | 0, 262 | "Unable to calculate packet loss correctly when all responses successful" 263 | ) 264 | 265 | def test_all_packets_lost(self): 266 | rs = executor.ResponseList([ 267 | FailingResponseMock(None, 1), 268 | FailingResponseMock(None, 1), 269 | FailingResponseMock(None, 1), 270 | FailingResponseMock(None, 1) 271 | ]) 272 | self.assertEqual(rs.stats_packets_returned, 0, 'unable to correctly count sent and returned packets when all responses failed') 273 | self.assertEqual( 274 | rs.stats_lost_ratio, 275 | 1.0, 276 | "Unable to calculate packet loss correctly when all responses failed" 277 | ) 278 | 279 | def test_some_packets_lost(self): 280 | rs = executor.ResponseList([ 281 | SuccessfulResponseMock(None, 1), 282 | SuccessfulResponseMock(None, 1), 283 | FailingResponseMock(None, 1), 284 | FailingResponseMock(None, 1) 285 | ]) 286 | self.assertEqual(rs.stats_packets_sent, 4, 'unable to correctly count sent packets when some of the responses failed') 287 | self.assertEqual(rs.stats_packets_returned, 2, 'unable to correctly count returned packets when some of the responses failed') 288 | self.assertEqual( 289 | rs.stats_lost_ratio, 290 | 0.5, 291 | "Unable to calculate packet loss correctly when some of the responses failed" 292 | ) 293 | 294 | def test_some_packets_lost_mixed(self): 295 | rs = executor.ResponseList([ 296 | FailingResponseMock(None, 1), 297 | SuccessfulResponseMock(None, 1), 298 | FailingResponseMock(None, 1), 299 | SuccessfulResponseMock(None, 1), 300 | ]) 301 | self.assertEqual(rs.stats_packets_sent, 4, 'unable to correctly count sent packets when when failing responses are mixed with successful responses') 302 | self.assertEqual(rs.stats_packets_returned, 2, 'unable to correctly count returned packets when failing responses are mixed with successful responses') 303 | self.assertEqual( 304 | rs.packet_loss, 305 | 0.5, 306 | "Unable to calculate packet loss correctly when failing responses are mixed with successful responses" 307 | ) 308 | 309 | 310 | class CommunicatorTestCase(unittest.TestCase): 311 | """Tests for Communicator""" 312 | 313 | def test_increase_seq(self): 314 | """Verifies Communicator can increase sequence number correctly""" 315 | self.assertEqual(executor.Communicator.increase_seq(1), 2, 'Increasing sequence number 1 did not return 2') 316 | self.assertEqual(executor.Communicator.increase_seq(100), 101, 'Increasing sequence number 1 did not return 2') 317 | self.assertEqual(executor.Communicator.increase_seq(0xFFFF), 1, 318 | 'Not returned to 1 when exceeding sequence number maximum length') 319 | self.assertEqual(executor.Communicator.increase_seq(0xFFFE), 0xFFFF, 320 | 'Increasing sequence number 0xFFFE did not return 0xFFFF') 321 | -------------------------------------------------------------------------------- /test/test_icmp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pythonping import icmp 3 | 4 | 5 | class ICMPTestCase(unittest.TestCase): 6 | """Tests for the ICMP class""" 7 | 8 | def test_checksum(self): 9 | """Verifies that checksum calculation is correct""" 10 | self.assertEqual( 11 | icmp.checksum(b'\x08\x00\x00\x00\x00*\x01\x009TPM'), 12 | 13421, 13 | "Checksum validation failed (even length)" 14 | ) 15 | self.assertEqual( 16 | icmp.checksum(b'\x08\x00\x00\x00\x01*\x01\x001PJ2'), 17 | 21370, 18 | "Checksum validation failed (even length)" 19 | ) 20 | self.assertEqual( 21 | icmp.checksum(b'\x08\x00\x00\x00\x02*\x01\x006K3J'), 22 | 16523, 23 | "Checksum validation failed (even length)" 24 | ) 25 | self.assertEqual( 26 | icmp.checksum(b'\x08\x00\x00\x00\x03*\x01\x00COSA'), 27 | 17757, 28 | "Checksum validation failed (even length)" 29 | ) 30 | self.assertEqual( 31 | icmp.checksum(b'\x08\x00\x00\x00\xb4$\x01\x0012344'), 32 | 29866, 33 | "Checksum validation failed (odd length)" 34 | ) 35 | self.assertEqual( 36 | icmp.checksum(b'\x08\x00\x00\x00$F\x01\x00fag'), 37 | 22533, 38 | "Checksum validation failed (odd length)" 39 | ) 40 | self.assertEqual( 41 | icmp.checksum(b'\x08\x00\x00\x00\x8cH\x01\x00random text goes here'), 42 | 47464, 43 | "Checksum validation failed (odd length)" 44 | ) 45 | 46 | def test_pack(self): 47 | """Verifies that creates the correct pack""" 48 | self.assertEqual( 49 | icmp.ICMP(icmp.Types.EchoReply, payload='banana', identifier=19700).packet, 50 | b'\x00\x00\xcb\x8e\xf4L\x01\x00banana', 51 | "Fail to pack ICMP structure to packet" 52 | ) 53 | self.assertEqual( 54 | icmp.ICMP(icmp.Types.EchoReply, payload='random text goes here', identifier=12436).packet, 55 | b'\x00\x00h\xd1\x940\x01\x00random text goes here', 56 | "Fail to pack ICMP structure to packet" 57 | ) 58 | self.assertEqual( 59 | icmp.ICMP(icmp.Types.EchoRequest, payload='random text goes here', identifier=18676).packet, 60 | b'\x08\x00\x00\xb9\xf4H\x01\x00random text goes here', 61 | "Fail to unpack ICMP structure to packet" 62 | ) 63 | 64 | def test_unpack(self): 65 | """Verifies that reads data correctly from a packet""" 66 | ip_header_offset = b''.join([b'0' for _ in range(20)]) 67 | 68 | packet = icmp.ICMP.generate_from_raw(ip_header_offset + b'\x00\x00\xbe\xdb\x01\x00\x01\x00banana') 69 | self.assertEqual(packet.message_type, 0, 'Failed to extract message type') 70 | self.assertEqual(packet.message_code, 0, 'Failed to extract message code') 71 | self.assertEqual(packet.id, 1, 'Failed to extract id') 72 | self.assertEqual(packet.payload, b'banana', 'Failed to extract payload') 73 | 74 | packet = icmp.ICMP.generate_from_raw(ip_header_offset + b'\x00\x00Q\x9d\x10\x00\x01\x00random text goes here') 75 | self.assertEqual(packet.message_type, 0, 'Failed to extract message type') 76 | self.assertEqual(packet.message_code, 0, 'Failed to extract message code') 77 | self.assertEqual(packet.id, 16, 'Failed to extract id') 78 | self.assertEqual(packet.payload, b'random text goes here', 'Failed to extract payload') 79 | 80 | packet = icmp.ICMP.generate_from_raw(ip_header_offset + b'\x08\x00I\x9d\x10\x00\x01\x00random text goes here') 81 | self.assertEqual(packet.message_type, 8, 'Failed to extract message type') 82 | self.assertEqual(packet.message_code, 0, 'Failed to extract message code') 83 | self.assertEqual(packet.id, 16, 'Failed to extract id') 84 | self.assertEqual(packet.payload, b'random text goes here', 'Failed to extract payload') 85 | 86 | def test_is_valid(self): 87 | """Verifies that understands if receives a packet with valid or invalid checksum""" 88 | ip_header_offset = b''.join([b'0' for _ in range(20)]) 89 | # Following two packets have a good checksum 90 | packet = icmp.ICMP.generate_from_raw(ip_header_offset + b'\x00\x00\xbe\xdb\x01\x00\x01\x00banana') 91 | self.assertEqual(packet.received_checksum, packet.expected_checksum, 'Checksum validation failed') 92 | packet = icmp.ICMP.generate_from_raw(ip_header_offset + b'\x08\x00@\xca\xb47\x01\x00random text goes here') 93 | self.assertEqual(packet.received_checksum, packet.expected_checksum, 'Checksum validation failed') 94 | # Packet 'does' instead of 'goes' 95 | packet = icmp.ICMP.generate_from_raw(ip_header_offset + b'\x08\x00\x9c\xd0X1\x01\x00random text does here') 96 | self.assertNotEqual(packet.received_checksum, packet.expected_checksum, 'Checksum validation failed') 97 | 98 | def test_checksum_creation(self): 99 | """Verifies it generates the correct checksum, given packet data""" 100 | packet = icmp.ICMP(icmp.Types.EchoRequest, payload='random text goes here', identifier=16) 101 | self.assertEqual(packet.expected_checksum, 485, 'Checksum creation failed') 102 | packet = icmp.ICMP(icmp.Types.EchoReply, payload='foobar', identifier=11) 103 | self.assertEqual(packet.expected_checksum, 48060, 'Checksum creation failed') 104 | 105 | def test_blank_header_creation(self): 106 | """Verifies it generates the correct header (no checksum) given packet data""" 107 | packet = icmp.ICMP(icmp.Types.EchoRequest, payload='random text goes here', identifier=16) 108 | self.assertEqual(packet._header(), b'\x08\x00\x00\x00\x10\x00\x01\x00', 109 | 'Blank header creation failed (without checksum)') 110 | packet = icmp.ICMP(icmp.Types.EchoReply, payload='foo', identifier=11) 111 | self.assertEqual(packet._header(), b'\x00\x00\x00\x00\x0b\x00\x01\x00', 112 | 'Blank header creation failed (without checksum)') 113 | -------------------------------------------------------------------------------- /test/test_network.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pythonping.network import Socket 3 | 4 | class UtilsTestCase(unittest.TestCase): 5 | """Tests for Socket class""" 6 | 7 | 8 | 9 | def test_raise_explicative_error_on_name_resolution_failure(self): 10 | """Test a runtime error is generated if the name cannot be resolved""" 11 | with self.assertRaises(RuntimeError): 12 | Socket('invalid', 'raw') -------------------------------------------------------------------------------- /test/test_payload_provider.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pexpect import ExceptionPexpect 4 | from pythonping import payload_provider 5 | 6 | 7 | class PayloadProviderTestCase(unittest.TestCase): 8 | """Tests for PayloadProvider class""" 9 | 10 | def test_basic(self): 11 | """Verifies that a provider raises error when calling own functions""" 12 | self.assertRaises(NotImplementedError, payload_provider.PayloadProvider) 13 | 14 | 15 | def test_list(self): 16 | """Verifies that a list provider generates the correct payloads""" 17 | payloads = [ 18 | b'payload A', 19 | b'second payload' 20 | b'payload 3+' 21 | ] 22 | res = [] 23 | provider = payload_provider.List(payloads) 24 | for payload in provider: 25 | res.append(payload) 26 | for num, payload in enumerate(payloads): 27 | self.assertEqual(res[num], payload, 'Payload not expected in position {0}'.format(num)) 28 | 29 | def test_repeat(self): 30 | """Verifies that a repeat provider generates the correct payloads""" 31 | pattern = b'this is a pattern' 32 | count = 5 33 | provider = payload_provider.Repeat(pattern, count) 34 | for payload in provider: 35 | self.assertEqual(payload, pattern, 'Payload does not reflect the pattern') 36 | count -= 1 37 | self.assertEqual(count, 0, 'Generated a wrong number of payloads') 38 | 39 | def sweep_tester(self, pattern, start, end): 40 | """Runs the creation of a sweep provider and performs some basics tests on it""" 41 | provider = payload_provider.Sweep(pattern, start, end) 42 | payloads_generated = 0 43 | for payload in provider: 44 | self.assertEqual(len(payload), start, 'Generated a payload with a wrong size') 45 | start += 1 46 | payloads_generated += 1 47 | self.assertEqual(start, end+1, 'Generated a wrong number of payloads') 48 | return payloads_generated 49 | 50 | def test_sweep_normal(self): 51 | """Verifies that a sweep provider generates the correct payloads""" 52 | self.sweep_tester(b'abc', 10, 20) 53 | 54 | def test_sweep_one(self): 55 | """Verifies that a sweep provider generates one correct payload if start and end sizes are equal""" 56 | self.assertEqual( 57 | self.sweep_tester(b'frog', 40, 40), 58 | 1, 59 | 'Unable to generate exactly 1 payload during a sweep') 60 | 61 | def test_sweep_reversed(self): 62 | """Verifies that it is not possible to generate a payload with start size greater than end size""" 63 | with self.assertRaises(ValueError): 64 | payload_provider.Sweep(b'123', 100, 45) 65 | 66 | def test_sweep_no_pattern(self): 67 | """Verifies that it is not possible to generate a payload with an empty pattern""" 68 | with self.assertRaises(ValueError): 69 | payload_provider.Sweep(b'', 1, 10) 70 | -------------------------------------------------------------------------------- /test/test_ping.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from pythonping import ping 4 | 5 | 6 | class PingCase(unittest.TestCase): 7 | """Tests for actual ping against localhost""" 8 | 9 | def test_ping_execution(self): 10 | """Verifies that random_text generates text of correct size""" 11 | # NOTE, this may be considered an e2e test 12 | self.assertEqual(len(ping('10.127.0.1', count=4, size=10)), 4, 13 | 'Sent 4 pings to localhost, but not received 4 responses') 14 | 15 | # Github Actions does not support ICMP 16 | if os.getenv("GITHUB_ACTIONS") is None: 17 | self.assertEqual(ping('8.8.8.8', count=4, size=992).success(), True, 18 | 'Sent 4 large pings to google DNS A with payload match off, received all replies') 19 | 20 | self.assertEqual(ping('8.8.8.8', count=4, size=992, match=True).success(), False, 21 | 'Sent 4 large pings to google DNS A with payload match on,' 22 | + 'expected all to fail since they truncate large payloads') 23 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pythonping import utils 3 | 4 | 5 | class UtilsTestCase(unittest.TestCase): 6 | """Tests for Utils class""" 7 | 8 | def test_random_text_generation(self): 9 | """Verifies that random_text generates text of correct size""" 10 | sizes = [0, 4, 1500, 1, 33, 44, 11] 11 | for size in sizes: 12 | self.assertEqual( 13 | len(utils.random_text(size)), size, 14 | 'Unable to generate a random string of {0} characters'.format(size)) 15 | --------------------------------------------------------------------------------