#!/usr/bin/python

import sys, time, random, socket, struct, hashlib
import datetime

from datetime import datetime

# How many incoming msg's are considered ack of block send
BLOCK_ACKS = 3

###########################################################################
from curses.ascii import isprint

def printable(input):
    return ''.join(char if isprint(char) else '.' for char in input)

def hexdump(chars, width):
    print "len=%d" % len(chars)
    while chars:
        line = chars[:width]
        chars = chars[width:]
        dmp = ' '.join("%02x" % ord(c) for c in line)
        txt = printable(line)
        print "%s%s%s" % (dmp.ljust(width*3, ' '), ' ', txt)
###########################################################################

##############################################################################

# Mandatory args: TargetHost TargetPort Blockfile
if len(sys.argv[1:]) != 3:
    # If no args, print usage and exit:
    print sys.argv[0] + " TARGETHOST TARGETPORT BLOCKFILE.BLK"
    exit(0)

# The command line params :
TargetHost = sys.argv[1]
TargetPort = int(sys.argv[2])
BlockPath  = sys.argv[3]

###########################################################################

# Hardcoded config for inessentials
MyHost    = "127.0.0.1"
MyPort    = 0
MyBtcVer  = 99999
MyJumpers = 1
MyBlocks  = 0
MyBanner  = "Block Feeder"

###########################################################################
## BTC Node Interrogator
###########################################################################

# Designate btc protocol eggogs
class BtcEggog(Exception):
    pass


# Encode uint to btc 'varint'
def int2varint(n):
    if n < 0xfd:
        return struct.pack('<B', n)
    elif n < 0xffff:
        return struct.pack('<cH', '\xfd', n)
    elif n < 0xffffffff:
        return struct.pack('<cL', '\xfe', n)
    else:
        return struct.pack('<cQ', '\xff', n)


# Encode string to btc 'varstr'
def str2varstr(s):
    return int2varint(len(s)) + s


# Decode btc 'varint' to uint
def varint2int(bytes):
    n0 = ord(bytes[0])
    if n0 < 0xfd:
        return [n0, 1]
    elif n0 == 0xfd:
        return [struct.unpack('<H', bytes[1:3])[0], 3]
    elif n0 == 0xfe:
        return [struct.unpack('<L', bytes[1:5])[0], 5]
    else:
        return [struct.unpack('<Q', bytes[1:5])[0], 7]


# Decode btc 'varstr' to string
def varstr2str(bytes):
    n, length = varint2int(bytes)
    return [bytes[length:length+n], length + n]


## Encode ipv4 addr and port tuple to btc 'netaddr'
def mk_netaddr(ipaddr, port):
    services = MyJumpers
    return (struct.pack('<Q12s', services,
                        '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff') +
            struct.pack('>4sH', ipaddr, port))


# Decode 26-byte btc 'netaddr' to human string
def addr2str(bytes):
    if len(bytes) < 26:
        raise BtcEggog("addr2str given invalid input!")
    return '%d.%d.%d.%d:%d' % (ord(bytes[20]), ord(bytes[21]),
                               ord(bytes[22]), ord(bytes[23]),
                               struct.unpack('!H', bytes[24:26])[0])


# Encode given payload bytes into outgoing btc message packet
def mk_btc_message(command, payload):
    # Mandatory 'double-sha2' checksum:
    checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[0:4]
    # Encode the message into the expected format:
    return struct.pack('<L12sL4s',
                       0xd9b4bef9, command, len(payload), checksum) + payload


# Encode an outgoing 'version' message
def mk_btc_ver_msg(local_addr, local_port, remote_addr, remote_port):
    # Origin addr/port
    addr_me = mk_netaddr(socket.inet_aton(local_addr), local_port)
    
    # Destination addr/port
    addr_you = mk_netaddr(socket.inet_aton(remote_addr), remote_port)
    
    # Outgoing ID banner
    banner = str2varstr(MyBanner)
    
    # Format the message payload:
    pformat = '<LQQ26s26sQ%dsL' % len(banner)
    payload = struct.pack(pformat,
                          MyBtcVer, MyJumpers, int(time.time()),
                          addr_me,  # My external addr
                          addr_you, # Peer's external addr
                          random.getrandbits(64), # mandatory rubbish
                          banner, MyBlocks)
    
    return mk_btc_message('version', payload)


# Make a btc inv entry of a given type, with the given hash
def mk_btc_inv(inv_hash, inv_hash_type):
    return struct.pack('<L32s', inv_hash_type, inv_hash[::-1])


# Encode an outgoing 'block' message
def mk_btc_block_msg(payload):
    # Return the completed block message packet
    return mk_btc_message('block', payload)


# Heuristic for determining type of node using its 'jumper' settings
def species_heuristic(jumpers):
    if jumpers == 0:
        # Self-confessed pseudos :
        return "Pseudonode?"
    elif jumpers == 1:
        # Likely TRB or compat. traditionals :
        return "TRB-Compat."
    else:
        # PRB. Determine subspecies:
        prb = "PRB: "
        if jumpers & 2    != 0:
            prb += "UTXO;"
        if jumpers & 4    != 0:
            prb += "BLOOM;"
        if jumpers & 8    != 0:
            prb += "WITNESS;"
        if jumpers & 1024 != 0:
            prb += "NETWORK_LIMITED"
        return prb


# Receive a btc message on given socket:
def rcv_btc_message(sock, expect_payload):
    # Receive a btc-compat. header:
    header = sock.recv(24)
    
    # The early portion of the header is always of fixed length:
    if len(header) != 24:
        raise BtcEggog("Bad header length!")
    
    # Decode the header:
    magic, cmd, payload_len, checksum = struct.unpack('<L12sL4s', header)

    # If expected a payload, but received none:
    if expect_payload and payload_len == 0:
        raise BtcEggog("Expected a payload, got none!")
    
    # Receive the 'tail' of the message:
    payload = sock.recv(payload_len)
    
    # Validate payload length:
    if len(payload) != payload_len:
        raise BtcEggog("Invalid payload length!")
    
    # Calculate mandatory 'double-sha2' checksum:
    our_checksum = hashlib.sha256(
        hashlib.sha256(payload).digest()).digest()[0:4]
    
    # If invalid checksum:
    if checksum != our_checksum:
        raise BtcEggog("Payload failed checksum!")
    
    # Remove null termination from cmd name
    cmd = cmd.replace('\0', '')

    # Indicate receipt on stdout:
    print "<INCOMING MESSAGE: '%s', %d bytes>" % (cmd, len(payload))
    
    # Return reply's command and payload
    return cmd, payload


# Dump abridged summary of an incoming btc message
def dmp_btc_message(cmd, payload):
    print "<INCOMING MESSAGE: '%s', %d bytes>" % (cmd, len(payload))


# Feed the given host/port, with given message, and display response
def feed_node(host, port, message):
    # Make a fresh socket:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # Disable Nagle's algorithm for tx/rx operations
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
    
    try:
        # Attempt connection to given peer:
        sock.connect((host, port))
        
        # Send our 'version' greeting:
        sock.sendall(mk_btc_ver_msg(MyHost, MyPort, host, port))
        
        # Receive peer's version
        cmd, payload = rcv_btc_message(sock, True)
        
        # Verify that we in fact received a 'version' message:
        if cmd != 'version':
            raise BtcEggog("Asked for 'version', got %s!" % cmd)
        
        try:
            # Decode fixed-length version header fields:
            version, services, timestamp, addr_recv, addr_from, nonce \
                = struct.unpack('<LQQ26s26sQ', payload[:80])
            
            # Decode variable-length version header fields:
            agent, agent_len = varstr2str(payload[80:])
            
            # Decode block height field:
            start_height \
                = struct.unpack('I', payload[80 + agent_len:84 + agent_len])[0]
            
            # Only show 'return address' if different from where we connected:
            show_addr = ''
            peer_addr = addr2str(addr_from)
            expect_origin = "%s:%d" % (host, port)
            if peer_addr != expect_origin:
                show_addr = " Return Addr=%s" % peer_addr
            
            # Print the peer version info we're interested in:
            print 'Alive: V=%d (%s) Jumpers=0x%x (%s)%s Blocks=%d' % \
                (version, agent,
                 services, species_heuristic(services),
                 show_addr,
                 start_height)
            
        except Exception:
            # If any failure in decoding message:
            raise BtcEggog("Received malformed 'version' message !")
        
        # Send our 'verack'
        sock.sendall(mk_btc_message('verack', ''))
        
        # Wait for peer's 'verack'
        cmd, payload = rcv_btc_message(sock, False)

        # Now, the given message:
        print "Sending %d-byte message packet..." % len(message)
        sock.sendall(message)

        print "Now listening for replies (Ctl-C to quit...)"
        for i in range(0, BLOCK_ACKS):
            cmd, payload = rcv_btc_message(sock, False)
        
    except socket.error:
        print "Could not connect!"
    except BtcEggog as e:
        print "Violated BTC Protocol: %s" % e
    except Exception as e:
        print "Unknown Eggog: %s" % e
    
    # Close socket
    sock.close()

###########################################################################

# Read block from disk:
with open(BlockPath, mode='rb') as blkfile:
    blk_data = blkfile.read()
    blk_msg = mk_btc_block_msg(blk_data)

# Send to designated target:
feed_node(TargetHost, TargetPort, blk_msg)
