diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..d1dcbb8 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,24 @@ +AUTHORS / CONTRIBUTORS: + + * Steve Clement -- https://github.com/SteveClement + * FliegendeWurst -- https://github.com/FliegendeWurst + * Cowles, Matthew Dixon -- ftp://ftp.visi.com/users/mdc/ping.py + * Diemer, Jens -- http://www.jensdiemer.de + * Esteban, Mendoza + * Falatic, Martin -- http://www.falatic.com + * Hallman, Chris -- http://cdhallman.blogspot.com + * incidence -- https://github.com/incidence + * jcborras -- https://github.com/jcborras + * Kolev, Georgi -- https://github.com/l4m3rx + * Notaras, George -- http://www.g-loaded.eu + * Poincheval, Jerome + * Sarkhel, Kunal -- https://github.com/techwizrd + * Stauffer, Samuel -- https://github.com/samuel + * Zach Ware + * zed -- https://github.com/zed + * Frassinelli, Francesco -- http://www.frafra.eu + * Estemendoza, Esteban -- https://github.com/estemendoza + * Auke Willem + * Tom Wilkie -- http://hithub.com/tomwilkie + * simudream -- https://github.com/simudream + * Ariana Giroux -- https://github.com/eclectickmedia diff --git a/ChangeLog.creole b/ChangeLog.creole new file mode 100644 index 0000000..2447ab3 --- /dev/null +++ b/ChangeLog.creole @@ -0,0 +1,202 @@ +== Revision history == + +==== November 4, 2016 ==== +---------------- + * Renaming MyStats class to avoid confusion + * Making myStats global variable so we can print stats when terminating with signal + * Fixing small bug in quiet_ping() preventing in from ever exiting. + +==== November 3, 2016 ==== +---------------- + * Converting quiet and verbose ping functions to generator so we can do infinite ping. + All the work was done by Ariana Giroux. + https://github.com/l4m3rx/python-ping/issues/14 + +==== November 3, 2016 ==== +---------------- + * Adding exception when sending packets to handle socket.error (Python2.x issue) + Thanks to Ariana Giroux https://github.com/l4m3rx/python-ping/issues/16 + * Fixing MyStats printing. + +==== November 2, 2016 ==== +---------------- + * Fix. Replacing print() with print('') to avoid python2.x printing "()" liens + * Introduce a basic CLI interface via Python's argparse by Ariana Giroux + https://github.com/l4m3rx/python-ping/pull/15/commits/0ea79021dfba8ce57ee967f0e923846f627aac71 + * Rename variables to avoid overwriting python building functions/objects. + ping.py: bytes -> _bytes + setup.py: hash -> mhash + icmp_messages.py: type -> itype + +==== November 2, 2016 ==== +---------------- + * Updating Authors + * Fixing up function names as suggested by EclectickMedia + https://github.com/l4m3rx/python-ping/issues/13 + +==== October 26, 2016 ==== +---------------- + * PEP8 Stuff + * Code clean up + * Remove revision history from ping.py + +==== October 25, 2016 ==== +---------------- + * New unittest (tests.py) thanks to EclectickMedia + * Switching .iteritems() to .items() in icmp_messages.py for better competability (thanks to Steve Clement) + * Small fixes + +==== October 24, 2016 ==== +---------------- + * Python 3 competability + * Fixing small bug (verbose ping printing) + +==== February 4, 2015 ==== +---------------- + * Fix statistics dump on unexpected exit + * Fixing a bug where we match the ICMP request as respons + - Fix based on samuel's work https://github.com/samuel/python-ping/commit/745c159dacce6afebb0f82cac8e9ed5bb2189491#diff-0ee81781c0132dc8f743df3a41b71918R128 + * Merging simudream's patch to fix python3 error handeling. + + +==== January 21, 2015 ==== +---------------- +* Set socket options to allow sending pings to broadcast address + - Based on Tom Wilkie's patch + https://github.com/tomwilkie/pyping/commit/7d017b03ee462fae72e2d0ff96a042bcba295564 + +==== August 6, 2014 ==== +-------------- +* Fixing an bug introduced with last changes :S + - Python on Windows dosn't have socket.inet_ntop + Adding try/except to handle AttributeError. + +==== June 30, 2014 ==== +------------- +* For problems/suggestions https://github.com/l4m3rx/python-ping/issues + +* Forgot to add stuff to this README :p + +==== June 29, 2014 ==== +------------- +* Merging parts of code from all the python-ping forks on github + * Litlle modifications by Auke Willem Oosterhoff: + - Added support for simultaneous pings on multiple hosts. + https://bitbucket.org/OrangeTux/python-ping/commits/d4aa720662995a57cf18fa6b8ea689e9d11d26c7/raw/ + + * Faster and cleaner checksum creation + Based on frfra's patch + https://github.com/alexlouden/python-ping/commit/b9fc3acb2c36ccc895d1f7ba7336b951dc033ce9 + + * Changing 'except' calls to work with 2.x and 3.x + Based on pferate's patch + https://github.com/pferate/python_ping/commit/4e761ea99582ac1699c7965d149ce16e6b62f0ac + +* Some small PIP8 stuff... + +* Merging stuff from/with https://github.com/estemendoza/python-ping + Both repos are in sync now. + +==== June 19, 2013 ==== +-------------- +* Added support for IPv6. Taken from implementation of Lars Strand. + +==== March 19, 2013 ==== +-------------- +* Fixing bug to prevent divide by 0 during run-time. + +==== January 26, 2012 ==== +---------------- +* Fixing BUG #4 - competability with python 2.x [tested with 2.7] +- Packet data building is different for 2.x and 3.x. +'cose of the string/bytes difference. +* Fixing BUG #10 - the multiple resolv issue. +- When pinging domain names insted of hosts (for exmaple google.com) +you can get different IP every time you try to resolv it, we should +resolv the host only once and stick to that IP. +* Fixing BUGs #3 #10 - Doing hostname resolv only once. +* Fixing BUG #14 - Removing all 'global' stuff. +- You should not use globul! Its bad for you...and its not thread safe! +* Fix - forcing the use of different times on linux/windows for +more accurate mesurments. (time.time - linux/ time.clock - windows) +* Adding quiet_ping function - This way we'll be able to use this script +as external lib. +* Changing default timeout to 3s. (1second is not enought) +* Switching data syze to packet size. It's easyer for the user to ignore the +fact that the packet headr is 8b and the datasize 64 will make packet with + +==== 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(). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fbb1510 --- /dev/null +++ b/LICENSE @@ -0,0 +1,35 @@ +A pure python implementation of netkit's ping.c + +Copyright (C) 2016, python-ping team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +To report a change or a bug, open an issue on +https://github.com/l4m3rx/python-ping or email one of the contributors. + +------------------------------------------------------------------------------- +PREVIOUS LICENSING STATEMENT. + +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. 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..d39be02 --- /dev/null +++ b/README.creole @@ -0,0 +1,84 @@ +== python-ping == +A pure python ping implementation using raw sockets. + * Compatible with Python2 & Python3 + * Note that ICMP messages can only be sent from processes running as root + (in Windows, you must run this script as 'Administrator'). + +== Original Version == +* [[ftp://ftp.visi.com/users/mdc/ping.py|Matthew Dixon Cowles]] +* copyleft 1989-2016 by the python-ping team, see [[https://github.com/l4m3rx/python-ping/blob/master/AUTHORS|AUTHORS]] for more details. +* license: GNU GPL v2, see [[https://github.com/l4m3rx/python-ping/blob/master/LICENSE|LICENSE]] for more details. + +=== Usage === + +{{{ +$ sudo python3 ping.py -h +usage: python-ping [-h] [-t TIMEOUT] [-c REQUEST_COUNT] [-i] [-I] + [-s PACKET_SIZE] [-T] + address + +A pure python implementation of the ping protocol. *REQUIRES ROOT* + +positional arguments: + address The address to attempt to ping. + +optional arguments: + -h, --help show this help message and exit + -t TIMEOUT, --timeout TIMEOUT + The maximum amount of time to wait until ping timeout. + -c REQUEST_COUNT, --request_count REQUEST_COUNT + The number of attempts to make. See --infinite to + attempt requests until stopped. + -i, --infinite Flag to continuously ping a host until stopped. + -I, --ipv6 Flag to use IPv6. + -s PACKET_SIZE, --packet_size PACKET_SIZE + Designate the amount of data to send per packet. + -T, --test_case Flag to run the default test case suite. +}}} + + +=== Using as lib === +{{{ +Python 2.7.11 (default, Mar 3 2016, 13:35:30) +[GCC 5.3.0] on linux2 +Type "help", "copyright", "credits" or "license" for more information. +>>> import ping +>>> ping.verbose_ping('google.com', timeout=3000, count=3, numDataBytes=1300) + +>>> list(ping.verbose_ping('google.com', timeout=3000, count=3, numDataBytes=1300)) + +PYTHON PING google.com (216.58.212.46): 1300 data bytes +72 bytes from 216.58.212.46: icmp_seq=0 ttl=59 time=4.42 ms +72 bytes from 216.58.212.46: icmp_seq=1 ttl=59 time=4.70 ms +72 bytes from 216.58.212.46: icmp_seq=2 ttl=59 time=4.44 ms +72 bytes from 216.58.212.46: icmp_seq=3 ttl=59 time=4.47 ms + +----216.58.212.46 PYTHON PING Statistics---- +4 packets transmitted, 4 packets received, 0.0% packet loss +round-trip (ms) min/avg/max = 4.4/4.5/4.7 + +[1, 2, 3, False] + + +>>> list(ping.quiet_ping('google.com', timeout=3000, count=3, numDataBytes=1300)) +[1, 2, 3, (4.508256912231445, 4.332065582275391, 4.399617513020833, 0.0)] +>>> list(ping.quiet_ping('google.com', timeout=3000, count=3, numDataBytes=1300))[-1] +(4.535675048828125, 4.359245300292969, 4.423936208089192, 0.0) + +}}} + +== TODOs == + * Make delay between sending packets as input parm. + +== contribute == + +[[http://help.github.com/fork-a-repo/|Fork this repo]] on [[https://github.com/l4m3rx/python-ping/|GitHub]] and [[http://help.github.com/send-pull-requests/|send pull requests]]. Thank you. + + +== Revision history == +[[https://github.com/l4m3rx/python-ping/blob/master/ChangeLog.creole|ChangeLog / Revision history]] + + +== Links == + +| Sourcecode at GitHub | https://github.com/l4m3rx/python-ping | diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/icmp_messages.py b/icmp_messages.py new file mode 100755 index 0000000..e687c07 --- /dev/null +++ b/icmp_messages.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + ICMP Control Messages + https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Control_messages + ICMP Types 0, 3, 4, 5, 8, 11, 12, 13, 14 from RFC792 + ICMP Types 9, 11 from RFC1256 +""" +ICMP_CONTROL_MESSAGE = \ + {0: {0: 'Echo reply', + }, + 3: {0: 'Destination network unreachable', + 1: 'Destination host unreachable', + 2: 'Destination protocol unreachable', + 3: 'Destination port unreachable', + 4: 'Fragmentation required, and DF flag set', + 5: 'Source route failed', + 6: 'Destination network unknown', + 7: 'Destination host unknown', + 8: 'Source host isolated', + 9: 'Network administratively prohibited', + 10: 'Host administratively prohibited', + 11: 'Network unreachable for TOS', + 12: 'Host unreachable for TOS', + 13: 'Communication administratively prohibited', + 14: 'Host Precedence Violation', + 15: 'Precedence cutoff in effect', + }, + 4: {0: 'Source quench', + }, + 5: {0: 'Redirect Datagram for the Network', + 1: 'Redirect Datagram for the Host', + 2: 'Redirect Datagram for the TOS & network', + 3: 'Redirect Datagram for the TOS & host', + }, + 8: {0: 'Echo request', + }, + 9: {0: 'Router Advertisement', + }, + 10:{0: 'Router discovery/selection/solicitation', + }, + 11:{0: 'TTL expired in transit', + 1: 'Fragment reassembly time exceeded', + }, + 12:{0: 'Pointer indicates the error', + 1: 'Missing a required option', + 2: 'Bad length', + }, + 13:{0: 'Timestamp', + }, + 14:{0: 'Timestamp reply', + }, + } + + +""" + ICMPv6 Control Messages + https://en.wikipedia.org/wiki/ICMPv6#Types_of_ICMPv6_messages + ICMPv6 Types 0-127 are Error Messages + ICMPv6 Types 128-255 are Informational Messages +""" +ICMPv6_CONTROL_MESSAGE = \ + {1: {0: 'no route to destination', + 1: 'communication with destination administratively prohibited', + 2: 'beyond scope of source address', + 3: 'address unreachable', + 4: 'port unreachable', + 5: 'source address failed ingress/egress policy', + 6: 'reject route to destination', + 7: 'Error in Source Routing Header', + }, + 2: {0: 'packet too big', + }, + 3: {0: 'hop limit exceeded in transit', + 1: 'fragment reassembly time exceeded', + }, + 4: {0: 'erroneous header field encountered', + 1: 'unrecognized Next Header type encountered', + 2: 'unrecognized IPv6 option encountered', + }, + } + +if __name__ == '__main__': + # Print all defined ICMP Control Messages + print("ICMP Control Messages") + print("Type\tCode:\tMessage") + for (itype, codes) in ICMP_CONTROL_MESSAGE.items(): + print("") + for (code, message) in codes.items(): + print("[%d]\t[%d]:\t%s" % (itype, code, message)) + print("") + + # Print all defined ICMPv6 Control Messages + print("ICMPv6 Control Messages") + print("Type\tCode:\tMessage") + for (itype, codes) in ICMPv6_CONTROL_MESSAGE.items(): + print("") + for (code, message) in codes.items(): + print("[%d]\t[%d]:\t%s" % (itype, code, message)) diff --git a/ping.py b/ping.py old mode 100644 new mode 100755 index 73400fa..7694a14 --- a/ping.py +++ b/ping.py @@ -1,11 +1,17 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- -""" - A pure python ping implementation using raw socket. +from __future__ import division, print_function +""" + A pure python ping implementation using raw sockets. - Note that ICMP messages can only be sent from processes running as root. + Compatibility: + OS: Linux, Windows, MacOSX + Python: 2.6 - 3.5 + Note that due to the usage of RAW sockets root/Administrator + privileges are requied. Derived from ping.c distributed in Linux's netkit. That code is copyright (c) 1989 by The Regents of the University of California. @@ -13,63 +19,34 @@ US Army Ballistic Research Laboratory in December, 1983 and placed in the public domain. They have my thanks. - 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(). + website: https://github.com/l4m3rx/python-ping - December 4, 2000 - Changed the struct.pack() calls to pack the checksum and ID as - unsigned. My thanks to Jerome Poincheval for the fix. +""" - Januari 27, 2015 - Changed receive response to not accept ICMP request messages. - It was possible to receive the very request that was sent. +# TODO Remove any calls to time.sleep +# This would enable extension into larger framework that aren't multi threaded. +import os +import sys +import time +import array +import socket +import struct +import select +import signal - Last commit info: - ~~~~~~~~~~~~~~~~~ - $LastChangedDate: $ - $Rev: $ - $Author: $ -""" +if __name__ == '__main__': + import argparse -import os, sys, socket, struct, select, time +try: + from _thread import get_ident +except ImportError: + def get_ident(): + return 0 if sys.platform == "win32": # On Windows, the best timer is time.clock() @@ -78,146 +55,612 @@ # 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_ECHO_IPV6 = 128 # Echo request (per RFC4443) +ICMP_ECHO_IPV6_REPLY = 129 # Echo request (per RFC4443) +ICMP_MAX_RECV = 2048 # Max size of incoming buffer + +MAX_SLEEP = 1000 + + +class MStats2(object): + + def __init__(self): + self._this_ip = '0.0.0.0' + self.reset() + + def reset(self): + self._timing_list = [] + self._packets_sent = 0 + self._packets_rcvd = 0 + + self._reset_statistics() + + @property + def thisIP(self): + return self._this_ip + + @thisIP.setter + def thisIP(self, value): + self._this_ip = value + + @property + def pktsSent(self): + return self._packets_sent + + @property + def pktsRcvd(self): + return self._packets_rcvd + + @property + def pktsLost(self): + return self._packets_sent - self._packets_rcvd + + @property + def minTime(self): + return min(self._timing_list) if self._timing_list else None + + @property + def maxTime(self): + return max(self._timing_list) if self._timing_list else None + + @property + def totTime(self): + if self._total_time is None: + self._total_time = sum(self._timing_list) + return self._total_time + + def _get_mean_time(self): + if self._mean_time is None: + if len(self._timing_list) > 0: + self._mean_time = self.totTime / len(self._timing_list) + return self._mean_time + mean_time = property(_get_mean_time) + avrgTime = property(_get_mean_time) + + @property + def median_time(self): + if self._median_time is None: + self._median_time = self._calc_median_time() + return self._median_time + + @property + def pstdev_time(self): + """Returns the 'Population Standard Deviation' of the set.""" + if self._pstdev_time is None: + self._pstdev_time = self._calc_pstdev_time() + return self._pstdev_time + + @property + def fracLoss(self): + if self._frac_loss is None: + if self.pktsSent > 0: + self._frac_loss = self.pktsLost / self.pktsSent + return self._frac_loss + + def packet_sent(self, n=1): + self._packets_sent += n + + def packet_received(self, n=1): + self._packets_rcvd += n + + def record_time(self, value): + self._timing_list.append(value) + self._reset_statistics() + + def _reset_statistics(self): + self._total_time = None + self._mean_time = None + self._median_time = None + self._pstdev_time = None + self._frac_loss = None + + def _calc_median_time(self): + n = len(self._timing_list) + if n == 0: + return None + if n & 1 == 1: # Odd number of samples? Return the middle. + return sorted(self._timing_list)[n // 2] + # Even number of samples? Return the mean of the two middle samples. + else: + halfn = n // 2 + return sum(sorted(self._timing_list)[halfn:halfn + 2]) / 2 + def _calc_sum_square_time(self): + mean = self.mean_time + return sum(((t - mean)**2 for t in self._timing_list)) -def 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 - """ - sum = 0 - countTo = (len(source_string)/2)*2 - count = 0 - while count> 16) + (sum & 0xffff) - sum = sum + (sum >> 16) - answer = ~sum - answer = answer & 0xffff - - # Swap bytes. Bugger me if I know why. - answer = answer >> 8 | (answer << 8 & 0xff00) + def _calc_pstdev_time(self): + pvar = self._calc_sum_square_time() / len(self._timing_list) + return pvar**0.5 - return answer + +# Used as 'global' variale so we can print +# stats when exiting by signal +myStats = MStats2() -def receive_one_ping(my_socket, ID, timeout): +def _checksum(source_string): """ - receive the ping from the socket. + 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 """ - timeLeft = timeout - while True: - startedSelect = default_timer() - whatReady = select.select([my_socket], [], [], timeLeft) - howLongInSelect = (default_timer() - startedSelect) - if whatReady[0] == []: # Timeout - return + if (len(source_string) % 2): + source_string += "\x00" + converted = array.array("H", source_string) + if sys.byteorder == "big": + converted.bytewap() + val = sum(converted) - timeReceived = default_timer() - recPacket, addr = my_socket.recvfrom(1024) - icmpHeader = recPacket[20:28] - type, code, checksum, packetID, sequence = struct.unpack( - "bbHHh", icmpHeader - ) - # Filters out the echo request itself. - # This can be tested by pinging 127.0.0.1 - # You'll see your own request - if type != 8 and packetID == ID: - bytesInDouble = struct.calcsize("d") - timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0] - return timeReceived - timeSent + val &= 0xffffffff # Truncate val to 32 bits (a variance from ping.c, which + # uses signed ints, but overflow is unlikely in ping) - timeLeft = timeLeft - howLongInSelect - if timeLeft <= 0: - return + val = (val >> 16) + (val & 0xffff) # Add high 16 bits to low 16 bits + val += (val >> 16) # Add carry from above (if any) + answer = ~val & 0xffff # Invert and truncate to 16 bits + answer = socket.htons(answer) + + return answer -def send_one_ping(my_socket, dest_addr, ID): +def single_ping(destIP, hostname, timeout, mySeqNumber, numDataBytes, + myStats=None, ipv6=False, verbose=True, sourceIP=None): + """ + Returns either the delay (in ms) or None on timeout. """ - Send one ping to the given >dest_addr<. + delay = None + + if ipv6: + try: # One could use UDP here, but it's obscure + mySocket = socket.socket(socket.AF_INET6, socket.SOCK_RAW, + socket.getprotobyname("ipv6-icmp")) + if sourceIP is not None: + mySocket.bind((sourceIP, 0)) + mySocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + mySocket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_RECVHOPLIMIT, + 1) + except OSError as e: + if verbose: + print("failed. (socket error: '%s')" % str(e)) + print('Note that python-ping uses RAW sockets' + 'and requiers root rights.') + raise # raise the original error + else: + + try: # One could use UDP here, but it's obscure + mySocket = socket.socket(socket.AF_INET, socket.SOCK_RAW, + socket.getprotobyname("icmp")) + if sourceIP is not None: + mySocket.bind((sourceIP, 0)) + mySocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + except OSError as e: + if verbose: + print("failed. (socket error: '%s')" % str(e)) + print('Note that python-ping uses RAW sockets' + 'and requires root rights.') + raise # raise the original error + + my_ID = (os.getpid() ^ get_ident()) & 0xFFFF + + sentTime = _send(mySocket, destIP, my_ID, mySeqNumber, numDataBytes, ipv6, + verbose) + if sentTime is None: + mySocket.close() + return delay, (None,) + + if myStats is not None: + myStats.packet_sent() + + recvTime, dataSize, iphSrcIP, icmpSeqNumber, iphTTL \ + = _receive(mySocket, my_ID, timeout, ipv6) + + mySocket.close() + + if recvTime: + delay = (recvTime - sentTime) * 1000 + if ipv6: + host_addr = hostname + else: + try: + host_addr = socket.inet_ntop(socket.AF_INET, struct.pack( + "!I", iphSrcIP)) + except AttributeError: + # Python on windows dosn't have inet_ntop. + host_addr = hostname + + if verbose: + print("%d bytes from %s: icmp_seq=%d ttl=%d time=%.2f ms" % ( + dataSize, host_addr, icmpSeqNumber, iphTTL, delay) + ) + + if myStats is not None: + assert isinstance(myStats, MStats2) + myStats.packet_received() + myStats.record_time(delay) + else: + delay = None + if verbose: + print("Request timed out.") + + return delay, (recvTime, dataSize, iphSrcIP, icmpSeqNumber, iphTTL) + + +def _send(mySocket, destIP, myID, mySeqNumber, numDataBytes, ipv6=False, + verbose=True): """ - dest_addr = socket.gethostbyname(dest_addr) + Send one ping to the given >destIP<. + """ + # destIP = socket.gethostbyname(destIP) # Header is type (8), code (8), checksum (16), id (16), sequence (16) - my_checksum = 0 + # (numDataBytes - 8) - Remove header size from packet size + myChecksum = 0 # 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 + if ipv6: + header = struct.pack( + "!BbHHh", ICMP_ECHO_IPV6, 0, myChecksum, myID, mySeqNumber + ) + else: + header = struct.pack( + "!BBHHH", ICMP_ECHO, 0, myChecksum, myID, mySeqNumber + ) + + padBytes = [] + startVal = 0x42 + # 'cose of the string/byte changes in python 2/3 we have + # to build the data differnely for different version + # or it will make packets with unexpected size. + if sys.version[:1] == '2': + _bytes = struct.calcsize("d") + data = ((numDataBytes - 8) - _bytes) * "Q" + data = struct.pack("d", default_timer()) + data + else: + for i in range(startVal, startVal + (numDataBytes - 8)): + padBytes += [(i & 0xff)] # Keep chars in the 0-255 range + # data = bytes(padBytes) + data = bytearray(padBytes) # Calculate the checksum on the data and the dummy header. - my_checksum = checksum(header + data) + myChecksum = _checksum(header + data) # Checksum is in network order # 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 - ) + if ipv6: + header = struct.pack( + "!BbHHh", ICMP_ECHO_IPV6, 0, myChecksum, myID, mySeqNumber + ) + else: + header = struct.pack( + "!BBHHH", ICMP_ECHO, 0, myChecksum, myID, mySeqNumber + ) + packet = header + data - my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1 + + sendTime = default_timer() + + try: + mySocket.sendto(packet, (destIP, 58)) # Port number is irrelevant + except OSError as e: + if verbose: + print("General failure (%s)" % str(e)) + return + except socket.error as e: + if verbose: + print("General failure (%s)" % str(e)) + return + + return sendTime -def do_one(dest_addr, timeout): +def _receive(mySocket, myID, timeout, ipv6=False): """ - Returns either the delay (in seconds) or none on timeout. + Receive the ping from the socket. Timeout = in ms """ - 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 + timeLeft = timeout / 1000 + + while True: # Loop while waiting for packet or timeout + startedSelect = default_timer() + whatReady = select.select([mySocket], [], [], timeLeft) + howLongInSelect = (default_timer() - startedSelect) + if whatReady[0] == []: # Timeout + return None, 0, 0, 0, 0 + + timeReceived = default_timer() + + iphSrcIP = 0 + iphDestIP = 0 + if ipv6: + recPacket, ancdata, flags, addr = mySocket.recvmsg(ICMP_MAX_RECV) + iphTTL = 0 + if len(ancdata) == 1: + cmsg_level, cmsg_type, cmsg_data = ancdata[0] + a = array.array("i") + a.frombytes(cmsg_data) + iphTTL = a[0] + print("ttl: " + str(iphTTL)) + else: + recPacket, addr = mySocket.recvfrom(ICMP_MAX_RECV) + ipHeader = recPacket[:20] + iphVersion, iphTypeOfSvc, iphLength, iphID, iphFlags, iphTTL, \ + iphProtocol, iphChecksum, iphSrcIP, iphDestIP = struct.unpack( + "!BBHHHBBHII", ipHeader) + + if ipv6: + icmpHeader = recPacket[0:8] + else: + icmpHeader = recPacket[20:28] + + icmpType, icmpCode, icmpChecksum, icmpPacketID, icmpSeqNumber \ + = struct.unpack("!BBHHH", icmpHeader) + + # Match only the packets we care about + if (icmpType != 8) and (icmpPacketID == myID): + dataSize = len(recPacket) - 28 + return timeReceived, (dataSize + 8), iphSrcIP, icmpSeqNumber, \ + iphTTL + + timeLeft = timeLeft - howLongInSelect + if timeLeft <= 0: + return None, 0, 0, 0, 0 + + +def _dump_stats(myStats): + """ + Show stats when pings are done + """ + print("\n----%s PYTHON PING Statistics----" % (myStats.thisIP)) + + print("%d packets transmitted, %d packets received, %0.1f%% packet loss" + % (myStats.pktsSent, myStats.pktsRcvd, 100.0 * myStats.fracLoss)) - my_ID = os.getpid() & 0xFFFF + if myStats.pktsRcvd > 0: + print("round-trip (ms) min/avg/max = %0.1f/%0.1f/%0.1f" % ( + myStats.minTime, myStats.avrgTime, myStats.maxTime + )) + print(' median/pstddev = %0.2f/%0.2f' % ( + myStats.median_time, myStats.pstdev_time + )) - send_one_ping(my_socket, dest_addr, my_ID) - delay = receive_one_ping(my_socket, my_ID, timeout) + print('') + return - my_socket.close() - return delay +def _signal_handler(signum, frame): + """ Handle exit via signals """ + global myStats + _dump_stats(myStats) + print("\n(Terminated with signal %d)\n" % (signum)) + sys.exit(0) -def verbose_ping(dest_addr, timeout = 2, count = 4): + +def _pathfind_ping(destIP, hostname, timeout, mySeqNumber, numDataBytes, + ipv6=None, sourceIP=None): + single_ping(destIP, hostname, timeout, + mySeqNumber, numDataBytes, ipv6=ipv6, verbose=False, + sourceIP=sourceIP) + time.sleep(0.5) + + +def verbose_ping(hostname, timeout=3000, count=3, + numDataBytes=64, path_finder=False, ipv6=False, + sourceIP=None): """ - Send >count< ping to >dest_addr< with the given >timeout< and display + Send >count< ping to >destIP< with the given >timeout< and display the result. + + To continuously attempt ping requests, set >count< to None. + + To consume the generator, use the following syntax: + >>> import ping + >>> for return_val in ping.verbose_ping('google.ca'): + pass # COLLECT YIELDS AND PERFORM LOGIC. + + Alternatively, you can consume the generator by using list comprehension: + >>> import ping + >>> consume = list(ping.verbose_ping('google.ca')) + + Via the same syntax, you can successfully get the exit code via: + >>> import ping + >>> consume = list(ping.verbose_ping('google.ca')) + >>> exit_code = consume[:-1] # The last yield is the exit code. + >>> sys.exit(exit_code) """ - 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] + + global myStats + + # Handle Ctrl+C + signal.signal(signal.SIGINT, _signal_handler) + if hasattr(signal, "SIGBREAK"): # Handle Ctrl-Break /Windows/ + signal.signal(signal.SIGBREAK, _signal_handler) + + myStats = MStats2() # Reset the stats + mySeqNumber = 0 # Starting value + + try: + if ipv6: + info = socket.getaddrinfo(hostname, None)[0] + destIP = info[4][0] + else: + destIP = socket.gethostbyname(hostname) + print("\nPYTHON PING %s (%s): %d data bytes" % (hostname, destIP, + numDataBytes)) + except socket.gaierror as e: + print("\nPYTHON PING: Unknown host: %s (%s)" % (hostname, str(e))) + print('') + return + + myStats.thisIP = destIP + + # This will send packet that we don't care about 0.5 seconds before it + # starts actually pinging. This is needed in big MAN/LAN networks where + # you sometimes loose the first packet. (while the switches find the way) + if path_finder: + print("PYTHON PING %s (%s): Sending pathfinder ping" % + (hostname, destIP)) + _pathfind_ping(destIP, hostname, timeout, + mySeqNumber, numDataBytes, ipv6=ipv6, sourceIP=sourceIP) + print() + + i = 0 + while 1: + delay = single_ping(destIP, hostname, timeout, mySeqNumber, + numDataBytes, ipv6=ipv6, myStats=myStats, + sourceIP=sourceIP) + delay = 0 if delay is None else delay[0] + + mySeqNumber += 1 + + # Pause for the remainder of the MAX_SLEEP period (if applicable) + if (MAX_SLEEP > delay): + time.sleep((MAX_SLEEP - delay) / 1000) + + if count is not None and i < count: + i += 1 + yield myStats.pktsRcvd + elif count is None: + yield myStats.pktsRcvd + elif count is not None and i >= count: break - if delay == None: - print "failed. (timeout within %ssec.)" % timeout + _dump_stats(myStats) + # 0 if we receive at least one packet + # 1 if we don't receive any packets + yield not myStats.pktsRcvd + + +def quiet_ping(hostname, timeout=3000, count=3, advanced_statistics=False, + numDataBytes=64, path_finder=False, ipv6=False, sourceIP=None): + """ Same as verbose_ping, but the results are yielded as a tuple """ + myStats = MStats2() # Reset the stats + mySeqNumber = 0 # Starting value + + try: + if ipv6: + info = socket.getaddrinfo(hostname, None)[0] + destIP = info[4][0] else: - delay = delay * 1000 - print "get ping in %0.4fms" % delay - print + destIP = socket.gethostbyname(hostname) + except socket.gaierror: + yield False + return + + myStats.thisIP = destIP + + # This will send packet that we don't care about 0.5 seconds before it + # starts actually pinging. This is needed in big MAN/LAN networks where + # you sometimes loose the first packet. (while the switches find the way) + if path_finder: + _pathfind_ping(destIP, hostname, timeout, + mySeqNumber, numDataBytes, ipv6=ipv6, sourceIP=sourceIP) + + i = 1 + while 1: + delay = single_ping(destIP, hostname, timeout, mySeqNumber, + numDataBytes, ipv6=ipv6, myStats=myStats, + verbose=False, sourceIP=sourceIP) + delay = 0 if delay[0] is None else delay[0] + + mySeqNumber += 1 + # Pause for the remainder of the MAX_SLEEP period (if applicable) + if (MAX_SLEEP > delay): + time.sleep((MAX_SLEEP - delay) / 1000) + + yield myStats.pktsSent + if count is not None and i < count: + i += 1 + elif count is not None and i >= count: + break + elif count is not None: + yield myStats.pktsSent + + if advanced_statistics: + # return tuple(max_rtt, min_rtt, avrg_rtt, percent_lost, + # median, pop.std.dev) + yield myStats.maxTime, myStats.minTime, myStats.avrgTime, + myStats.fracLoss, myStats.median_time, myStats.pstdev_time + else: + # return tuple(max_rtt, min_rtt, avrg_rtt, percent_lost) + yield myStats.maxTime, myStats.minTime, myStats.avrgTime, + myStats.fracLoss 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 (mostly fixed) + if sys.argv.count('-T') or sys.argv.count('--test_case'): + print('Running PYTHON PING test case.') + # These should work: + for val in verbose_ping("127.0.0.1"): + pass + for val in verbose_ping("8.8.8.8"): + pass + for val in verbose_ping("heise.de"): + pass + for val in verbose_ping("google.com"): + pass + + # Inconsistent on Windows w/ ActivePython (Python 3.2 resolves + # correctly to the local host, but 2.7 tries to resolve to the local + # *gateway*) + for val in verbose_ping("localhost"): + pass + + # Should fail with 'getaddrinfo failed': + for val in verbose_ping("foobar_url.fooobar"): + pass + + # Should fail (timeout), but it depends on the local network: + for val in verbose_ping("192.168.255.254"): + pass + + # Should fails with 'The requested address is not valid in its context' + for val in verbose_ping("0.0.0.0"): + pass + + exit() + + parser = argparse.ArgumentParser(prog='python-ping', + description='A pure python implementation\ + of the ping protocol. *REQUIRES ROOT*') + parser.add_argument('address', help='The address to attempt to ping.') + + parser.add_argument('-t', '--timeout', help='The maximum amount of time to\ + wait until ping timeout.', type=int, default=3000) + + parser.add_argument('-c', '--request_count', help='The number of attempts \ + to make. See --infinite to attempt requests until \ + stopped.', type=int, default=3) + + parser.add_argument('-i', '--infinite', help='Flag to continuously ping \ + a host until stopped.', action='store_true') + + parser.add_argument('-I', '--ipv6', action='store_true', help='Flag to \ + use IPv6.') + + parser.add_argument('-s', '--packet_size', type=int, help='Designate the\ + amount of data to send per packet.', default=64) + + parser.add_argument('-T', '--test_case', action='store_true', help='Flag \ + to run the default test case suite.') + + parser.add_argument('-S', '--source_address', help='Source address from which \ + ICMP Echo packets will be sent.') + + parsed = parser.parse_args() + + if parsed.infinite: + sys.exit(list(verbose_ping(parsed.address, parsed.timeout, + None, parsed.packet_size, + ipv6=parsed.ipv6, + sourceIP=parsed.source_address))[:-1]) + + else: + sys.exit(list(verbose_ping(parsed.address, parsed.timeout, + parsed.request_count, parsed.packet_size, + ipv6=parsed.ipv6, + sourceIP=parsed.source_address))[:-1]) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..02cc4eb --- /dev/null +++ b/setup.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" + distutils setup + ~~~~~~~~~~~~~~~ + + :homepage: https://github.com/l4m3rx/python-ping/ + :copyleft: 1989-2016 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 as 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, mhash = output.split("-", 1) + timestamp = int(raw_timestamp) + except Exception as 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 as err: + return _error("can't convert %r to time string: %s" % (timestamp, err)) + + return "%s.%s" % (timestamp_formatted, mhash) + + +# 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).with_traceback(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 as err: + authors = "[Error: %s]" % err + return authors + + +setup( + name='python-ping', +# version=get_version_from_git(), + version='25102016', + description='A pure python ICMP ping implementation using raw sockets.', + long_description=long_description, + author=get_authors(), + maintainer="Georgi Kolev", + maintainer_email="georgi.kolev@gmail.com", + url='https://github.com/l4m3rx/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", +)