diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..611e05b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.py[co] +*~ +*.egg-info +/dist +/build +.pydevproject +/.settings \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..6695946 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + python-ping + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..c0b9980 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,14 @@ +AUTHORS / CONTRIBUTORS (alphabetic order): + + * Cowles, Matthew Dixon -- ftp://ftp.visi.com/users/mdc/ping.py + * Diemer, Jens -- http://www.jensdiemer.de + * Falatic, Martin -- http://www.falatic.com + * Hallman, Chris -- http://cdhallman.blogspot.com + * incidence -- https://github.com/incidence + * jcborras -- https://github.com/jcborras + * Notaras, George -- http://www.g-loaded.eu + * Poincheval, Jerome + * Sarkhel, Kunal -- https://github.com/techwizrd + * Stauffer, Samuel + * Zach Ware + * zed -- https://github.com/zed diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cc1267 --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +The original code derived from ping.c distributed in Linux's netkit. +That code is copyright (c) 1989 by The Regents of the University of California. +That code is in turn derived from code written by Mike Muuss of the +US Army Ballistic Research Laboratory in December, 1983 and +placed in the public domain. They have my thanks. + +Copyright (c) Matthew Dixon Cowles, . +Distributable under the terms of the GNU General Public License +version 2. Provided with no warranties of any sort. + +See AUTHORS for complete list of authors and contributors. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2cf163c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include AUTHORS LICENSE MANIFEST.in README.creole +recursive-exclude * *.pyc +recursive-exclude * *.pyo \ No newline at end of file diff --git a/README b/README deleted file mode 100644 index 1488af0..0000000 --- a/README +++ /dev/null @@ -1,2 +0,0 @@ -Originally from http://svn.pylucid.net/pylucid/CodeSnippets/ping.py -This version maintained at http://github.com/samuel/python-ping \ No newline at end of file diff --git a/README.creole b/README.creole new file mode 100644 index 0000000..b86e961 --- /dev/null +++ b/README.creole @@ -0,0 +1,112 @@ +A pure python ping implementation using raw sockets. + +Note that ICMP messages can only be sent from processes running as root +(in Windows, you must run this script as 'Administrator'). + +Original Version from [[ftp://ftp.visi.com/users/mdc/ping.py|Matthew Dixon Cowles]] + +* copyleft 1989-2011 by the python-ping team, see [[https://github.com/jedie/python-ping/blob/master/AUTHORS|AUTHORS]] for more details. +* license: GNU GPL v2, see [[https://github.com/jedie/python-ping/blob/master/LICENSE|LICENSE]] for more details. + + +=== usage === + +{{{ +~/python-ping$ sudo ./ping.py google.com + +PYTHON-PING google.com (209.85.148.99): 55 data bytes +64 bytes from google.com (209.85.148.99): icmp_seq=0 ttl=54 time=56.2 ms +64 bytes from google.com (209.85.148.99): icmp_seq=1 ttl=54 time=55.7 ms +64 bytes from google.com (209.85.148.99): icmp_seq=2 ttl=54 time=55.5 ms + +----google.com PYTHON PING Statistics---- +3 packets transmitted, 3 packets received, 0.0% packet loss +round-trip (ms) min/avg/max = 55.468/55.795/56.232 +}}} + + +== TODOs == + +* refactor ping.py +* create a CLI interface +* add a "suprocess ping", with output parser + + +== contribute == + +[[http://help.github.com/fork-a-repo/|Fork this repo]] on [[https://github.com/jedie/python-ping/|GitHub]] and [[http://help.github.com/send-pull-requests/|send pull requests]]. Thank you. + + +== Revision history == + +==== Oct. 17, 2011 ==== +* [[https://github.com/jedie/python-ping/pull/6|Bugfix if host is unknown]] + +==== Oct. 12, 2011 ==== +Merge sources and create a seperate github repository: +* https://github.com/jedie/python-ping + +Add a simple CLI interface. + +==== September 12, 2011 ==== +Bugfixes + cleanup by Jens Diemer +Tested with Ubuntu + Windows 7 + +==== September 6, 2011 ==== +[[http://www.falatic.com/index.php/39/pinging-with-python|Cleanup by Martin Falatic.]] +Restored lost comments and docs. Improved functionality: constant time between +pings, internal times consistently use milliseconds. Clarified annotations +(e.g., in the checksum routine). Using unsigned data in IP & ICMP header +pack/unpack unless otherwise necessary. Signal handling. Ping-style output +formatting and stats. + +==== August 3, 2011 ==== +Ported to py3k by Zach Ware. Mostly done by 2to3; also minor changes to +deal with bytes vs. string changes (no more ord() in checksum() because +>source_string< is actually bytes, added .encode() to data in +send_one_ping()). That's about it. + +==== March 11, 2010 ==== +changes by Samuel Stauffer: +replaced time.clock with default_timer which is set to +time.clock on windows and time.time on other systems. + +==== November 8, 2009 ==== +Fixes by [[http://www.g-loaded.eu/2009/10/30/python-ping/|George Notaras]], +reported by [[http://cdhallman.blogspot.com|Chris Hallman]]: + +Improved compatibility with GNU/Linux systems. + +Changes in this release: + +Re-use time.time() instead of time.clock(). The 2007 implementation +worked only under Microsoft Windows. Failed on GNU/Linux. +time.clock() behaves differently under [[http://docs.python.org/library/time.html#time.clock|the two OSes]]. + +==== May 30, 2007 ==== +little [[http://www.python-forum.de/post-69122.html#69122|rewrite by Jens Diemer]]: + * change socket asterisk import to a normal import + * replace time.time() with time.clock() + * delete "return None" (or change to "return" only) + * in checksum() rename "str" to "source_string" + +==== December 4, 2000 ==== +Changed the struct.pack() calls to pack the checksum and ID as +unsigned. My thanks to Jerome Poincheval for the fix. + +==== November 22, 1997 ==== +Initial hack. Doesn't do much, but rather than try to guess +what features I (or others) will want in the future, I've only +put in what I need now. + +==== December 16, 1997 ==== +For some reason, the checksum bytes are in the wrong order when +this is run under Solaris 2.X for SPARC but it works right under +Linux x86. Since I don't know just what's wrong, I'll swap the +bytes always and then do an htons(). + +== Links == + +| Sourcecode at GitHub | https://github.com/jedie/python-ping | +| Python Package Index | http://pypi.python.org/pypi/python-ping/ | +| IRC | [[http://www.pylucid.org/permalink/304/irc-channel|#pylucid on freenode.net]] diff --git a/ping.py b/ping.py old mode 100644 new mode 100755 index af41f1e..89cff36 --- a/ping.py +++ b/ping.py @@ -1,217 +1,373 @@ #!/usr/bin/env python +# coding: utf-8 """ - A pure python ping implementation using raw socket. + A pure python ping implementation using raw sockets. - - Note that ICMP messages can only be sent from processes running as root. - - - Derived from ping.c distributed in Linux's netkit. That code is - copyright (c) 1989 by The Regents of the University of California. - That code is in turn derived from code written by Mike Muuss of the - US Army Ballistic Research Laboratory in December, 1983 and - placed in the public domain. They have my thanks. + Note that ICMP messages can only be send from processes running as root + (in Windows, you must run this script as 'Administrator'). Bugs are naturally mine. I'd be glad to hear about them. There are - certainly word - size dependenceies here. - - Copyright (c) Matthew Dixon Cowles, . - Distributable under the terms of the GNU General Public License - version 2. Provided with no warranties of any sort. - - Original Version from Matthew Dixon Cowles: - -> ftp://ftp.visi.com/users/mdc/ping.py - - Rewrite by Jens Diemer: - -> http://www.python-forum.de/post-69122.html#69122 - - - Revision history - ~~~~~~~~~~~~~~~~ - - March 11, 2010 - changes by Samuel Stauffer: - - replaced time.clock with default_timer which is set to - time.clock on windows and time.time on other systems. - - May 30, 2007 - little rewrite by Jens Diemer: - - change socket asterisk import to a normal import - - replace time.time() with time.clock() - - delete "return None" (or change to "return" only) - - in checksum() rename "str" to "source_string" - - November 22, 1997 - Initial hack. Doesn't do much, but rather than try to guess - what features I (or others) will want in the future, I've only - put in what I need now. - - December 16, 1997 - For some reason, the checksum bytes are in the wrong order when - this is run under Solaris 2.X for SPARC but it works right under - Linux x86. Since I don't know just what's wrong, I'll swap the - bytes always and then do an htons(). - - December 4, 2000 - Changed the struct.pack() calls to pack the checksum and ID as - unsigned. My thanks to Jerome Poincheval for the fix. - - - Last commit info: - ~~~~~~~~~~~~~~~~~ - $LastChangedDate: $ - $Rev: $ - $Author: $ + certainly word - size dependencies here. + + :homepage: https://github.com/jedie/python-ping/ + :copyleft: 1989-2011 by the python-ping team, see AUTHORS for more details. + :license: GNU GPL v2, see LICENSE for more details. """ -import os, sys, socket, struct, select, time +import os +import select +import signal +import socket +import struct +import sys +import time -if sys.platform == "win32": + +if sys.platform.startswith("win32"): # On Windows, the best timer is time.clock() default_timer = time.clock else: # On most other platforms the best timer is time.time() default_timer = time.time -# From /usr/include/linux/icmp.h; your milage may vary. -ICMP_ECHO_REQUEST = 8 # Seems to be the same on Solaris. +# ICMP parameters +ICMP_ECHOREPLY = 0 # Echo reply (per RFC792) +ICMP_ECHO = 8 # Echo request (per RFC792) +ICMP_MAX_RECV = 2048 # Max size of incoming buffer + +MAX_SLEEP = 1000 -def checksum(source_string): + +def calculate_checksum(source_string): """ - I'm not too confident that this is right but testing seems - to suggest that it gives the same answers as in_cksum in ping.c + A port of the functionality of in_cksum() from ping.c + Ideally this would act on the string as a series of 16-bit ints (host + packed), but this works. + Network data is big-endian, hosts are typically little-endian """ + countTo = (int(len(source_string) / 2)) * 2 sum = 0 - countTo = (len(source_string)/2)*2 count = 0 - while count> 16) + (sum & 0xffff) - sum = sum + (sum >> 16) - answer = ~sum - answer = answer & 0xffff + sum &= 0xffffffff # Truncate sum to 32 bits (a variance from ping.c, which + # uses signed ints, but overflow is unlikely in ping) - # Swap bytes. Bugger me if I know why. - answer = answer >> 8 | (answer << 8 & 0xff00) + sum = (sum >> 16) + (sum & 0xffff) # Add high 16 bits to low 16 bits + sum += (sum >> 16) # Add carry from above (if any) + answer = ~sum & 0xffff # Invert and truncate to 16 bits + answer = socket.htons(answer) return answer -def receive_one_ping(my_socket, ID, timeout): - """ - receive the ping from the socket. - """ - timeLeft = timeout - while True: - startedSelect = default_timer() - whatReady = select.select([my_socket], [], [], timeLeft) - howLongInSelect = (default_timer() - startedSelect) - if whatReady[0] == []: # Timeout - return +def is_valid_ip4_address(addr): + parts = addr.split(".") + if not len(parts) == 4: + return False + for part in parts: + try: + number = int(part) + except ValueError: + return False + if number > 255: + return False + return True + +def to_ip(addr): + if is_valid_ip4_address(addr): + return addr + return socket.gethostbyname(addr) + + +class Ping(object): + def __init__(self, destination, timeout=1000, packet_size=55, own_id=None): + self.destination = destination + self.timeout = timeout + self.packet_size = packet_size + if own_id is None: + self.own_id = os.getpid() & 0xFFFF + else: + self.own_id = own_id - timeReceived = default_timer() - recPacket, addr = my_socket.recvfrom(1024) - icmpHeader = recPacket[20:28] - type, code, checksum, packetID, sequence = struct.unpack( - "bbHHh", icmpHeader - ) - if packetID == ID: - bytesInDouble = struct.calcsize("d") - timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0] - return timeReceived - timeSent + try: + # FIXME: Use destination only for display this line here? see: https://github.com/jedie/python-ping/issues/3 + self.dest_ip = to_ip(self.destination) + except socket.gaierror as e: + self.print_unknown_host(e) + else: + self.print_start() + + self.seq_number = 0 + self.send_count = 0 + self.receive_count = 0 + self.min_time = 999999999 + self.max_time = 0.0 + self.total_time = 0.0 + + #-------------------------------------------------------------------------- - timeLeft = timeLeft - howLongInSelect - if timeLeft <= 0: + def print_start(self): + print("\nPYTHON-PING %s (%s): %d data bytes" % (self.destination, self.dest_ip, self.packet_size)) + + def print_unknown_host(self, e): + print("\nPYTHON-PING: Unknown host: %s (%s)\n" % (self.destination, e.args[1])) + sys.exit(-1) + + def print_success(self, delay, ip, packet_size, ip_header, icmp_header): + if ip == self.destination: + from_info = ip + else: + from_info = "%s (%s)" % (self.destination, ip) + + print("%d bytes from %s: icmp_seq=%d ttl=%d time=%.1f ms" % ( + packet_size, from_info, icmp_header["seq_number"], ip_header["ttl"], delay) + ) + #print("IP header: %r" % ip_header) + #print("ICMP header: %r" % icmp_header) + + def print_failed(self): + print("Request timed out.") + + def print_exit(self): + print("\n----%s PYTHON PING Statistics----" % (self.destination)) + + lost_count = self.send_count - self.receive_count + #print("%i packets lost" % lost_count) + lost_rate = float(lost_count) / self.send_count * 100.0 + + print("%d packets transmitted, %d packets received, %0.1f%% packet loss" % ( + self.send_count, self.receive_count, lost_rate + )) + + if self.receive_count > 0: + print("round-trip (ms) min/avg/max = %0.3f/%0.3f/%0.3f" % ( + self.min_time, self.total_time / self.receive_count, self.max_time + )) + + print("") + + #-------------------------------------------------------------------------- + + def signal_handler(self, signum, frame): + """ + Handle print_exit via signals + """ + self.print_exit() + print("\n(Terminated with signal %d)\n" % (signum)) + sys.exit(0) + + def setup_signal_handler(self): + signal.signal(signal.SIGINT, self.signal_handler) # Handle Ctrl-C + if hasattr(signal, "SIGBREAK"): + # Handle Ctrl-Break e.g. under Windows + signal.signal(signal.SIGBREAK, self.signal_handler) + + #-------------------------------------------------------------------------- + + def header2dict(self, names, struct_format, data): + """ unpack the raw received IP and ICMP header informations to a dict """ + unpacked_data = struct.unpack(struct_format, data) + return dict(zip(names, unpacked_data)) + + #-------------------------------------------------------------------------- + + def run(self, count=None, deadline=None): + """ + send and receive pings in a loop. Stop if count or until deadline. + """ + self.setup_signal_handler() + + while True: + delay = self.do() + + self.seq_number += 1 + if count and self.seq_number >= count: + break + if deadline and self.total_time >= deadline: + break + + if delay == None: + delay = 0 + + # Pause for the remainder of the MAX_SLEEP period (if applicable) + if (MAX_SLEEP > delay): + time.sleep((MAX_SLEEP - delay) / 1000.0) + + self.print_exit() + + def do(self): + """ + Send one ICMP ECHO_REQUEST and receive the response until self.timeout + """ + try: # One could use UDP here, but it's obscure + current_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp")) + except socket.error, (errno, msg): + if errno == 1: + # Operation not permitted - Add more information to traceback + etype, evalue, etb = sys.exc_info() + evalue = etype( + "%s - Note that ICMP messages can only be send from processes running as root." % evalue + ) + raise etype, evalue, etb + raise # raise the original error + + send_time = self.send_one_ping(current_socket) + if send_time == None: return + self.send_count += 1 + + receive_time, packet_size, ip, ip_header, icmp_header = self.receive_one_ping(current_socket) + current_socket.close() + + if receive_time: + self.receive_count += 1 + delay = (receive_time - send_time) * 1000.0 + self.total_time += delay + if self.min_time > delay: + self.min_time = delay + if self.max_time < delay: + self.max_time = delay + + self.print_success(delay, ip, packet_size, ip_header, icmp_header) + return delay + else: + self.print_failed() + + def send_one_ping(self, current_socket): + """ + Send one ICMP ECHO_REQUEST + """ + # Header is type (8), code (8), checksum (16), id (16), sequence (16) + checksum = 0 + + # Make a dummy header with a 0 checksum. + header = struct.pack( + "!BBHHH", ICMP_ECHO, 0, checksum, self.own_id, self.seq_number + ) + padBytes = [] + startVal = 0x42 + for i in range(startVal, startVal + (self.packet_size)): + padBytes += [(i & 0xff)] # Keep chars in the 0-255 range + data = bytes(padBytes) -def send_one_ping(my_socket, dest_addr, ID): - """ - Send one ping to the given >dest_addr<. - """ - dest_addr = socket.gethostbyname(dest_addr) + # Calculate the checksum on the data and the dummy header. + checksum = calculate_checksum(header + data) # Checksum is in network order - # Header is type (8), code (8), checksum (16), id (16), sequence (16) - my_checksum = 0 + # Now that we have the right checksum, we put that in. It's just easier + # to make up a new header than to stuff it into the dummy. + header = struct.pack( + "!BBHHH", ICMP_ECHO, 0, checksum, self.own_id, self.seq_number + ) - # Make a dummy heder with a 0 checksum. - header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1) - bytesInDouble = struct.calcsize("d") - data = (192 - bytesInDouble) * "Q" - data = struct.pack("d", default_timer()) + data + packet = header + data - # Calculate the checksum on the data and the dummy header. - my_checksum = checksum(header + data) + send_time = default_timer() - # Now that we have the right checksum, we put that in. It's just easier - # to make up a new header than to stuff it into the dummy. - header = struct.pack( - "bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1 - ) - packet = header + data - my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1 + try: + current_socket.sendto(packet, (self.destination, 1)) # Port number is irrelevant for ICMP + except socket.error as e: + print("General failure (%s)" % (e.args[1])) + current_socket.close() + return + return send_time -def do_one(dest_addr, timeout): - """ - Returns either the delay (in seconds) or none on timeout. - """ - icmp = socket.getprotobyname("icmp") - try: - my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) - except socket.error, (errno, msg): - if errno == 1: - # Operation not permitted - msg = msg + ( - " - Note that ICMP messages can only be sent from processes" - " running as root." - ) - raise socket.error(msg) - raise # raise the original error + def receive_one_ping(self, current_socket): + """ + Receive the ping from the socket. timeout = in ms + """ + timeout = self.timeout / 1000.0 - my_ID = os.getpid() & 0xFFFF + while True: # Loop while waiting for packet or timeout + select_start = default_timer() + inputready, outputready, exceptready = select.select([current_socket], [], [], timeout) + select_duration = (default_timer() - select_start) + if inputready == []: # timeout + return None, 0, 0, 0, 0 - send_one_ping(my_socket, dest_addr, my_ID) - delay = receive_one_ping(my_socket, my_ID, timeout) + receive_time = default_timer() - my_socket.close() - return delay + packet_data, address = current_socket.recvfrom(ICMP_MAX_RECV) + icmp_header = self.header2dict( + names=[ + "type", "code", "checksum", + "packet_id", "seq_number" + ], + struct_format="!BBHHH", + data=packet_data[20:28] + ) -def verbose_ping(dest_addr, timeout = 2, count = 4): - """ - Send >count< ping to >dest_addr< with the given >timeout< and display - the result. - """ - for i in xrange(count): - print "ping %s..." % dest_addr, - try: - delay = do_one(dest_addr, timeout) - except socket.gaierror, e: - print "failed. (socket error: '%s')" % e[1] - break + if icmp_header["packet_id"] == self.own_id: # Our packet + ip_header = self.header2dict( + names=[ + "version", "type", "length", + "id", "flags", "ttl", "protocol", + "checksum", "src_ip", "dest_ip" + ], + struct_format="!BBHHHBBHII", + data=packet_data[:20] + ) + packet_size = len(packet_data) - 28 + ip = socket.inet_ntoa(struct.pack("!I", ip_header["src_ip"])) + # XXX: Why not ip = address[0] ??? + return receive_time, packet_size, ip, ip_header, icmp_header - if delay == None: - print "failed. (timeout within %ssec.)" % timeout - else: - delay = delay * 1000 - print "get ping in %0.4fms" % delay - print + timeout = timeout - select_duration + if timeout <= 0: + return None, 0, 0, 0, 0 + + +def verbose_ping(hostname, timeout=1000, count=3, packet_size=55): + p = Ping(hostname, timeout, packet_size) + p.run(count) if __name__ == '__main__': - verbose_ping("heise.de") - verbose_ping("google.com") - verbose_ping("a-test-url-taht-is-not-available.com") - verbose_ping("192.168.1.1") + # FIXME: Add a real CLI + if len(sys.argv) == 1: + print "DEMO" + + # These should work: + verbose_ping("heise.de") + verbose_ping("google.com") + + # Inconsistent on Windows w/ ActivePython (Python 3.2 resolves correctly + # to the local host, but 2.7 tries to resolve to the local *gateway*) + verbose_ping("localhost") + + # Should fail with 'getaddrinfo print_failed': + verbose_ping("foobar_url.foobar") + + # Should fail (timeout), but it depends on the local network: + verbose_ping("192.168.255.254") + + # Should fails with 'The requested address is not valid in its context': + verbose_ping("0.0.0.0") + elif len(sys.argv) == 2: + verbose_ping(sys.argv[1]) + else: + print "Error: call ./ping.py domain.tld" diff --git a/ping_header_info.txt b/ping_header_info.txt new file mode 100644 index 0000000..371c729 --- /dev/null +++ b/ping_header_info.txt @@ -0,0 +1,50 @@ +IP header info from RFC791 + -> http://tools.ietf.org/html/rfc791) + +0 1 2 3 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +|Version| IHL |Type of Service| Total Length | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Identification |Flags| Fragment Offset | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Time to Live | Protocol | Header Checksum | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Source Address | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Destination Address | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Options | Padding | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +=========================================================================== +ICMP Echo / Echo Reply Message header info from RFC792 + -> http://tools.ietf.org/html/rfc792 + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... + +-+-+-+-+- + +=========================================================================== +ICMP parameter info: + -> http://www.iana.org/assignments/icmp-parameters/icmp-parameters.xml + +=========================================================================== +An example of ping's typical output: + +PING heise.de (193.99.144.80): 56 data bytes +64 bytes from 193.99.144.80: icmp_seq=0 ttl=240 time=127 ms +64 bytes from 193.99.144.80: icmp_seq=1 ttl=240 time=127 ms +64 bytes from 193.99.144.80: icmp_seq=2 ttl=240 time=126 ms +64 bytes from 193.99.144.80: icmp_seq=3 ttl=240 time=126 ms +64 bytes from 193.99.144.80: icmp_seq=4 ttl=240 time=127 ms + +----heise.de PING Statistics---- +5 packets transmitted, 5 packets received, 0.0% packet loss +round-trip (ms) min/avg/max/med = 126/127/127/127 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..a5ecc8c --- /dev/null +++ b/setup.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" + distutils setup + ~~~~~~~~~~~~~~~ + + :homepage: https://github.com/jedie/python-ping/ + :copyleft: 1989-2011 by the python-ping team, see AUTHORS for more details. + :license: GNU GPL v2, see LICENSE for more details. +""" + +import os +import subprocess +import sys +import time +import warnings + +from setuptools import setup, find_packages, Command + +PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__)) + + +#VERBOSE = True +VERBOSE = False + +def _error(msg): + if VERBOSE: + warnings.warn(msg) + return "" + +def get_version_from_git(): + try: + process = subprocess.Popen( + # %ct: committer date, UNIX timestamp + ["/usr/bin/git", "log", "--pretty=format:%ct-%h", "-1", "HEAD"], + shell=False, cwd=PACKAGE_ROOT, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + except Exception, err: + return _error("Can't get git hash: %s" % err) + + process.wait() + returncode = process.returncode + if returncode != 0: + return _error( + "Can't get git hash, returncode was: %r" + " - git stdout: %r" + " - git stderr: %r" + % (returncode, process.stdout.readline(), process.stderr.readline()) + ) + + output = process.stdout.readline().strip() + try: + raw_timestamp, hash = output.split("-", 1) + timestamp = int(raw_timestamp) + except Exception, err: + return _error("Error in git log output! Output was: %r" % output) + + try: + timestamp_formatted = time.strftime("%Y.%m.%d", time.gmtime(timestamp)) + except Exception, err: + return _error("can't convert %r to time string: %s" % (timestamp, err)) + + return "%s.%s" % (timestamp_formatted, hash) + + +# convert creole to ReSt on-the-fly, see also: +# https://code.google.com/p/python-creole/wiki/UseInSetup +try: + from creole.setup_utils import get_long_description +except ImportError: + if "register" in sys.argv or "sdist" in sys.argv or "--long-description" in sys.argv: + etype, evalue, etb = sys.exc_info() + evalue = etype("%s - Please install python-creole >= v0.8 - e.g.: pip install python-creole" % evalue) + raise etype, evalue, etb + long_description = None +else: + long_description = get_long_description(PACKAGE_ROOT) + + +def get_authors(): + authors = [] + try: + f = file(os.path.join(PACKAGE_ROOT, "AUTHORS"), "r") + for line in f: + if not line.strip().startswith("*"): + continue + if "--" in line: + line = line.split("--", 1)[0] + authors.append(line.strip(" *\r\n")) + f.close() + authors.sort() + except Exception, err: + authors = "[Error: %s]" % err + return authors + + +setup( + name='python-ping', + version=get_version_from_git(), + description='A pure python ICMP ping implementation using raw sockets.', + long_description=long_description, + author=get_authors(), + maintainer="Jens Diemer", + maintainer_email="python-ping@jensdiemer.de", + url='https://github.com/jedie/python-ping/', + keywords="ping icmp network latency", + packages=find_packages(), + include_package_data=True, # include package data under svn source control + zip_safe=False, + scripts=["ping.py"], + classifiers=[ + # http://pypi.python.org/pypi?%3Aaction=list_classifiers +# "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Networking :: Monitoring", + ], + test_suite="tests", +) diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..74d6431 --- /dev/null +++ b/tests.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" + python-ping unittests + ~~~~~~~~~~~~~~~~~~~~~ + + Note that ICMP messages can only be send from processes running as root. + So you must run this tests also as root, e.g.: + + .../python-ping$ sudo python tests.py + + :homepage: https://github.com/jedie/python-ping/ + :copyleft: 1989-2011 by the python-ping team, see AUTHORS for more details. + :license: GNU GPL v2, see LICENSE for more details. +""" + +import socket +import unittest + +from ping import Ping, is_valid_ip4_address, to_ip + + +class PingTest(Ping): + """ + Used in TestPythonPing for check if print methods are called. + This is also a way how to subclass Ping ;) + """ + def __init__(self, *args, **kwargs): + self.start_call_count = 0 + self.unknown_host_call_count = 0 + self.success_call_count = 0 + self.failed_call_count = 0 + self.exit_call_count = 0 + super(PingTest, self).__init__(*args, **kwargs) + + def print_start(self): + self.start_call_count += 1 + + def print_unknown_host(self, e): + self.unknown_host_call_count += 1 + + def print_success(self, delay, ip, packet_size, ip_header, icmp_header): + self.success_call_count += 1 + + def print_failed(self): + self.failed_call_count += 1 + + def print_exit(self): + self.exit_call_count += 1 + + +class TestPythonPing(unittest.TestCase): + def testIp4AddrPositives(self): + self.assertTrue(is_valid_ip4_address('0.0.0.0')) + self.assertTrue(is_valid_ip4_address('1.2.3.4')) + self.assertTrue(is_valid_ip4_address('12.34.56.78')) + self.assertTrue(is_valid_ip4_address('255.255.255.255')) + + def testIp4AddrNegatives(self): + self.assertFalse(is_valid_ip4_address('0.0.0.0.0')) + self.assertFalse(is_valid_ip4_address('1.2.3')) + self.assertFalse(is_valid_ip4_address('a2.34.56.78')) + self.assertFalse(is_valid_ip4_address('255.255.255.256')) + + def testDestAddr1(self): + self.assertTrue(is_valid_ip4_address(to_ip('www.wikipedia.org'))) + self.assertRaises(socket.gaierror, to_ip, ('www.doesntexist.tld')) + + def testDestAddr2(self): + self.assertTrue(to_ip('10.10.10.1')) + self.assertTrue(to_ip('10.10.010.01')) + self.assertTrue(to_ip('10.010.10.1')) + + def test_init_only(self): + p = PingTest("www.google.com") + self.assertEqual(p.start_call_count, 1) + self.assertEqual(p.unknown_host_call_count, 0) + self.assertEqual(p.success_call_count, 0) + self.assertEqual(p.failed_call_count, 0) + self.assertEqual(p.exit_call_count, 0) + + def test_do_one_ping(self): + p = PingTest("www.google.com") + p.do() + self.assertEqual(p.send_count, 1) + self.assertEqual(p.receive_count, 1) + + self.assertEqual(p.start_call_count, 1) + self.assertEqual(p.unknown_host_call_count, 0) + self.assertEqual(p.success_call_count, 1) + self.assertEqual(p.failed_call_count, 0) + self.assertEqual(p.exit_call_count, 0) + + def test_do_one_failed_ping(self): + p = PingTest("www.doesntexist.tld") + self.assertEqual(p.start_call_count, 0) + self.assertEqual(p.unknown_host_call_count, 1) + self.assertEqual(p.success_call_count, 0) + self.assertEqual(p.failed_call_count, 0) + self.assertEqual(p.exit_call_count, 0) + + def test_run_ping(self): + p = PingTest("www.google.com") + p.run(count=2) + self.assertEqual(p.send_count, 2) + self.assertEqual(p.receive_count, 2) + + self.assertEqual(p.start_call_count, 1) + self.assertEqual(p.unknown_host_call_count, 0) + self.assertEqual(p.success_call_count, 2) + self.assertEqual(p.failed_call_count, 0) + self.assertEqual(p.exit_call_count, 1) + + def test_run_failed_pings(self): + p = PingTest("www.google.com", timeout=0.01) + p.run(count=2) + self.assertEqual(p.send_count, 2) + self.assertEqual(p.receive_count, 0) + + self.assertEqual(p.start_call_count, 1) + self.assertEqual(p.unknown_host_call_count, 0) + self.assertEqual(p.success_call_count, 0) + self.assertEqual(p.failed_call_count, 2) + self.assertEqual(p.exit_call_count, 1) + + +if __name__ == '__main__': + unittest.main() +