/* ==================================================================== * Public domain * Version 20031023 * * ==================================================================== * * This module would not have been possible without taking a very close * look at source code originally written by or for the Apache Software * Foundation (see http://www.apache.org). That's why a few dozen lines * might look familiar :-] */ /* * mod_log_mysql, 23. October 2003 * * Logging into an SQL database. Needs the modular mod_log_config, also * available from http://bitbrook.de/software/mod_log_mysql/ . * * Changes since 20031005: * - works without threads, i.e. if APR_HAS_THREADS == 0 * --- st */ /* * NOTE: There's a bug in apr_reslist.c <= 0.9.4: * apr_reslist_acquire() always returns success, even if no resource * was created. This module would therefore crash due to the bogus * resource returned whenever the database server is not available * upon an _initial_ connection attempt. * * A work-around in this module would mean creating zombie resources, * so I chose to do it the right way in here and instead supply a patch * for apr_reslist.c to fix this bug. * The patch should have come with this file or is available on the Net * at http://bitbrook.de/software/mod_log_mysql/apr_reslist.diff . * * This bug has been fixed in Apache 2.0.48. */ #include "apr_strings.h" #include "apr_lib.h" #include "apr_hash.h" #include "apr_optional.h" #include "apr_reslist.h" #define APR_WANT_STRFUNC #include "apr_want.h" #include "ap_config.h" #include "mod_log_config.h" #include "httpd.h" #include "http_config.h" #include "http_log.h" #include "http_protocol.h" #include "util_time.h" #include #include #if APR_HAVE_UNISTD_H #include #endif #ifdef HAVE_LIMITS_H #include #endif /* Some default database connection parameters: */ #define DBCONNECTION_INITIAL 0 #define DBCONNECTION_SOFT 1 #define DBCONNECTION_HARD 256 #define DBCONNECTION_TIMEOUT 36000 #define FALLBACK_RETRY_DELAY 30 /* * This module uses a connection pool. In a threaded Apache, connection(s) * to the same database server (actually: log target) are shared among * childs and additional connections will be opened as needed. * * DBCONNECTION_INITIAL is the number of initial connections per pool, * _HARD is the absolute maximum number of connections (threads will * have to wait if this limit is reached), _TIMEOUT is the duration an * unused connection is kept before removing it, _SOFT is the minimum * number of connections to keep in any case, regardless of _TIMEOUT. * * 1. DBCONNECTION_INITIAL is 0 to avoid creating MySQL connections within * the master process, which are a) not needed by the master and b) I'm * not sure of if they can simply be copied to the childs. * As soon as needed, a child will open its own connection and keep at * least _SOFT connections open until it dies anyway. * Unfortunatly no initial connections means you will not notice a broken * log URI or an unreachable MySQL server until you actually start logging. * Does this need to be fixed? xxx * 2. There is no need to raise DBCONNECTION_SOFT (and _INITIAL) above 1 * if you are not using a threaded Apache. Pools only work per process. * 3. DBCONNECTION_TIMEOUT interferes with MySQL's own connection timeout. * Use a "mod_log_mysql" paragraph in your my.cnf to set this limit (and * other stuff) withing the MySQL configuration (that's why it is set to * ten hours up there). * * FALLBACK_RETRY_DELAY is the time between two attempts to open the * database connection again after it failed. */ module AP_MODULE_DECLARE_DATA log_mysql_module; /* module configuration block: */ typedef struct { const char *fallbackDir; int reconnect_timeout; } mysql_log_config; /* A data block to manage a single log target: */ typedef struct { #if APR_HAS_THREADS apr_reslist_t *dbs; /* connection pool */ #else MYSQL *db; /* no threads, no pool */ #endif const char *uri; /* the complete log uri.. */ char *host; /* ..and its parts */ char *user; char *passwd; int port; char *socket; char *database; char *fn; /* fallback file name */ apr_file_t *fh; /* " file handle */ apr_time_t ft; /* " start time, also indicates fallback active if > 0 */ apr_pool_t *p; /* dbs above uses this pool to create database connections */ } mysql_log; /* The list which keeps all known log targets, i.e. mysql_log items */ static apr_hash_t *db_hash; /* open a new database connection, called automatically by mysql_log.dbs */ static apr_status_t open_db_connection(void **resource, void *param, apr_pool_t *p) { mysql_log *l = param; MYSQL *db; db = apr_palloc(p, sizeof(MYSQL)); mysql_init(db); mysql_options(db, MYSQL_READ_DEFAULT_GROUP, "mod_log_mysql"); if (! mysql_real_connect(db, l->host, l->user, l->passwd, l->database, l->port, l->socket, 0)) { ap_log_perror(APLOG_MARK, APLOG_CRIT, 0, p, "log database %s: %s", l->uri, mysql_error(db)); *resource = NULL; return !APR_SUCCESS; } else { *resource = db; return APR_SUCCESS; } } /* close a database connection, called automatically by mysql_log.dbs, too */ static apr_status_t close_db_connection(void *resource, void *param, apr_pool_t *p) { if (resource) { mysql_close((MYSQL *)resource); } } /* setup a new log target, called from mod_log_config */ static void *mysql_log_setup(apr_pool_t *p, server_rec *s, const char* name) { mysql_log *l; char *uri; char *c = NULL; apr_status_t as; mysql_log_config *conf = ap_get_module_config(s->module_config, &log_mysql_module); if (! (l = apr_hash_get(db_hash, name, APR_HASH_KEY_STRING))) { l = apr_palloc(p, sizeof(mysql_log)); l->p = p; /* for use with sql_fallback */ #if APR_HAS_THREADS /* no initial connections created here in order to avoid opening things in the root process */ if (apr_reslist_create(&l->dbs, DBCONNECTION_INITIAL, DBCONNECTION_SOFT, DBCONNECTION_HARD, DBCONNECTION_TIMEOUT, open_db_connection, close_db_connection, l, p) != APR_SUCCESS) { return NULL; } #else l->db = NULL; #endif l->uri = apr_pstrdup(p, name); /* keep our full name */ uri = apr_pstrdup(p,name); /* duplicate for l.user, .passwd, etc. */ l->user = NULL; /* initialize connection data */ l->passwd = NULL; l->host = NULL; l->port = 0; l->socket = 0; if (l->database = ap_strrchr(uri,'/')) { /* got /database or /sock/et/ */ if (*(l->database + 1) == '\0') { /* found slash is last character in name, i.e. part of socket path */ c = l->database; /* keep position for socket below */ l->database = NULL; } else { *l->database = '\0'; l->database++; } } /* xxx needs better handling of invalid uris, especially empty fields */ if (l->database != uri + 1) { /* name is not "/database", look for host and user */ l->host = ap_strchr(uri,'@'); if (l->host ) { /* got @host, @host:sock/et/ or @host:port */ *l->host = '\0'; l->host++; l->socket = ap_strchr(l->host,':'); if ((*l->host == ':') || (*l->host == '\0')) /* empty host */ l->host = NULL; if (l->socket) { /* got socket or port. end is determined by 'database' above */ *l->socket = '\0'; l->socket++; if (*l->socket == '\0') { /* empty socket */ l->socket = NULL; } else { if (c) /* set eos marker at beginning of database */ *c = '\0'; l->port = apr_strtoi64(l->socket,&c,10); if (*c == '\0') { l->socket = NULL; } else { l->port = 0; l->socket = ap_server_root_relative(p, l->socket); } } } } if (l->host != uri + 1) { /* name is not "@host..", read user!passwd or user */ l->user = uri; /* got user or user!passwd. end is determined by 'host' or 'database' above */ if (l->passwd = ap_strchr(l->user, '!')) { /* got !passwd */ *l->passwd = '\0'; l->passwd++; } } } if (l->passwd) { /* hide password */ c = ap_strchr(l->uri, '!'); c++; while ((*c != '@') && (*c != '/') && (*c !='\0')) { *c = 'X'; c++; } } l->fh = NULL; l->ft = 0; if (! conf->fallbackDir) { l->fn = NULL; } else { l->fn = apr_pstrdup(p, l->uri); c = l->fn; while (*c != '\0') { /* make uri filesystem-safe, might be a bit..dumb */ if (! apr_isalnum(*c)) { *c = '_'; } c++; } if (as = apr_filepath_merge(&l->fn, conf->fallbackDir, l->fn, APR_FILEPATH_SECUREROOT, p) != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, as, s, "cannot merge fallback dir path and fallback filename"); return NULL; } l->fn = ap_server_root_relative(p, l->fn); } apr_hash_set(db_hash, name, APR_HASH_KEY_STRING, l); } return l; } /* write to file if database connection is down, called from mysql_log_write */ static void sql_fallback(request_rec *r, mysql_log *l, char *s) { apr_status_t as; if ((l->ft) && (! l->fh)) { /* could not open fallback file earlier */ return; } if (! l->ft) { /* called the first time: activate timer, output a warning */ l->ft = r->request_time; if (! l->fn) { /* no fallback directory */ ap_log_rerror(APLOG_MARK, APLOG_ALERT, 0, r, "log database server gone, no fallback file specified, will loose log data!"); return; } ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "log database server gone. trying fallback %s.", l->fn); } if (! l->fh) { as = apr_file_open(&l->fh, l->fn, APR_WRITE | APR_CREATE | APR_APPEND | APR_XTHREAD, APR_OS_DEFAULT, l->p); if (as != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ALERT, as, r, "cannot open fallback file %s, will loose log data!", l->fn); return; } } apr_file_printf(l->fh, "%s" APR_EOL_STR, s); } /* log a request */ static apr_status_t mysql_log_write(request_rec *r, void *handle, apr_array_header_t *data) { char *str; char *s; char **strs; int *strl; int i,j,a; int len = 0; apr_status_t rv; const ap_log_ehandler_data *d; apr_time_exp_t xt; apr_size_t retcode; char tstr[15]; MYSQL *db; mysql_log *l = (mysql_log*)handle; mysql_log_config *conf; #if APR_HAS_THREADS if (l->ft) { /* connection failed in an earlier request */ conf = ap_get_module_config(r->server->module_config, &log_mysql_module); if (l->ft > r->request_time - apr_time_from_sec(conf->reconnect_timeout)) { if (apr_reslist_acquire(l->dbs, (void *)&db) != APR_SUCCESS) { db = NULL; } l->ft = r->request_time; /* reset connection retry timer */ } } else if (apr_reslist_acquire(l->dbs, (void *)&db) != APR_SUCCESS) { db = NULL; } #else if ((! l->db) && (open_db_connection((void*)&l->db, l, l->p) != APR_SUCCESS)) { db = NULL; } else { db = l->db; } #endif strs = apr_palloc(r->pool, sizeof(char *) * (data->nelts)); strl = apr_palloc(r->pool, sizeof(int) * (data->nelts)); for (i = 0; i < data->nelts; ++i) { d=&(((ap_log_ehandler_data*)(data->elts))[i]); if ((d) && (d->data)) { switch (d->type) { case AP_LOG_EHANDLER_RETURN_OLDSTYLE: case AP_LOG_EHANDLER_RETURN_STRING: j = strlen(d->data); strs[i] = apr_palloc(r->pool, 2*j+3); if (d->arg && (apr_strnatcasecmp(d->arg,"mysqlname")==0 )) { for (a = 0, s = d->data; *s != '\0'; s++) { if (isalnum(*s)) { strs[i][a] = *s; a++; } } strl[i] = a; } else { strs[i][0] = '"'; if (db) { strl[i] = mysql_real_escape_string(db, strs[i] + 1, d->data, j) + 2; } else { /* database failed */ strl[i] = mysql_escape_string(strs[i] + 1, d->data, j) + 2; } strs[i][strl[i] - 1] = '"'; } strs[i][strl[i]] = '\0'; break; case AP_LOG_EHANDLER_RETURN_CONST: strs[i] = d->data; strl[i] = strlen(strs[i]); break; case AP_LOG_EHANDLER_RETURN_NUMBER: strs[i] = apr_psprintf(r->pool,"%" AP_LOG_NUMBER_T_FMT,*(ap_log_number_t *) d->data); // xxx strl[i] = strlen(strs[i]); break; case AP_LOG_EHANDLER_RETURN_UNUMBER: strs[i] = apr_psprintf(r->pool,"%" AP_LOG_UNUMBER_T_FMT,*(ap_log_unumber_t *) d->data); // xxx strl[i] = strlen(strs[i]); break; case AP_LOG_EHANDLER_RETURN_DATETIME: ap_explode_recent_localtime(&xt, *(apr_time_t*)d->data); /* if (d->arg && (*(char *)(d->arg))) { xxx */ apr_strftime(tstr, &retcode, 15, "%Y%m%d%H%M%S", &xt); strs[i] = apr_pstrdup(r->pool, tstr); strl[i] = strlen(strs[i]); break; } } else { strs[i] = "NULL"; strl[i] = 4; } len += strl[i]; } str = apr_palloc(r->pool, len + 1); for (i = 0, s = str; i < data->nelts; ++i) { memcpy(s, strs[i], strl[i]); s += strl[i]; } *s = '\0'; if (!db) { sql_fallback(r, l, str); } else if (mysql_real_query(db, str, len)) { switch (mysql_errno(db)) { case CR_SERVER_LOST: case CR_SERVER_GONE_ERROR: case CR_CONNECTION_ERROR: sql_fallback(r, l, str); break; default: ap_log_rerror(APLOG_MARK, APLOG_CRIT, 0, r, "\"%s\": %s", str, mysql_error(db)); } } else if (l->ft) { ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "resuming logging to database %s", l->uri); l->ft = 0; /* connection open again, unset connection retry timer */ } #if APR_HAS_THREADS if (db) { apr_reslist_release(l->dbs, db); } #endif return OK; } /***************************************************************** * * Module glue... */ static void *make_log_mysql_config(apr_pool_t *p, server_rec *s) { mysql_log_config *conf; conf = (mysql_log_config *)apr_palloc(p, sizeof(mysql_log_config)); conf->fallbackDir = NULL; conf->reconnect_timeout = FALLBACK_RETRY_DELAY; return conf; } static const char *logmysql_fallback(cmd_parms *cmd, void *dummy, const char *fn, const char *timeout) { mysql_log_config *conf = ap_get_module_config(cmd->server->module_config, &log_mysql_module); conf->fallbackDir = fn; if (timeout) { conf->reconnect_timeout = apr_atoi64(timeout); } return OK; } static const command_rec log_mysql_cmds[] = { AP_INIT_TAKE12("LogMySQLFallback", logmysql_fallback, NULL, RSRC_CONF, "directory to store SQL commands in if database connection is down and connection retry delay"), {NULL} }; static int log_mysql_pre_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp) { static APR_OPTIONAL_FN_TYPE(ap_register_log_ewriter) *register_writer; register_writer = APR_RETRIEVE_OPTIONAL_FN(ap_register_log_ewriter); if (register_writer) { register_writer(p, "mysql", mysql_log_setup, mysql_log_write, NULL, NULL); } return OK; } static apr_status_t log_mysql_child_exit(void *data) { apr_pool_t *p = data; apr_hash_index_t *i; mysql_log *l; for (i = apr_hash_first(p, db_hash); i; i = apr_hash_next(i)) { apr_hash_this(i, NULL, NULL, (void*) &l); #if APR_HAS_THREADS apr_reslist_destroy(l->dbs); #else close_db_connection(l->db, l, l->p); #endif } } static void log_mysql_child_init(apr_pool_t *p, server_rec *s) { apr_pool_cleanup_register(p, p, log_mysql_child_exit, log_mysql_child_exit); } static void register_hooks(apr_pool_t *p) { static const char *pre[] = { "mod_log_config.c", NULL }; /* register our log writer before mod_log_config starts */ db_hash = apr_hash_make(p); ap_hook_pre_config(log_mysql_pre_config, pre, NULL, APR_HOOK_REALLY_FIRST); ap_hook_child_init(log_mysql_child_init, NULL, NULL, APR_HOOK_MIDDLE); } module AP_MODULE_DECLARE_DATA log_mysql_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-dir config */ NULL, /* merge per-dir config */ make_log_mysql_config, /* server config */ NULL, /* merge server config */ log_mysql_cmds, /* command apr_table_t */ register_hooks /* register hooks */ };