Skip to content

Commit

Permalink
Merge pull request #84 from alessandromaggio/master
Browse files Browse the repository at this point in the history
Align dev branch to master
  • Loading branch information
alessandromaggio authored Jul 30, 2022
2 parents 4559ca4 + 5b6d672 commit a42fdf1
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 19 deletions.
7 changes: 5 additions & 2 deletions pythonping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def ping(target,
df=False,
verbose=False,
out=sys.stdout,
match=False):
match=False,
out_format='legacy'):
"""Pings a remote host and handles the responses
:param target: The remote hostname or IP address to ping
Expand Down Expand Up @@ -49,6 +50,8 @@ def ping(target,
8.8.8.8 with 1000 bytes and reply is truncated to only the first 74 of request payload with packet identifiers
the same in request and reply)
:type match: bool
:param repr_format: How to __repr__ the response. Allowed: legacy, None
:type repr_format: str
:return: List with the result of each ping
:rtype: executor.ResponseList"""
provider = payload_provider.Repeat(b'', 0)
Expand All @@ -74,7 +77,7 @@ def ping(target,
break

comm = executor.Communicator(target, provider, timeout, interval, socket_options=options, verbose=verbose, output=out,
seed_id=seed_id)
seed_id=seed_id, repr_format=out_format)
comm.run(match_payloads=match)

SEED_IDs.remove(seed_id)
Expand Down
54 changes: 40 additions & 14 deletions pythonping/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def send(self, source_socket):
:type source_socket: network.Socket"""
source_socket.send(self.packet.packet)

def __repr__(self):
return repr(self.packet)


def represent_seconds_in_ms(seconds):
"""Converts seconds into human-readable milliseconds with 2 digits decimal precision
Expand All @@ -65,15 +68,21 @@ def represent_seconds_in_ms(seconds):

class Response:
"""Represents a response to an ICMP message, with metadata like timing"""
def __init__(self, message, time_elapsed):
def __init__(self, message, time_elapsed, source_request=None, repr_format=None):
"""Creates a representation of ICMP message received in response
:param message: The message received
:type message: Union[None, Message]
:param time_elapsed: Time elapsed since the original request was sent, in seconds
:type time_elapsed: float"""
:type time_elapsed: float
:param source_request: ICMP packet represeting the request that originated this response
:type source_request: ICMP
:param repr_format: How to __repr__ the response. Allowed: legacy, None
:type repr_format: str"""
self.message = message
self.time_elapsed = time_elapsed
self.source_request = source_request
self.repr_format = repr_format

@property
def success(self):
Expand Down Expand Up @@ -119,17 +128,31 @@ def error_message(self):
def time_elapsed_ms(self):
return represent_seconds_in_ms(self.time_elapsed)

def __repr__(self):
def legacy_repr(self):
if self.message is None:
return 'Request timed out'
elif self.success:
return 'Reply from {0}, {1} bytes in {2}ms'.format(self.message.source,
len(self.message.packet.packet),
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)

def __repr__(self):
if self.repr_format == 'legacy':
return self.legacy_repr()
if self.message is None:
return 'Timed out'
elif 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)

class ResponseList:
"""Represents a series of ICMP responses"""
Expand Down Expand Up @@ -228,7 +251,7 @@ def __iter__(self):
class Communicator:
"""Instance actually communicating over the network, sending messages and handling responses"""
def __init__(self, target, payload_provider, timeout, interval, socket_options=(), seed_id=None,
verbose=False, output=sys.stdout):
verbose=False, output=sys.stdout, repr_format=None):
"""Creates an instance that can handle communication with the target device
:param target: IP or hostname of the remote device
Expand All @@ -246,13 +269,16 @@ def __init__(self, target, payload_provider, timeout, interval, socket_options=(
:param verbose: Flag to enable verbose mode, defaults to False
:type verbose: bool
:param output: File where to write verbose output, defaults to stdout
:type output: file"""
:type output: file
:param repr_format: How to __repr__ the response. Allowed: legacy, None
:type repr_format: str"""
self.socket = network.Socket(target, 'icmp', source=None, options=socket_options)
self.provider = payload_provider
self.timeout = timeout
self.interval = interval
self.responses = ResponseList(verbose=verbose, output=output)
self.seed_id = seed_id
self.repr_format = repr_format
# note that to make Communicator instances thread safe, the seed ID must be unique per thread
if self.seed_id is None:
self.seed_id = os.getpid() & 0xFFFF
Expand All @@ -269,15 +295,15 @@ def send_ping(self, packet_id, sequence_number, payload):
:type sequence_number: int
:param payload: The payload of the ICMP message
:type payload: Union[str, bytes]
:rtype: bytes"""
:rtype: ICMP"""
i = icmp.ICMP(
icmp.Types.EchoRequest,
payload=payload,
identifier=packet_id, sequence_number=sequence_number)
self.socket.send(i.packet)
return i.payload
return i

def listen_for(self, packet_id, timeout, payload_pattern=None):
def listen_for(self, packet_id, timeout, payload_pattern=None, source_request=None):
"""Listens for a packet of a given id for a given timeout
:param packet_id: The ID of the packet to listen for, the same for request and response
Expand Down Expand Up @@ -307,8 +333,8 @@ def listen_for(self, packet_id, timeout, payload_pattern=None):
payload_matched = (payload_pattern == response.payload)

if payload_matched:
return Response(Message('', response, source_socket[0]), timeout - time_left)
return Response(None, timeout)
return Response(Message('', response, source_socket[0]), timeout - time_left, source_request, repr_format=self.repr_format)
return Response(None, timeout, source_request, repr_format=self.repr_format)

@staticmethod
def increase_seq(sequence_number):
Expand All @@ -332,11 +358,11 @@ def run(self, match_payloads=False):
identifier = self.seed_id
seq = 1
for payload in self.provider:
payload_bytes_sent = self.send_ping(identifier, seq, payload)
icmp_out = self.send_ping(identifier, seq, payload)
if not match_payloads:
self.responses.append(self.listen_for(identifier, self.timeout))
self.responses.append(self.listen_for(identifier, self.timeout, None, icmp_out))
else:
self.responses.append(self.listen_for(identifier, self.timeout, payload_bytes_sent))
self.responses.append(self.listen_for(identifier, self.timeout, icmp_out.payload, icmp_out))

seq = self.increase_seq(seq)

Expand Down
11 changes: 9 additions & 2 deletions pythonping/icmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,14 @@ def __init__(self, message_type=Types.EchoReply, payload=None, identifier=None,
self.id = identifier & 0xFFFF # Prevent identifiers bigger than 16 bits
self.sequence_number = sequence_number
self.received_checksum = None
self.raw = None

@property
def packet(self):
"""The raw packet with header, ready to be sent from a socket"""
return self._header(check=self.expected_checksum) + self.payload
p = self._header(check=self.expected_checksum) + self.payload
if (self.raw is None): self.raw = p
return p

def _header(self, check=0):
"""The raw ICMP header
Expand All @@ -175,6 +178,9 @@ def _header(self, check=0):
self.id,
self.sequence_number)

def __repr__(self):
return ' '.join('{:02x}'.format(b) for b in self.raw)

@property
def is_valid(self):
"""True if the received checksum is valid, otherwise False"""
Expand Down Expand Up @@ -209,9 +215,10 @@ def unpack(self, raw):
:param raw: The raw packet, including payload
:type raw: bytes"""
self.raw = raw
self.message_type, \
self.message_code, \
self.received_checksum, \
self.id, \
sequence = struct.unpack("bbHHh", raw[20:28])
self.sequence_number = struct.unpack("bbHHh", raw[20:28])
self.payload = raw[28:]
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
long_description = file.read()

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

0 comments on commit a42fdf1

Please sign in to comment.