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 <string.h> 10 #include <stdlib.h> 11 12 #include <dbi/dbi.h> 13 14 #include "mod_vhostdb_api.h" 15 #include "base.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 * "sqlite3_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 (sqlquery && !buffer_is_blank(sqlquery) && dbname && dbtype) { 108 /* create/initialise database */ 109 vhostdb_config *dbconf; 110 dbi_inst dbinst = NULL; 111 dbi_conn dbconn; 112 if (dbi_initialize_r(NULL, &dbinst) < 1) { 113 log_error(srv->errh, __FILE__, __LINE__, 114 "dbi_initialize_r() failed. " 115 "Do you have the DBD for this db type installed?"); 116 return -1; 117 } 118 dbconn = dbi_conn_new_r(dbtype->ptr, dbinst); 119 if (NULL == dbconn) { 120 log_error(srv->errh, __FILE__, __LINE__, 121 "dbi_conn_new_r() failed. " 122 "Do you have the DBD for this db type installed?"); 123 dbi_shutdown_r(dbinst); 124 return -1; 125 } 126 127 /* set options */ 128 for (size_t j = 0; j < opts->used; ++j) { 129 data_unset *du = opts->data[j]; 130 const buffer *opt = &du->key; 131 if (!buffer_is_blank(opt)) { 132 if (du->type == TYPE_INTEGER) { 133 data_integer *di = (data_integer *)du; 134 dbi_conn_set_option_numeric(dbconn, opt->ptr, di->value); 135 } else if (du->type == TYPE_STRING) { 136 data_string *ds = (data_string *)du; 137 if (&ds->value != sqlquery && &ds->value != dbtype) { 138 dbi_conn_set_option(dbconn, opt->ptr, ds->value.ptr); 139 } 140 } 141 } 142 } 143 144 dbconf = (vhostdb_config *)calloc(1, sizeof(*dbconf)); 145 dbconf->dbinst = dbinst; 146 dbconf->dbconn = dbconn; 147 dbconf->sqlquery = sqlquery; 148 dbconf->errh = srv->errh; 149 dbconf->reconnect_count = 0; 150 *vdata = dbconf; 151 152 /* used to automatically reconnect to the database */ 153 dbi_conn_error_handler(dbconn, mod_vhostdb_dbi_error_callback, dbconf); 154 155 /* connect to database */ 156 mod_vhostdb_dbi_error_callback(dbconn, dbconf); 157 if (dbconf->reconnect_count >= 3) { 158 mod_vhostdb_dbconf_free(dbconf); 159 return -1; 160 } 161 } 162 163 return 0; 164 } 165 166 static void mod_vhostdb_patch_config (request_st * const r, plugin_data * const p); 167 168 static int mod_vhostdb_dbi_query(request_st * const r, void *p_d, buffer *docroot) 169 { 170 plugin_data *p = (plugin_data *)p_d; 171 vhostdb_config *dbconf; 172 dbi_result result; 173 unsigned long long nrows; 174 int retry_count = 0; 175 176 /*(reuse buffer for sql query before generating docroot result)*/ 177 buffer *sqlquery = docroot; 178 buffer_clear(sqlquery); /*(also resets docroot (alias))*/ 179 180 mod_vhostdb_patch_config(r, p); 181 if (NULL == p->conf.vdata) return 0; /*(after resetting docroot)*/ 182 dbconf = (vhostdb_config *)p->conf.vdata; 183 184 for (char *b = dbconf->sqlquery->ptr, *d; *b; b = d+1) { 185 if (NULL != (d = strchr(b, '?'))) { 186 /* escape the uri.authority */ 187 char *esc = NULL; 188 size_t len = dbi_conn_escape_string_copy(dbconf->dbconn, 189 r->uri.authority.ptr,&esc); 190 buffer_append_str2(sqlquery, b, (size_t)(d - b), esc, len); 191 free(esc); 192 if (0 == len) return -1; 193 } else { 194 d = dbconf->sqlquery->ptr + buffer_clen(dbconf->sqlquery); 195 buffer_append_string_len(sqlquery, b, (size_t)(d - b)); 196 break; 197 } 198 } 199 200 /* reset our reconnect-attempt counter, this is a new query. */ 201 dbconf->reconnect_count = 0; 202 203 do { 204 result = dbi_conn_query(dbconf->dbconn, sqlquery->ptr); 205 } while (!result && ++retry_count < 2); 206 207 buffer_clear(docroot); /*(reset buffer to store result)*/ 208 209 if (!result) { 210 const char *errmsg; 211 dbi_conn_error(dbconf->dbconn, &errmsg); 212 log_error(r->conf.errh, __FILE__, __LINE__, "%s", errmsg); 213 return -1; 214 } 215 216 nrows = dbi_result_get_numrows(result); 217 if (nrows && nrows != DBI_ROW_ERROR && dbi_result_next_row(result)) { 218 buffer_copy_string(docroot, dbi_result_get_string_idx(result, 1)); 219 } /* else no such virtual host */ 220 221 dbi_result_free(result); 222 return 0; 223 } 224 225 226 227 228 INIT_FUNC(mod_vhostdb_init) { 229 static http_vhostdb_backend_t http_vhostdb_backend_dbi = 230 { "dbi", mod_vhostdb_dbi_query, NULL }; 231 plugin_data *p = calloc(1, sizeof(*p)); 232 233 /* register http_vhostdb_backend_dbi */ 234 http_vhostdb_backend_dbi.p_d = p; 235 http_vhostdb_backend_set(&http_vhostdb_backend_dbi); 236 237 return p; 238 } 239 240 FREE_FUNC(mod_vhostdb_cleanup) { 241 plugin_data * const p = p_d; 242 if (NULL == p->cvlist) return; 243 /* (init i to 0 if global context; to 1 to skip empty global context) */ 244 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { 245 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 246 for (; -1 != cpv->k_id; ++cpv) { 247 if (cpv->vtype != T_CONFIG_LOCAL || NULL == cpv->v.v) continue; 248 switch (cpv->k_id) { 249 case 0: /* vhostdb.<db> */ 250 mod_vhostdb_dbconf_free(cpv->v.v); 251 break; 252 default: 253 break; 254 } 255 } 256 } 257 } 258 259 static void mod_vhostdb_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { 260 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 261 case 0: /* vhostdb.<db> */ 262 if (cpv->vtype == T_CONFIG_LOCAL) 263 pconf->vdata = cpv->v.v; 264 break; 265 default:/* should not happen */ 266 return; 267 } 268 } 269 270 static void mod_vhostdb_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { 271 do { 272 mod_vhostdb_merge_config_cpv(pconf, cpv); 273 } while ((++cpv)->k_id != -1); 274 } 275 276 static void mod_vhostdb_patch_config(request_st * const r, plugin_data * const p) { 277 p->conf = p->defaults; /* copy small struct instead of memcpy() */ 278 /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ 279 for (int i = 1, used = p->nconfig; i < used; ++i) { 280 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 281 mod_vhostdb_merge_config(&p->conf,p->cvlist + p->cvlist[i].v.u2[0]); 282 } 283 } 284 285 SETDEFAULTS_FUNC(mod_vhostdb_set_defaults) { 286 static const config_plugin_keys_t cpk[] = { 287 { CONST_STR_LEN("vhostdb.dbi"), 288 T_CONFIG_ARRAY_KVANY, 289 T_CONFIG_SCOPE_CONNECTION } 290 ,{ NULL, 0, 291 T_CONFIG_UNSET, 292 T_CONFIG_SCOPE_UNSET } 293 }; 294 295 plugin_data * const p = p_d; 296 if (!config_plugin_values_init(srv, p, cpk, "mod_vhostdb_dbi")) 297 return HANDLER_ERROR; 298 299 /* process and validate config directives 300 * (init i to 0 if global context; to 1 to skip empty global context) */ 301 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { 302 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 303 for (; -1 != cpv->k_id; ++cpv) { 304 switch (cpv->k_id) { 305 case 0: /* vhostdb.<db> */ 306 if (cpv->v.a->used) { 307 if (0 != mod_vhostdb_dbconf_setup(srv, cpv->v.a, &cpv->v.v)) 308 return HANDLER_ERROR; 309 if (NULL != cpv->v.v) 310 cpv->vtype = T_CONFIG_LOCAL; 311 } 312 break; 313 default:/* should not happen */ 314 break; 315 } 316 } 317 } 318 319 /* initialize p->defaults from global config context */ 320 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 321 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 322 if (-1 != cpv->k_id) 323 mod_vhostdb_merge_config(&p->defaults, cpv); 324 } 325 326 return HANDLER_GO_ON; 327 } 328 329 330 int mod_vhostdb_dbi_plugin_init (plugin *p); 331 int mod_vhostdb_dbi_plugin_init (plugin *p) 332 { 333 p->version = LIGHTTPD_VERSION_ID; 334 p->name = "vhostdb_dbi"; 335 336 p->init = mod_vhostdb_init; 337 p->cleanup = mod_vhostdb_cleanup; 338 p->set_defaults = mod_vhostdb_set_defaults; 339 340 return 0; 341 } 342