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 <sys/utsname.h> 21 #include <stdlib.h> 22 #include <string.h> 23 24 #include <sasl/sasl.h> 25 26 #include "mod_auth_api.h" 27 #include "base.h" 28 #include "log.h" 29 #include "plugin.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 else { 152 static const buffer saslauthd = { "saslauthd", sizeof("saslauthd"), 0 }; 153 pwcheck_method = &saslauthd; 154 } 155 156 ds = (const data_string *) 157 array_get_element_klen(opts, CONST_STR_LEN("sasldb_path")); 158 if (NULL != ds && !buffer_is_blank(&ds->value)) sasldb_path = &ds->value; 159 160 plugin_config *pconf = malloc(sizeof(plugin_config)); 161 force_assert(pconf); 162 pconf->service = service; 163 pconf->fqdn = fqdn; 164 pconf->pwcheck_method = pwcheck_method; 165 pconf->sasldb_path = sasldb_path; 166 return pconf; 167 } 168 169 SETDEFAULTS_FUNC(mod_authn_sasl_set_defaults) { 170 static const config_plugin_keys_t cpk[] = { 171 { CONST_STR_LEN("auth.backend.sasl.opts"), 172 T_CONFIG_ARRAY_KVSTRING, 173 T_CONFIG_SCOPE_CONNECTION } 174 ,{ NULL, 0, 175 T_CONFIG_UNSET, 176 T_CONFIG_SCOPE_UNSET } 177 }; 178 179 plugin_data * const p = p_d; 180 if (!config_plugin_values_init(srv, p, cpk, "mod_authn_sasl")) 181 return HANDLER_ERROR; 182 183 /* process and validate config directives 184 * (init i to 0 if global context; to 1 to skip empty global context) */ 185 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { 186 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 187 for (; -1 != cpv->k_id; ++cpv) { 188 switch (cpv->k_id) { 189 case 0: /* auth.backend.sasl.opts */ 190 if (cpv->v.a->used) { 191 cpv->v.v = mod_authn_sasl_parse_opts(srv, cpv->v.a); 192 if (NULL == cpv->v.v) return HANDLER_ERROR; 193 cpv->vtype = T_CONFIG_LOCAL; 194 } 195 break; 196 default:/* should not happen */ 197 break; 198 } 199 } 200 } 201 202 /* initialize p->defaults from global config context */ 203 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 204 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 205 if (-1 != cpv->k_id) 206 mod_authn_sasl_merge_config(&p->defaults, cpv); 207 } 208 209 return HANDLER_GO_ON; 210 } 211 212 static int mod_authn_sasl_cb_getopt(void *p_d, const char *plugin_name, const char *opt, const char **res, unsigned *len) { 213 plugin_data *p = (plugin_data *)p_d; 214 size_t sz; 215 216 if (0 == strcmp(opt, "pwcheck_method")) { 217 *res = p->conf.pwcheck_method->ptr; 218 sz = buffer_clen(p->conf.pwcheck_method); 219 } 220 else if (0 == strcmp(opt, "sasldb_path") && p->conf.sasldb_path) { 221 *res = p->conf.sasldb_path->ptr; 222 sz = buffer_clen(p->conf.sasldb_path); 223 } 224 else if (0 == strcmp(opt, "auxprop_plugin")) { 225 *res = "sasldb"; 226 sz = sizeof("sasldb")-1; 227 } 228 else { 229 UNUSED(plugin_name); 230 return SASL_FAIL; 231 } 232 233 if (len) *len = (unsigned int)sz; 234 return SASL_OK; 235 } 236 237 static int mod_authn_sasl_cb_log(void *vreq, int level, const char *message) { 238 switch (level) { 239 #if 0 240 case SASL_LOG_NONE: 241 case SASL_LOG_NOTE: 242 case SASL_LOG_DEBUG: 243 case SASL_LOG_TRACE: 244 case SASL_LOG_PASS: 245 #endif 246 default: 247 break; 248 case SASL_LOG_ERR: 249 case SASL_LOG_FAIL: 250 case SASL_LOG_WARN: /* (might omit SASL_LOG_WARN if too noisy in logs) */ 251 log_error(((request_st *)vreq)->conf.errh, __FILE__, __LINE__, 252 "%s", message); 253 break; 254 } 255 return SASL_OK; 256 } 257 258 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) { 259 plugin_data *p = (plugin_data *)p_d; 260 sasl_conn_t *sc; 261 sasl_callback_t const cb[] = { 262 { SASL_CB_GETOPT, (int(*)())mod_authn_sasl_cb_getopt, (void *) p }, 263 { SASL_CB_LOG, (int(*)())mod_authn_sasl_cb_log, (void *) r }, 264 { SASL_CB_LIST_END, NULL, NULL } 265 }; 266 int rc; 267 268 mod_authn_sasl_patch_config(r, p); 269 270 if (!p->initonce) { 271 /* must be done once, but after fork() if multiple lighttpd workers */ 272 rc = sasl_server_init(cb, NULL); 273 if (SASL_OK != rc) return HANDLER_ERROR; 274 p->initonce = 1; 275 } 276 277 rc = sasl_server_new(p->conf.service, p->conf.fqdn, 278 realm, NULL, NULL, cb, 0, &sc); 279 if (SASL_OK == rc) { 280 rc = sasl_checkpass(sc, BUF_PTR_LEN(username), pw, strlen(pw)); 281 sasl_dispose(&sc); 282 } 283 284 return (SASL_OK == rc) ? HANDLER_GO_ON : HANDLER_ERROR; 285 } 286 287 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) { 288 char *realm = require->realm->ptr; 289 handler_t rc = mod_authn_sasl_query(r, p_d, username, realm, pw); 290 if (HANDLER_GO_ON != rc) return rc; 291 return http_auth_match_rules(require, username->ptr, NULL, NULL) 292 ? HANDLER_GO_ON /* access granted */ 293 : HANDLER_ERROR; 294 } 295 296 int mod_authn_sasl_plugin_init(plugin *p); 297 int mod_authn_sasl_plugin_init(plugin *p) { 298 p->version = LIGHTTPD_VERSION_ID; 299 p->name = "authn_sasl"; 300 p->init = mod_authn_sasl_init; 301 p->set_defaults= mod_authn_sasl_set_defaults; 302 p->cleanup = mod_authn_sasl_free; 303 304 return 0; 305 } 306