PageRenderTime 25ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/ping.py

https://gitlab.com/rshipp/python-ping
Python | 364 lines | 343 code | 2 blank | 19 comment | 3 complexity | 761e305fb20298ac8a44cd5c0dc02b85 MD5 | raw file
  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. """
  4. A pure python ping implementation using raw sockets.
  5. Note that ICMP messages can only be send from processes running as root
  6. (in Windows, you must run this script as 'Administrator').
  7. Bugs are naturally mine. I'd be glad to hear about them. There are
  8. certainly word - size dependencies here.
  9. :homepage: https://github.com/jedie/python-ping/
  10. :copyleft: 1989-2011 by the python-ping team, see AUTHORS for more details.
  11. :license: GNU GPL v2, see LICENSE for more details.
  12. """
  13. import array
  14. import os
  15. import select
  16. import signal
  17. import socket
  18. import struct
  19. import sys
  20. import time
  21. if sys.platform.startswith("win32"):
  22. # On Windows, the best timer is time.clock()
  23. default_timer = time.clock
  24. else:
  25. # On most other platforms the best timer is time.time()
  26. default_timer = time.time
  27. # ICMP parameters
  28. ICMP_ECHOREPLY = 0 # Echo reply (per RFC792)
  29. ICMP_ECHO = 8 # Echo request (per RFC792)
  30. ICMP_MAX_RECV = 2048 # Max size of incoming buffer
  31. MAX_SLEEP = 1000
  32. def calculate_checksum(source_string):
  33. """
  34. A port of the functionality of in_cksum() from ping.c
  35. Ideally this would act on the string as a series of 16-bit ints (host
  36. packed), but this works.
  37. Network data is big-endian, hosts are typically little-endian
  38. """
  39. if len(source_string)%2:
  40. source_string += "\x00"
  41. converted = array.array("H", source_string)
  42. if sys.byteorder == "big":
  43. converted.byteswap()
  44. val = sum(converted)
  45. val &= 0xffffffff # Truncate val to 32 bits (a variance from ping.c, which
  46. # uses signed ints, but overflow is unlikely in ping)
  47. val = (val >> 16) + (val & 0xffff) # Add high 16 bits to low 16 bits
  48. val += (val >> 16) # Add carry from above (if any)
  49. answer = ~val & 0xffff # Invert and truncate to 16 bits
  50. answer = socket.htons(answer)
  51. return answer
  52. def is_valid_ip4_address(addr):
  53. parts = addr.split(".")
  54. if not len(parts) == 4:
  55. return False
  56. for part in parts:
  57. try:
  58. number = int(part)
  59. except ValueError:
  60. return False
  61. if number > 255:
  62. return False
  63. return True
  64. def to_ip(addr):
  65. if is_valid_ip4_address(addr):
  66. return addr
  67. return socket.gethostbyname(addr)
  68. class Ping(object):
  69. def __init__(self, destination, timeout=1000, packet_size=55, own_id=None):
  70. self.destination = destination
  71. self.timeout = timeout
  72. self.packet_size = packet_size
  73. if own_id is None:
  74. self.own_id = os.getpid() & 0xFFFF
  75. else:
  76. self.own_id = own_id
  77. try:
  78. # FIXME: Use destination only for display this line here? see: https://github.com/jedie/python-ping/issues/3
  79. self.dest_ip = to_ip(self.destination)
  80. except socket.gaierror as e:
  81. self.print_unknown_host(e)
  82. self.seq_number = 0
  83. self.send_count = 0
  84. self.receive_count = 0
  85. self.min_time = 999999999
  86. self.max_time = 0.0
  87. self.total_time = 0.0
  88. #--------------------------------------------------------------------------
  89. def print_start(self):
  90. print("\nPYTHON-PING %s (%s): %d data bytes" % (self.destination, self.dest_ip, self.packet_size))
  91. def print_unknown_host(self, e):
  92. print("\nPYTHON-PING: Unknown host: %s (%s)\n" % (self.destination, e.args[1]))
  93. sys.exit(-1)
  94. def print_success(self, delay, ip, packet_size, ip_header, icmp_header):
  95. if ip == self.destination:
  96. from_info = ip
  97. else:
  98. from_info = "%s (%s)" % (self.destination, ip)
  99. print("%d bytes from %s: icmp_seq=%d ttl=%d time=%.1f ms" % (
  100. packet_size, from_info, icmp_header["seq_number"], ip_header["ttl"], delay)
  101. )
  102. #print("IP header: %r" % ip_header)
  103. #print("ICMP header: %r" % icmp_header)
  104. def print_failed(self):
  105. print("Request timed out.")
  106. def print_exit(self):
  107. print("\n----%s PYTHON PING Statistics----" % (self.destination))
  108. lost_count = self.send_count - self.receive_count
  109. #print("%i packets lost" % lost_count)
  110. lost_rate = float(lost_count) / self.send_count * 100.0
  111. print("%d packets transmitted, %d packets received, %0.1f%% packet loss" % (
  112. self.send_count, self.receive_count, lost_rate
  113. ))
  114. if self.receive_count > 0:
  115. print("round-trip (ms) min/avg/max = %0.3f/%0.3f/%0.3f" % (
  116. self.min_time, self.total_time / self.receive_count, self.max_time
  117. ))
  118. print("")
  119. #--------------------------------------------------------------------------
  120. def signal_handler(self, signum, frame):
  121. """
  122. Handle print_exit via signals
  123. """
  124. self.print_exit()
  125. print("\n(Terminated with signal %d)\n" % (signum))
  126. sys.exit(0)
  127. def setup_signal_handler(self):
  128. signal.signal(signal.SIGINT, self.signal_handler) # Handle Ctrl-C
  129. if hasattr(signal, "SIGBREAK"):
  130. # Handle Ctrl-Break e.g. under Windows
  131. signal.signal(signal.SIGBREAK, self.signal_handler)
  132. #--------------------------------------------------------------------------
  133. def header2dict(self, names, struct_format, data):
  134. """ unpack the raw received IP and ICMP header informations to a dict """
  135. unpacked_data = struct.unpack(struct_format, data)
  136. return dict(zip(names, unpacked_data))
  137. #--------------------------------------------------------------------------
  138. def run(self, count=None, deadline=None):
  139. """
  140. send and receive pings in a loop. Stop if count or until deadline.
  141. """
  142. self.setup_signal_handler()
  143. while True:
  144. delay = self.do()
  145. self.seq_number += 1
  146. if count and self.seq_number >= count:
  147. break
  148. if deadline and self.total_time >= deadline:
  149. break
  150. if delay == None:
  151. delay = 0
  152. # Pause for the remainder of the MAX_SLEEP period (if applicable)
  153. if (MAX_SLEEP > delay):
  154. time.sleep((MAX_SLEEP - delay) / 1000.0)
  155. self.print_exit()
  156. def do(self):
  157. """
  158. Send one ICMP ECHO_REQUEST and receive the response until self.timeout
  159. Returns a dictionary containing response information if a
  160. response is recieved, or None otherwise.
  161. """
  162. try: # One could use UDP here, but it's obscure
  163. current_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
  164. except socket.error, (errno, msg):
  165. if errno == 1:
  166. # Operation not permitted - Add more information to traceback
  167. etype, evalue, etb = sys.exc_info()
  168. evalue = etype(
  169. "%s - Note that ICMP messages can only be send from processes running as root." % evalue
  170. )
  171. raise etype, evalue, etb
  172. raise # raise the original error
  173. send_time = self.send_one_ping(current_socket)
  174. if send_time == None:
  175. return None
  176. self.send_count += 1
  177. receive_time, packet_size, ip, ip_header, icmp_header = self.receive_one_ping(current_socket)
  178. current_socket.close()
  179. if receive_time:
  180. self.receive_count += 1
  181. delay = (receive_time - send_time) * 1000.0
  182. self.total_time += delay
  183. if self.min_time > delay:
  184. self.min_time = delay
  185. if self.max_time < delay:
  186. self.max_time = delay
  187. response = {'delay': delay,
  188. 'ip': ip,
  189. 'packet_size': packet_size,
  190. 'ip_header': ip_header,
  191. 'icmp_header': icmp_header,
  192. }
  193. return response
  194. else:
  195. return None
  196. def send_one_ping(self, current_socket):
  197. """
  198. Send one ICMP ECHO_REQUEST
  199. """
  200. # Header is type (8), code (8), checksum (16), id (16), sequence (16)
  201. checksum = 0
  202. # Make a dummy header with a 0 checksum.
  203. header = struct.pack(
  204. "!BBHHH", ICMP_ECHO, 0, checksum, self.own_id, self.seq_number
  205. )
  206. padBytes = []
  207. startVal = 0x42
  208. for i in range(startVal, startVal + (self.packet_size)):
  209. padBytes += [(i & 0xff)] # Keep chars in the 0-255 range
  210. data = bytes(padBytes)
  211. # Calculate the checksum on the data and the dummy header.
  212. checksum = calculate_checksum(header + data) # Checksum is in network order
  213. # Now that we have the right checksum, we put that in. It's just easier
  214. # to make up a new header than to stuff it into the dummy.
  215. header = struct.pack(
  216. "!BBHHH", ICMP_ECHO, 0, checksum, self.own_id, self.seq_number
  217. )
  218. packet = header + data
  219. send_time = default_timer()
  220. try:
  221. current_socket.sendto(packet, (self.destination, 1)) # Port number is irrelevant for ICMP
  222. except socket.error as e:
  223. print("General failure (%s)" % (e.args[1]))
  224. current_socket.close()
  225. return
  226. return send_time
  227. def receive_one_ping(self, current_socket):
  228. """
  229. Receive the ping from the socket. timeout = in ms
  230. """
  231. timeout = self.timeout / 1000.0
  232. while True: # Loop while waiting for packet or timeout
  233. select_start = default_timer()
  234. inputready, outputready, exceptready = select.select([current_socket], [], [], timeout)
  235. select_duration = (default_timer() - select_start)
  236. if inputready == []: # timeout
  237. return None, 0, 0, 0, 0
  238. receive_time = default_timer()
  239. packet_data, address = current_socket.recvfrom(ICMP_MAX_RECV)
  240. icmp_header = self.header2dict(
  241. names=[
  242. "type", "code", "checksum",
  243. "packet_id", "seq_number"
  244. ],
  245. struct_format="!BBHHH",
  246. data=packet_data[20:28]
  247. )
  248. if icmp_header["packet_id"] == self.own_id: # Our packet
  249. ip_header = self.header2dict(
  250. names=[
  251. "version", "type", "length",
  252. "id", "flags", "ttl", "protocol",
  253. "checksum", "src_ip", "dest_ip"
  254. ],
  255. struct_format="!BBHHHBBHII",
  256. data=packet_data[:20]
  257. )
  258. packet_size = len(packet_data) - 28
  259. ip = socket.inet_ntoa(struct.pack("!I", ip_header["src_ip"]))
  260. # XXX: Why not ip = address[0] ???
  261. return receive_time, packet_size, ip, ip_header, icmp_header
  262. timeout = timeout - select_duration
  263. if timeout <= 0:
  264. return None, 0, 0, 0, 0
  265. def verbose_ping(hostname, timeout=1000, count=3, packet_size=55):
  266. p = Ping(hostname, timeout, packet_size)
  267. p.run(count)
  268. if __name__ == '__main__':
  269. # FIXME: Add a real CLI
  270. if len(sys.argv) == 1:
  271. print "DEMO"
  272. # These should work:
  273. verbose_ping("heise.de")
  274. verbose_ping("google.com")
  275. # Inconsistent on Windows w/ ActivePython (Python 3.2 resolves correctly
  276. # to the local host, but 2.7 tries to resolve to the local *gateway*)
  277. verbose_ping("localhost")
  278. # Should fail with 'getaddrinfo print_failed':
  279. verbose_ping("foobar_url.foobar")
  280. # Should fail (timeout), but it depends on the local network:
  281. verbose_ping("192.168.255.254")
  282. # Should fails with 'The requested address is not valid in its context':
  283. verbose_ping("0.0.0.0")
  284. elif len(sys.argv) == 2:
  285. verbose_ping(sys.argv[1])
  286. else:
  287. print "Error: call ./ping.py domain.tld"