1/* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements.  See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License.  You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include "apr_strings.h"
18#include "apr_md5.h"            /* for apr_password_validate */
19
20#include "ap_config.h"
21#include "ap_provider.h"
22#include "httpd.h"
23#include "http_config.h"
24#include "http_core.h"
25#include "http_log.h"
26#include "http_protocol.h"
27#include "http_request.h"
28
29#include "mod_auth.h"
30
31#include "ap_socache.h"
32#include "util_mutex.h"
33#include "apr_optional.h"
34
35module AP_MODULE_DECLARE_DATA authn_socache_module;
36
37typedef struct authn_cache_dircfg {
38    apr_interval_time_t timeout;
39    apr_array_header_t *providers;
40    const char *context;
41} authn_cache_dircfg;
42
43/* FIXME:
44 * I think the cache and mutex should be global
45 */
46static apr_global_mutex_t *authn_cache_mutex = NULL;
47static ap_socache_provider_t *socache_provider = NULL;
48static ap_socache_instance_t *socache_instance = NULL;
49static const char *const authn_cache_id = "authn-socache";
50static int configured;
51
52static apr_status_t remove_lock(void *data)
53{
54    if (authn_cache_mutex) {
55        apr_global_mutex_destroy(authn_cache_mutex);
56        authn_cache_mutex = NULL;
57    }
58    return APR_SUCCESS;
59}
60static apr_status_t destroy_cache(void *data)
61{
62    if (socache_instance) {
63        socache_provider->destroy(socache_instance, (server_rec*)data);
64        socache_instance = NULL;
65    }
66    return APR_SUCCESS;
67}
68
69
70static int authn_cache_precfg(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptmp)
71{
72    apr_status_t rv = ap_mutex_register(pconf, authn_cache_id,
73                                        NULL, APR_LOCK_DEFAULT, 0);
74    if (rv != APR_SUCCESS) {
75        ap_log_perror(APLOG_MARK, APLOG_CRIT, rv, plog, APLOGNO(01673)
76                      "failed to register %s mutex", authn_cache_id);
77        return 500; /* An HTTP status would be a misnomer! */
78    }
79    socache_provider = ap_lookup_provider(AP_SOCACHE_PROVIDER_GROUP,
80                                          AP_SOCACHE_DEFAULT_PROVIDER,
81                                          AP_SOCACHE_PROVIDER_VERSION);
82    configured = 0;
83    return OK;
84}
85static int authn_cache_post_config(apr_pool_t *pconf, apr_pool_t *plog,
86                                   apr_pool_t *ptmp, server_rec *s)
87{
88    apr_status_t rv;
89    static struct ap_socache_hints authn_cache_hints = {64, 32, 60000000};
90
91    if (!configured) {
92        return OK;    /* don't waste the overhead of creating mutex & cache */
93    }
94    if (socache_provider == NULL) {
95        ap_log_perror(APLOG_MARK, APLOG_CRIT, 0, plog, APLOGNO(01674)
96                      "Please select a socache provider with AuthnCacheSOCache "
97                      "(no default found on this platform). Maybe you need to "
98                      "load mod_socache_shmcb or another socache module first");
99        return 500; /* An HTTP status would be a misnomer! */
100    }
101
102    rv = ap_global_mutex_create(&authn_cache_mutex, NULL,
103                                authn_cache_id, NULL, s, pconf, 0);
104    if (rv != APR_SUCCESS) {
105        ap_log_perror(APLOG_MARK, APLOG_CRIT, rv, plog, APLOGNO(01675)
106                      "failed to create %s mutex", authn_cache_id);
107        return 500; /* An HTTP status would be a misnomer! */
108    }
109    apr_pool_cleanup_register(pconf, NULL, remove_lock, apr_pool_cleanup_null);
110
111    rv = socache_provider->init(socache_instance, authn_cache_id,
112                                &authn_cache_hints, s, pconf);
113    if (rv != APR_SUCCESS) {
114        ap_log_perror(APLOG_MARK, APLOG_CRIT, rv, plog, APLOGNO(01677)
115                      "failed to initialise %s cache", authn_cache_id);
116        return 500; /* An HTTP status would be a misnomer! */
117    }
118    apr_pool_cleanup_register(pconf, (void*)s, destroy_cache, apr_pool_cleanup_null);
119    return OK;
120}
121static void authn_cache_child_init(apr_pool_t *p, server_rec *s)
122{
123    const char *lock;
124    apr_status_t rv;
125    if (!configured) {
126        return;       /* don't waste the overhead of creating mutex & cache */
127    }
128    lock = apr_global_mutex_lockfile(authn_cache_mutex);
129    rv = apr_global_mutex_child_init(&authn_cache_mutex, lock, p);
130    if (rv != APR_SUCCESS) {
131        ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(01678)
132                     "failed to initialise mutex in child_init");
133    }
134}
135
136static const char *authn_cache_socache(cmd_parms *cmd, void *CFG,
137                                       const char *arg)
138{
139    const char *errmsg = ap_check_cmd_context(cmd, GLOBAL_ONLY);
140    const char *sep, *name;
141
142    if (errmsg)
143        return errmsg;
144
145    /* Argument is of form 'name:args' or just 'name'. */
146    sep = ap_strchr_c(arg, ':');
147    if (sep) {
148        name = apr_pstrmemdup(cmd->pool, arg, sep - arg);
149        sep++;
150    }
151    else {
152        name = arg;
153    }
154
155    socache_provider = ap_lookup_provider(AP_SOCACHE_PROVIDER_GROUP, name,
156                                          AP_SOCACHE_PROVIDER_VERSION);
157    if (socache_provider == NULL) {
158        errmsg = apr_psprintf(cmd->pool,
159                              "Unknown socache provider '%s'. Maybe you need "
160                              "to load the appropriate socache module "
161                              "(mod_socache_%s?)", arg, arg);
162    }
163    else {
164        errmsg = socache_provider->create(&socache_instance, sep,
165                                          cmd->temp_pool, cmd->pool);
166    }
167
168    if (errmsg) {
169        errmsg = apr_psprintf(cmd->pool, "AuthnCacheSOCache: %s", errmsg);
170    }
171    return errmsg;
172}
173
174static const char *authn_cache_enable(cmd_parms *cmd, void *CFG)
175{
176    const char *errmsg = ap_check_cmd_context(cmd, GLOBAL_ONLY);
177    configured = 1;
178    return errmsg;
179}
180
181static const char *const directory = "directory";
182static void* authn_cache_dircfg_create(apr_pool_t *pool, char *s)
183{
184    authn_cache_dircfg *ret = apr_palloc(pool, sizeof(authn_cache_dircfg));
185    ret->timeout = apr_time_from_sec(300);
186    ret->providers = NULL;
187    ret->context = directory;
188    return ret;
189}
190/* not sure we want this.  Might be safer to document use-all-or-none */
191static void* authn_cache_dircfg_merge(apr_pool_t *pool, void *BASE, void *ADD)
192{
193    authn_cache_dircfg *base = BASE;
194    authn_cache_dircfg *add = ADD;
195    authn_cache_dircfg *ret = apr_pmemdup(pool, add, sizeof(authn_cache_dircfg));
196    /* preserve context and timeout if not defaults */
197    if (add->context == directory) {
198        ret->context = base->context;
199    }
200    if (add->timeout == apr_time_from_sec(300)) {
201        ret->timeout = base->timeout;
202    }
203    if (add->providers == NULL) {
204        ret->providers = base->providers;
205    }
206    return ret;
207}
208
209static const char *authn_cache_setprovider(cmd_parms *cmd, void *CFG,
210                                           const char *arg)
211{
212    authn_cache_dircfg *cfg = CFG;
213    if (cfg->providers == NULL) {
214        cfg->providers = apr_array_make(cmd->pool, 4, sizeof(const char*));
215    }
216    APR_ARRAY_PUSH(cfg->providers, const char*) = arg;
217    configured = 1;
218    return NULL;
219}
220
221static const char *authn_cache_timeout(cmd_parms *cmd, void *CFG,
222                                       const char *arg)
223{
224    authn_cache_dircfg *cfg = CFG;
225    int secs = atoi(arg);
226    cfg->timeout = apr_time_from_sec(secs);
227    return NULL;
228}
229
230static const command_rec authn_cache_cmds[] =
231{
232    /* global stuff: cache and mutex */
233    AP_INIT_TAKE1("AuthnCacheSOCache", authn_cache_socache, NULL, RSRC_CONF,
234                  "socache provider for authn cache"),
235    AP_INIT_NO_ARGS("AuthnCacheEnable", authn_cache_enable, NULL, RSRC_CONF,
236                    "enable socache configuration in htaccess even if not enabled anywhere else"),
237    /* per-dir stuff */
238    AP_INIT_ITERATE("AuthnCacheProvideFor", authn_cache_setprovider, NULL,
239                    OR_AUTHCFG, "Determine what authn providers to cache for"),
240    AP_INIT_TAKE1("AuthnCacheTimeout", authn_cache_timeout, NULL,
241                  OR_AUTHCFG, "Timeout (secs) for cached credentials"),
242    AP_INIT_TAKE1("AuthnCacheContext", ap_set_string_slot,
243                  (void*)APR_OFFSETOF(authn_cache_dircfg, context),
244                  ACCESS_CONF, "Context for authn cache"),
245    {NULL}
246};
247
248static const char *construct_key(request_rec *r, const char *context,
249                                 const char *user, const char *realm)
250{
251    /* handle "special" context values */
252    if (!strcmp(context, "directory")) {
253        /* FIXME: are we at risk of this blowing up? */
254        char *new_context;
255        char *slash = strrchr(r->uri, '/');
256        new_context = apr_palloc(r->pool, slash - r->uri +
257                                 strlen(r->server->server_hostname) + 1);
258        strcpy(new_context, r->server->server_hostname);
259        strncat(new_context, r->uri, slash - r->uri);
260        context = new_context;
261    }
262    else if (!strcmp(context, "server")) {
263        context = r->server->server_hostname;
264    }
265    /* any other context value is literal */
266
267    if (realm == NULL) {                              /* basic auth */
268        return apr_pstrcat(r->pool, context, ":", user, NULL);
269    }
270    else {                                            /* digest auth */
271        return apr_pstrcat(r->pool, context, ":", user, ":", realm, NULL);
272    }
273}
274static void ap_authn_cache_store(request_rec *r, const char *module,
275                                 const char *user, const char *realm,
276                                 const char* data)
277{
278    apr_status_t rv;
279    authn_cache_dircfg *dcfg;
280    const char *key;
281    apr_time_t expiry;
282    int i;
283    int use_cache = 0;
284
285    /* first check whether we're cacheing for this module */
286    dcfg = ap_get_module_config(r->per_dir_config, &authn_socache_module);
287    if (!configured || !dcfg->providers) {
288        return;
289    }
290    for (i = 0; i < dcfg->providers->nelts; ++i) {
291        if (!strcmp(module, APR_ARRAY_IDX(dcfg->providers, i, const char*))) {
292            use_cache = 1;
293            break;
294        }
295    }
296    if (!use_cache) {
297        return;
298    }
299
300    /* OK, we're on.  Grab mutex to do our business */
301    rv = apr_global_mutex_trylock(authn_cache_mutex);
302    if (APR_STATUS_IS_EBUSY(rv)) {
303        /* don't wait around; just abandon it */
304        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, APLOGNO(01679)
305                      "authn credentials for %s not cached (mutex busy)", user);
306        return;
307    }
308    else if (rv != APR_SUCCESS) {
309        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01680)
310                      "Failed to cache authn credentials for %s in %s",
311                      module, dcfg->context);
312        return;
313    }
314
315    /* We have the mutex, so go ahead */
316    /* first build our key and determine expiry time */
317    key = construct_key(r, dcfg->context, user, realm);
318    expiry = apr_time_now() + dcfg->timeout;
319
320    /* store it */
321    rv = socache_provider->store(socache_instance, r->server,
322                                 (unsigned char*)key, strlen(key), expiry,
323                                 (unsigned char*)data, strlen(data), r->pool);
324    if (rv == APR_SUCCESS) {
325        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01681)
326                      "Cached authn credentials for %s in %s",
327                      user, dcfg->context);
328    }
329    else {
330        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01682)
331                      "Failed to cache authn credentials for %s in %s",
332                      module, dcfg->context);
333    }
334
335    /* We're done with the mutex */
336    rv = apr_global_mutex_unlock(authn_cache_mutex);
337    if (rv != APR_SUCCESS) {
338        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01683) "Failed to release mutex!");
339    }
340    return;
341}
342
343#define MAX_VAL_LEN 100
344static authn_status check_password(request_rec *r, const char *user,
345                                   const char *password)
346{
347
348    /* construct key
349     * look it up
350     * if found, test password
351     *
352     * mutexing here would be a big performance drag.
353     * It's definitely unnecessary with some backends (like ndbm or gdbm)
354     * Is there a risk in the general case?  I guess the only risk we
355     * care about is a race condition that gets us a dangling pointer
356     * to no-longer-defined memory.  Hmmm ...
357     */
358    apr_status_t rv;
359    const char *key;
360    authn_cache_dircfg *dcfg;
361    unsigned char val[MAX_VAL_LEN];
362    unsigned int vallen = MAX_VAL_LEN - 1;
363    dcfg = ap_get_module_config(r->per_dir_config, &authn_socache_module);
364    if (!configured || !dcfg->providers) {
365        return AUTH_USER_NOT_FOUND;
366    }
367    key = construct_key(r, dcfg->context, user, NULL);
368    rv = socache_provider->retrieve(socache_instance, r->server,
369                                    (unsigned char*)key, strlen(key),
370                                    val, &vallen, r->pool);
371
372    if (APR_STATUS_IS_NOTFOUND(rv)) {
373        /* not found - just return */
374        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01684)
375                      "Authn cache: no credentials found for %s", user);
376        return AUTH_USER_NOT_FOUND;
377    }
378    else if (rv == APR_SUCCESS) {
379        /* OK, we got a value */
380        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01685)
381                      "Authn cache: found credentials for %s", user);
382        val[vallen] = 0;
383    }
384    else {
385        /* error: give up and pass the buck */
386        /* FIXME: getting this for NOTFOUND - prolly a bug in mod_socache */
387        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01686)
388                      "Error accessing authentication cache");
389        return AUTH_USER_NOT_FOUND;
390    }
391
392    rv = apr_password_validate(password, (char*) val);
393    if (rv != APR_SUCCESS) {
394        return AUTH_DENIED;
395    }
396
397    return AUTH_GRANTED;
398}
399
400static authn_status get_realm_hash(request_rec *r, const char *user,
401                                   const char *realm, char **rethash)
402{
403    apr_status_t rv;
404    const char *key;
405    authn_cache_dircfg *dcfg;
406    unsigned char val[MAX_VAL_LEN];
407    unsigned int vallen = MAX_VAL_LEN - 1;
408    dcfg = ap_get_module_config(r->per_dir_config, &authn_socache_module);
409    if (!configured || !dcfg->providers) {
410        return AUTH_USER_NOT_FOUND;
411    }
412    key = construct_key(r, dcfg->context, user, realm);
413    rv = socache_provider->retrieve(socache_instance, r->server,
414                                    (unsigned char*)key, strlen(key),
415                                    val, &vallen, r->pool);
416
417    if (APR_STATUS_IS_NOTFOUND(rv)) {
418        /* not found - just return */
419        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01687)
420                      "Authn cache: no credentials found for %s", user);
421        return AUTH_USER_NOT_FOUND;
422    }
423    else if (rv == APR_SUCCESS) {
424        /* OK, we got a value */
425        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01688)
426                      "Authn cache: found credentials for %s", user);
427    }
428    else {
429        /* error: give up and pass the buck */
430        /* FIXME: getting this for NOTFOUND - prolly a bug in mod_socache */
431        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01689)
432                      "Error accessing authentication cache");
433        return AUTH_USER_NOT_FOUND;
434    }
435    *rethash = apr_pstrmemdup(r->pool, (char *)val, vallen);
436
437    return AUTH_USER_FOUND;
438}
439
440static const authn_provider authn_cache_provider =
441{
442    &check_password,
443    &get_realm_hash,
444};
445static void register_hooks(apr_pool_t *p)
446{
447    ap_register_auth_provider(p, AUTHN_PROVIDER_GROUP, "socache",
448                              AUTHN_PROVIDER_VERSION,
449                              &authn_cache_provider, AP_AUTH_INTERNAL_PER_CONF);
450    APR_REGISTER_OPTIONAL_FN(ap_authn_cache_store);
451    ap_hook_pre_config(authn_cache_precfg, NULL, NULL, APR_HOOK_MIDDLE);
452    ap_hook_post_config(authn_cache_post_config, NULL, NULL, APR_HOOK_MIDDLE);
453    ap_hook_child_init(authn_cache_child_init, NULL, NULL, APR_HOOK_MIDDLE);
454}
455
456AP_DECLARE_MODULE(authn_socache) =
457{
458    STANDARD20_MODULE_STUFF,
459    authn_cache_dircfg_create,
460    authn_cache_dircfg_merge,
461    NULL,
462    NULL,
463    authn_cache_cmds,
464    register_hooks
465};
466