#!/usr/bin/python
#needs at least python 1.5

#***************************************************************************
#		copyright            : (C) 2001 by McRee		   *
#		email                : mcree@freemail.hu		   *
#***************************************************************************
					     
#***************************************************************************
#*                                                                         *
#*   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.                                   *
#*                                                                         *
#***************************************************************************
									   

database = "/var/local/muddlestats.db";
muddle_log = "/var/log/muddleftpd.log";

import re;
import pprint;
import cPickle;
from zlib import compress, decompress;
from time import *;

#print "This is MuddleStats";

# lots of threads are simultaniously logging to the same logfile. we have to separate them by:
re_thread_id 	= re.compile(r': [a-zA-Z0-9]+@[a-zA-Z0-9._-]+\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)/[0-9]+ :');
# when the user logs out of a thread all loglist data is cleared for that thread
re_logout 	= re.compile(r'User logged out');
# when the server exits, all loglist data for all threads is killed
re_server_exit 	= re.compile(r'Info - Received SIG.*');
# unix timestamp - must find this in every row - or else: log syntax error
re_timestamp 	= re.compile(r'[A-Z][a-z]{2} [A-Z][a-z]{2} [ 0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4}');
# hostname(ipnumber) of a user
re_host 	= re.compile(r'@[a-zA-Z0-9._-]+\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)');
# username
re_user 	= re.compile(r': [a-zA-Z0-9]+@');
# xxx bytes
re_bytes 	= re.compile(r'[0-9]* bytes');                                                            
# transfers can be finished or aborted
re_finish 	= re.compile(r'.* 226 Transfer done. [0-9]+ bytes transferred|.* 426 Transfer aborted. [0-9]+ bytes transferred');
# retrieve command
re_retr 	= re.compile(r'^\+\+.*retrieve');
# store commands
re_stor 	= re.compile(r'^\+\+.*store|^\+\+.*append');

loglist = {}; # must init dictionaries
userlist = {};# ...
oldstamp = ''; # must init timestamp

def updatedb(s, mode = "RETR"):
    "updates userlist database form the given 's' logline using 'retr' or 'stor' modes"

    global userlist; # we need this global data
        
    # extract some data from the logline
    user = (re_user.search(s)).group()[2:-1];
    bytes = (re_bytes.search(s)).group()[:-5];                                                 
    ttime = strptime(re_timestamp.search(s).group());
    host = (re_host.search(s)).group()[1:];
    times = "%02d" % ttime[0] + "-%02d" % ttime[1] + "-%02d" % ttime[2];
    
    # maintain database tree
    if not userlist.has_key(user): # if user does not exist
	userlist[user]={host: {}};
    if not userlist[user].has_key(host): # if this the never connected from this host
	userlist[user][host]={times: {"RETR": 0, "STOR": 0}};
    if not userlist[user][host].has_key(times): # if the user did not do anything 'today'
	userlist[user][host][times]={"RETR": 0, "STOR": 0};
	
    # this is in fact the 'real' update: we increment the byte counter
    userlist[user][host][times][mode]=long(userlist[user][host][times][mode]) + long(bytes);
    
# load database
try:
    dbfile = open (database,"r");
    s = dbfile.readline();
    l = cPickle.loads(decompress(dbfile.read()));
    userlist = l[0];
    loglist = l[1];
    oldstamp = l[2];
    dbfile.close();
except IOError:
    pass;    # no panic if file not found

log=open(muddle_log,"r");		# open logfile

rows=1;
s = log.readline();
while s != '':				# read and analize all lines from logfile

    m = re_timestamp.search(s);
    if not m:	# huh!
	print "Error in logfile at line "+str(rows)+":";
	print s;
	print "Invalid FTPD logfile format! - Aborting MuddleStats\n";
	raise IOError;
    timestamp=strptime(m.group());

    m = re_server_exit.search(s);
    if m:				# muddleftpd stopped. all users kicked
	loglist.clear();

    m = re_thread_id.search(s);
    if m and timestamp>oldstamp:	# if we read 'younger' data than the old stamp

	currt = m.group(); 		# get current thread
	if not loglist.has_key(currt):
	    loglist[currt]={"RETRFLAG": 0,"STORFLAG": 0};

        m = re_retr.search(s);
        if m:				# we got 'retrieve'
	    ss = m.group();
	    loglist[currt]["RETRFLAG"]=1;

        m = re_stor.search(s);
        if m:				# we got 'store'
	    ss = m.group();
	    loglist[currt]["STORFLAG"]=1;

	m = re_finish.search(s);
        if m and loglist[currt]["RETRFLAG"]==1:		# we got 'transfer done' after 'retrieve'
	    loglist[currt]["RETRFLAG"]=0;
	    updatedb(s,"RETR");
        elif m and loglist[currt]["STORFLAG"]==1:	# we got 'transfer done' after 'store'
	    loglist[currt]["STORFLAG"]=0;
	    updatedb(s,"STOR");

	m = re_logout.search(s);
	if m: 				# user logged out - drop thread
	    del loglist[currt];
	elif loglist[currt]["RETRFLAG"]==0 and loglist[currt]["STORFLAG"]==0: 
	    del loglist[currt]; 	# drop inactive tread

    s = log.readline();
    rows = rows+1;

#pprint.pprint(loglist);
#pprint.pprint(userlist);

log.close();

# save database
dbfile = open (database,"w");
dbfile.writelines(["MuddleStats datafile v2 - DO NOT MODIFY\n"]);
dbfile.write(compress(cPickle.dumps([userlist,loglist,timestamp])));
dbfile.close();

