Skip to content

Commit

Permalink
feat: Release 1.1.4 (#96)
Browse files Browse the repository at this point in the history
* fix(icmp.py): ICMP header format string

All values in the struct should be unsigned.

* pylint issues resolved

Fixed a bunch of pylint issues

* First Readme push headers updated.

* Update README.md

* Update README.md

* UMLs created.

* UMLs created.

* Folder Structure Created.

* backticks added.

* Supporting words added.

* Grammer fix

* Code Styles section finished.

* Fix made

* Fix made

* Readme fixed.

* Readme fixed.

* Ready for pull request.

* Finally ready for pull request.

* Testcase added,

* feat: integrate #89, #92, and #93

Allow count greater than 16-bit, fix pylint issues, update and expand readme

Co-authored-by: David A. Bell <[email protected]>
Co-authored-by: Mark Mayo <[email protected]>
Co-authored-by: Aleksa Zatezalo <[email protected]>
Co-authored-by: Aleksa Zatezalo <[email protected]>
  • Loading branch information
5 people authored Oct 25, 2022
1 parent 1c5a69f commit afa1e95
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 78 deletions.
Binary file added .coverage
Binary file not shown.
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"test"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
9 changes: 4 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ here on GitHub.
Please note we have a code of conduct, please follow it in all your interactions with the project.

## Pull Request Process
** 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.

1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
2. Make sure all corresponding test cases pass.
3. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
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/).
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,91 @@ which payloads to send to the remote device. For that, we have several classes i
`payload_provider` module. You may want to create your own provider by extending
`payload_provider.PayloadProvider`. If you are interested in that, you should check the
documentation of both `executor` and `payload_provider` module.

## Code Structure

### Top Level Directory Layout
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.

```
.
├── pythonping # Source files
├── test # Automated Testcases for the package
├── CODE_OF_CONDUCT # An md file containing code of conduct
├── CONTRIBUTING # Contributing Guidlins
├── LICENSE # MIT License
├── README.md # An md file
└── setup.py # Instalation
```

A UML Diagram of the code structure is below:

![ER1](https://raw.githubusercontent.com/alessandromaggio/pythonping/master/docs/UML-Diagram.png)

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.

### Utils
Simply generates random text. See function random_text.

### Network
Opens a socket to send and recive data. See functions send, recv, and del.

### Payload Provider
Generates ICMP Payloads with no Headers. It's functionaly a interface. It has three
functions init, iter, and next, which are all implmented by subclasses List, Repeat, and Sweep which store payloads in diffrent lists.

### ICMP
Generates the ICMP heaser through subclass ICMPType, and various helper functions.

### Executor
Has various subclasses including Message, Response, Success, and Communicator used for sending icmp packets and collecting data.

### Init
Uses network, executor, payload_provider and utils.random_text to construct and send ICMP packets to ping a network.

## Tests
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.

To run testcases we can simply use the ```unitest discover``` utility by running the following command:

```
python -m unittest discover <test_directory>
```

To run the test cases in a specific file FILE we must run the following command:

```
python -m unittest discover -s <test_directory> -p FILE
```

Another option is to run the following from the top level directory:

```
pytest test
```

To test for coverage simply run:

```
coverage run -m pytest test
```

## Contributing
Before contributing read through the contribution guidlines found the CONTRIBUTING file.

### Code Style
A few key points when contributing to this repo are as follows:
1. Use tabs over spaces.
2. Format doc strings as such:
```
DESCRIPTION
:param X: DESCRIPTION
:type X: Type
:param Y: DESCRIPTION
:type Y: Type
```
Please add doc strings to all functions added.
3. Do not add spaces between docstring and first function line.
4. Do not go over 200 characters per line.
5. When closing multiline items under brackets('()', '[]', ... etc) put the closing bracket on it's own line.
Binary file added docs/UML-Diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pythonping/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sys
from random import randint
from . import network, executor, payload_provider
from .utils import random_text
from random import randint


# this needs to be available across all thread usages and will hold ints
Expand Down
86 changes: 44 additions & 42 deletions pythonping/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,35 +92,34 @@ def success(self):
def error_message(self):
if self.message is None:
return 'No response'
else:
if self.message.packet.message_type == 0 and self.message.packet.message_code == 0:
# Echo Reply, response OK - no error
return None
elif self.message.packet.message_type == 3:
# Destination unreachable, returning more details based on message code
unreachable_messages = [
'Network Unreachable',
'Host Unreachable',
'Protocol Unreachable',
'Port Unreachable',
'Fragmentation Required',
'Source Route Failed',
'Network Unknown',
'Host Unknown',
'Source Host Isolated',
'Communication with Destination Network is Administratively Prohibited',
'Communication with Destination Host is Administratively Prohibited',
'Network Unreachable for ToS',
'Host Unreachable for ToS',
'Communication Administratively Prohibited',
'Host Precedence Violation',
'Precedence Cutoff in Effect'
]
try:
return unreachable_messages[self.message.packet.message_code]
except IndexError:
# Should never generate IndexError, this serves as additional protection
return 'Unreachable'
if self.message.packet.message_type == 0 and self.message.packet.message_code == 0:
# Echo Reply, response OK - no error
return None
if self.message.packet.message_type == 3:
# Destination unreachable, returning more details based on message code
unreachable_messages = [
'Network Unreachable',
'Host Unreachable',
'Protocol Unreachable',
'Port Unreachable',
'Fragmentation Required',
'Source Route Failed',
'Network Unknown',
'Host Unknown',
'Source Host Isolated',
'Communication with Destination Network is Administratively Prohibited',
'Communication with Destination Host is Administratively Prohibited',
'Network Unreachable for ToS',
'Host Unreachable for ToS',
'Communication Administratively Prohibited',
'Host Precedence Violation',
'Precedence Cutoff in Effect'
]
try:
return unreachable_messages[self.message.packet.message_code]
except IndexError:
# Should never generate IndexError, this serves as additional protection
return 'Unreachable'
# Error was not identified
return 'Network Error'

Expand All @@ -131,28 +130,26 @@ def time_elapsed_ms(self):
def legacy_repr(self):
if self.message is None:
return 'Request timed out'
elif self.success:
if self.success:
return 'Reply from {0}, {1} bytes in {2}ms'.format(self.message.source,
len(self.message.packet.raw),
self.time_elapsed_ms)
else:
# Not successful, but with some code (e.g. destination unreachable)
return '{0} from {1} in {2}ms'.format(self.error_message, self.message.source, self.time_elapsed_ms)
# Not successful, but with some code (e.g. destination unreachable)
return '{0} from {1} in {2}ms'.format(self.error_message, self.message.source, self.time_elapsed_ms)

def __repr__(self):
if self.repr_format == 'legacy':
return self.legacy_repr()
if self.message is None:
return 'Timed out'
elif self.success:
if self.success:
return 'status=OK\tfrom={0}\tms={1}\t\tbytes\tsnt={2}\trcv={3}'.format(
self.message.source,
self.time_elapsed_ms,
len(self.source_request.raw)+20,
len(self.message.packet.raw)
)
else:
return 'status=ERR\tfrom={1}\terror="{0}"'.format(self.message.source, self.error_message)
return 'status=ERR\tfrom={1}\terror="{0}"'.format(self.message.source, self.error_message)

class ResponseList:
"""Represents a series of ICMP responses"""
Expand Down Expand Up @@ -231,23 +228,28 @@ def append(self, value):
self.rtt_max = value.time_elapsed
if value.time_elapsed < self.rtt_min:
self.rtt_min = value.time_elapsed
if value.success: self.stats_packets_returned += 1
if value.success:
self.stats_packets_returned += 1

if self.verbose:
print(value, file=self.output)

@property
def stats_packets_lost(self): return self.stats_packets_sent - self.stats_packets_returned
def stats_packets_lost(self):
return self.stats_packets_sent - self.stats_packets_returned

@property
def stats_success_ratio(self): return self.stats_packets_returned / self.stats_packets_sent
def stats_success_ratio(self):
return self.stats_packets_returned / self.stats_packets_sent

@property
def stats_lost_ratio(self): return 1 - self.stats_success_ratio
def stats_lost_ratio(self):
return 1 - self.stats_success_ratio

@property
def packets_lost(self): return self.stats_lost_ratio

def packets_lost(self):
return self.stats_lost_ratio

def __len__(self):
return len(self._responses)

Expand Down
37 changes: 17 additions & 20 deletions pythonping/icmp.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import os
import socket
import struct
import select
import time


def checksum(data):
"""Creates the ICMP checksum as in RFC 1071
Expand Down Expand Up @@ -61,64 +57,64 @@ class DestinationUnreachable(ICMPType):
class SourceQuench(ICMPType):
type_id = 4
SOURCE_QUENCH = (type_id, 0,)

class Redirect(ICMPType):
type_id = 5
FOR_NETWORK = (type_id, 0,)
FOR_HOST = (type_id, 1,)
FOR_TOS_AND_NETWORK = (type_id, 2,)
FOR_TOS_AND_HOST = (type_id, 3,)

class EchoRequest(ICMPType):
type_id = 8
ECHO_REQUEST = (type_id, 0,)

class RouterAdvertisement(ICMPType):
type_id = 9
ROUTER_ADVERTISEMENT = (type_id, 0,)

class RouterSolicitation(ICMPType):
type_id = 10
ROUTER_SOLICITATION = (type_id, 0)
# Aliases
ROUTER_DISCOVERY = ROUTER_SOLICITATION
ROUTER_SELECTION = ROUTER_SOLICITATION

class TimeExceeded(ICMPType):
type_id = 11
TTL_EXPIRED_IN_TRANSIT = (type_id, 0)
FRAGMENT_REASSEMBLY_TIME_EXCEEDED = (type_id, 1)

class BadIPHeader(ICMPType):
type_id = 12
POINTER_INDICATES_ERROR = (type_id, 0)
MISSING_REQUIRED_OPTION = (type_id, 1)
BAD_LENGTH = (type_id, 2)

class Timestamp(ICMPType):
type_id = 13
TIMESTAMP = (type_id, 0)

class TimestampReply(ICMPType):
type_id = 14
TIMESTAMP_REPLY = (type_id, 0)

class InformationRequest(ICMPType):
type_id = 15
INFORMATION_REQUEST = (type_id, 0)

class InformationReply(ICMPType):
type_id = 16
INFORMATION_REPLY = (type_id, 0)

class AddressMaskRequest(ICMPType):
type_id = 17
ADDRESS_MASK_REQUEST = (type_id, 0)

class AddressMaskReply(ICMPType):
type_id = 18
ADDRESS_MASK_REPLY = (type_id, 0)

class Traceroute(ICMPType):
type_id = 30
INFORMATION_REQUEST = (type_id, 30)
Expand Down Expand Up @@ -160,7 +156,8 @@ def __init__(self, message_type=Types.EchoReply, payload=None, identifier=None,
def packet(self):
"""The raw packet with header, ready to be sent from a socket"""
p = self._header(check=self.expected_checksum) + self.payload
if (self.raw is None): self.raw = p
if self.raw is None:
self.raw = p
return p

def _header(self, check=0):
Expand All @@ -171,7 +168,7 @@ def _header(self, check=0):
:return: The packed header
:rtype: bytes"""
# TODO implement sequence number
return struct.pack("bbHHh",
return struct.pack("BBHHH",
self.message_type,
self.message_code,
check,
Expand Down Expand Up @@ -220,5 +217,5 @@ def unpack(self, raw):
self.message_code, \
self.received_checksum, \
self.id, \
self.sequence_number = struct.unpack("bbHHh", raw[20:28])
self.sequence_number = struct.unpack("BBHHH", raw[20:28])
self.payload = raw[28:]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pexpect==4.0.8
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from setuptools import setup

with open('README.md', 'r') as file:
with open('README.md', 'r', encoding='utf-8') as file:
long_description = file.read()

setup(name='pythonping',
version='1.1.3',
version='1.1.4',
description='A simple way to ping in Python',
url='https://github.com/alessandromaggio/pythonping',
author='Alessandro Maggio',
Expand Down
Loading

0 comments on commit afa1e95

Please sign in to comment.