#!/usr/bin/python
#
#  Copyright (c) 1998, 1999, Sean Reifschneider, tummy.com, ltd.  
#	All Rights Reserved.
#
#  Imports data into "shelve" from accounting logs.  This data is then
#  used by "stdreport" to generate reports.
#
#  http://www.tummy.com/radiusContext/
#  ftp://ftp.tummy.com/pub/tummy/radiusContext/

import fileinput
import string, re
import types
import time
import shelve
import sys
import getopt
try:
	import gdbm
	usedbm = gdbm
except ImportError:
	import anydbm
	usedbm = anydbm

revision = "$Revision: 1.76 $"
rcsid = "$Id: raddetail,v 1.76 2001/10/18 02:56:58 jafo Exp $"

#  Fields to convert to integers
convToInt = { "Acct-Output-Octets" : 1, 
				"Acct-Input-Octets" : 1,
				"Acct-Session-Time" : 1, 
				"Timestamp" : 1 }

dateFormat = "%Y-%m-%d %H:%M:%S %a"
databaseName = "SessionData"

#  regular expressions for fall-back date parser
dateRe = [
	r'^(?P<day>\d+)-(?P<month>\d+)-(?P<year>\d{4,4})\s+(?P<hour>\d+):'
			r'(?P<minute>\d+):(?P<second>\d+)',
	]
dateReList = []
for regex in dateRe:
	dateReList.append(re.compile(regex))

###################################################
#  convert a time string into a seconds since epoch
#  return epoch or 0 if conversion failed
def stringToTimestamp(str):
	monthDict = {
		'jan' : 1, 'feb' : 2, 'mar' : 3, 'apr' : 4, 'may' : 5, 'jun' : 6,
		'jul' : 7, 'aug' : 8, 'sep' : 9, 'oct' : 10, 'nov' : 11, 'dec' : 12,
		}
	hour = -1
	minute = -1
	sec = -1
	mday = -1
	month = -1
	year = -1

	#  handle unix ctime date format and some similar ones
	for field in string.split(str):
		lcfield = string.lower(field)
		if monthDict.has_key(lcfield):
			month = monthDict[lcfield]
			continue

		#  look for num:num:num time string
		colonPos = string.find(field, ':')
		if colonPos > -1:
			try:
				timeStr = string.split(field, ':')
				if len(timeStr) > 2:
					hour = string.atoi(timeStr[0])
					minute = string.atoi(timeStr[1])
					sec = string.atoi(timeStr[2])
			except:
				hour = -1
				minute = -1
				sec = -1
			continue

		#  convert to number, is it year or month day?
		try:
			num = string.atoi(field)
			if num > 33: year = num
			else: mday = num
		except:
			pass

	#  try using (slower) regular-expression match
	if month < 1:
		for regex in dateReList:
			match = regex.match(str)
			if match:
				month = match.group('month')
				if monthDict.has_key(month): month = monthDict[month]
				if type(month) == types.StringType:
					month = string.atoi(month)
				month = string.atoi(match.group('month'))
				mday = string.atoi(match.group('day'))
				year = string.atoi(match.group('year'))
				if year < 100: year = year + 1900
				hour = string.atoi(match.group('hour'))
				minute = string.atoi(match.group('minute'))
				sec = string.atoi(match.group('second'))

	try:
		return(int(time.mktime(( year, month, mday, hour, minute, sec, 0, 0,
				-1 ))))
	except Exception, e:
		print 'Exception: %s' % e
		pass
	return(0)


#############################################################
#  process the current record and store it if it's acceptable

def handleData(timeStr, haveData, data, sessionData):
	import dbm

	#  check for incomplete records data and discard them
	process = 1
	if haveData < 1: process = 0
	# throw away anything but "Stop" records
	elif data.get('Acct-Status-Type') != 'Stop': process = 0
	#  records missing fields
	elif not data.has_key('User-Name'): process = 0
	elif not data.has_key('Acct-Session-Time'): process = 0
	#  throw away "NAS-Prompt-User" records
	elif data.has_key('Service-Type') \
			and data['Service-Type'] == 'NAS-Prompt-User':
		process = 0

	#  if this is not a throw-away record, store it to database file
	if process:
		#  if the record doesn't have a timestamp, parse the time from
		#  the first line of the record.
		if not data.has_key('Timestamp'):
			data['Timestamp'] = stringToTimestamp(timeStr)
		dateEnd = time.strftime(dateFormat, time.localtime(data["Timestamp"]))
		dateStart = time.strftime(dateFormat,
				time.localtime(data["Timestamp"] - data["Acct-Session-Time"]))
		data["Session-End-Date"] = dateEnd
		data["Session-Start-Date"] = dateStart
		id = 0
		if data.has_key('Acct-Session-Id'): id = data['Acct-Session-Id']
		key = "%s:%s:%s" % ( data["User-Name"], data["Session-End-Date"], id )

		#  ignore dbm.errors
		try:
			sessionData[key] = data
		except dbm.error: pass


#################################
#  process command-line arguments
shortArgs = "d:hv?"
longArgs = [
		"database=",
		"help",
		"version",
		]

#  process options, retaining program name and non-option arguments
optlist, sys.argv[1:] = getopt.getopt(sys.argv[1:], shortArgs, longArgs)
for arg in optlist:
	if arg[0] == "-d" or arg[0] == "--databasedir":
		databaseName = arg[1]

	if arg[0] == "-h" or arg[0] == "-?" or arg[0] == "--help":
		print "usage: raddetail [<arguments>] <detail file> [...]"
		print
		print "\t[ -d | --database <file> ]       Specify database file name."
		print "\t[ -v | --version  ]              Display program version."
		print "\t[ -h | -? | --help  ]            Display this usage message."
		sys.exit(0)

	if arg[0] == "-v" or arg[0] == "--version":
		print "Version", string.split(revision)[1]
		sys.exit(0)

################################################
#  main code body, read records and process them
sessionData = shelve.Shelf(usedbm.open(databaseName, 'c'))

timeStr = ''
haveData = 0
data = {}
for line in fileinput.input():
	#  new record found, process last record
	if len(line) > 0 and line[0] != "\t":
		handleData(timeStr, haveData, data, sessionData)

		#  reset record
		data.clear()
		haveData = 0
		timeStr = string.strip(line)
		continue

	#  look for name=value line and save it for processing later
	eqPos = string.find(line, "=")
	if eqPos > 0:
		Name = string.strip(line[:eqPos - 1])
		Value = string.strip(line[eqPos + 1:])
		if Value[0] == "\"" and Value[-1] == "\"":
			Value = Value[1:-1]

		if convToInt.get(Name, 0) == 1:
			Value = string.atoi(Value)

		data[Name] = Value
		haveData = 1

#  process left-over data (if any)
handleData(timeStr, haveData, data, sessionData)
