/* $Id: mod_accounting.c,v 1.17 2002/09/08 15:17:22 tellini Exp $ */ /* Copyright (c) 2001-2002 Simone Tellini Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //#define DEBUG #include #include #include "mod_accounting.h" #define MOD_ACCOUNTING_VERSION_INFO_STRING "mod_accounting/0.5" module MODULE_VAR_EXPORT accounting_module; typedef int ( *DBQuery )( accounting_state *cfg, server_rec *server, pool *p, char *query ); typedef int ( *DBSetup )( accounting_state *cfg ); typedef void ( *DBClose )( accounting_state *cfg ); typedef struct _DBHandler { char *ID; DBSetup Setup; DBClose Close; DBQuery Query; } DBHandler; static const DBHandler DBDrivers[] = { #ifdef NEED_POSTGRES { "postgres", PgSetup, PgClose, PgQuery }, #endif #ifdef NEED_MYSQL { "mysql", MySetup, MyClose, MyQuery }, #endif }; #define DB_MAX ( sizeof( DBDrivers ) / sizeof( DBDrivers[0] )) // ------------------- UTILITY FUNCS ----------------- #ifdef DEBUG request_rec *rr; #endif // hook function pass to ap_table_do() static int GetHeaderLen( long *count, const char *key, const char *val ) { *count += strlen( key ) + strlen( val ) + 4; // 4 for ": " + CR + LF #ifdef DEBUG ap_log_error( APLOG_MARK, ERRLEVEL, rr->server, ap_pstrcat( rr->pool, key, ": ", val, NULL )); #endif return( 1 ); } // computes the length of a table static long TableLen( request_rec *r, table *tab ) { long count = 0; if( tab ) ap_table_do((int (*) (void *, const char *, const char *)) GetHeaderLen, (void *) &count, tab, NULL ); return( count ); } // computes the number of bytes sent static long BytesSent( request_rec *r ) { long sent, status_len = 0; char *custom_response; #ifdef DEBUG ap_log_error( APLOG_MARK, ERRLEVEL, rr->server, "BytesSent" ); #endif // let's see if it's a failed redirect // I'm using the same logic of ap_send_error_response() if( custom_response = (char *)ap_response_code_string( r, ap_index_of_response( r->status ))) { // if so, find the original request_rec if( custom_response[0] != '"' ) while( r->prev && ( r->prev->status != HTTP_OK )) r = r->prev; } if( r->status_line ) status_len = strlen( r->status_line ); sent = TableLen( r, r->headers_out ) + TableLen( r, r->err_headers_out ) + 2 + // 2 for CRLF 11 + status_len + // HTTP/1.x nnn blah 10 + strlen( ap_get_server_version() ) + // Server: line 8 + strlen( ap_gm_timestr_822( r->pool, r->request_time )); // Date: line if(( sent >= 255 ) && ( sent <= 257 )) sent += sizeof( "X-Pad: avoid browser bug" ) + 1; if( r->sent_bodyct ) { if( r->connection ) { long int bs; // this is more accurate than bytes_sent in presence of modules // which manipulate the output (eg. mod_gzip) ap_bgetopt( r->connection->client, BO_BYTECT, &bs ); sent += bs; } else sent += r->bytes_sent; } return( sent ); } // computes the number of bytes received static long BytesRecvd( request_rec *r ) { long recvd; const char *len; #ifdef DEBUG ap_log_error( APLOG_MARK, ERRLEVEL, rr->server, "BytesRecvd" ); #endif recvd = strlen( r->the_request ) + TableLen( r, r->headers_in ) + 4; // 2 for CRLF after the request, 2 for CRLF after all headers len = ap_table_get( r->headers_in, "Content-Length" ); if( len ) recvd += atol( len ); return( recvd ); } // extract the remote user name static char *get_user( request_rec *r ) { char *user = NULL; if( r ) user = r->connection->user; return( user ? user : "" ); } // should we ignore this host? static int ignore( request_rec *r, ignored_host *ign ) { int ret = 0; while( ign && !ret ) { unsigned int ip; memcpy( &ip, &r->connection->remote_addr.sin_addr, sizeof( ip )); switch( ign->Type ) { case IGN_MASK: if(( ign->IP & ign->Args.Mask ) == ( ip & ign->Args.Mask )) ret = 1; break; case IGN_RANGE: ip = (unsigned int)ntohl( ip ); if(((unsigned int)ntohl( ign->IP ) <= ip ) && ( ip <= (unsigned int)ntohl( ign->Args.IP2 ))) ret = 1; break; } ign = ign->Next; } return( ret ); } // ------------------- CONFIG ----------------- static char *set_db( cmd_parms *parms, void *dummy, char *arg ) { accounting_state *cfg = (accounting_state *) ap_get_module_config( parms->server->module_config, &accounting_module ); cfg->DBName = arg; return( NULL ); } static char *set_driver( cmd_parms *parms, void *dummy, char *arg ) { accounting_state *cfg = (accounting_state *) ap_get_module_config( parms->server->module_config, &accounting_module ); int i, found = 0; char *ptr = arg; while( *ptr ) *ptr++ = tolower( *ptr ); // let's see if we have the requested driver for( i = 0; i < DB_MAX; i++ ) if( !strcmp( DBDrivers[ i ].ID, arg )) { cfg->DBDriver = i; found = 1; break; } return( found ? NULL : "wrong DB driver" ); } static char *set_db_host( cmd_parms *parms, void *dummy, char *host, char *port ) { accounting_state *cfg = (accounting_state *) ap_get_module_config( parms->server->module_config, &accounting_module ); cfg->DBHost = host; cfg->DBPort = port; return( NULL ); } static char *set_login_info( cmd_parms *parms, void *dummy, char *user, char *pwd ) { accounting_state *cfg = (accounting_state *) ap_get_module_config( parms->server->module_config, &accounting_module ); cfg->DBUser = user; cfg->DBPwd = pwd; return( NULL ); } static char *set_query_fmt( cmd_parms *parms, void *dummy, char *arg ) { accounting_state *cfg = (accounting_state *) ap_get_module_config( parms->server->module_config, &accounting_module ); char *err = NULL; cfg->QueryFmt = arg; if( strstr( arg, "%u" ) && cfg->UpdateTimeSpan ) err = "You cannot use %u in the query format together with AccountingTimedUpdates!"; return( err ); } static char *set_timed_updates( cmd_parms *parms, void *dummy, char *arg ) { accounting_state *cfg = (accounting_state *) ap_get_module_config( parms->server->module_config, &accounting_module ); char *err = NULL; cfg->UpdateTimeSpan = atoi( arg ); if( cfg->QueryFmt && strstr( cfg->QueryFmt, "%u" )) err = "You cannot use %u in the query format together with AccountingTimedUpdates!"; return( err ); } static char *add_ignored_hosts( cmd_parms *parms, void *dummy, char *arg ) { accounting_state *cfg = (accounting_state *) ap_get_module_config( parms->server->module_config, &accounting_module ); ignored_host host; char *ptr, *err = NULL; if( ptr = strchr( arg, '-' )) { *ptr = '\0'; host.Type = IGN_RANGE; host.IP = inet_addr( arg ); *ptr++ = '-'; host.Args.IP2 = inet_addr( ptr ); if(( host.IP == INADDR_NONE ) || ( host.Args.IP2 == INADDR_NONE )) err = "Wrong range format"; } else if( ptr = strchr( arg, '/' )) { *ptr = '\0'; host.Type = IGN_MASK; host.IP = inet_addr( arg ); *ptr++ = '/'; host.Args.Mask = inet_addr( ptr ); if( host.IP == INADDR_NONE ) err = "Wrong IP address"; } else { host.Type = IGN_MASK; host.Args.Mask = 0xFFFFFFFF; host.IP = inet_addr( arg ); if( host.IP == INADDR_NONE ) err = "Wrong IP address"; } if( !err ) { ignored_host *ign = (ignored_host *)ap_palloc( parms->pool, sizeof( ignored_host )); memcpy( ign, &host, sizeof( ignored_host )); ign->Next = cfg->Ignore; cfg->Ignore = ign; } return( err ); } /* Setup of the available httpd.conf configuration commands. * command, function called, NULL, where available, how many arguments, verbose description */ typedef const char *(*hook_func)(); static command_rec acct_cmds[] = { { "AccountingQueryFmt", (hook_func) set_query_fmt, NULL, RSRC_CONF, TAKE1, "The query to execute to log the transactions. " "Available placeholders are %s for sent bytes, %r for received ones, " "%h for the virtual host name, %u for the user name." }, { "AccountingDatabase", (hook_func) set_db, NULL, RSRC_CONF, TAKE1, "The name of the database for logging" }, { "AccountingDatabaseDriver", (hook_func) set_driver, NULL, RSRC_CONF, TAKE1, "The kind of the database for logging" }, { "AccountingDBHost", (hook_func) set_db_host, NULL, RSRC_CONF, TAKE2, "Host and port needed to connect to the database" }, { "AccountingLoginInfo", (hook_func) set_login_info, NULL, RSRC_CONF, TAKE2, "User and password required for logging into the database" }, { "AccountingTimedUpdates", (hook_func) set_timed_updates, NULL, RSRC_CONF, TAKE1, "Number of seconds to wait between 2 update queries (performance tuning)" }, { "AccountingIgnoreHosts", (hook_func) add_ignored_hosts, NULL, RSRC_CONF, ITERATE, "Ip of hosts to ignore" }, { NULL } }; // ------------------- HOOKS ----------------- /* Set up space for the various major configuration options */ static void *acct_make_state( pool *p, server_rec *s ) { accounting_state *cfg = ( accounting_state * ) ap_palloc( p, sizeof( accounting_state )); memset( cfg, 0, sizeof( *cfg )); return( cfg ); } /* Routine to perform the actual construction and execution of the relevant * query */ static void do_query( accounting_state *cfg, pool *p, server_rec *server, request_rec *r ) { if(!( cfg->Sent || cfg->Received )) return; #ifdef DEBUG if( !server ) ap_log_error( APLOG_MARK, ERRLEVEL, rr->server, "do_query() - server is NULL" ); if( !p ) ap_log_error( APLOG_MARK, ERRLEVEL, rr->server, "do_query() - p is NULL" ); #endif if(!( *DBDrivers[ cfg->DBDriver ].Setup )( cfg )) { ap_log_error( APLOG_MARK, ERRLEVEL, server, "Accounting: couldn't setup the database link!" ); } else { char *query = "", *ptr = cfg->QueryFmt; char sent[ 32 ], recvd[ 32 ]; sprintf( sent, "%ld", cfg->Sent ); sprintf( recvd, "%ld", cfg->Received ); // build the query string from the template while( ptr ) { char *next; next = strchr( ptr, '%' ); if( next ) { char tmp[ 2 ]; *next++ = '\0'; switch( *next++ ) { case 'h': query = ap_pstrcat( p, query, ptr, cfg->ServerName ? cfg->ServerName : "-", NULL ); break; case 's': query = ap_pstrcat( p, query, ptr, sent, NULL ); break; case 'r': query = ap_pstrcat( p, query, ptr, recvd, NULL ); break; case 'u': query = ap_pstrcat( p, query, ptr, get_user( r ), NULL ); break; default: tmp[0] = next[ -1 ]; tmp[1] = '\0'; query = ap_pstrcat( p, query, ptr, tmp, NULL ); break; } next[ -2 ] = '%'; } else query = ap_pstrcat( p, query, ptr, NULL ); ptr = next; } ( *DBDrivers[ cfg->DBDriver ].Query )( cfg, server, p, query ); cfg->Received = cfg->Sent = 0; } } static int acct_transaction( request_rec *orig ) { accounting_state *cfg = ap_get_module_config( orig->server->module_config, &accounting_module ); request_rec *r = orig; int do_it = 1; char *host; // get to the last of the chain, we need the correct byte counters ;-) while( r->next ) r = r->next; if( ignore( r, cfg->Ignore )) return( OK ); #ifdef DEBUG rr = r; #endif host = (char *)ap_get_server_name( orig ); #ifdef DEBUG if( !host ) ap_log_error( APLOG_MARK, ERRLEVEL, rr->server, "ap_get_server_name() returned NULL" ); #endif if( strcmp( host, cfg->ServerName )) { // different host than the last one served? if( cfg->UpdateTimeSpan ) // if we were piggy-backing, perform an update for the old host do_query( cfg, r->pool, r->server, NULL ); strncpy( cfg->ServerName, host, sizeof( cfg->ServerName )); cfg->ServerName[ sizeof( cfg->ServerName ) - 1 ] = '\0'; } cfg->Received += BytesRecvd( orig ); cfg->Sent += BytesSent( r ); if( cfg->UpdateTimeSpan ) { // group updates? time_t now; time( &now ); if( now - cfg->LastUpdate < cfg->UpdateTimeSpan ) do_it = 0; else cfg->LastUpdate = now; } if( do_it ) do_query( cfg, r->pool, r->server, r ); return( OK ); } /* Called on the exit of an httpd child process */ static void acct_child_exit( server_rec *s, pool *p ) { accounting_state *cfg = ap_get_module_config( s->module_config, &accounting_module ); if( cfg->Sent || cfg->Received ) do_query( cfg, p, s, NULL ); ( *DBDrivers[ cfg->DBDriver ].Close )( cfg ); } static void mod_acct_init( server_rec *server, pool *p ) { ap_add_version_component(MOD_ACCOUNTING_VERSION_INFO_STRING); } // ------------------- MOD CONFIG ----------------- /* The configuration array that sets up the hooks into the module. */ module accounting_module = { STANDARD_MODULE_STUFF, mod_acct_init, /* initializer */ NULL, /* create per-dir config */ NULL, /* merge per-dir config */ acct_make_state, /* server config */ NULL, /* merge server config */ acct_cmds, /* command table */ NULL, /* handlers */ NULL, /* filename translation */ NULL, /* check_user_id */ NULL, /* check auth */ NULL, /* check access */ NULL, /* type_checker */ NULL, /* fixups */ acct_transaction, /* logger */ #if MODULE_MAGIC_NUMBER >= 19970103 NULL, /* header parser */ #endif #if MODULE_MAGIC_NUMBER >= 19970719 NULL, /* child_init */ #endif #if MODULE_MAGIC_NUMBER >= 19970728 acct_child_exit, /* process exit/cleanup */ #endif #if MODULE_MAGIC_NUMBER >= 19970902 NULL /* [#0] post read-request */ #endif };