/* This file is part of libvmod_dbrw
Copyright (C) 2013 Sergey Poznyakoff
Libvmod_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.
Libvmod_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 libvmod_dbrw. If not, see .
*/
#include "dbrw.h"
#include
#include "wordsplit.h"
#include "vrt.h"
#include "vcc_if.h"
#include "bin/varnishd/cache.h"
static pthread_once_t thread_once = PTHREAD_ONCE_INIT;
static pthread_key_t thread_key;
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 void
conn_free(void *f)
{
struct dbrw_connection *p = f;
sql_disconnect(p);
sql_destroy(p);
}
static void
make_key()
{
pthread_key_create(&thread_key, conn_free);
}
int
dbrw_init(struct vmod_priv *priv, const struct VCL_conf *vclconf)
{
struct dbrw_config *conf = calloc(1, sizeof(*conf));
if (!conf) {
dbrw_error("not enough memory");
abort();
}
priv->priv = conf;
pthread_once(&thread_once, make_key);
return 0;
}
char *
findparam(char **params, char *name)
{
char *p, *q;
while (*params) {
p = *params++;
for (q = name; *p && *q && *p == *q; p++, q++);
if (*q == 0 && *p == '=')
return p+1;
}
return NULL;
}
static void
argv_free(char **argv)
{
if (!argv)
return;
while (*argv) {
free(*argv);
++*argv;
}
}
static struct dbrw_backend *bcktab[] = {
#ifdef USE_SQL_MYSQL
&mysql_backend,
#endif
#ifdef USE_SQL_PGSQL
&pgsql_backend,
#endif
NULL
};
static struct dbrw_backend *
find_backend(const char *name)
{
int i;
for (i = 0; bcktab[i]; i++) {
if (strcmp(bcktab[i]->name, name) == 0)
return bcktab[i];
}
return NULL;
}
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*:
db=S
port=N
socket=S
user=S
password=S
options=S
debug=N
QUERY - Query to obtain the redirection target
*/
void
vmod_config(struct sess *sp, struct vmod_priv *priv, const char *bkname,
const char *param, const char *query)
{
char *s;
struct wordsplit ws;
struct dbrw_config *conf = priv->priv;
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));
return;
}
conf->param = ws.ws_wordv;
ws.ws_wordv = NULL;
ws.ws_wordc = 0;
wordsplit_free(&ws);
conf->query = strdup(query);
if (!conf->query) {
dbrw_error("not enough memory");
argv_free(conf->param);
conf->param = NULL;
return;
}
/* Select backend */
conf->backend = find_backend(bkname);
if (!conf->backend) {
dbrw_error("unsupported backend: %s", bkname);
argv_free(conf->param);
free(conf->query);
conf->query = NULL;
conf->param = NULL;
return;
}
s = findparam(conf->param, "debug");
conf->debug_level = atoi(s);
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)) {
argv_free(conf->param);
free(conf->query);
conf->query = NULL;
conf->param = NULL;
}
}
static char *
expand_backref(struct sess *sp, const char *str, const char *val,
size_t matchcount, regmatch_t *matches, char *qry)
{
unsigned u;
char *b, *p;
u = WS_Reserve(sp->wrk->ws, 0);
p = b = sp->wrk->ws->f;
while (*val) {
if (u == 0) {
WS_Release(sp->wrk->ws, 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(sp->wrk->ws, 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(sp->wrk->ws, 0);
return NULL;
}
if (strchr(b, '?'))
*p++ = '&';
else
*p++ = '?';
memcpy(p, qry, len);
p += len;
u -= len;
}
if (u == 0) {
WS_Release(sp->wrk->ws, 0);
return NULL;
}
*p = 0;
WS_ReleaseP(sp->wrk->ws, p);
return b;
}
#define ISEMPTY(s) ((s) == NULL || (s)[0] == 0)
static char *
findmatch(struct sess *sp, 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_Dup(sp->ws, sql_get_column(conn, 0, 0));
/* 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_Dup(sp->ws, sql_get_column(conn, i, 0));
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(sp, 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));
VRT_SetHdr(sp, HDR_REQ,
"\023X-VMOD-DBRW-Status:",
status,
vrt_magic_string_end);
}
break;
}
}
if (wsflags & WRDSF_REUSE)
wordsplit_free(&ws);
return res;
}
static struct dbrw_connection *
get_connection(struct dbrw_config *conf)
{
struct dbrw_connection *cp = pthread_getspecific(thread_key);
if (!cp) {
cp = malloc(sizeof(*cp));
if (!cp)
return NULL;
cp->state = state_init;
cp->conf = conf;
cp->data = NULL;
cp->matches = NULL;
cp->matchsize = 0;
if (sql_init(cp)) {
free(cp);
return NULL;
}
if (pthread_setspecific(thread_key, cp)) {
dbrw_error("cannot set thread-specific data");
sql_destroy(cp);
free(cp);
return NULL;
}
}
return cp;
}
const char *
vmod_rewrite(struct sess *sp, struct vmod_priv *priv, const char *arg)
{
struct dbrw_config *conf = priv->priv;
struct dbrw_connection *cp;
struct wordsplit ws, wsenv;
int i, rc;
char *res;
debug(conf, 2, ("vmod_rewrite(%s) begin", arg));
if (!conf || !conf->query) {
debug(conf, 2, ("vmod_rewrite: not configured; exiting"));
return NULL;
}
cp = get_connection(conf);
if (!cp) {
debug(conf, 2, ("vmod_rewrite: no private data; exiting"));
return NULL;
}
if (sql_connect(cp) || cp->state != state_connected)
return NULL;
debug(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 (conf->backend->sql_escape) {
debug(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(conf, 3, ("%d: %s",i,p));
}
}
debug(conf, 2, ("expanding query"));
ws.ws_env = (const char **)wsenv.ws_wordv;
rc = wordsplit(conf->query, &ws,
WRDSF_NOCMD | WRDSF_QUOTE |
WRDSF_NOSPLIT |
WRDSF_ENV | WRDSF_UNDEF);
if (rc) {
dbrw_error("cannot expand query `%s': %s",
conf->query, wordsplit_strerror(&ws));
wordsplit_free(&wsenv);
return NULL;
}
rc = sql_query(cp, ws.ws_wordv[0]);
wordsplit_free(&ws);
if (rc) {
debug(conf, 1, ("vmod_rewrite: query failed"));
wordsplit_free(&wsenv);
return NULL;
}
res = findmatch(sp, cp, wsenv.ws_wordv);
wordsplit_free(&wsenv);
sql_free_result(cp);
debug(conf, 1, ("vmod_rewrite: res=%s", res ? res : "(NULL)"));
return res;
}