#!/usr/bin/env python
#coding=utf8
#
# A python implementation of send_arp.c.
#
# ARP code and license inspired by arprequest by Antoine Millet
# See http://pypi.python.org/pypi/arprequest
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2011 Kristoffer Gronlund
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
#

from optparse import OptionParser
import time
import socket
import itertools
import os
import signal
import atexit
import sys
from struct import pack


def commandline():
    parser = OptionParser(
        usage="%prog [OPTIONS] device src_ip_addr src_hw_addr broadcast_ip_addr netmask")
    parser.add_option("-i", "--interval", dest="interval", default="1000",
                      help="Repeat interval in ms", metavar="INTERVAL")
    parser.add_option("-r", "--repeat", dest="repeat", default="1",
                      help="Repeat count, 0 or less leads to infinite repeat",
                      metavar="REPEAT")
    parser.add_option("-p", "--pidfile", dest="pidfile",
                      default="/tmp/arp.pid",
                      help="PID file", metavar="PID")

    (options, args) = parser.parse_args()

    if len(args) != 5:
        parser.error("Expects: [-i repeatinterval-ms] [-r repeatcount] [-p pidfile] \\\n" +
                     "        device src_ip_addr src_hw_addr broadcast_ip_addr netmask")

    class Args(object):
        pass
    ret = Args()
    ret.interval = int(options.interval)
    ret.repeat = int(options.repeat)
    ret.pidfile = options.pidfile
    ret.device = args[0]
    ret.src_ip_addr = args[1]
    ret.src_hw_addr = args[2]
    ret.broadcast_ip_addr = args[3]
    ret.netmask = args[4]
    return ret


def mssleep(ms):
    time.sleep(ms/1000.0)

bcast_mac = pack('!6B', *(0xFF,)*6)
zero_mac = pack('!6B', *(0x00,)*6)
ARPOP_REQUEST = pack('!H', 0x0001)
ARPOP_REPLY = pack('!H', 0x0002)
# Ethernet protocol type (=ARP)
ETHERNET_PROTOCOL_TYPE_ARP = pack('!H', 0x0806)
# ARP logical protocol type (Ethernet/IP)
ARP_PROTOCOL_TYPE_ETHERNET_IP = pack('!HHBB', 0x0001, 0x0800, 0x0006, 0x0004)


def send_arp(ip, device, sender_mac, broadcast, netmask, arptype,
             request_target_mac=zero_mac):
    #if_ipaddr = socket.gethostbyname(socket.gethostname())
    sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.SOCK_RAW)
    sock.bind((device, socket.SOCK_RAW))

    socket_mac = sock.getsockname()[4]
    if sender_mac == 'auto':
        sender_mac = socket_mac
#    else:
#        raise Exception("Can't ARP this: " + sender_mac)

    arpop = None
    target_mac = None
    if arptype == 'REQUEST':
        target_mac = request_target_mac
        arpop = ARPOP_REQUEST
    else:
        target_mac = sender_mac
        arpop = ARPOP_REPLY

    sender_ip = pack('!4B', *[int(x) for x in ip.split('.')])
    target_ip = pack('!4B', *[int(x) for x in ip.split('.')])

    arpframe = [
        # ## ETHERNET
        # destination MAC addr
        bcast_mac,
        # source MAC addr
        socket_mac,
        ETHERNET_PROTOCOL_TYPE_ARP,

        # ## ARP
        ARP_PROTOCOL_TYPE_ETHERNET_IP,
        # operation type
        arpop,
        # sender MAC addr
        sender_mac,
        # sender IP addr
        sender_ip,
        # target hardware addr
        target_mac,
        # target IP addr
        target_ip
        ]

    # send the ARP
    sock.send(''.join(arpframe))


def write_pid_file(file_path):
    # http://stackoverflow.com/a/10979569/83741
    handle = os.open(file_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
    with os.fdopen(handle, 'w') as f:
        f.write(str(os.getpid()))


def remove_pid_file(file_path):
    os.unlink(file_path)


def setup_pid_file(file_path):
    write_pid_file(file_path)

    def signal_handler(signum=None, frame=None):
        remove_pid_file(file_path)
        sys.exit(0)

    # http://stackoverflow.com/a/11858588/83741
    # Only a limited set of signals are available on Windows (see #2)
    # Also see https://docs.python.org/2/library/signal.html#signal.signal
    import platform
    if platform.system() == 'Windows':
        signal_list = [signal.SIGTERM]
    else:
        signal_list = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT]
    for sig in signal_list:
        signal.signal(sig, signal_handler)

    atexit.register(lambda: remove_pid_file(file_path))


def main():
    args = commandline()
    setup_pid_file(args.pidfile)
    span = range(args.repeat) if args.repeat > 0 else itertools.count()
    for j in span:

        # Send the same packets as outlined here:
        # http://support.citrix.com/article/ctx109980

        send_arp(args.src_ip_addr, args.device,
                 args.src_hw_addr,
                 args.broadcast_ip_addr,
                 args.netmask, 'REQUEST',
                 request_target_mac=bcast_mac)

        send_arp(args.src_ip_addr, args.device,
                 args.src_hw_addr,
                 args.broadcast_ip_addr,
                 args.netmask, 'REQUEST',
                 request_target_mac=zero_mac)

        send_arp(args.src_ip_addr, args.device,
                 args.src_hw_addr,
                 args.broadcast_ip_addr,
                 args.netmask, 'REPLY')

        if j != args.repeat - 1:
            mssleep(args.interval)


if __name__ == "__main__":
    main()