1 /* 2 * mod_vhostdb_dbi - virtual hosts mapping from backend DBI interface 3 * 4 * Copyright(c) 2017 Glenn Strauss gstrauss()gluelogic.com All rights reserved 5 * License: BSD 3-clause (same as lighttpd) 6 */ 7 #include "first.h" 8 9 #include <dbi/dbi.h> 10 11 #include <string.h> 12 #include <stdlib.h> 13 14 #include "base.h" 15 #include "http_vhostdb.h" 16 #include "fdevent.h" 17 #include "log.h" 18 #include "plugin.h" 19 20 /* 21 * virtual host plugin using DBI for domain to directory lookups 22 * 23 * e.g. 24 * vhostdb.dbi = ( "sql" => "SELECT docroot FROM vhosts WHERE host='?'" 25 * "dbtype" => "sqlite3", 26 * "dbname" => "mydb.sqlite", 27 * "sqlite_dbdir" => "/path/to/sqlite/dbs/" ) 28 */ 29 30 typedef struct { 31 dbi_conn dbconn; 32 dbi_inst dbinst; 33 const buffer *sqlquery; 34 log_error_st *errh; 35 short reconnect_count; 36 } vhostdb_config; 37 38 typedef struct { 39 void *vdata; 40 } plugin_config; 41 42 typedef struct { 43 PLUGIN_DATA; 44 plugin_config defaults; 45 plugin_config conf; 46 } plugin_data; 47 48 /* used to reconnect to the database when we get disconnected */ 49 static void mod_vhostdb_dbi_error_callback (dbi_conn dbconn, void *vdata) 50 { 51 vhostdb_config *dbconf = (vhostdb_config *)vdata; 52 const char *errormsg = NULL; 53 /*assert(dbconf->dbconn == dbconn);*/ 54 55 while (++dbconf->reconnect_count <= 3) { /* retry */ 56 if (0 == dbi_conn_connect(dbconn)) { 57 fdevent_setfd_cloexec(dbi_conn_get_socket(dbconn)); 58 return; 59 } 60 } 61 62 dbi_conn_error(dbconn, &errormsg); 63 log_error(dbconf->errh,__FILE__,__LINE__,"dbi_conn_connect(): %s",errormsg); 64 } 65 66 static void mod_vhostdb_dbconf_free (void *vdata) 67 { 68 vhostdb_config *dbconf = (vhostdb_config *)vdata; 69 if (!dbconf) return; 70 dbi_conn_close(dbconf->dbconn); 71 dbi_shutdown_r(dbconf->dbinst); 72 free(dbconf); 73 } 74 75 static int mod_vhostdb_dbconf_setup (server *srv, const array *opts, void **vdata) 76 { 77 const buffer *sqlquery = NULL; 78 const buffer *dbtype=NULL, *dbname=NULL; 79 80 for (size_t i = 0; i < opts->used; ++i) { 81 const data_string *ds = (data_string *)opts->data[i]; 82 if (ds->type == TYPE_STRING) { 83 if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("sql"))) { 84 sqlquery = &ds->value; 85 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("dbname"))) { 86 dbname = &ds->value; 87 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("dbtype"))) { 88 dbtype = &ds->value; 89 } 90 } 91 } 92 93 /* required: 94 * - sql (sql query) 95 * - dbtype 96 * - dbname 97 * 98 * optional: 99 * - username, some databases don't require this (sqlite) 100 * - password, default: empty 101 * - socket, default: database type default 102 * - hostname, if set overrides socket 103 * - port, default: database default 104 * - encoding, default: database default 105 */ 106 107 if (!buffer_string_is_empty(sqlquery) 108 && !buffer_is_empty(dbname) && !buffer_is_empty(dbtype)) { 109 /* create/initialise database */ 110 vhostdb_config *dbconf; 111 dbi_inst dbinst = NULL; 112 dbi_conn dbconn; 113 if (dbi_initialize_r(NULL, &dbinst) < 1) { 114 log_error(srv->errh, __FILE__, __LINE__, 115 "dbi_initialize_r() failed. " 116 "Do you have the DBD for this db type installed?"); 117 return -1; 118 } 119 dbconn = dbi_conn_new_r(dbtype->ptr, dbinst); 120 if (NULL == dbconn) { 121 log_error(srv->errh, __FILE__, __LINE__, 122 "dbi_conn_new_r() failed. " 123 "Do you have the DBD for this db type installed?"); 124 dbi_shutdown_r(dbinst); 125 return -1; 126 } 127 128 /* set options */ 129 for (size_t j = 0; j < opts->used; ++j) { 130 data_unset *du = opts->data[j]; 131 const buffer *opt = &du->key; 132 if (!buffer_string_is_empty(opt)) { 133 if (du->type == TYPE_INTEGER) { 134 data_integer *di = (data_integer *)du; 135 dbi_conn_set_option_numeric(dbconn, opt->ptr, di->value); 136 } else if (du->type == TYPE_STRING) { 137 data_string *ds = (data_string *)du; 138 if (&ds->value != sqlquery && &ds->value != dbtype) { 139 dbi_conn_set_option(dbconn, opt->ptr, ds->value.ptr); 140 } 141 } 142 } 143 } 144 145 dbconf = (vhostdb_config *)calloc(1, sizeof(*dbconf)); 146 dbconf->dbinst = dbinst; 147 dbconf->dbconn = dbconn; 148 dbconf->sqlquery = sqlquery; 149 dbconf->errh = srv->errh; 150 dbconf->reconnect_count = 0; 151 *vdata = dbconf; 152 153 /* used to automatically reconnect to the database */ 154 dbi_conn_error_handler(dbconn, mod_vhostdb_dbi_error_callback, dbconf); 155 156 /* connect to database */ 157 mod_vhostdb_dbi_error_callback(dbconn, dbconf); 158 if (dbconf->reconnect_count >= 3) { 159 mod_vhostdb_dbconf_free(dbconf); 160 return -1; 161 } 162 } 163 164 return 0; 165 } 166 167 static void mod_vhostdb_patch_config (request_st * const r, plugin_data * const p); 168 169 static int mod_vhostdb_dbi_query(request_st * const r, void *p_d, buffer *docroot) 170 { 171 plugin_data *p = (plugin_data *)p_d; 172 vhostdb_config *dbconf; 173 dbi_result result; 174 unsigned long long nrows; 175 int retry_count = 0; 176 177 /*(reuse buffer for sql query before generating docroot result)*/ 178 buffer *sqlquery = docroot; 179 buffer_clear(sqlquery); /*(also resets docroot (alias))*/ 180 181 mod_vhostdb_patch_config(r, p); 182 if (NULL == p->conf.vdata) return 0; /*(after resetting docroot)*/ 183 dbconf = (vhostdb_config *)p->conf.vdata; 184 185 for (char *b = dbconf->sqlquery->ptr, *d; *b; b = d+1) { 186 if (NULL != (d = strchr(b, '?'))) { 187 /* escape the uri.authority */ 188 char *esc = NULL; 189 size_t len = dbi_conn_escape_string_copy(dbconf->dbconn, r->uri.authority.ptr, &esc); 190 buffer_append_string_len(sqlquery, b, (size_t)(d - b)); 191 buffer_append_string_len(sqlquery, esc, len); 192 free(esc); 193 if (0 == len) return -1; 194 } else { 195 d = dbconf->sqlquery->ptr + buffer_string_length(dbconf->sqlquery); 196 buffer_append_string_len(sqlquery, b, (size_t)(d - b)); 197 break; 198 } 199 } 200 201 /* reset our reconnect-attempt counter, this is a new query. */ 202 dbconf->reconnect_count = 0; 203 204 do { 205 result = dbi_conn_query(dbconf->dbconn, sqlquery->ptr); 206 } while (!result && ++retry_count < 2); 207 208 buffer_clear(docroot); /*(reset buffer to store result)*/ 209 210 if (!result) { 211 const char *errmsg; 212 dbi_conn_error(dbconf->dbconn, &errmsg); 213 log_error(r->conf.errh, __FILE__, __LINE__, "%s", errmsg); 214 return -1; 215 } 216 217 nrows = dbi_result_get_numrows(result); 218 if (nrows && nrows != DBI_ROW_ERROR && dbi_result_next_row(result)) { 219 buffer_copy_string(docroot, dbi_result_get_string_idx(result, 1)); 220 } /* else no such virtual host */ 221 222 dbi_result_free(result); 223 return 0; 224 } 225 226 227 228 229 INIT_FUNC(mod_vhostdb_init) { 230 static http_vhostdb_backend_t http_vhostdb_backend_dbi = 231 { "dbi", mod_vhostdb_dbi_query, NULL }; 232 plugin_data *p = calloc(1, sizeof(*p)); 233 234 /* register http_vhostdb_backend_dbi */ 235 http_vhostdb_backend_dbi.p_d = p; 236 http_vhostdb_backend_set(&http_vhostdb_backend_dbi); 237 238 return p; 239 } 240 241 FREE_FUNC(mod_vhostdb_cleanup) { 242 plugin_data * const p = p_d; 243 if (NULL == p->cvlist) return; 244 /* (init i to 0 if global context; to 1 to skip empty global context) */ 245 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { 246 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 247 for (; -1 != cpv->k_id; ++cpv) { 248 if (cpv->vtype != T_CONFIG_LOCAL || NULL == cpv->v.v) continue; 249 switch (cpv->k_id) { 250 case 0: /* vhostdb.<db> */ 251 mod_vhostdb_dbconf_free(cpv->v.v); 252 break; 253 default: 254 break; 255 } 256 } 257 } 258 } 259 260 static void mod_vhostdb_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { 261 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 262 case 0: /* vhostdb.<db> */ 263 if (cpv->vtype == T_CONFIG_LOCAL) 264 pconf->vdata = cpv->v.v; 265 break; 266 default:/* should not happen */ 267 return; 268 } 269 } 270 271 static void mod_vhostdb_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { 272 do { 273 mod_vhostdb_merge_config_cpv(pconf, cpv); 274 } while ((++cpv)->k_id != -1); 275 } 276 277 static void mod_vhostdb_patch_config(request_st * const r, plugin_data * const p) { 278 p->conf = p->defaults; /* copy small struct instead of memcpy() */ 279 /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ 280 for (int i = 1, used = p->nconfig; i < used; ++i) { 281 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 282 mod_vhostdb_merge_config(&p->conf,p->cvlist + p->cvlist[i].v.u2[0]); 283 } 284 } 285 286 SETDEFAULTS_FUNC(mod_vhostdb_set_defaults) { 287 static const config_plugin_keys_t cpk[] = { 288 { CONST_STR_LEN("vhostdb.dbi"), 289 T_CONFIG_ARRAY_KVANY, 290 T_CONFIG_SCOPE_CONNECTION } 291 ,{ NULL, 0, 292 T_CONFIG_UNSET, 293 T_CONFIG_SCOPE_UNSET } 294 }; 295 296 plugin_data * const p = p_d; 297 if (!config_plugin_values_init(srv, p, cpk, "mod_vhostdb_dbi")) 298 return HANDLER_ERROR; 299 300 /* process and validate config directives 301 * (init i to 0 if global context; to 1 to skip empty global context) */ 302 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { 303 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 304 for (; -1 != cpv->k_id; ++cpv) { 305 switch (cpv->k_id) { 306 case 0: /* vhostdb.<db> */ 307 if (cpv->v.a->used) { 308 if (0 != mod_vhostdb_dbconf_setup(srv, cpv->v.a, &cpv->v.v)) 309 return HANDLER_ERROR; 310 if (NULL != cpv->v.v) 311 cpv->vtype = T_CONFIG_LOCAL; 312 } 313 break; 314 default:/* should not happen */ 315 break; 316 } 317 } 318 } 319 320 /* initialize p->defaults from global config context */ 321 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 322 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 323 if (-1 != cpv->k_id) 324 mod_vhostdb_merge_config(&p->defaults, cpv); 325 } 326 327 return HANDLER_GO_ON; 328 } 329 330 331 int mod_vhostdb_dbi_plugin_init (plugin *p); 332 int mod_vhostdb_dbi_plugin_init (plugin *p) 333 { 334 p->version = LIGHTTPD_VERSION_ID; 335 p->name = "vhostdb_dbi"; 336 337 p->init = mod_vhostdb_init; 338 p->cleanup = mod_vhostdb_cleanup; 339 p->set_defaults = mod_vhostdb_set_defaults; 340 341 return 0; 342 } 343