1 /* 2 * mod_authn_sasl - SASL backend for lighttpd HTTP auth 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 /* mod_authn_sasl 10 * 11 * FUTURE POTENTIAL PERFORMANCE ENHANCEMENTS: 12 * - database response is not cached 13 * TODO: db response caching (for limited time) to reduce load on db 14 * (only cache successful logins to prevent cache bloat?) 15 * (or limit number of entries (size) of cache) 16 * (maybe have negative cache (limited size) of names not found in database) 17 * - database query is synchronous and blocks waiting for response 18 */ 19 20 #include <sasl/sasl.h> 21 22 #include "base.h" 23 #include "http_auth.h" 24 #include "log.h" 25 #include "plugin.h" 26 27 #include <sys/utsname.h> 28 #include <stdlib.h> 29 #include <string.h> 30 31 typedef struct { 32 const char *service; 33 const char *fqdn; 34 const buffer *pwcheck_method; 35 const buffer *sasldb_path; 36 } plugin_config; 37 38 typedef struct { 39 PLUGIN_DATA; 40 plugin_config defaults; 41 plugin_config conf; 42 43 int initonce; 44 } plugin_data; 45 46 static handler_t mod_authn_sasl_basic(request_st *r, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw); 47 48 INIT_FUNC(mod_authn_sasl_init) { 49 static http_auth_backend_t http_auth_backend_sasl = 50 { "sasl", mod_authn_sasl_basic, NULL, NULL }; 51 plugin_data *p = calloc(1, sizeof(*p)); 52 53 /* register http_auth_backend_sasl */ 54 http_auth_backend_sasl.p_d = p; 55 http_auth_backend_set(&http_auth_backend_sasl); 56 57 return p; 58 } 59 60 FREE_FUNC(mod_authn_sasl_free) { 61 plugin_data * const p = p_d; 62 if (p->initonce) sasl_done(); 63 if (NULL == p->cvlist) return; 64 /* (init i to 0 if global context; to 1 to skip empty global context) */ 65 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { 66 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 67 for (; -1 != cpv->k_id; ++cpv) { 68 switch (cpv->k_id) { 69 case 0: /* auth.backend.sasl.opts */ 70 if (cpv->vtype == T_CONFIG_LOCAL) free(cpv->v.v); 71 break; 72 default: 73 break; 74 } 75 } 76 } 77 } 78 79 static void mod_authn_sasl_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { 80 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 81 case 0: /* auth.backend.sasl.opts */ 82 if (cpv->vtype == T_CONFIG_LOCAL) 83 memcpy(pconf, cpv->v.v, sizeof(plugin_config)); 84 break; 85 default:/* should not happen */ 86 return; 87 } 88 } 89 90 static void mod_authn_sasl_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { 91 do { 92 mod_authn_sasl_merge_config_cpv(pconf, cpv); 93 } while ((++cpv)->k_id != -1); 94 } 95 96 static void mod_authn_sasl_patch_config(request_st * const r, plugin_data * const p) { 97 p->conf = p->defaults; /* copy small struct instead of memcpy() */ 98 /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ 99 for (int i = 1, used = p->nconfig; i < used; ++i) { 100 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 101 mod_authn_sasl_merge_config(&p->conf, 102 p->cvlist + p->cvlist[i].v.u2[0]); 103 } 104 } 105 106 static plugin_config * mod_authn_sasl_parse_opts(server *srv, const array * const opts) { 107 const data_string *ds; 108 const char *service = NULL; 109 const char *fqdn = NULL; 110 const buffer *pwcheck_method = NULL; 111 const buffer *sasldb_path = NULL; 112 113 ds = (const data_string *) 114 array_get_element_klen(opts, CONST_STR_LEN("service")); 115 service = (NULL != ds) ? ds->value.ptr : "http"; 116 117 ds = (const data_string *) 118 array_get_element_klen(opts, CONST_STR_LEN("fqdn")); 119 if (NULL != ds) fqdn = ds->value.ptr; 120 if (NULL == fqdn) { 121 static struct utsname uts; 122 if (uts.nodename[0] == '\0') { 123 if (0 != uname(&uts)) { 124 log_perror(srv->errh, __FILE__, __LINE__, "uname()"); 125 return NULL; 126 } 127 } 128 fqdn = uts.nodename; 129 } 130 131 ds = (const data_string *) 132 array_get_element_klen(opts, CONST_STR_LEN("pwcheck_method")); 133 if (NULL != ds) { 134 pwcheck_method = &ds->value; 135 if (!buffer_is_equal_string(&ds->value, CONST_STR_LEN("saslauthd")) 136 && !buffer_is_equal_string(&ds->value, CONST_STR_LEN("auxprop")) 137 && !buffer_is_equal_string(&ds->value, CONST_STR_LEN("sasldb"))){ 138 log_error(srv->errh, __FILE__, __LINE__, 139 "sasl pwcheck_method must be one of saslauthd, " 140 "sasldb, or auxprop, not: %s", ds->value.ptr); 141 return NULL; 142 } 143 if (buffer_is_equal_string(&ds->value, CONST_STR_LEN("sasldb"))) { 144 /* Cyrus libsasl2 expects "auxprop" instead of "sasldb" 145 * (mod_authn_sasl_cb_getopt auxprop_plugin returns "sasldb") */ 146 buffer *b; 147 *(const buffer **)&b = &ds->value; 148 buffer_copy_string_len(b, CONST_STR_LEN("auxprop")); 149 } 150 } 151 152 ds = (const data_string *) 153 array_get_element_klen(opts, CONST_STR_LEN("sasldb_path")); 154 if (NULL != ds) sasldb_path = &ds->value; 155 156 plugin_config *pconf = malloc(sizeof(plugin_config)); 157 force_assert(pconf); 158 pconf->service = service; 159 pconf->fqdn = fqdn; 160 pconf->pwcheck_method = pwcheck_method; 161 pconf->sasldb_path = sasldb_path; 162 return pconf; 163 } 164 165 SETDEFAULTS_FUNC(mod_authn_sasl_set_defaults) { 166 static const config_plugin_keys_t cpk[] = { 167 { CONST_STR_LEN("auth.backend.sasl.opts"), 168 T_CONFIG_ARRAY_KVSTRING, 169 T_CONFIG_SCOPE_CONNECTION } 170 ,{ NULL, 0, 171 T_CONFIG_UNSET, 172 T_CONFIG_SCOPE_UNSET } 173 }; 174 175 plugin_data * const p = p_d; 176 if (!config_plugin_values_init(srv, p, cpk, "mod_authn_sasl")) 177 return HANDLER_ERROR; 178 179 /* process and validate config directives 180 * (init i to 0 if global context; to 1 to skip empty global context) */ 181 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { 182 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 183 for (; -1 != cpv->k_id; ++cpv) { 184 switch (cpv->k_id) { 185 case 0: /* auth.backend.sasl.opts */ 186 if (cpv->v.a->used) { 187 cpv->v.v = mod_authn_sasl_parse_opts(srv, cpv->v.a); 188 if (NULL == cpv->v.v) return HANDLER_ERROR; 189 cpv->vtype = T_CONFIG_LOCAL; 190 } 191 break; 192 default:/* should not happen */ 193 break; 194 } 195 } 196 } 197 198 /* initialize p->defaults from global config context */ 199 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 200 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 201 if (-1 != cpv->k_id) 202 mod_authn_sasl_merge_config(&p->defaults, cpv); 203 } 204 205 return HANDLER_GO_ON; 206 } 207 208 static int mod_authn_sasl_cb_getopt(void *p_d, const char *plugin_name, const char *opt, const char **res, unsigned *len) { 209 plugin_data *p = (plugin_data *)p_d; 210 size_t sz; 211 212 if (0 == strcmp(opt, "pwcheck_method")) { 213 if (!buffer_string_is_empty(p->conf.pwcheck_method)) { 214 *res = p->conf.pwcheck_method->ptr; 215 sz = buffer_string_length(p->conf.pwcheck_method); 216 } 217 else { /* default */ 218 *res = "saslauthd"; 219 sz = sizeof("saslauthd")-1; 220 } 221 } 222 else if (0 == strcmp(opt, "sasldb_path") 223 && !buffer_string_is_empty(p->conf.sasldb_path)) { 224 *res = p->conf.sasldb_path->ptr; 225 sz = buffer_string_length(p->conf.sasldb_path); 226 } 227 else if (0 == strcmp(opt, "auxprop_plugin")) { 228 *res = "sasldb"; 229 sz = sizeof("sasldb")-1; 230 } 231 else { 232 UNUSED(plugin_name); 233 return SASL_FAIL; 234 } 235 236 if (len) *len = (unsigned int)sz; 237 return SASL_OK; 238 } 239 240 static int mod_authn_sasl_cb_log(void *vreq, int level, const char *message) { 241 switch (level) { 242 #if 0 243 case SASL_LOG_NONE: 244 case SASL_LOG_NOTE: 245 case SASL_LOG_DEBUG: 246 case SASL_LOG_TRACE: 247 case SASL_LOG_PASS: 248 #endif 249 default: 250 break; 251 case SASL_LOG_ERR: 252 case SASL_LOG_FAIL: 253 case SASL_LOG_WARN: /* (might omit SASL_LOG_WARN if too noisy in logs) */ 254 log_error(((request_st *)vreq)->conf.errh, __FILE__, __LINE__, 255 "%s", message); 256 break; 257 } 258 return SASL_OK; 259 } 260 261 static handler_t mod_authn_sasl_query(request_st * const r, void *p_d, const buffer * const username, const char * const realm, const char * const pw) { 262 plugin_data *p = (plugin_data *)p_d; 263 sasl_conn_t *sc; 264 sasl_callback_t const cb[] = { 265 { SASL_CB_GETOPT, (int(*)())mod_authn_sasl_cb_getopt, (void *) p }, 266 { SASL_CB_LOG, (int(*)())mod_authn_sasl_cb_log, (void *) r }, 267 { SASL_CB_LIST_END, NULL, NULL } 268 }; 269 int rc; 270 271 mod_authn_sasl_patch_config(r, p); 272 273 if (!p->initonce) { 274 /* must be done once, but after fork() if multiple lighttpd workers */ 275 rc = sasl_server_init(cb, NULL); 276 if (SASL_OK != rc) return HANDLER_ERROR; 277 p->initonce = 1; 278 } 279 280 rc = sasl_server_new(p->conf.service, p->conf.fqdn, 281 realm, NULL, NULL, cb, 0, &sc); 282 if (SASL_OK == rc) { 283 rc = sasl_checkpass(sc, CONST_BUF_LEN(username), pw, strlen(pw)); 284 sasl_dispose(&sc); 285 } 286 287 return (SASL_OK == rc) ? HANDLER_GO_ON : HANDLER_ERROR; 288 } 289 290 static handler_t mod_authn_sasl_basic(request_st * const r, void *p_d, const http_auth_require_t * const require, const buffer * const username, const char * const pw) { 291 char *realm = require->realm->ptr; 292 handler_t rc = mod_authn_sasl_query(r, p_d, username, realm, pw); 293 if (HANDLER_GO_ON != rc) return rc; 294 return http_auth_match_rules(require, username->ptr, NULL, NULL) 295 ? HANDLER_GO_ON /* access granted */ 296 : HANDLER_ERROR; 297 } 298 299 int mod_authn_sasl_plugin_init(plugin *p); 300 int mod_authn_sasl_plugin_init(plugin *p) { 301 p->version = LIGHTTPD_VERSION_ID; 302 p->name = "authn_sasl"; 303 p->init = mod_authn_sasl_init; 304 p->set_defaults= mod_authn_sasl_set_defaults; 305 p->cleanup = mod_authn_sasl_free; 306 307 return 0; 308 } 309