#include <freeradius-devel/ident.h>
RCSID("$Id$")
#include <freeradius-devel/radiusd.h>
#include <freeradius-devel/modules.h>
#include <ctype.h>
#define MAX_QUERY_LEN 1024
static int sqlcounter_detach(void *instance);
typedef struct rlm_sqlcounter_t {
char *counter_name;
char *check_name;
char *reply_name;
char *key_name;
char *sqlmod_inst;
char *query;
char *reset;
char *allowed_chars;
time_t reset_time;
time_t last_reset;
int key_attr;
int dict_attr;
int reply_attr;
} rlm_sqlcounter_t;
static const CONF_PARSER module_config[] = {
{ "counter-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,counter_name), NULL, NULL },
{ "check-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,check_name), NULL, NULL },
{ "reply-name", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reply_name), NULL, NULL },
{ "key", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,key_name), NULL, NULL },
{ "sqlmod-inst", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,sqlmod_inst), NULL, NULL },
{ "query", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,query), NULL, NULL },
{ "reset", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,reset), NULL, NULL },
{ "safe-characters", PW_TYPE_STRING_PTR, offsetof(rlm_sqlcounter_t,allowed_chars), NULL, "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /"},
{ NULL, -1, 0, NULL, NULL }
};
static char *allowed_chars = NULL;
static size_t sql_escape_func(char *out, size_t outlen, const char *in)
{
int len = 0;
while (in[0]) {
if ((in[0] < 32) ||
strchr(allowed_chars, *in) == NULL) {
if (outlen <= 3) {
break;
}
snprintf(out, outlen, "=%02X", (unsigned char) in[0]);
in++;
out += 3;
outlen -= 3;
len += 3;
continue;
}
if (outlen <= 1) {
break;
}
*out = *in;
out++;
in++;
outlen--;
len++;
}
*out = '\0';
return len;
}
static int find_next_reset(rlm_sqlcounter_t *data, time_t timeval)
{
int ret = 0;
size_t len;
unsigned int num = 1;
char last = '\0';
struct tm *tm, s_tm;
char sCurrentTime[40], sNextTime[40];
tm = localtime_r(&timeval, &s_tm);
len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
if (len == 0) *sCurrentTime = '\0';
tm->tm_sec = tm->tm_min = 0;
if (data->reset == NULL)
return -1;
if (isdigit((int) data->reset[0])){
len = strlen(data->reset);
if (len == 0)
return -1;
last = data->reset[len - 1];
if (!isalpha((int) last))
last = 'd';
num = atoi(data->reset);
DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
}
if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
tm->tm_hour += num;
data->reset_time = mktime(tm);
} else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
tm->tm_hour = 0;
tm->tm_mday += num;
data->reset_time = mktime(tm);
} else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
tm->tm_hour = 0;
tm->tm_mday += (7 - tm->tm_wday) +(7*(num-1));
data->reset_time = mktime(tm);
} else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
tm->tm_hour = 0;
tm->tm_mday = 1;
tm->tm_mon += num;
data->reset_time = mktime(tm);
} else if (strcmp(data->reset, "never") == 0) {
data->reset_time = 0;
} else {
radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
data->reset);
return -1;
}
len = strftime(sNextTime, sizeof(sNextTime),"%Y-%m-%d %H:%M:%S",tm);
if (len == 0) *sNextTime = '\0';
DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Next reset %li [%s]",
timeval, sCurrentTime, data->reset_time, sNextTime);
return ret;
}
static int find_prev_reset(rlm_sqlcounter_t *data, time_t timeval)
{
int ret = 0;
size_t len;
unsigned int num = 1;
char last = '\0';
struct tm *tm, s_tm;
char sCurrentTime[40], sPrevTime[40];
tm = localtime_r(&timeval, &s_tm);
len = strftime(sCurrentTime, sizeof(sCurrentTime), "%Y-%m-%d %H:%M:%S", tm);
if (len == 0) *sCurrentTime = '\0';
tm->tm_sec = tm->tm_min = 0;
if (data->reset == NULL)
return -1;
if (isdigit((int) data->reset[0])){
len = strlen(data->reset);
if (len == 0)
return -1;
last = data->reset[len - 1];
if (!isalpha((int) last))
last = 'd';
num = atoi(data->reset);
DEBUG("rlm_sqlcounter: num=%d, last=%c",num,last);
}
if (strcmp(data->reset, "hourly") == 0 || last == 'h') {
tm->tm_hour -= num - 1;
data->last_reset = mktime(tm);
} else if (strcmp(data->reset, "daily") == 0 || last == 'd') {
tm->tm_hour = 0;
tm->tm_mday -= num - 1;
data->last_reset = mktime(tm);
} else if (strcmp(data->reset, "weekly") == 0 || last == 'w') {
tm->tm_hour = 0;
tm->tm_mday -= (7 - tm->tm_wday) +(7*(num-1));
data->last_reset = mktime(tm);
} else if (strcmp(data->reset, "monthly") == 0 || last == 'm') {
tm->tm_hour = 0;
tm->tm_mday = 1;
tm->tm_mon -= num - 1;
data->last_reset = mktime(tm);
} else if (strcmp(data->reset, "never") == 0) {
data->reset_time = 0;
} else {
radlog(L_ERR, "rlm_sqlcounter: Unknown reset timer \"%s\"",
data->reset);
return -1;
}
len = strftime(sPrevTime, sizeof(sPrevTime), "%Y-%m-%d %H:%M:%S", tm);
if (len == 0) *sPrevTime = '\0';
DEBUG2("rlm_sqlcounter: Current Time: %li [%s], Prev reset %li [%s]",
timeval, sCurrentTime, data->last_reset, sPrevTime);
return ret;
}
static int sqlcounter_expand(char *out, int outlen, const char *fmt, void *instance)
{
rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
int c,freespace;
const char *p;
char *q;
char tmpdt[40];
q = out;
for (p = fmt; *p ; p++) {
freespace = outlen - (q - out);
if (freespace <= 1)
break;
c = *p;
if ((c != '%') && (c != '\\')) {
*q++ = *p;
continue;
}
if (*++p == '\0') break;
if (c == '\\') switch(*p) {
case '\\':
*q++ = *p;
break;
case 't':
*q++ = '\t';
break;
case 'n':
*q++ = '\n';
break;
default:
*q++ = c;
*q++ = *p;
break;
} else if (c == '%') switch(*p) {
case '%':
*q++ = *p;
break;
case 'b':
snprintf(tmpdt, sizeof(tmpdt), "%lu", data->last_reset);
strlcpy(q, tmpdt, freespace);
q += strlen(q);
break;
case 'e':
snprintf(tmpdt, sizeof(tmpdt), "%lu", data->reset_time);
strlcpy(q, tmpdt, freespace);
q += strlen(q);
break;
case 'k':
strlcpy(q, data->key_name, freespace);
q += strlen(q);
break;
case 'S':
strlcpy(q, data->sqlmod_inst, freespace);
q += strlen(q);
break;
default:
*q++ = '%';
*q++ = *p;
break;
}
}
*q = '\0';
DEBUG2("sqlcounter_expand: '%s'", out);
return strlen(out);
}
static int sqlcounter_cmp(void *instance, REQUEST *req,
UNUSED VALUE_PAIR *request, VALUE_PAIR *check,
VALUE_PAIR *check_pairs, VALUE_PAIR **reply_pairs)
{
rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
int counter;
char querystr[MAX_QUERY_LEN];
char responsestr[MAX_QUERY_LEN];
check_pairs = check_pairs;
reply_pairs = reply_pairs;
sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance);
radius_xlat(responsestr, MAX_QUERY_LEN, querystr, req, sql_escape_func);
snprintf(querystr, sizeof(querystr), "%%{%%S:%s}", responsestr);
sqlcounter_expand(responsestr, MAX_QUERY_LEN, querystr, instance);
radius_xlat(querystr, MAX_QUERY_LEN, responsestr, req, sql_escape_func);
counter = atoi(querystr);
return counter - check->vp_integer;
}
static int sqlcounter_instantiate(CONF_SECTION *conf, void **instance)
{
rlm_sqlcounter_t *data;
DICT_ATTR *dattr;
ATTR_FLAGS flags;
time_t now;
char buffer[MAX_STRING_LEN];
data = rad_malloc(sizeof(*data));
if (!data) {
radlog(L_ERR, "rlm_sqlcounter: Not enough memory.");
return -1;
}
memset(data, 0, sizeof(*data));
if (cf_section_parse(conf, data, module_config) < 0) {
radlog(L_ERR, "rlm_sqlcounter: Unable to parse parameters.");
sqlcounter_detach(data);
return -1;
}
if (data->query == NULL) {
radlog(L_ERR, "rlm_sqlcounter: 'query' must be set.");
sqlcounter_detach(data);
return -1;
}
allowed_chars = data->allowed_chars;
if (data->key_name == NULL) {
radlog(L_ERR, "rlm_sqlcounter: 'key' must be set.");
sqlcounter_detach(data);
return -1;
}
sql_escape_func(buffer, sizeof(buffer), data->key_name);
if (strcmp(buffer, data->key_name) != 0) {
radlog(L_ERR, "rlm_sqlcounter: The value for option 'key' is too long or contains unsafe characters.");
sqlcounter_detach(data);
return -1;
}
dattr = dict_attrbyname(data->key_name);
if (dattr == NULL) {
radlog(L_ERR, "rlm_sqlcounter: No such attribute %s",
data->key_name);
sqlcounter_detach(data);
return -1;
}
data->key_attr = dattr->attr;
if (data->reply_name == NULL) {
DEBUG2("rlm_sqlcounter: Reply attribute set to Session-Timeout.");
data->reply_attr = PW_SESSION_TIMEOUT;
data->reply_name = strdup("Session-Timeout");
}
else {
dattr = dict_attrbyname(data->reply_name);
if (dattr == NULL) {
radlog(L_ERR, "rlm_sqlcounter: No such attribute %s",
data->reply_name);
sqlcounter_detach(data);
return -1;
}
data->reply_attr = dattr->attr;
DEBUG2("rlm_sqlcounter: Reply attribute %s is number %d",
data->reply_name, dattr->attr);
}
if (data->sqlmod_inst == NULL) {
radlog(L_ERR, "rlm_sqlcounter: 'sqlmod-inst' must be set.");
sqlcounter_detach(data);
return -1;
}
sql_escape_func(buffer, sizeof(buffer), data->sqlmod_inst);
if (strcmp(buffer, data->sqlmod_inst) != 0) {
radlog(L_ERR, "rlm_sqlcounter: The value for option 'sqlmod-inst' is too long or contains unsafe characters.");
sqlcounter_detach(data);
return -1;
}
if (data->counter_name == NULL) {
radlog(L_ERR, "rlm_sqlcounter: 'counter-name' must be set.");
sqlcounter_detach(data);
return -1;
}
memset(&flags, 0, sizeof(flags));
dict_addattr(data->counter_name, 0, PW_TYPE_INTEGER, -1, flags);
dattr = dict_attrbyname(data->counter_name);
if (dattr == NULL) {
radlog(L_ERR, "rlm_sqlcounter: Failed to create counter attribute %s",
data->counter_name);
sqlcounter_detach(data);
return -1;
}
data->dict_attr = dattr->attr;
DEBUG2("rlm_sqlcounter: Counter attribute %s is number %d",
data->counter_name, data->dict_attr);
if (data->check_name == NULL) {
radlog(L_ERR, "rlm_sqlcounter: 'check-name' must be set.");
sqlcounter_detach(data);
return -1;
}
dict_addattr(data->check_name, 0, PW_TYPE_INTEGER, -1, flags);
dattr = dict_attrbyname(data->check_name);
if (dattr == NULL) {
radlog(L_ERR, "rlm_sqlcounter: Failed to create check attribute %s",
data->check_name);
sqlcounter_detach(data);
return -1;
}
DEBUG2("rlm_sqlcounter: Check attribute %s is number %d",
data->check_name, dattr->attr);
if (data->reset == NULL) {
radlog(L_ERR, "rlm_sqlcounter: 'reset' must be set.");
sqlcounter_detach(data);
return -1;
}
now = time(NULL);
data->reset_time = 0;
if (find_next_reset(data,now) == -1) {
radlog(L_ERR, "rlm_sqlcounter: Failed to find the next reset time.");
sqlcounter_detach(data);
return -1;
}
data->last_reset = 0;
if (find_prev_reset(data,now) == -1) {
radlog(L_ERR, "rlm_sqlcounter: Failed to find the previous reset time.");
sqlcounter_detach(data);
return -1;
}
paircompare_register(data->dict_attr, 0, sqlcounter_cmp, data);
*instance = data;
return 0;
}
static int sqlcounter_authorize(void *instance, REQUEST *request)
{
rlm_sqlcounter_t *data = (rlm_sqlcounter_t *) instance;
int ret=RLM_MODULE_NOOP;
unsigned int counter;
DICT_ATTR *dattr;
VALUE_PAIR *key_vp, *check_vp;
VALUE_PAIR *reply_item;
char msg[128];
char querystr[MAX_QUERY_LEN];
char responsestr[MAX_QUERY_LEN];
instance = instance;
request = request;
if (data->reset_time && (data->reset_time <= request->timestamp)) {
data->last_reset = data->reset_time;
find_next_reset(data,request->timestamp);
}
DEBUG2("rlm_sqlcounter: Entering module authorize code");
key_vp = (data->key_attr == PW_USER_NAME) ? request->username : pairfind(request->packet->vps, data->key_attr);
if (key_vp == NULL) {
DEBUG2("rlm_sqlcounter: Could not find Key value pair");
return ret;
}
if ((dattr = dict_attrbyname(data->check_name)) == NULL) {
return ret;
}
if ((check_vp= pairfind(request->config_items, dattr->attr)) == NULL) {
DEBUG2("rlm_sqlcounter: Could not find Check item value pair");
return ret;
}
sqlcounter_expand(querystr, MAX_QUERY_LEN, data->query, instance);
radius_xlat(responsestr, MAX_QUERY_LEN, querystr, request, sql_escape_func);
snprintf(querystr, sizeof(querystr), "%%{%%S:%s}", responsestr);
sqlcounter_expand(responsestr, MAX_QUERY_LEN, querystr, instance);
radius_xlat(querystr, MAX_QUERY_LEN, responsestr, request, sql_escape_func);
if (sscanf(querystr, "%u", &counter) != 1) {
DEBUG2("rlm_sqlcounter: No integer found in string \"%s\"",
querystr);
return RLM_MODULE_NOOP;
}
if (check_vp->vp_integer > counter) {
unsigned int res = check_vp->lvalue - counter;
DEBUG2("rlm_sqlcounter: Check item is greater than query result");
if (data->reset_time &&
(res >= (data->reset_time - request->timestamp))) {
res = data->reset_time - request->timestamp;
res += check_vp->vp_integer;
}
if ((reply_item = pairfind(request->reply->vps, data->reply_attr)) != NULL) {
if (reply_item->vp_integer > res)
reply_item->vp_integer = res;
} else {
reply_item = radius_paircreate(request,
&request->reply->vps,
data->reply_attr,
PW_TYPE_INTEGER);
reply_item->vp_integer = res;
}
ret=RLM_MODULE_OK;
DEBUG2("rlm_sqlcounter: Authorized user %s, check_item=%u, counter=%u",
key_vp->vp_strvalue,check_vp->vp_integer,counter);
DEBUG2("rlm_sqlcounter: Sent Reply-Item for user %s, Type=%s, value=%u",
key_vp->vp_strvalue,data->reply_name,reply_item->vp_integer);
}
else{
char module_fmsg[MAX_STRING_LEN];
VALUE_PAIR *module_fmsg_vp;
DEBUG2("rlm_sqlcounter: (Check item - counter) is less than zero");
snprintf(msg, sizeof(msg), "Your maximum %s usage time has been reached", data->reset);
reply_item=pairmake("Reply-Message", msg, T_OP_EQ);
pairadd(&request->reply->vps, reply_item);
snprintf(module_fmsg, sizeof(module_fmsg), "rlm_sqlcounter: Maximum %s usage time reached", data->reset);
module_fmsg_vp = pairmake("Module-Failure-Message", module_fmsg, T_OP_EQ);
pairadd(&request->packet->vps, module_fmsg_vp);
ret=RLM_MODULE_REJECT;
DEBUG2("rlm_sqlcounter: Rejected user %s, check_item=%u, counter=%u",
key_vp->vp_strvalue,check_vp->vp_integer,counter);
}
return ret;
}
static int sqlcounter_detach(void *instance)
{
int i;
char **p;
rlm_sqlcounter_t *inst = (rlm_sqlcounter_t *)instance;
allowed_chars = NULL;
paircompare_unregister(inst->dict_attr, sqlcounter_cmp);
for (i = 0; module_config[i].name != NULL; i++) {
if (module_config[i].type != PW_TYPE_STRING_PTR) {
continue;
}
p = (char **) (((char *)inst) + module_config[i].offset);
if (!*p) {
continue;
}
free(*p);
*p = NULL;
}
free(inst);
return 0;
}
module_t rlm_sqlcounter = {
RLM_MODULE_INIT,
"SQL Counter",
RLM_TYPE_THREAD_SAFE,
sqlcounter_instantiate,
sqlcounter_detach,
{
NULL,
sqlcounter_authorize,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL
},
};