/* This file is part of vmod-dbrw
Copyright (C) 2013-2018 Sergey Poznyakoff
Vmod-dbrw 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 3, or (at your option)
any later version.
Vmod-dbrw is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with vmod-dbrw. If not, see .
*/
#include "dbrw.h"
#include
#include "wordsplit.h"
#include "pthread.h"
enum {
QDISP_NONE,
QDISP_APPEND,
QDISP_DISCARD
};
void
dbrw_debug(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsyslog(LOG_DAEMON|LOG_DEBUG, fmt, ap);
va_end(ap);
}
void
dbrw_error(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsyslog(LOG_DAEMON|LOG_ERR, fmt, ap);
va_end(ap);
}
static pthread_mutex_t config_pool_mtx = PTHREAD_MUTEX_INITIALIZER;
static VTAILQ_HEAD(, dbrw_config) config_pool =
VTAILQ_HEAD_INITIALIZER(config_pool);
static pthread_mutex_t connect_pool_mtx = PTHREAD_MUTEX_INITIALIZER;
static VTAILQ_HEAD(, dbrw_connection) connect_pool =
VTAILQ_HEAD_INITIALIZER(connect_pool);
static struct dbrw_connection *
dbrw_connection_get(struct dbrw_config *cfg)
{
struct dbrw_connection *cp;
AZ(pthread_mutex_lock(&connect_pool_mtx));
VTAILQ_FOREACH(cp, &connect_pool, list) {
CHECK_OBJ_NOTNULL(cp, DBRW_CONNECTION_MAGIC);
if (cp->conf == cfg && !cp->busy)
break;
}
if (!cp) {
ALLOC_OBJ(cp, DBRW_CONNECTION_MAGIC);
AN(cp);
cp->state = state_init;
cp->conf = cfg;
cp->data = NULL;
cp->matches = NULL;
cp->matchsize = 0;
if (sql_init(cp)) {
free(cp);
cp = NULL;
} else
VTAILQ_INSERT_HEAD(&connect_pool, cp, list);
} else if (cp->state == state_connected
&& cfg->idle_timeout >= 0
&& cp->timestamp + cfg->idle_timeout < time(NULL)) {
sql_disconnect(cp);
}
cp->busy = 1;
if (cfg->idle_timeout == -2) {
cfg->idle_timeout = sql_idle_timeout(cp);
if (cfg->idle_timeout < -1)
cfg->idle_timeout = -1;
}
pthread_mutex_unlock(&connect_pool_mtx);
return cp;
}
static void
dbrw_connection_release(struct dbrw_connection *cp)
{
AZ(pthread_mutex_lock(&connect_pool_mtx));
cp->busy = 0;
pthread_mutex_unlock(&connect_pool_mtx);
}
static void
disconnect(void)
{
AZ(pthread_mutex_lock(&connect_pool_mtx));
while (!VTAILQ_EMPTY(&connect_pool)) {
struct dbrw_connection *cp = VTAILQ_FIRST(&connect_pool);
VTAILQ_REMOVE(&connect_pool, cp, list);
sql_disconnect(cp);
sql_destroy(cp);
}
pthread_mutex_unlock(&connect_pool_mtx);
}
int
dbrw_event(VRT_CTX, struct vmod_priv *priv, enum vcl_event_e e)
{
switch (e) {
case VCL_EVENT_LOAD:
atexit(disconnect);
break;
case VCL_EVENT_DISCARD:
break;
default:
/* ignore */
break;
}
return 0;
}
static void
argv_free(char **argv)
{
if (!argv)
return;
while (*argv) {
free(*argv);
++*argv;
}
}
static int
is_http_status(const char *arg)
{
if (strlen(arg) != HTTP_STATUS_LEN)
return 0;
return '1' <= arg[0] && arg[0] <= '5' &&
'0' <= arg[1] && arg[1] <= '9' &&
'0' <= arg[2] && arg[2] <= '9';
}
static int
parse_flags(const char *arg, int *qdisp, int *flags, char status[])
{
struct wordsplit ws;
int rc = 0;
int i;
if (!arg)
return 0;
ws.ws_delim = ",";
if (wordsplit(arg, &ws,
WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_QUOTE |
WRDSF_CESCAPES |
WRDSF_DELIM)) {
dbrw_error("cannot split string `%s': %s",
arg, wordsplit_strerror (&ws));
return 1;
}
for (i = 0; i < ws.ws_wordc; i++) {
if (strcmp(ws.ws_wordv[i], "nocase") == 0 ||
strcmp(ws.ws_wordv[i], "NC") == 0)
*flags |= REG_ICASE;
else if (strcmp(ws.ws_wordv[i], "case") == 0)
*flags &= ~REG_ICASE;
else if (strcmp(ws.ws_wordv[i], "qsappend") == 0 ||
strcmp(ws.ws_wordv[i], "QSA") == 0)
*qdisp = QDISP_APPEND;
else if (strcmp(ws.ws_wordv[i], "qsdiscard") == 0 ||
strcmp(ws.ws_wordv[i], "QSD") == 0)
*qdisp = QDISP_DISCARD;
else if (strncmp(ws.ws_wordv[i], "redirect=", 9) == 0) {
if (!is_http_status(ws.ws_wordv[i] + 9)) {
dbrw_error("invalid status code: %s",
ws.ws_wordv[i] + 9);
rc = 1;
} else
strncpy(status, ws.ws_wordv[i] + 9,
HTTP_STATUS_LEN);
} else if (strncmp(ws.ws_wordv[i], "R=", 2) == 0) {
if (!is_http_status(ws.ws_wordv[i] + 2)) {
dbrw_error("invalid status code: %s",
ws.ws_wordv[i] + 2);
rc = 1;
} else
strncpy(status, ws.ws_wordv[i] + 2,
HTTP_STATUS_LEN);
} else {
dbrw_error("unrecognized flag: %s", ws.ws_wordv[i]);
rc = 1;
}
}
wordsplit_free(&ws);
return rc;
}
/* Configure the module.
BACKEND - "mysql" or "pgsql"
PARAM - VAR=VALUE*
QUERY - Query to obtain the redirection target
*/
VCL_VOID
vmod_config(VRT_CTX, struct vmod_priv *priv,
VCL_STRING bkname, VCL_STRING param, VCL_STRING query)
{
struct dbrw_config *conf;
struct dbrw_backend *backend;
AZ(priv->priv);
backend = dbrw_backend_select(bkname);
if (!backend) {
dbrw_error("unsupported backend: %s", bkname);
abort();
}
AZ(pthread_mutex_lock(&config_pool_mtx));
VTAILQ_FOREACH(conf, &config_pool, list) {
CHECK_OBJ_NOTNULL(conf, DBRW_CONFIG_MAGIC);
if (conf->backend == backend
&& strcmp(conf->query, query) == 0
&& strcmp(conf->param_str, param) == 0)
break;
}
pthread_mutex_unlock(&config_pool_mtx);
if (!conf) {
char *s;
struct wordsplit ws;
ws.ws_delim = ";";
if (wordsplit(param, &ws,
WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_QUOTE |
WRDSF_CESCAPES |
WRDSF_DELIM)) {
dbrw_error("cannot split string `%s': %s",
param, wordsplit_strerror (&ws));
abort();
}
ALLOC_OBJ(conf, DBRW_CONFIG_MAGIC);
AN(conf);
conf->param = ws.ws_wordv;
ws.ws_wordv = NULL;
ws.ws_wordc = 0;
wordsplit_free(&ws);
conf->param_str = strdup(param);
AN(conf->param_str);
conf->query = strdup(query);
AN(conf->query);
conf->backend = backend;
if ((s = findparam(conf->param, "debug")) != NULL)
conf->debug_level = atoi(s);
if ((s = findparam(conf->param, "timeout")) != NULL)
conf->idle_timeout = atoi(s);
else
conf->idle_timeout = -2;
conf->qdisp = QDISP_NONE;
conf->regflags = REG_EXTENDED;
conf->status[0] = 0;
s = findparam(conf->param, "flags");
if (s)
parse_flags(s,
&conf->qdisp,
&conf->regflags,
conf->status);
AZ(pthread_mutex_lock(&config_pool_mtx));
VTAILQ_INSERT_HEAD(&config_pool, conf, list);
pthread_mutex_unlock(&config_pool_mtx);
}
priv->priv = conf;
}
static char *
expand_backref(VRT_CTX, const char *str, const char *val,
size_t matchcount, regmatch_t *matches, char *qry)
{
unsigned u;
char *b, *p;
u = WS_Reserve(WSPTR(ctx), 0);
p = b = WSPTR(ctx)->f;
while (*val) {
if (u == 0) {
WS_Release(WSPTR(ctx), 0);
return NULL;
}
if (*val == '\\' || *val == '$') {
if (val[1] >= 0 && val[1] <= '9') {
int n = val[1] - '0';
if (n < matchcount) {
size_t len;
len = matches[n].rm_eo -
matches[n].rm_so;
if (len > u) {
WS_Release(WSPTR(ctx), 0);
return NULL;
}
memcpy(p, str + matches[n].rm_so, len);
p += len;
u -= len;
val += 2;
continue;
}
} else if (*val == '\\' && val[1]) {
*p++ = val[1];
u--;
val += 2;
continue;
}
}
*p++ = *val++;
u--;
}
if (qry) {
size_t len = strlen(qry);
if (len + 1 >= u) {
WS_Release(WSPTR(ctx), 0);
return NULL;
}
if (strchr(b, '?'))
*p++ = '&';
else
*p++ = '?';
memcpy(p, qry, len);
p += len;
u -= len;
}
if (u == 0) {
WS_Release(WSPTR(ctx), 0);
return NULL;
}
*p++ = 0;
WS_ReleaseP(WSPTR(ctx), p);
return b;
}
#define ISEMPTY(s) ((s) == NULL || (s)[0] == 0)
static void
dbrw_sethdr(VRT_CTX, int where, const char *what, const char *value)
{
struct gethdr_s s = { where, what };
VRT_SetHdr(ctx, &s, value, vrt_magic_string_end);
}
static char *
findmatch(VRT_CTX, struct dbrw_connection *conn, char **param)
{
unsigned i;
unsigned nt, nf;
char *res;
struct wordsplit ws;
int wsflags = WRDSF_NOCMD | WRDSF_QUOTE |
WRDSF_NOSPLIT |
WRDSF_ENV | WRDSF_UNDEF;
if ((nt = sql_num_tuples(conn)) == 0 ||
(nf = sql_num_fields(conn)) == 0)
return NULL;
debug(conn->conf, 2, ("query returned %u tuples, %u columns", nt, nf));
if (nf < 3)
return WS_Copy(WSPTR(ctx), sql_get_column(conn, 0, 0), -1);
/* Three or four fields:
result
pattern
value
[flags]
*/
ws.ws_env = (const char **)param;
res = NULL;
for (i = 0; i < nt; i++) {
const char *pat = sql_get_column(conn, i, 1);
const char *val = sql_get_column(conn, i, 2);
regex_t re;
int rc;
size_t matchcount; /* Number of used entries in matches */
int rflags = conn->conf->regflags;
char status[HTTP_STATUS_LEN+1];
int qdisp = conn->conf->qdisp;
char *qry = NULL;
if (ISEMPTY(pat)) {
res = WS_Copy(WSPTR(ctx), sql_get_column(conn, i, 0), -1);
break;
}
debug(conn->conf, 1, ("considering \"%s\" ~ \"%s\"", pat, val));
if (wordsplit(val, &ws, wsflags)) {
dbrw_error("cannot expand string `%s': %s",
val, wordsplit_strerror(&ws));
continue;
}
wsflags |= WRDSF_REUSE;
val = ws.ws_wordv[0];
debug(conn->conf, 1, ("expanded to \"%s\" ~ \"%s\"",
pat, val));
strcpy(status, conn->conf->status);
if (nf == 4) {
if (parse_flags(sql_get_column(conn, i, 3),
&qdisp, &rflags, status))
continue;
}
switch (qdisp) {
case QDISP_NONE:
break;
case QDISP_APPEND:
qry = strchr(val, '?');
if (qry)
*qry++ = 0;
break;
case QDISP_DISCARD:
qry = strchr(val, '?');
if (qry) {
debug(conn->conf, 1,
("discarding query part \"%s\"", qry));
*qry = 0;
qry = NULL;
}
}
rc = regcomp(&re, pat, rflags);
if (rc) {
char errbuf[512];
regerror(rc, &re, errbuf, sizeof(errbuf));
dbrw_error("cannot compile regexp \"%s\": %s",
pat, errbuf);
continue;
}
matchcount = re.re_nsub + 1;
if (conn->matchsize < matchcount) {
void *p = realloc(conn->matches,
sizeof(conn->matches[0]) * matchcount);
if (!p) {
dbrw_error("not enough memory");
regfree(&re);
continue;
}
conn->matches = p;
conn->matchsize = matchcount;
}
rc = regexec(&re, val, matchcount, conn->matches, 0);
if (rc == 0)
res = expand_backref(ctx, val,
sql_get_column(conn, i, 0),
matchcount, conn->matches, qry);
regfree(&re);
if (rc == 0) {
debug(conn->conf, 1, ("match"));
if (status[0]) {
debug(conn->conf, 1,
("setting status %s", status));
dbrw_sethdr(ctx, HDR_REQ,
"\023X-VMOD-DBRW-Status:",
status);
}
break;
}
}
if (wsflags & WRDSF_REUSE)
wordsplit_free(&ws);
return res;
}
static char *
do_rewrite(VRT_CTX, struct dbrw_connection *cp, VCL_STRING arg)
{
struct wordsplit ws, wsenv;
int i, rc;
char *res;
if (sql_connect(cp) || cp->state != state_connected)
return NULL;
debug(cp->conf, 2, ("vmod_rewrite: splitting arg"));
wsenv.ws_delim = ";";
if (wordsplit(arg, &wsenv, WRDSF_NOVAR|WRDSF_NOCMD|WRDSF_DELIM)) {
dbrw_error("cannot split string `%s': %s",
arg, wordsplit_strerror(&wsenv));
return NULL;
}
if (cp->conf->backend->sql_escape) {
debug(cp->conf, 2, ("escaping variables"));
for (i = 0; i < wsenv.ws_wordc; i++) {
char *p = sql_escape(cp, wsenv.ws_wordv[i]);
if (!p) {
dbrw_error("cannot expand argument");
wordsplit_free(&wsenv);
return NULL;
}
free(wsenv.ws_wordv[i]);
wsenv.ws_wordv[i] = p;
debug(cp->conf, 3, ("%d: %s",i,p));
}
}
debug(cp->conf, 2, ("expanding query"));
ws.ws_env = (const char **)wsenv.ws_wordv;
rc = wordsplit(cp->conf->query, &ws,
WRDSF_NOCMD | WRDSF_QUOTE |
WRDSF_NOSPLIT |
WRDSF_ENV | WRDSF_UNDEF);
if (rc) {
dbrw_error("cannot expand query `%s': %s",
cp->conf->query, wordsplit_strerror(&ws));
wordsplit_free(&wsenv);
return NULL;
}
rc = sql_query(cp, ws.ws_wordv[0]);
wordsplit_free(&ws);
if (rc) {
debug(cp->conf, 1, ("vmod_rewrite: query failed"));
wordsplit_free(&wsenv);
return NULL;
}
res = findmatch(ctx, cp, wsenv.ws_wordv);
wordsplit_free(&wsenv);
sql_free_result(cp);
return res;
}
VCL_STRING
vmod_rewrite(VRT_CTX, struct vmod_priv *priv, VCL_STRING arg)
{
struct dbrw_config *conf = priv->priv;
struct dbrw_connection *cp;
char *res = NULL;
AN(conf);
debug(conf, 2, ("vmod_rewrite(%s) begin", arg));
cp = dbrw_connection_get(conf);
AN(cp);
res = do_rewrite(ctx, cp, arg);
dbrw_connection_release(cp);
debug(conf, 1, ("vmod_rewrite: res=%s", res ? res : "(NULL)"));
return res;
}