mod_vhostdb - vhost docroot lookups backends: mod_vhostdb_dbi mod_vhostdb_ldap mod_vhostdb_mysql (now preferred over mod_mysql_vhost.c) mod_vhostdb_pgsql STATUS: experimental (testing and feedback appreciated) x-ref: "PostgreSQL virtual host support" https://redmine.lighttpd.net/issues/485 "LDAP Virtual Host Definition Storage Integration" https://redmine.lighttpd.net/issues/1936 "mod_dbi_vhost (patch included)" https://redmine.lighttpd.net/issues/2297personal/stbuehler/mod-csrf
parent
eda72ebfc7
commit
2f83aac9fb
@ -0,0 +1,30 @@
|
||||
#include "first.h"
|
||||
|
||||
#include "http_vhostdb.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
|
||||
static http_vhostdb_backend_t http_vhostdb_backends[8];
|
||||
|
||||
const http_vhostdb_backend_t * http_vhostdb_backend_get (const buffer *name)
|
||||
{
|
||||
int i = 0;
|
||||
while (NULL != http_vhostdb_backends[i].name
|
||||
&& 0 != strcmp(http_vhostdb_backends[i].name, name->ptr)) {
|
||||
++i;
|
||||
}
|
||||
return (NULL != http_vhostdb_backends[i].name)
|
||||
? http_vhostdb_backends+i
|
||||
: NULL;
|
||||
}
|
||||
|
||||
void http_vhostdb_backend_set (const http_vhostdb_backend_t *backend)
|
||||
{
|
||||
unsigned int i = 0;
|
||||
while (NULL != http_vhostdb_backends[i].name) ++i;
|
||||
/*(must resize http_vhostdb_backends[] if too many different backends)*/
|
||||
force_assert(
|
||||
i < (sizeof(http_vhostdb_backends)/sizeof(http_vhostdb_backend_t))-1);
|
||||
memcpy(http_vhostdb_backends+i, backend, sizeof(http_vhostdb_backend_t));
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
#ifndef _HTTP_VHOST_H_
|
||||
#define _HTTP_VHOST_H_
|
||||
#include "first.h"
|
||||
|
||||
#include "base.h"
|
||||
|
||||
struct http_vhostdb_backend_t;
|
||||
|
||||
typedef struct http_vhostdb_backend_t {
|
||||
const char *name;
|
||||
int(*query)(server *srv, connection *con, void *p_d, buffer *result);
|
||||
void *p_d;
|
||||
} http_vhostdb_backend_t;
|
||||
|
||||
const http_vhostdb_backend_t * http_vhostdb_backend_get (const buffer *name);
|
||||
void http_vhostdb_backend_set (const http_vhostdb_backend_t *backend);
|
||||
|
||||
#endif
|
@ -0,0 +1,238 @@
|
||||
#include "first.h"
|
||||
|
||||
#include "plugin.h"
|
||||
#include "http_vhostdb.h"
|
||||
#include "log.h"
|
||||
#include "stat_cache.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* vhostdb framework
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
buffer *vhostdb_backend_conf;
|
||||
|
||||
/* generated */
|
||||
const http_vhostdb_backend_t *vhostdb_backend;
|
||||
} plugin_config;
|
||||
|
||||
typedef struct {
|
||||
PLUGIN_DATA;
|
||||
plugin_config **config_storage;
|
||||
plugin_config conf;
|
||||
|
||||
buffer *tmp_buf;
|
||||
} plugin_data;
|
||||
|
||||
INIT_FUNC(mod_vhostdb_init) {
|
||||
plugin_data *p = calloc(1, sizeof(*p));
|
||||
p->tmp_buf = buffer_init();
|
||||
return p;
|
||||
}
|
||||
|
||||
FREE_FUNC(mod_vhostdb_free) {
|
||||
plugin_data *p = p_d;
|
||||
if (!p) return HANDLER_GO_ON;
|
||||
|
||||
if (p->config_storage) {
|
||||
size_t i;
|
||||
for (i = 0; i < srv->config_context->used; i++) {
|
||||
plugin_config *s = p->config_storage[i];
|
||||
if (NULL == s) continue;
|
||||
buffer_free(s->vhostdb_backend_conf);
|
||||
free(s);
|
||||
}
|
||||
free(p->config_storage);
|
||||
}
|
||||
|
||||
free(p->tmp_buf);
|
||||
free(p);
|
||||
|
||||
UNUSED(srv);
|
||||
return HANDLER_GO_ON;
|
||||
}
|
||||
|
||||
SETDEFAULTS_FUNC(mod_vhostdb_set_defaults) {
|
||||
plugin_data *p = p_d;
|
||||
config_values_t cv[] = {
|
||||
{ "vhostdb.backend", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
|
||||
|
||||
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
|
||||
};
|
||||
|
||||
p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
|
||||
|
||||
for (size_t i = 0; i < srv->config_context->used; ++i) {
|
||||
data_config const *config = (data_config const*)srv->config_context->data[i];
|
||||
plugin_config *s = calloc(1, sizeof(plugin_config));
|
||||
s->vhostdb_backend_conf = buffer_init();
|
||||
|
||||
cv[0].destination = s->vhostdb_backend_conf;
|
||||
|
||||
p->config_storage[i] = s;
|
||||
|
||||
if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
|
||||
return HANDLER_ERROR;
|
||||
}
|
||||
|
||||
if (!buffer_string_is_empty(s->vhostdb_backend_conf)) {
|
||||
s->vhostdb_backend =
|
||||
http_vhostdb_backend_get(s->vhostdb_backend_conf);
|
||||
if (NULL == s->vhostdb_backend) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "sb",
|
||||
"vhostdb.backend not supported:",
|
||||
s->vhostdb_backend_conf);
|
||||
return HANDLER_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return HANDLER_GO_ON;
|
||||
}
|
||||
|
||||
#define PATCH(x) \
|
||||
p->conf.x = s->x;
|
||||
static int mod_vhostdb_patch_connection(server *srv, connection *con, plugin_data *p) {
|
||||
plugin_config *s = p->config_storage[0];
|
||||
PATCH(vhostdb_backend);
|
||||
|
||||
/* skip the first, the global context */
|
||||
for (size_t i = 1; i < srv->config_context->used; ++i) {
|
||||
data_config *dc = (data_config *)srv->config_context->data[i];
|
||||
s = p->config_storage[i];
|
||||
|
||||
/* condition didn't match */
|
||||
if (!config_check_cond(srv, con, dc)) continue;
|
||||
|
||||
/* merge config */
|
||||
for (size_t j = 0; j < dc->value->used; ++j) {
|
||||
data_unset *du = dc->value->data[j];
|
||||
|
||||
if (buffer_is_equal_string(du->key, CONST_STR_LEN("vhostdb.backend"))) {
|
||||
PATCH(vhostdb_backend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
#undef PATCH
|
||||
|
||||
typedef struct {
|
||||
buffer *server_name;
|
||||
buffer *document_root;
|
||||
} vhostdb_entry;
|
||||
|
||||
static vhostdb_entry * vhostdb_entry_init (void)
|
||||
{
|
||||
vhostdb_entry *ve = calloc(1, sizeof(*ve));
|
||||
ve->server_name = buffer_init();
|
||||
ve->document_root = buffer_init();
|
||||
return ve;
|
||||
}
|
||||
|
||||
static void vhostdb_entry_free (vhostdb_entry *ve)
|
||||
{
|
||||
buffer_free(ve->server_name);
|
||||
buffer_free(ve->document_root);
|
||||
free(ve);
|
||||
}
|
||||
|
||||
CONNECTION_FUNC(mod_vhostdb_handle_connection_close) {
|
||||
plugin_data *p = p_d;
|
||||
vhostdb_entry *ve;
|
||||
|
||||
if ((ve = con->plugin_ctx[p->id])) {
|
||||
con->plugin_ctx[p->id] = NULL;
|
||||
vhostdb_entry_free(ve);
|
||||
}
|
||||
|
||||
UNUSED(srv);
|
||||
return HANDLER_GO_ON;
|
||||
}
|
||||
|
||||
static handler_t mod_vhostdb_error_500 (connection *con)
|
||||
{
|
||||
con->http_status = 500; /* Internal Server Error */
|
||||
con->mode = DIRECT;
|
||||
return HANDLER_FINISHED;
|
||||
}
|
||||
|
||||
static handler_t mod_vhostdb_found (connection *con, vhostdb_entry *ve)
|
||||
{
|
||||
/* fix virtual server and docroot */
|
||||
buffer_copy_buffer(con->server_name, ve->server_name);
|
||||
buffer_copy_buffer(con->physical.doc_root, ve->document_root);
|
||||
return HANDLER_GO_ON;
|
||||
}
|
||||
|
||||
CONNECTION_FUNC(mod_vhostdb_handle_docroot) {
|
||||
plugin_data *p = p_d;
|
||||
vhostdb_entry *ve;
|
||||
const http_vhostdb_backend_t *backend;
|
||||
buffer *b;
|
||||
stat_cache_entry *sce;
|
||||
|
||||
/* no host specified? */
|
||||
if (buffer_string_is_empty(con->uri.authority)) return HANDLER_GO_ON;
|
||||
|
||||
/* XXX: future: implement larger, managed cache
|
||||
* of database responses (positive and negative) */
|
||||
|
||||
/* check if cached this connection */
|
||||
ve = con->plugin_ctx[p->id];
|
||||
if (ve && buffer_is_equal(ve->server_name, con->uri.authority)) {
|
||||
return mod_vhostdb_found(con, ve); /* HANDLER_GO_ON */
|
||||
}
|
||||
|
||||
mod_vhostdb_patch_connection(srv, con, p);
|
||||
if (!p->conf.vhostdb_backend) return HANDLER_GO_ON;
|
||||
|
||||
b = p->tmp_buf;
|
||||
backend = p->conf.vhostdb_backend;
|
||||
if (0 != backend->query(srv, con, backend->p_d, b)) {
|
||||
return mod_vhostdb_error_500(con); /* HANDLER_FINISHED */
|
||||
}
|
||||
|
||||
if (buffer_string_is_empty(b)) {
|
||||
/* no such virtual host */
|
||||
return HANDLER_GO_ON;
|
||||
}
|
||||
|
||||
/* sanity check that really is a directory */
|
||||
buffer_append_slash(b);
|
||||
if (HANDLER_ERROR == stat_cache_get_entry(srv, con, b, &sce)) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "sb", strerror(errno), b);
|
||||
return mod_vhostdb_error_500(con); /* HANDLER_FINISHED */
|
||||
}
|
||||
if (!S_ISDIR(sce->st.st_mode)) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "sb", "Not a directory", b);
|
||||
return mod_vhostdb_error_500(con); /* HANDLER_FINISHED */
|
||||
}
|
||||
|
||||
/* cache the data */
|
||||
if (!ve) con->plugin_ctx[p->id] = ve = vhostdb_entry_init();
|
||||
buffer_copy_buffer(ve->server_name, con->uri.authority);
|
||||
buffer_copy_buffer(ve->document_root, b);
|
||||
|
||||
return mod_vhostdb_found(con, ve); /* HANDLER_GO_ON */
|
||||
}
|
||||
|
||||
int mod_vhostdb_plugin_init(plugin *p);
|
||||
int mod_vhostdb_plugin_init(plugin *p) {
|
||||
p->version = LIGHTTPD_VERSION_ID;
|
||||
p->name = buffer_init_string("vhostdb");
|
||||
p->init = mod_vhostdb_init;
|
||||
p->cleanup = mod_vhostdb_free;
|
||||
p->set_defaults = mod_vhostdb_set_defaults;
|
||||
p->handle_docroot = mod_vhostdb_handle_docroot;
|
||||
p->connection_reset = mod_vhostdb_handle_connection_close;
|
||||
|
||||
p->data = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,324 @@
|
||||
#include "first.h"
|
||||
|
||||
#include <dbi/dbi.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "base.h"
|
||||
#include "http_vhostdb.h"
|
||||
#include "log.h"
|
||||
#include "plugin.h"
|
||||
|
||||
/*
|
||||
* virtual host plugin using DBI for domain to directory lookups
|
||||
*
|
||||
* e.g.
|
||||
* vhostdb.dbi = ( "sql" => "SELECT docroot FROM vhosts WHERE host='?'"
|
||||
* "dbtype" => "sqlite3",
|
||||
* "dbname" => "mydb.sqlite",
|
||||
* "sqlite_dbdir" => "/path/to/sqlite/dbs/" )
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
dbi_conn dbconn;
|
||||
dbi_inst dbinst;
|
||||
buffer *sqlquery;
|
||||
server *srv;
|
||||
short reconnect_count;
|
||||
} vhostdb_config;
|
||||
|
||||
typedef struct {
|
||||
void *vdata;
|
||||
array *options;
|
||||
} plugin_config;
|
||||
|
||||
typedef struct {
|
||||
PLUGIN_DATA;
|
||||
plugin_config **config_storage;
|
||||
plugin_config conf;
|
||||
} plugin_data;
|
||||
|
||||
/* used to reconnect to the database when we get disconnected */
|
||||
static void mod_vhostdb_dbi_error_callback (dbi_conn dbconn, void *vdata)
|
||||
{
|
||||
vhostdb_config *dbconf = (vhostdb_config *)vdata;
|
||||
const char *errormsg = NULL;
|
||||
/*assert(dbconf->dbconn == dbconn);*/
|
||||
|
||||
while (++dbconf->reconnect_count <= 3) { /* retry */
|
||||
if (0 == dbi_conn_connect(dbconn)) {
|
||||
fd_close_on_exec(dbi_conn_get_socket(dbconn));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dbi_conn_error(dbconn, &errormsg);
|
||||
log_error_write(dbconf->srv, __FILE__, __LINE__, "ss",
|
||||
"dbi_conn_connect():", errormsg);
|
||||
}
|
||||
|
||||
static void mod_vhostdb_dbconf_free (void *vdata)
|
||||
{
|
||||
vhostdb_config *dbconf = (vhostdb_config *)vdata;
|
||||
if (!dbconf) return;
|
||||
dbi_conn_close(dbconf->dbconn);
|
||||
dbi_shutdown_r(dbconf->dbinst);
|
||||
free(dbconf);
|
||||
}
|
||||
|
||||
static int mod_vhostdb_dbconf_setup (server *srv, array *opts, void **vdata)
|
||||
{
|
||||
buffer *sqlquery = NULL;
|
||||
const buffer *dbtype=NULL, *dbname=NULL;
|
||||
|
||||
for (size_t i = 0; i < opts->used; ++i) {
|
||||
const data_string *ds = (data_string *)opts->data[i];
|
||||
if (ds->type == TYPE_STRING) {
|
||||
if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("sql"))) {
|
||||
sqlquery = ds->value;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("dbname"))) {
|
||||
dbname = ds->value;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("dbtype"))) {
|
||||
dbtype = ds->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* required:
|
||||
* - sql (sql query)
|
||||
* - dbtype
|
||||
* - dbname
|
||||
*
|
||||
* optional:
|
||||
* - username, some databases don't require this (sqlite)
|
||||
* - password, default: empty
|
||||
* - socket, default: database type default
|
||||
* - hostname, if set overrides socket
|
||||
* - port, default: database default
|
||||
* - encoding, default: database default
|
||||
*/
|
||||
|
||||
if (!buffer_string_is_empty(sqlquery)
|
||||
&& !buffer_is_empty(dbname) && !buffer_is_empty(dbtype)) {
|
||||
/* create/initialise database */
|
||||
vhostdb_config *dbconf;
|
||||
dbi_inst dbinst = NULL;
|
||||
dbi_conn dbconn;
|
||||
if (dbi_initialize_r(NULL, &dbinst) < 1) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "s",
|
||||
"dbi_initialize_r() failed. "
|
||||
"Do you have the DBD for this db type installed?");
|
||||
return -1;
|
||||
}
|
||||
dbconn = dbi_conn_new_r(dbtype->ptr, dbinst);
|
||||
if (NULL == dbconn) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "s",
|
||||
"dbi_conn_new_r() failed. "
|
||||
"Do you have the DBD for this db type installed?");
|
||||
dbi_shutdown_r(dbinst);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* set options */
|
||||
for (size_t j = 0; j < opts->used; ++j) {
|
||||
data_unset *du = opts->data[j];
|
||||
const buffer *opt = du->key;
|
||||
if (!buffer_string_is_empty(opt)) {
|
||||
if (du->type == TYPE_INTEGER) {
|
||||
data_integer *di = (data_integer *)du;
|
||||
dbi_conn_set_option_numeric(dbconn, opt->ptr, di->value);
|
||||
} else if (du->type == TYPE_STRING) {
|
||||
data_string *ds = (data_string *)du;
|
||||
if (ds->value != sqlquery && ds->value != dbtype) {
|
||||
dbi_conn_set_option(dbconn, opt->ptr, ds->value->ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbconf = (vhostdb_config *)calloc(1, sizeof(*dbconf));
|
||||
dbconf->dbinst = dbinst;
|
||||
dbconf->dbconn = dbconn;
|
||||
dbconf->sqlquery = sqlquery;
|
||||
dbconf->srv = srv;
|
||||
dbconf->reconnect_count = 0;
|
||||
*vdata = dbconf;
|
||||
|
||||
/* used to automatically reconnect to the database */
|
||||
dbi_conn_error_handler(dbconn, mod_vhostdb_dbi_error_callback, dbconf);
|
||||
|
||||
/* connect to database */
|
||||
mod_vhostdb_dbi_error_callback(dbconn, dbconf);
|
||||
if (dbconf->reconnect_count >= 3) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mod_vhostdb_patch_connection (server *srv, connection *con, plugin_data *p);
|
||||
|
||||
static int mod_vhostdb_dbi_query(server *srv, connection *con, void *p_d, buffer *docroot)
|
||||
{
|
||||
plugin_data *p = (plugin_data *)p_d;
|
||||
vhostdb_config *dbconf;
|
||||
dbi_result result;
|
||||
unsigned long long nrows;
|
||||
int retry_count = 0;
|
||||
|
||||
/*(reuse buffer for sql query before generating docroot result)*/
|
||||
buffer *sqlquery = docroot;
|
||||
buffer_string_set_length(sqlquery, 0); /*(also resets docroot (alias))*/
|
||||
|
||||
mod_vhostdb_patch_connection(srv, con, p);
|
||||
if (NULL == p->conf.vdata) return 0; /*(after resetting docroot)*/
|
||||
dbconf = (vhostdb_config *)p->conf.vdata;
|
||||
|
||||
for (char *b = dbconf->sqlquery->ptr, *d; *b; b = d+1) {
|
||||
if (NULL != (d = strchr(b, '?'))) {
|
||||
/* escape the uri.authority */
|
||||
char *esc = NULL;
|
||||
size_t len = dbi_conn_escape_string_copy(dbconf->dbconn, con->uri.authority->ptr, &esc);
|
||||
buffer_append_string_len(sqlquery, b, (size_t)(d - b));
|
||||
buffer_append_string_len(sqlquery, esc, len);
|
||||
free(esc);
|
||||
if (0 == len) return -1;
|
||||
} else {
|
||||
d = dbconf->sqlquery->ptr + buffer_string_length(dbconf->sqlquery);
|
||||
buffer_append_string_len(sqlquery, b, (size_t)(d - b));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* reset our reconnect-attempt counter, this is a new query. */
|
||||
dbconf->reconnect_count = 0;
|
||||
|
||||
do {
|
||||
result = dbi_conn_query(dbconf->dbconn, sqlquery->ptr);
|
||||
} while (!result && ++retry_count < 2);
|
||||
|
||||
buffer_string_set_length(docroot, 0); /*(reset buffer to store result)*/
|
||||
|
||||
if (!result) {
|
||||
const char *errmsg;
|
||||
dbi_conn_error(dbconf->dbconn, &errmsg);
|
||||
log_error_write(srv, __FILE__, __LINE__, "s", errmsg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
nrows = dbi_result_get_numrows(result);
|
||||
if (nrows && nrows != DBI_ROW_ERROR && dbi_result_next_row(result)) {
|
||||
buffer_copy_string(docroot, dbi_result_get_string_idx(result, 1));
|
||||
} /* else no such virtual host */
|
||||
|
||||
dbi_result_free(result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
INIT_FUNC(mod_vhostdb_init) {
|
||||
static http_vhostdb_backend_t http_vhostdb_backend_dbi =
|
||||
{ "dbi", mod_vhostdb_dbi_query, NULL };
|
||||
plugin_data *p = calloc(1, sizeof(*p));
|
||||
|
||||
/* register http_vhostdb_backend_dbi */
|
||||
http_vhostdb_backend_dbi.p_d = p;
|
||||
http_vhostdb_backend_set(&http_vhostdb_backend_dbi);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
FREE_FUNC(mod_vhostdb_cleanup) {
|
||||
plugin_data *p = p_d;
|
||||
if (!p) return HANDLER_GO_ON;
|
||||
|
||||
if (p->config_storage) {
|
||||
for (size_t i = 0; i < srv->config_context->used; i++) {
|
||||
plugin_config *s = p->config_storage[i];
|
||||
if (!s) continue;
|
||||
mod_vhostdb_dbconf_free(s->vdata);
|
||||
array_free(s->options);
|
||||
free(s);
|
||||
}
|
||||
free(p->config_storage);
|
||||
}
|
||||
free(p);
|
||||
|
||||
UNUSED(srv);
|
||||
return HANDLER_GO_ON;
|
||||
}
|
||||
|
||||
SETDEFAULTS_FUNC(mod_vhostdb_set_defaults) {
|
||||
plugin_data *p = p_d;
|
||||
|
||||
config_values_t cv[] = {
|
||||
{ "vhostdb.dbi", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION },
|
||||
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
|
||||
};
|
||||
|
||||
p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *));
|
||||
|
||||
for (size_t i = 0; i < srv->config_context->used; ++i) {
|
||||
data_config const *config = (data_config const*)srv->config_context->data[i];
|
||||
plugin_config *s = calloc(1, sizeof(plugin_config));
|
||||
|
||||
s->options = array_init();
|
||||
cv[0].destination = s->options;
|
||||
|
||||
p->config_storage[i] = s;
|
||||
|
||||
if (config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
|
||||
return HANDLER_ERROR;
|
||||
}
|
||||
|
||||
if (s->options->used
|
||||
&& 0 != mod_vhostdb_dbconf_setup(srv, s->options, &s->vdata)) {
|
||||
return HANDLER_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
return HANDLER_GO_ON;
|
||||
}
|
||||
|
||||
#define PATCH(x) \
|
||||
p->conf.x = s->x;
|
||||
static void mod_vhostdb_patch_connection (server *srv, connection *con, plugin_data *p)
|
||||
{
|
||||
plugin_config *s = p->config_storage[0];
|
||||
PATCH(vdata);
|
||||
|
||||
/* skip the first, the global context */
|
||||
for (size_t i = 1; i < srv->config_context->used; ++i) {
|
||||
data_config *dc = (data_config *)srv->config_context->data[i];
|
||||
s = p->config_storage[i];
|
||||
|
||||
/* condition didn't match */
|
||||
if (!config_check_cond(srv, con, dc)) continue;
|
||||
|
||||
/* merge config */
|
||||
for (size_t j = 0; j < dc->value->used; ++j) {
|
||||
data_unset *du = dc->value->data[j];
|
||||
|
||||
if (buffer_is_equal_string(du->key, CONST_STR_LEN("vhostdb.dbi"))) {
|
||||
PATCH(vdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#undef PATCH
|
||||
|
||||
/* this function is called at dlopen() time and inits the callbacks */
|
||||
int mod_vhostdb_dbi_plugin_init (plugin *p);
|
||||
int mod_vhostdb_dbi_plugin_init (plugin *p)
|
||||
{
|
||||
p->version = LIGHTTPD_VERSION_ID;
|
||||
p->name = buffer_init_string("vhostdb_dbi");
|
||||
|
||||
p->init = mod_vhostdb_init;
|
||||
p->cleanup = mod_vhostdb_cleanup;
|
||||
p->set_defaults = mod_vhostdb_set_defaults;
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,512 @@
|
||||
#include "first.h"
|
||||
|
||||
#include <ldap.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "base.h"
|
||||
#include "http_vhostdb.h"
|
||||
#include "log.h"
|
||||
#include "plugin.h"
|
||||
|
||||
/*
|
||||
* virtual host plugin using LDAP for domain to directory lookups
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
LDAP *ldap;
|
||||
buffer *filter;
|
||||
|
||||
const char *attr;
|
||||
const char *host;
|
||||
const char *basedn;
|
||||
const char *binddn;
|
||||
const char *bindpw;
|
||||
const char *cafile;
|
||||
unsigned short starttls;
|
||||
} vhostdb_config;
|
||||
|
||||
typedef struct {
|
||||
void *vdata;
|
||||
array *options;
|
||||
} plugin_config;
|
||||
|
||||
typedef struct {
|
||||
PLUGIN_DATA;
|
||||
plugin_config **config_storage;
|
||||
plugin_config conf;
|
||||
} plugin_data;
|
||||
|
||||
static void mod_vhostdb_dbconf_free (void *vdata)
|
||||
{
|
||||
vhostdb_config *dbconf = (vhostdb_config *)vdata;
|
||||
if (!dbconf) return;
|
||||
if (NULL != dbconf->ldap) ldap_unbind_ext_s(dbconf->ldap, NULL, NULL);
|
||||
free(dbconf);
|
||||
}
|
||||
|
||||
static int mod_vhostdb_dbconf_setup (server *srv, array *opts, void **vdata)
|
||||
{
|
||||
buffer *filter = NULL;
|
||||
const char *attr = "documentRoot";
|
||||
const char *basedn=NULL,*binddn=NULL,*bindpw=NULL,*host=NULL,*cafile=NULL;
|
||||
unsigned short starttls = 0;
|
||||
|
||||
for (size_t i = 0; i < opts->used; ++i) {
|
||||
const data_string *ds = (data_string *)opts->data[i];
|
||||
if (ds->type == TYPE_STRING) {
|
||||
if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("filter"))) {
|
||||
filter = ds->value;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("attr"))) {
|
||||
if (!buffer_string_is_empty(ds->value)) attr = ds->value->ptr;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("host"))) {
|
||||
host = ds->value->ptr;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("base-dn"))) {
|
||||
if (!buffer_string_is_empty(ds->value)) basedn = ds->value->ptr;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("bind-dn"))) {
|
||||
if (!buffer_string_is_empty(ds->value)) binddn = ds->value->ptr;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("bind-pw"))) {
|
||||
bindpw = ds->value->ptr;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("ca-file"))) {
|
||||
if (!buffer_string_is_empty(ds->value)) cafile = ds->value->ptr;
|
||||
} else if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("starttls"))) {
|
||||
starttls = !buffer_is_equal_string(ds->value, CONST_STR_LEN("disable"))
|
||||
&& !buffer_is_equal_string(ds->value, CONST_STR_LEN("0"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* required:
|
||||
* - host
|
||||
* - filter (LDAP query)
|
||||
* - base-dn
|
||||
*
|
||||
* optional:
|
||||
* - attr (LDAP attribute with docroot; default "documentRoot")
|
||||
* - bind-dn
|
||||
* - bind-pw
|
||||
* - ca-file
|
||||
* - starttls
|
||||
*/
|
||||
|
||||
if (!buffer_string_is_empty(filter) && NULL != host && NULL != basedn) {
|
||||
vhostdb_config *dbconf;
|
||||
|
||||
if (NULL == strchr(filter->ptr, '?')) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "s",
|
||||
"ldap: filter is missing a replace-operator '?'");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* openldap sets FD_CLOEXEC on database socket descriptors
|
||||
* (still race between creation of socket and fcntl FD_CLOEXEC)
|
||||
* (YMMV with other LDAP client libraries) */
|
||||
|
||||
dbconf = (vhostdb_config *)calloc(1, sizeof(*dbconf));
|
||||
dbconf->ldap = NULL;
|
||||
dbconf->filter = filter;
|
||||
dbconf->attr = attr;
|
||||
dbconf->host = host;
|
||||
dbconf->basedn = basedn;
|
||||
dbconf->binddn = binddn;
|
||||
dbconf->bindpw = bindpw;
|
||||
dbconf->cafile = cafile;
|
||||
dbconf->starttls = starttls;
|
||||
*vdata = dbconf;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Note: a large portion of the LDAP code is copied verbatim from mod_authn_ldap
|
||||
* with only changes being use of vhostdb_config instead of plugin_config struct
|
||||
* and (const char *) strings in vhostdb_config instead of (buffer *).
|
||||
*/
|
||||
|
||||
static void mod_authn_ldap_err(server *srv, const char *file, unsigned long line, const char *fn, int err)
|
||||
{
|
||||
log_error_write(srv,file,line,"sSss","ldap:",fn,":",ldap_err2string(err));
|
||||
}
|
||||
|
||||
static void mod_authn_ldap_opt_err(server *srv, const char *file, unsigned long line, const char *fn, LDAP *ld)
|
||||
{
|
||||
int err;
|
||||
ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &err);
|
||||
mod_authn_ldap_err(srv, file, line, fn, err);
|
||||
}
|
||||
|
||||
static void mod_authn_append_ldap_filter_escape(buffer * const filter, const buffer * const raw) {
|
||||
/* [RFC4515] 3. String Search Filter Definition
|
||||
*
|
||||
* [...]
|
||||
*
|
||||
* The <valueencoding> rule ensures that the entire filter string is a
|
||||
* valid UTF-8 string and provides that the octets that represent the
|
||||
* ASCII characters "*" (ASCII 0x2a), "(" (ASCII 0x28), ")" (ASCII
|
||||
* 0x29), "\" (ASCII 0x5c), and NUL (ASCII 0x00) are represented as a
|
||||
* backslash "\" (ASCII 0x5c) followed by the two hexadecimal digits
|
||||
* representing the value of the encoded octet.
|
||||
*
|
||||
* [...]
|
||||
*
|
||||
* As indicated by the <valueencoding> rule, implementations MUST escape
|
||||
* all octets greater than 0x7F that are not part of a valid UTF-8
|
||||
* encoding sequence when they generate a string representation of a
|
||||
* search filter. Implementations SHOULD accept as input strings that
|
||||
* are not valid UTF-8 strings. This is necessary because RFC 2254 did
|
||||
* not clearly define the term "string representation" (and in
|
||||
* particular did not mention that the string representation of an LDAP
|
||||
* search filter is a string of UTF-8-encoded Unicode characters).
|
||||
*
|
||||
*
|
||||
* https://www.ldap.com/ldap-filters
|
||||
* Although not required, you may escape any other characters that you want
|
||||
* in the assertion value (or substring component) of a filter. This may be
|
||||
* accomplished by prefixing the hexadecimal representation of each byte of
|
||||
* the UTF-8 encoding of the character to escape with a backslash character.
|
||||
*/
|
||||
const char * const b = raw->ptr;
|
||||
const size_t rlen = buffer_string_length(raw);
|
||||
for (size_t i = 0; i < rlen; ++i) {
|
||||
size_t len = i;
|
||||
char *f;
|
||||
do {
|
||||
/* encode all UTF-8 chars with high bit set
|
||||
* (instead of validating UTF-8 and escaping only invalid UTF-8) */
|
||||
if (((unsigned char *)b)[len] > 0x7f)
|
||||
break;
|
||||
switch (b[len]) {
|
||||
default:
|
||||
continue;
|
||||
case '\0': case '(': case ')': case '*': case '\\':
|
||||
break;
|
||||
}
|
||||
break;
|
||||
} while (++len < rlen);
|
||||
len -= i;
|
||||
|
||||
if (len) {
|
||||
buffer_append_string_len(filter, b+i, len);
|
||||
if ((i += len) == rlen) break;
|
||||
}
|
||||
|
||||
/* escape * ( ) \ NUL ('\0') (and all UTF-8 chars with high bit set) */
|
||||
buffer_string_prepare_append(filter, 3);
|
||||
f = filter->ptr + buffer_string_length(filter);
|
||||
f[0] = '\\';
|
||||
f[1] = "0123456789abcdef"[(((unsigned char *)b)[i] >> 4) & 0xf];
|
||||
f[2] = "0123456789abcdef"[(((unsigned char *)b)[i] ) & 0xf];
|
||||
buffer_commit(filter, 3);
|
||||
}
|
||||
}
|
||||
|
||||
static LDAP * mod_authn_ldap_host_init(server *srv, vhostdb_config *s) {
|
||||
LDAP *ld;
|
||||
int ret;
|
||||
|
||||
ld = ldap_init(s->host, LDAP_PORT);
|
||||
if (NULL == ld) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "sss", "ldap:", "ldap_init():",
|
||||
strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = LDAP_VERSION3;
|
||||
ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ret);
|
||||
if (LDAP_OPT_SUCCESS != ret) {
|
||||
mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_set_options()", ret);
|
||||
ldap_memfree(ld);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (s->starttls) {
|
||||
/* if no CA file is given, it is ok, as we will use encryption
|
||||
* if the server requires a CAfile it will tell us */
|
||||
if (s->cafile) {
|
||||
ret = ldap_set_option(NULL, LDAP_OPT_X_TLS_CACERTFILE, s->cafile);
|
||||
if (LDAP_OPT_SUCCESS != ret) {
|
||||
mod_authn_ldap_err(srv, __FILE__, __LINE__,
|
||||
"ldap_set_option(LDAP_OPT_X_TLS_CACERTFILE)",
|
||||
ret);
|
||||
ldap_memfree(ld);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
ret = ldap_start_tls_s(ld, NULL, NULL);
|
||||
if (LDAP_OPT_SUCCESS != ret) {
|
||||
mod_authn_ldap_err(srv,__FILE__,__LINE__,"ldap_start_tls_s()",ret);
|
||||
ldap_memfree(ld);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return ld;
|
||||
}
|
||||
|
||||
static int mod_authn_ldap_bind(server *srv, LDAP *ld, const char *dn, const char *pw) {
|
||||
#if 0
|
||||
struct berval creds;
|
||||
int ret;
|
||||
|
||||
if (NULL != pw) {
|
||||
*((const char **)&creds.bv_val) = pw; /*(cast away const)*/
|
||||
creds.bv_len = strlen(pw);
|
||||
} else {
|
||||
creds.bv_val = NULL;
|
||||
creds.bv_len = 0;
|
||||
}
|
||||
|
||||
/* RFE: add functionality: LDAP_SASL_EXTERNAL (or GSS-SPNEGO, etc.) */
|
||||
|
||||
ret = ldap_sasl_bind_s(ld,dn,LDAP_SASL_SIMPLE,&creds,NULL,NULL,NULL);
|
||||
if (ret != LDAP_SUCCESS) {
|
||||
mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_sasl_bind_s()", ret);
|
||||
}
|
||||
#else
|
||||
int ret = ldap_simple_bind_s(ld, dn, pw);
|
||||
if (ret != LDAP_SUCCESS) {
|
||||
mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_simple_bind_s()",ret);
|
||||
}
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static LDAPMessage * mod_authn_ldap_search(server *srv, vhostdb_config *s, char *base, char *filter) {
|
||||
LDAPMessage *lm = NULL;
|
||||
char *attrs[] = { LDAP_NO_ATTRS, NULL };
|
||||
int ret;
|
||||
|
||||
/*
|
||||
* 1. connect anonymously (if not already connected)
|
||||
* (ldap connection is kept open unless connection-level error occurs)
|
||||
* 2. issue search using filter
|
||||
*/
|
||||
|
||||
if (s->ldap != NULL) {
|
||||
ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
|
||||
attrs, 0, NULL, NULL, NULL, 0, &lm);
|
||||
if (LDAP_SUCCESS == ret) {
|
||||
return lm;
|
||||
} else if (LDAP_SERVER_DOWN != ret) {
|
||||
/* try again (or initial request);
|
||||
* ldap lib sometimes fails for the first call but reconnects */
|
||||
ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
|
||||
attrs, 0, NULL, NULL, NULL, 0, &lm);
|
||||
if (LDAP_SUCCESS == ret) {
|
||||
return lm;
|
||||
}
|
||||
}
|
||||
|
||||
ldap_unbind_ext_s(s->ldap, NULL, NULL);
|
||||
}
|
||||
|
||||
s->ldap = mod_authn_ldap_host_init(srv, s);
|
||||
if (NULL == s->ldap) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = mod_authn_ldap_bind(srv, s->ldap, s->binddn, s->bindpw);
|
||||
if (LDAP_SUCCESS != ret) {
|
||||
ldap_memfree(s->ldap);
|
||||
s->ldap = NULL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
|
||||
attrs, 0, NULL, NULL, NULL, 0, &lm);
|
||||
if (LDAP_SUCCESS != ret) {
|
||||
log_error_write(srv, __FILE__, __LINE__, "sSss",
|
||||
"ldap:", ldap_err2string(ret), "; filter:", filter);
|
||||
ldap_unbind_ext_s(s->ldap, NULL, NULL);
|
||||
s->ldap = NULL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return lm;
|
||||
}
|
||||
|
||||
static void mod_vhostdb_patch_connection (server *srv, connection *con, plugin_data *p);
|
||||
|
||||
static int mod_vhostdb_ldap_query(server *srv, connection *con, void *p_d, buffer *docroot)
|
||||
{
|
||||
plugin_data *p = (plugin_data *)p_d;
|
||||
vhostdb_config *dbconf;
|
||||
LDAP *ld;
|
||||
LDAPMessage *lm, *first;
|
||||
struct berval **vals;
|
||||
int count;
|
||||
char *basedn;
|
||||
buffer *template;
|
||||
|
||||
/*(reuse buffer for ldap query before generating docroot result)*/
|
||||
buffer *filter = docroot;
|
||||
buffer_string_set_length(filter, 0); /*(also resets docroot (alias))*/
|
||||
|
||||
mod_vhostdb_patch_connection(srv, con, p);
|
||||
if (NULL == p->conf.vdata) return 0; /*(after resetting docroot)*/
|
||||
dbconf = (vhostdb_config *)p->conf.vdata;
|
||||
|