diff --git a/pythonping/__init__.py b/pythonping/__init__.py index 53176fd..9a2ac9b 100644 --- a/pythonping/__init__.py +++ b/pythonping/__init__.py @@ -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 @@ -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) @@ -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) diff --git a/pythonping/executor.py b/pythonping/executor.py index ec7ee6e..7e42001 100644 --- a/pythonping/executor.py +++ b/pythonping/executor.py @@ -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 @@ -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): @@ -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""" @@ -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 @@ -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 @@ -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 @@ -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): @@ -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) diff --git a/pythonping/icmp.py b/pythonping/icmp.py index a5e3aeb..e060735 100644 --- a/pythonping/icmp.py +++ b/pythonping/icmp.py @@ -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 @@ -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""" @@ -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:] diff --git a/setup.py b/setup.py index 546f293..7940719 100644 --- a/setup.py +++ b/setup.py @@ -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',