#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
"""ping.py
 
 ping.py uses the ICMP protocol's mandatory ECHO_REQUEST
 datagram to elicit an ICMP ECHO_RESPONSE from a
 host or gateway.

 Copyright (C) 2004 - Lars Strand <lars strand at gnist org>
 
 Sound emission changes for One Laptop Per Child radio range testing
 Copyright (C) 2007 - James Cameron <quozl@us.netrek.org>
 
 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 --

 Must be running as root, or write a suid-wrapper. Since newer *nix
 variants, the kernel ignores the set[ug]id flags on #! scripts for
 security reasons

 RFC792, echo/reply message:

  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 ...
 +-+-+-+-+-


TODO:
 - do not create socket inside 'while' (but if not: ipv6 won't work)
 - add support for broadcast/multicast
 - add support for own payload string

CHANGELOG:
 DONE --> bugfix from Filip Van Raemdonck mechanix debian org
 DONE --> add more support for modules (raise instead of sys.exit)
 DONE --> locale func names
 DONE --> package def
 DONE --> some code cleanup
 
"""

import sys
import os
import struct
import array
import time
import select
import binascii
import math
import getopt
import string
import socket

import wave
import alsaaudio

def sounder_read(x):
    """Given a sound file number, read and return it.
    Sound files were generated in advance by
    http://quozl.linux.org.au/darcs/olpc-radio-testing/bin/af-table-create
    """
    fn = "/home/olpc/olpc-radio-testing/tmp/sine-"+str(x)+".wav"
    wav = wave.open(fn,"rb")
    (nc, sw, fr, nf, ct, cn) = wav.getparams()
    data = (wav.readframes(nf), nc, fr, nf)
    wav.close()
    return data

def sounder_cache(x):
    """Given a sound file number, return it from a cache or read it.
    """
    if x not in sounder_cache.data:
        sounder_cache.data[x] = sounder_read(x)
    return sounder_cache.data[x]

sounder_cache.data = {} # initialise the cache

def sounder(triptime):
    """Given a trip time in microseconds, convert it to a sound, and
    emit the sound.  This becomes the point in the program that
    constrains the overall pace of a flood, since the write will not
    return until there is room for the sound sample.

    Note: --noflood without --quiet will cause irregular sound output
    because the PCM device will underrun as a result of not being fed
    at top speed.
    """
    if triptime < 0:
        x = 85
    else:
        x = int(40+(triptime*1000))
        if x < 20 or x > 75:
            x = 75
    (data, nc, fr, nf) = sounder_cache(x)
    if sounder.out == 0:
        sounder.out = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK)
        # assume all sound files are same number of samples
        sounder.out.setchannels(nc)
        sounder.out.setrate(fr)
        sounder.out.setformat(alsaaudio.PCM_FORMAT_S16_LE)
        sounder.out.setperiodsize(nf)
    sounder.out.write(data)

# initialise output object
sounder.out = 0

# total size of data (payload)
ICMP_DATA_STR = 56  

# initial values of header variables
ICMP_TYPE = 8
ICMP_TYPE_IP6 = 128
ICMP_CODE = 0
ICMP_CHECKSUM = 0
ICMP_ID = 0
ICMP_SEQ_NR = 0

# Package definitions.
__program__   = 'ping'
__version__   = '0.5a'
__date__      = '2004/15/12'
__author__    = 'Lars Strand <lars at unik no>'
__licence__   = 'GPL'
__copyright__ = 'Copyright (C) 2004 Lars Strand'

def _construct(id, size, ipv6):
    """Constructs a ICMP echo packet of variable size
    """

    # size must be big enough to contain time sent
    if size < int(struct.calcsize("d")):
        _error("packetsize to small, must be at least %d" % int(struct.calcsize("d")))
    
    # construct header
    if ipv6:
        header = struct.pack('BbHHh', ICMP_TYPE_IP6, ICMP_CODE, ICMP_CHECKSUM, \
                             ICMP_ID, ICMP_SEQ_NR+id)
    else:
        header = struct.pack('bbHHh', ICMP_TYPE, ICMP_CODE, ICMP_CHECKSUM, \
                             ICMP_ID, ICMP_SEQ_NR+id)

    # if size big enough, embed this payload
    load = "-- http://dev.laptop.org/~quozl/ payload ! --"
    
    # space for time
    size -= struct.calcsize("d")

    # construct payload based on size, may be omitted :)
    rest = ""
    if size > len(load):
        rest = load
        size -= len(load)

    # pad the rest of payload
    rest += size * "X"

    # pack
    data = struct.pack("d", time.time()) + rest
    packet = header + data          # ping packet without checksum
    checksum = _in_cksum(packet)    # make checksum

    # construct header with correct checksum
    if ipv6:
        header = struct.pack('BbHHh', ICMP_TYPE_IP6, ICMP_CODE, checksum, \
                             ICMP_ID, ICMP_SEQ_NR+id)
    else:
        header = struct.pack('bbHHh', ICMP_TYPE, ICMP_CODE, checksum, ICMP_ID, \
                             ICMP_SEQ_NR+id)

    # ping packet *with* checksum
    packet = header + data 

    # a perfectly formatted ICMP echo packet
    return packet

def _in_cksum(packet):
    """THE RFC792 states: 'The 16 bit one's complement of
    the one's complement sum of all 16 bit words in the header.'

    Generates a checksum of a (ICMP) packet. Based on in_chksum found
    in ping.c on FreeBSD.
    """

    # add byte if not dividable by 2
    if len(packet) & 1:              
        packet = packet + '\0'

    # split into 16-bit word and insert into a binary array
    words = array.array('h', packet) 
    sum = 0

    # perform ones complement arithmetic on 16-bit words
    for word in words:
        sum += (word & 0xffff) 

    hi = sum >> 16 
    lo = sum & 0xffff 
    sum = hi + lo
    sum = sum + (sum >> 16)
    
    return (~sum) & 0xffff # return ones complement

def pingNode(alive=0, timeout=1.0, ipv6=0, number=sys.maxint, node=None, \
             flood=0, size=ICMP_DATA_STR, quiet=0, verbose=0):
    """Pings a node based on input given to the function.
    """

    # if no node, exit
    if not node:
        _error("")

    # if not a valid host, exit
    if ipv6:
        if socket.has_ipv6:
            try:
                info, port = socket.getaddrinfo(node, None)
                host = info[4][0]
                # do not print ipv6 twice if ipv6 address given as node
                if host == node: 
                    noPrintIPv6adr = 1
            except:
                _error("cannot resolve %s: Unknow host" % node)
        else:
            _error("No support for IPv6 on this plattform")
    else:    # IPv4
        try:
            host = socket.gethostbyname(node)
        except:
            _error("cannot resolve %s: Unknow host" % node)

    # trying to ping a network?
    if not ipv6:
        if int(string.split(host, ".")[-1]) == 0:
            _error("no support for network ping")

    # do some sanity check
    if number == 0:
        _error("invalid count of packets to transmit: '%s'" % str(a))
    if alive:
        number = 1

    # Send the ping(s)
    start = 1; mint = 999; maxt = 0.0; avg = 0.0
    lost = 0; tsum = 0.0; tsumsq = 0.0

    # tell the user what we do
    if verbose:
        if not alive:
            if ipv6:
                # do not print the ipv6 twice if ip adress given as node
                # (it can be to long in term window)
                if noPrintIPv6adr == 1:
                    # add 40 (header) + 8 (icmp header) + payload
                    print "PING %s : %d data bytes (40+8+%d)" % (str(node), \
                                                                 40+8+size, size)
                else:
                    # add 40 (header) + 8 (icmp header) + payload
                    print "PING %s (%s): %d data bytes (40+8+%d)" % (str(node), \
                                                                     str(host), 40+8+size, size)
            else:
                # add 20 (header) + 8 (icmp header) + payload
                print "PING %s (%s): %d data bytes (20+8+%d)" % (str(node), str(host), \
                                                                     20+8+size, size)
        
    # trap ctrl-d and ctrl-c
    try:
        
        # send the number of ping packets as given
        while start <= number:
            lost += 1 # in case user hit ctrl-c
            
            # create the IPv6/IPv4 socket
            if ipv6:
                # can not create a raw socket if not root or setuid to root
                try:
                    pingSocket = socket.socket(socket.AF_INET6, socket.SOCK_RAW, \
                                               socket.getprotobyname("ipv6-icmp"))
                except socket.error, e:
                    print "socket error: %s" % e
                    _error("You must be root (uses raw sockets)" % os.path.basename(sys.argv[0]))
                    
            # IPv4
            else:
                # can not create a raw socket if not root or setuid to root
                try:
                    pingSocket = socket.socket(socket.AF_INET, socket.SOCK_RAW, \
                                               socket.getprotobyname("icmp"))
                except socket.error, e:
                    print "socket error: %s" % e
                    _error("You must be root (%s uses raw sockets)" % os.path.basename(sys.argv[0]))
                
            packet = _construct(start, size, ipv6) # make a ping packet

            # send the ping
            try:
                pingSocket.sendto(packet,(node,1))
            except socket.error, e:
                _error("socket error: %s" % e)

            # reset values
            pong = ""; iwtd = []

            # wait until there is data in the socket
            while 1:
                # input, output, exceptional conditions
                iwtd, owtd, ewtd = select.select([pingSocket], [], [], timeout)
                break # no data and timout occurred 

            # data on socket - this means we have an answer
            if iwtd:  # ok, data on socket
                endtime = time.time()  # time packet received
                # read data (we only need the header)
                pong, address = pingSocket.recvfrom(size+48)
                lost -= 1 # in case user hit ctrl-c

                # examine packet
                # fetch TTL from IP header
                if ipv6:
                    # since IPv6 header and any extension header are never passed
                    # to a raw socket, we can *not* get hoplimit field..
                    # I hoped that a socket option would help, but it's not
                    # supported:
                    #   pingSocket.setsockopt(IPPROTO_IPV6, IPV6_RECVHOPLIMIT, 1)
                    # so we can't fetch hoplimit..

                    # fetch hoplimit
                    #rawPongHop = struct.unpack("c", pong[7])[0]

                    # fetch pong header
                    pongHeader = pong[0:8]
                    pongType, pongCode, pongChksum, pongID, pongSeqnr = \
                              struct.unpack("bbHHh", pongHeader)

                    # fetch starttime from pong
                    starttime = struct.unpack("d", pong[8:16])[0]

                # IPv4
                else:
                    # time to live
                    rawPongHop = struct.unpack("s", pong[8])[0]

                    # convert TTL from 8 bit to 16 bit integer
                    pongHop = int(binascii.hexlify(str(rawPongHop)), 16)

                    # fetch pong header
                    pongHeader = pong[20:28]
                    pongType, pongCode, pongChksum, pongID, pongSeqnr = \
                              struct.unpack("bbHHh", pongHeader)

                    # fetch starttime from pong
                    starttime = struct.unpack("d", pong[28:36])[0]

                # valid ping packet received?
                if not pongSeqnr == start:
                    pong = None

            # NO data on socket - timeout waiting for answer
            if not pong:
                if verbose:
                    if alive:
                        print "no reply from %s (%s)" % (str(node), str(host))
                    else:
                        print "ping timeout: %s (icmp_seq=%d) " % (host, start)
                if not quiet:
                    sounder(-1)

                # do not wait if just sending one packet
                if number != 1 and start < number:
                    time.sleep(flood ^ 1)
                start += 1
                continue  # lost a packet - try again

            triptime  = endtime - starttime # compute RRT
            tsum     += triptime            # triptime for all packets (stddev)
            tsumsq   += triptime * triptime # triptime^2  for all packets (stddev)

            # compute statistic
            maxt = max ((triptime, maxt))
            mint = min ((triptime, mint))

            if verbose:
                if alive:
                    print str(node) + " (" + str(host) +") is alive"
                else:
                    if ipv6:
                        # size + 8 = payload + header
                        print "%d bytes from %s: icmp_seq=%d time=%.5f ms" % \
                              (size+8, host, pongSeqnr, triptime*1000)
                    else:
                        print "%d bytes from %s: icmp_seq=%d ttl=%s time=%.5f ms" % \
                              (size+8, host, pongSeqnr, pongHop, triptime*1000)
            if not quiet:
                sounder(triptime)

            # do not wait if just sending one packet
            if number != 1 and start < number:
                # if flood = 1; do not sleep - just ping                
                time.sleep(flood ^ 1) # wait before send new packet

            # the last thing to do is update the counter - else the value
            # (can) get wrong when computing summary at the end (if user
            # hit ctrl-c when pinging)
            start += 1
            # end ping send/recv while

    # if user ctrl-d or ctrl-c
    except (EOFError, KeyboardInterrupt):
        # if user disrupts ping, it is most likly done before
        # the counter get updates - if do not update it here, the
        # summary get all wrong.
        start += 1
        pass

    # compute and print som stats
    # stddev computation based on ping.c from FreeBSD
    if start != 0 or lost > 0:  # do not print stats if 0 packet sent
        start -= 1              # since while is '<='
        avg = tsum / start      # avg round trip
        vari = tsumsq / start - avg * avg 
        # %-packet lost
        if start == lost:
            plost = 100
        else:
            plost = (lost/start)*100

        if verbose and not alive:
            print "\n--- %s ping statistics ---" % node
            print "%d packets transmitted, %d packets received, %d%% packet loss" % \
                  (start, start-lost, plost)
            # don't display summary if 100% packet-loss
            if plost != 100:
                print "round-trip min/avg/max/stddev = %.3f/%.3f/%.3f/%.3f ms" % \
                      (mint*1000, (tsum/start)*1000, maxt*1000, math.sqrt(vari)*1000)

    pingSocket.close()
    
def _error(err):
    """Exit if running standalone, else raise an exception
    """

    if __name__ == '__main__':
        print "%s: %s" % (os.path.basename(sys.argv[0]), str(err))
        print "Try `%s --help' for more information." % os.path.basename(sys.argv[0])
        sys.exit(1)
    else:
        raise Exception, str(err)
    
def _usage():
    """Print usage if run as a standalone program
    """
    print """usage: %s [OPTIONS] HOST
Send ICMP ECHO_REQUEST packets to network hosts.

Mandatory arguments to long options are mandatory for short options too.
  -c, --count=N    Stop after sending (and receiving) 'N' ECHO_RESPONSE
                   packets.
  -s, --size=S     Specify the number of data bytes to be sent. The default
                   is 56, which translates into 64 ICMP data bytes when
                   combined with the 8 bytes of ICMP header data.
  -n, --noflood    No flood ping.
  -6, --ipv6       Ping using IPv6.
  -t, --timeout=s  Specify a timeout, in seconds, before a ping packet is
                   considered 'lost'.
  -q, --quiet      OLPC radio testing, disable sound generation.
  -v, --verbose    OLPC radio testing, enable output to screen.
  -h, --help       Display this help and exit

Report bugs to quozl [at] us dot netrek dot org""" % os.path.basename(sys.argv[0])


if __name__ == '__main__':
    """Main loop
    """

    # version control
    version = string.split(string.split(sys.version)[0][:3], ".")
    if map(int, version) < [2, 3]:
        _error("You need Python ver 2.3 or higher to run!")

    try:
        # opts = arguments recognized,
        # args = arguments NOT recognized (leftovers)
        opts, args = getopt.getopt(sys.argv[1:-1], "hat:6c:ns:qv", \
                                   ["help", "alive", "timeout=", "ipv6", \
                                    "count=", "noflood", "packetsize=", \
                                    "quiet", "verbose"])
    except getopt.GetoptError:
        # print help information and exit:
        _error("illegal option(s) -- " + str(sys.argv[1:]))

    # test whether any host given
    if len(sys.argv) >= 2:
        node = sys.argv[-1:][0]   # host to be pinged
        if node[0] == '-' or node == '-h' or node == '--help' :  
            _usage()
    else:
        _error("No arguments given")

    if args:
        _error("illegal option -- %s" % str(args))
        
    # default variables
    alive = 0; timeout = 0.1; ipv6 = 0; count = sys.maxint;
    noflood = 0; size = ICMP_DATA_STR; quiet = 0; verbose = 0;

    # run through arguments and set variables
    for o, a in opts:
        if o == "-h" or o == "--help":    # display help and exit
            _usage()
            sys.exit(0)
        if o == "-t" or o == "--timeout": # timeout before "lost"
            try:
                timeout = float(a)
            except:
                _error("invalid timout: '%s'" % str(a))
        if o == "-6" or o == "--ipv6":    # ping ipv6
            ipv6 = 1
        if o == "-c" or o == "--count":   # how many pings?
            try:
                count = int(a)
            except:
                _error("invalid count of packets to transmit: '%s'" % str(a))
        if o == "-n" or o == "--noflood":   # no delay between ping send
            noflood = 1
        if o == "-s" or o == "--packetsize":  # set the ping payload size
            try:
                size = int(a)
            except:
                _error("invalid packet size: '%s'" % str(a))
        # just send one packet and say "it's alive"
        if o == "-a" or o == "--alive":   
            alive = 1
        if o == "-q" or o == "--quiet":   
            quiet = 1
        if o == "-v" or o == "--verbose":   
            verbose = 1

    # here we send
    pingNode(alive=alive, timeout=timeout, ipv6=ipv6, number=count, \
             node=node, flood=noflood ^ 1, size=size, quiet=quiet, \
             verbose=verbose)
    # if we made it this far, do a clean exit
    sys.exit(0)

### end