#!/usr/bin/env python

# Get latest IPv4 transfer stats from specified RIR, convert the JSON structure to
# simple .csv format. As not all RIRs publish the same info (ripencc vs. does
# not list source/recipient country, Lacnic tag RESOURCE_TRANSFER as Polic/Market)
# this script's output sticks to the common demoninator

from __future__ import print_function

import argparse
import codecs
import json
import sys
import re

try:
	# For Python 3.0 and later
	from urllib.request import urlopen
except ImportError:
	# Fall back to Python 2's urllib2
	from urllib2 import urlopen


def int2ip(ip):
	"""Convert an IPv4 address in integer to text representation
	"""
	ip1 = (ip & 0xFF000000) >> 24 
	ip2 = (ip & 0x00FF0000) >> 16 
	ip3 = (ip & 0x0000FF00) >>  8 
	ip4 = (ip & 0x000000FF) 

	ipnum = "%d.%d.%d.%d" % (ip1, ip2, ip3, ip4)

	return(ipnum)




def ip2int(text):
	"""Convert an IPv4 address in text form to integer

	@param text: The IPv4 address
	@type text: string
	@returns: string
	"""
	parts = text.split('.')
	if len(parts) != 4:
		raise SyntaxError("%s: more than 4 bytes???" % text)
	for part in parts:
		if not part.isdigit():
			raise SyntaxError("%s: not an IPv4 address" % text)
		#if len(part) > 1 and part[0] == '0':
			# No leading zeros

			# Why did I consider leading space syntax error?
			# seems to work just fine (and is present in ARIN data)
			#raise SyntaxError("%s: no leading zeroes" % text)
	ip = 0
	for part in parts:
		ip = 256 * ip + int(part)

	return ip 


def blockSize(b):

	# compute the size of IPv4 block b
	# b is either a prefix
	# or a full IPv4 range

	m=re.match("[\d\.]+\/(\d+)",b)
	if m:
		# a prefix
		size = 2 ** (32 - int(m.group(1)))
	else:
		m=re.match("(^[\d\.]+)-([\d\.]+)$",b)
		if m:
			ip1 = ip2int(m.group(1))
			ip2 = ip2int(m.group(2))
			size = ip2 - ip1 + 1
			if size < 0: 
				raise RunTimeError("%s: first IP larger than second?" % b)
		else:
			raise RuntimeError("%s: cannot determine size; not an IPv4 block?" % b)

	return(size)


def block2range(b):

	# convert block to dict
	# input: cidr prefix or range
	# output: tuple (startIP, size)

	m=re.match("^([\d\.]+)\/(\d+)",b)
	if m:
		# a prefix
		ip1 = ip2int(m.group(1))
		size = 2 ** (32 - int(m.group(2)))
	else:
		m=re.match("(^[\d\.]+)-([\d\.]+)$",b)
		if m:
			ip1 = ip2int(m.group(1))
			ip2 = ip2int(m.group(2))
			size = ip2 - ip1 + 1
			if size < 0: 
				raise RunTimeError("%s: first IP larger than second?" % b)
		else:
			raise RuntimeError("%s: cannot determine size; not an IPv4 block?" % b)

	return((ip1,size))

def process_transaction(transaction,startDate,endDate):
	
	# Process one set of transfers
	# Write details of each transferred resource to stdout

	# Example input (ripencc):
	#
	#      {
	#       "transfer_date": "2015-11-23T19:00:00Z",
	#       "asns": [],
	#       "ip4nets": [
	#         {
	#           "original_set": [
	#             null
	#           ],
	#           "transfer_set": [
	#             {
	#               "start": "207.189.192.0",
	#               "end": "207.189.207.255"
	#             }
	#           ]
	#         }
	#       ],
	#       "ip6nets": [],
	#       "type": "RESOURCE_TRANSFER",
	#       "source_organization": {
	#         "name": null
	#       },
	#       "recipient_organization": {
	#         "name": "Junet AB"
	#       },
	#       "source_rir": "ARIN",
	#       "recipient_rir": "RIPE NCC"
	#     },



	if not 'ip4nets' in transaction:
		# no IPv4 transferred here
		return

	if isinstance(transaction['ip4nets'], list):
		# RIPENCC has ip4nets as a list with either 0 or 1 elements
		# other RIRs have it as a dict which may or may not exist
		length = len(transaction['ip4nets'])
		if length == 0:
			# no IPv4 transferred here
			return
		elif length == 1:
			transferred_set = transaction['ip4nets'][0]['transfer_set']
		else:
			print("ABORT: multiple sets of transfer_sets in 'ipv4nets'?",transaction['ipv4nets'],file=sys.stderr)
			sys.exit()
	else:
		transferred_set = transaction['ip4nets']['transfer_set']

	ntransfers = len(transferred_set) 

	if 'completion_date' in transaction:
		# LACNIC, at least until 20191214
		date = transaction['completion_date'][0:10]
	else:
		date = transaction['transfer_date'][0:10]

	yyyymmdd = date[0:4] + date[5:7] + date[8:10]
	if (int(yyyymmdd) > endDate) or (int(yyyymmdd) < startDate):
		return

	# 20191214 - LACNIC still has a dict instead of string for 'type'

	#"type": {
	#   "description": "Policy 2.3.2.18 Intra-RIR Transfer",
	#   "signifier": "Policy/Market"
	#},

	if 'signifier' in transaction['type']:
		# LACNIC
		ttype = transaction['type']['signifier']
	else:
		ttype = transaction['type']

	if ttype == 'Policy/Market':
		#LACNIC
		ttype = 'RESOURCE_TRANSFER'
	elif ttype == 'TRANSFER':
		#AFRINIC
		ttype = 'RESOURCE_TRANSFER'

	from_rir = transaction['source_rir']
	to_rir = transaction['recipient_rir']

	if 'source_organization' in transaction:
		sourceOrigName = transaction['source_organization']['name']
	else:
		sourceCountry = ''
		sourceOrigName = ''

	if 'recipient_organization' in transaction:
		recipientOrigName = transaction['recipient_organization']['name']
	else:
		recipientOrigName = ''

	blocks = {}
	totalsize = 0;
	for transfer in transferred_set:
		try:
			#LACNIC, RIPENCC
			startIP = transfer['start']
			lastIP = transfer['end']
		except:
			#ARIN, APNIC, AFRINIC
			startIP = transfer['start_address']
			lastIP = transfer['end_address']
		ip1 = ip2int(startIP)
		ip2 = ip2int(lastIP)
		startIP = int2ip(ip1) # ditch the leading zeroes
		size = ip2 - ip1 + 1
		if size < 0:
			raise RunTimeError("%s: first IP larger than second?" % b)

		if sys.version_info.major == 2:
			# python2 needs explicit utf8 encoding to prevent it falling back
			# to ascii (and failing with error) when output is redirected
			# to a pipe or file
			print(("%s|%s|%s|%d|%s|%s|%s|%s|%s" % (date, startIP,lastIP,size,from_rir, to_rir, sourceOrigName,recipientOrigName, ttype)).encode('utf-8'))
		else:
			# python3 works as expected, no diff between output to terminal or
			# redirect to file. Adding explicit .encode('utf-8') results in backslah
			# encoded strings, which we do not want
			print(("%s|%s|%s|%d|%s|%s|%s|%s|%s" % (date, startIP,lastIP,size,from_rir, to_rir, sourceOrigName,recipientOrigName, ttype)))




def main():

	parser = argparse.ArgumentParser()
	parser.add_argument('-r','--rir',default="ripencc",help="RIR to fetch statistics from")
	parser.add_argument('-e','--enddate',default="20300000",help="end date, last day to consider, YYYYMMDD")
	parser.add_argument('-s','--startdate',default="20000000",help="start date, first day to includein stats, YYYYMMDD")
	args = parser.parse_args()

	endDate = int(args.enddate)
	startDate = int(args.startdate)
	
	rir=args.rir

	if rir=='ripencc':
		url="https://ftp.ripe.net/pub/stats/ripencc/transfers/transfers_latest.json"
	else:
		url="https://ftp." + rir + ".net/pub/stats/" + rir + "/transfers/transfers_latest.json"
	
	
	
	print("opening URL ",url,file=sys.stderr)
	transfers = urlopen(url)
	transfersT = transfers.read()
	transfersJ = json.loads(transfersT)
	#print json.dumps(transfersJ, indent=1)

	print("processing transfers", file=sys.stderr)

	print("%s|%s|%s|%s|%s|%s|%s|%s|%s" % ('date', 'start','end','size','source_rir', 'recipient_rir', 'source)origanisation','recipient_organisation', 'type'))
	
	for transaction in transfersJ['transfers']:
		process_transaction(transaction,startDate,endDate)


if __name__ == "__main__":
	main()
