source: mod_gnutls/src/gnutls_cache.c @ 368e581

debian/masterdebian/stretch-backportsupstream
Last change on this file since 368e581 was 6b4136c, checked in by Thomas Klute <thomas2.klute@…>, 3 years ago

Store OCSP responses in DBM cache before use

This is not proper caching yet (the cached response is updated on
every use), but it shows the approach to be used. Memcached support to
be added once DBM works properly.

  • Property mode set to 100644
File size: 19.1 KB
Line 
1/**
2 *  Copyright 2004-2005 Paul Querna
3 *  Copyright 2008 Nikos Mavrogiannopoulos
4 *  Copyright 2011 Dash Shendy
5 *  Copyright 2015-2016 Thomas Klute
6 *
7 *  Licensed under the Apache License, Version 2.0 (the "License");
8 *  you may not use this file except in compliance with the License.
9 *  You may obtain a copy of the License at
10 *
11 *      http://www.apache.org/licenses/LICENSE-2.0
12 *
13 *  Unless required by applicable law or agreed to in writing, software
14 *  distributed under the License is distributed on an "AS IS" BASIS,
15 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 *  See the License for the specific language governing permissions and
17 *  limitations under the License.
18 *
19 */
20
21#include "gnutls_cache.h"
22#include "mod_gnutls.h"
23
24#if HAVE_APR_MEMCACHE
25#include "apr_memcache.h"
26#endif
27
28#include "apr_dbm.h"
29
30#include "ap_mpm.h"
31
32#include <unistd.h>
33#include <sys/types.h>
34
35#if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE)
36#include "unixd.h"
37#endif
38
39/* it seems the default has some strange errors. Use SDBM
40 */
41#define MC_TAG "mod_gnutls:"
42#define MC_TAG_LEN sizeof(MC_TAG)
43#define STR_SESSION_LEN (GNUTLS_SESSION_ID_STRING_LEN + MC_TAG_LEN)
44
45#if MODULE_MAGIC_NUMBER_MAJOR < 20081201
46#define ap_unixd_config unixd_config
47#endif
48
49#ifdef APLOG_USE_MODULE
50APLOG_USE_MODULE(gnutls);
51#endif
52
53char *mgs_session_id2sz(unsigned char *id, int idlen,
54        char *str, int strsize) {
55    char *cp;
56    int n;
57
58    cp = str;
59    for (n = 0; n < idlen && n < GNUTLS_MAX_SESSION_ID; n++) {
60        apr_snprintf(cp, strsize - (cp - str), "%02X", id[n]);
61        cp += 2;
62    }
63    *cp = '\0';
64    return str;
65}
66
67/* Name the Session ID as:
68 * server:port.SessionID
69 * to disallow resuming sessions on different servers
70 */
71static int mgs_session_id2dbm(conn_rec *c, unsigned char *id, int idlen,
72                              gnutls_datum_t *dbmkey)
73{
74    char buf[STR_SESSION_LEN];
75    char *sz;
76
77    sz = mgs_session_id2sz(id, idlen, buf, sizeof (buf));
78    if (sz == NULL)
79        return -1;
80
81    char *newkey = apr_psprintf(c->pool, "%s:%d.%s",
82                                c->base_server->server_hostname,
83                                c->base_server->port, sz);
84    dbmkey->size = strlen(newkey);
85    /* signedness does not matter for arbitrary bits */
86    dbmkey->data = (unsigned char*) newkey;
87    return 0;
88}
89
90#define CTIME "%b %d %k:%M:%S %Y %Z"
91
92char *mgs_time2sz(time_t in_time, char *str, int strsize) {
93    apr_time_exp_t vtm;
94    apr_size_t ret_size;
95    apr_time_t t;
96
97
98    apr_time_ansi_put(&t, in_time);
99    apr_time_exp_gmt(&vtm, t);
100    apr_strftime(str, &ret_size, strsize - 1, CTIME, &vtm);
101
102    return str;
103}
104
105#if HAVE_APR_MEMCACHE
106
107/* Name the Session ID as:
108 * server:port.SessionID
109 * to disallow resuming sessions on different servers
110 */
111static char *mgs_session_id2mc(conn_rec * c, unsigned char *id, int idlen) {
112    char buf[STR_SESSION_LEN];
113    char *sz;
114
115    sz = mgs_session_id2sz(id, idlen, buf, sizeof (buf));
116    if (sz == NULL)
117        return NULL;
118
119    return apr_psprintf(c->pool, MC_TAG "%s:%d.%s",
120            c->base_server->server_hostname,
121            c->base_server->port, sz);
122}
123
124/**
125 * GnuTLS Session Cache using libmemcached
126 *
127 */
128
129/* The underlying apr_memcache system is thread safe... woohoo */
130static apr_memcache_t *mc;
131
132static int mc_cache_child_init(apr_pool_t * p, server_rec * s,
133        mgs_srvconf_rec * sc) {
134    apr_status_t rv = APR_SUCCESS;
135    int thread_limit = 0;
136    int nservers = 0;
137    char *cache_config;
138    char *split;
139    char *tok;
140
141    ap_mpm_query(AP_MPMQ_HARD_LIMIT_THREADS, &thread_limit);
142
143    /* Find all the servers in the first run to get a total count */
144    cache_config = apr_pstrdup(p, sc->cache_config);
145    split = apr_strtok(cache_config, " ", &tok);
146    while (split) {
147        nservers++;
148        split = apr_strtok(NULL, " ", &tok);
149    }
150
151    rv = apr_memcache_create(p, nservers, 0, &mc);
152    if (rv != APR_SUCCESS) {
153        ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s,
154                "[gnutls_cache] Failed to create Memcache Object of '%d' size.",
155                nservers);
156        return rv;
157    }
158
159    /* Now add each server to the memcache */
160    cache_config = apr_pstrdup(p, sc->cache_config);
161    split = apr_strtok(cache_config, " ", &tok);
162    while (split) {
163        apr_memcache_server_t *st;
164        char *host_str;
165        char *scope_id;
166        apr_port_t port;
167
168        rv = apr_parse_addr_port(&host_str, &scope_id, &port,
169                split, p);
170        if (rv != APR_SUCCESS) {
171            ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s,
172                    "[gnutls_cache] Failed to Parse Server: '%s'",
173                    split);
174            return rv;
175        }
176
177        if (host_str == NULL) {
178            ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s,
179                    "[gnutls_cache] Failed to Parse Server, "
180                    "no hostname specified: '%s'", split);
181            return rv;
182        }
183
184        if (port == 0) {
185            port = 11211; /* default port */
186        }
187
188        /* Should Max Conns be (thread_limit / nservers) ? */
189        rv = apr_memcache_server_create(p,
190                host_str, port,
191                0,
192                1, thread_limit, 600, &st);
193        if (rv != APR_SUCCESS) {
194            ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s,
195                    "[gnutls_cache] Failed to Create Server: %s:%d",
196                    host_str, port);
197            return rv;
198        }
199
200        rv = apr_memcache_add_server(mc, st);
201        if (rv != APR_SUCCESS) {
202            ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s,
203                    "[gnutls_cache] Failed to Add Server: %s:%d",
204                    host_str, port);
205            return rv;
206        }
207
208        split = apr_strtok(NULL, " ", &tok);
209    }
210    return rv;
211}
212
213static int mc_cache_store(void *baton, gnutls_datum_t key,
214        gnutls_datum_t data) {
215    apr_status_t rv = APR_SUCCESS;
216    mgs_handle_t *ctxt = baton;
217    char *strkey = NULL;
218    apr_uint32_t timeout;
219
220    strkey = mgs_session_id2mc(ctxt->c, key.data, key.size);
221    if (!strkey)
222        return -1;
223
224    timeout = apr_time_sec(ctxt->sc->cache_timeout);
225
226    rv = apr_memcache_set(mc, strkey, (char *) data.data, data.size, timeout,
227            0);
228
229    if (rv != APR_SUCCESS) {
230        ap_log_error(APLOG_MARK, APLOG_CRIT, rv,
231                ctxt->c->base_server,
232                "[gnutls_cache] error setting key '%s' "
233                "with %d bytes of data", strkey, data.size);
234        return -1;
235    }
236
237    return 0;
238}
239
240static gnutls_datum_t mc_cache_fetch(void *baton, gnutls_datum_t key) {
241    apr_status_t rv = APR_SUCCESS;
242    mgs_handle_t *ctxt = baton;
243    char *strkey = NULL;
244    char *value;
245    apr_size_t value_len;
246    gnutls_datum_t data = {NULL, 0};
247
248    strkey = mgs_session_id2mc(ctxt->c, key.data, key.size);
249    if (!strkey) {
250        return data;
251    }
252
253    rv = apr_memcache_getp(mc, ctxt->c->pool, strkey,
254            &value, &value_len, NULL);
255
256    if (rv != APR_SUCCESS) {
257#if MOD_GNUTLS_DEBUG
258        ap_log_error(APLOG_MARK, APLOG_DEBUG, rv,
259                ctxt->c->base_server,
260                "[gnutls_cache] error fetching key '%s' ",
261                strkey);
262#endif
263        data.size = 0;
264        data.data = NULL;
265        return data;
266    }
267
268    /* TODO: Eliminate this memcpy. gnutls-- */
269    data.data = gnutls_malloc(value_len);
270    if (data.data == NULL)
271        return data;
272
273    data.size = value_len;
274    memcpy(data.data, value, value_len);
275
276    return data;
277}
278
279static int mc_cache_delete(void *baton, gnutls_datum_t key) {
280    apr_status_t rv = APR_SUCCESS;
281    mgs_handle_t *ctxt = baton;
282    char *strkey = NULL;
283
284    strkey = mgs_session_id2mc(ctxt->c, key.data, key.size);
285    if (!strkey)
286        return -1;
287
288    rv = apr_memcache_delete(mc, strkey, 0);
289
290    if (rv != APR_SUCCESS) {
291        ap_log_error(APLOG_MARK, APLOG_DEBUG, rv,
292                ctxt->c->base_server,
293                "[gnutls_cache] error deleting key '%s' ",
294                strkey);
295        return -1;
296    }
297
298    return 0;
299}
300
301#endif  /* have_apr_memcache */
302
303static const char *db_type(mgs_srvconf_rec * sc) {
304    if (sc->cache_type == mgs_cache_gdbm)
305        return "gdbm";
306    else
307        return "db";
308}
309
310#define SSL_DBM_FILE_MODE ( APR_UREAD | APR_UWRITE | APR_GREAD | APR_WREAD )
311
312/***
313 * The signatures of the dbm_cache_...() functions may be a bit
314 * confusing: "store" and "expire" take a server_rec, "fetch" an
315 * mgs_handle_t, and "delete" the void* required for a
316 * gnutls_db_remove_func. The first three have matching ..._session
317 * functions to fit their respective GnuTLS session cache signatures.
318 *
319 * This is because "store", "expire", and "fetch" are also needed for
320 * the OCSP cache. Their ..._session variants have been created to
321 * take care of the session cache specific parts, mainly calculating
322 * the DB key from the session ID. They have to match the appropriate
323 * GnuTLS DB function signatures.
324 *
325 * To update cached OCSP responses independent of client connections,
326 * "store" and "expire" have to work without a connection context. On
327 * the other hand "fetch" does not need to do that, because cached
328 * OCSP responses will be retrieved for use in client connections.
329 ***/
330
331static void dbm_cache_expire(server_rec *s)
332{
333    mgs_srvconf_rec *sc = (mgs_srvconf_rec *)
334        ap_get_module_config(s->module_config, &gnutls_module);
335
336    apr_status_t rv;
337    apr_dbm_t *dbm;
338    apr_datum_t dbmkey;
339    apr_datum_t dbmval;
340    apr_time_t dtime;
341    apr_pool_t *spool;
342    int total, deleted;
343
344    apr_time_t now = apr_time_now();
345
346    if (now - sc->last_cache_check < (sc->cache_timeout) / 2)
347        return;
348
349    sc->last_cache_check = now;
350
351    apr_pool_create(&spool, NULL);
352
353    total = 0;
354    deleted = 0;
355
356    rv = apr_dbm_open_ex(&dbm, db_type(sc),
357            sc->cache_config, APR_DBM_RWCREATE,
358            SSL_DBM_FILE_MODE, spool);
359    if (rv != APR_SUCCESS) {
360        ap_log_error(APLOG_MARK, APLOG_NOTICE, rv, s,
361                "[gnutls_cache] error opening cache searcher '%s'",
362                sc->cache_config);
363        apr_pool_destroy(spool);
364        return;
365    }
366
367    apr_dbm_firstkey(dbm, &dbmkey);
368    while (dbmkey.dptr != NULL) {
369        apr_dbm_fetch(dbm, dbmkey, &dbmval);
370        if (dbmval.dptr != NULL
371                && dbmval.dsize >= sizeof (apr_time_t)) {
372            memcpy(&dtime, dbmval.dptr, sizeof (apr_time_t));
373
374            if (now >= dtime) {
375                apr_dbm_delete(dbm, dbmkey);
376                deleted++;
377            }
378            apr_dbm_freedatum(dbm, dbmval);
379        } else {
380            apr_dbm_delete(dbm, dbmkey);
381            deleted++;
382        }
383        total++;
384        apr_dbm_nextkey(dbm, &dbmkey);
385    }
386    apr_dbm_close(dbm);
387
388    ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s,
389            "[gnutls_cache] Cleaned up cache '%s'. Deleted %d and left %d",
390            sc->cache_config, deleted, total - deleted);
391
392    apr_pool_destroy(spool);
393
394    return;
395}
396
397gnutls_datum_t dbm_cache_fetch(mgs_handle_t *ctxt, gnutls_datum_t key)
398{
399    gnutls_datum_t data = {NULL, 0};
400    apr_dbm_t *dbm;
401    apr_datum_t dbmkey = {(char*) key.data, key.size};
402    apr_datum_t dbmval;
403    apr_status_t rv;
404
405    rv = apr_dbm_open_ex(&dbm, db_type(ctxt->sc),
406            ctxt->sc->cache_config, APR_DBM_READONLY,
407            SSL_DBM_FILE_MODE, ctxt->c->pool);
408    if (rv != APR_SUCCESS) {
409        ap_log_cerror(APLOG_MARK, APLOG_NOTICE, rv, ctxt->c,
410                      "error opening cache '%s'",
411                      ctxt->sc->cache_config);
412        return data;
413    }
414
415    rv = apr_dbm_fetch(dbm, dbmkey, &dbmval);
416
417    if (rv != APR_SUCCESS) {
418        apr_dbm_close(dbm);
419        return data;
420    }
421
422    if (dbmval.dptr == NULL || dbmval.dsize <= sizeof (apr_time_t)) {
423        apr_dbm_freedatum(dbm, dbmval);
424        apr_dbm_close(dbm);
425        return data;
426    }
427
428    data.size = dbmval.dsize - sizeof (apr_time_t);
429
430    data.data = gnutls_malloc(data.size);
431    if (data.data == NULL) {
432        apr_dbm_freedatum(dbm, dbmval);
433        apr_dbm_close(dbm);
434        return data;
435    }
436
437    ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, ctxt->c,
438                  "fetched %ld bytes from cache",
439                  dbmval.dsize);
440
441    memcpy(data.data, dbmval.dptr + sizeof (apr_time_t), data.size);
442
443    apr_dbm_freedatum(dbm, dbmval);
444    apr_dbm_close(dbm);
445
446    return data;
447}
448
449static gnutls_datum_t dbm_cache_fetch_session(void *baton, gnutls_datum_t key)
450{
451    gnutls_datum_t data = {NULL, 0};
452    gnutls_datum_t dbmkey;
453    mgs_handle_t *ctxt = baton;
454
455    if (mgs_session_id2dbm(ctxt->c, key.data, key.size, &dbmkey) < 0)
456        return data;
457
458    return dbm_cache_fetch(ctxt, dbmkey);
459}
460
461int dbm_cache_store(server_rec *s, gnutls_datum_t key,
462                    gnutls_datum_t data, apr_time_t expiry)
463{
464    mgs_srvconf_rec *sc = (mgs_srvconf_rec *)
465        ap_get_module_config(s->module_config, &gnutls_module);
466
467    apr_dbm_t *dbm;
468    apr_datum_t dbmkey = {(char*) key.data, key.size};
469    apr_datum_t dbmval;
470    apr_status_t rv;
471    apr_pool_t *spool;
472
473    /* we expire dbm only on every store */
474    dbm_cache_expire(s);
475
476    apr_pool_create(&spool, NULL);
477
478    /* create DBM value */
479    dbmval.dsize = data.size + sizeof (apr_time_t);
480    dbmval.dptr = (char *) apr_palloc(spool, dbmval.dsize);
481
482    /* prepend expiration time */
483    memcpy((char *) dbmval.dptr, &expiry, sizeof (apr_time_t));
484    memcpy((char *) dbmval.dptr + sizeof (apr_time_t),
485            data.data, data.size);
486
487    rv = apr_dbm_open_ex(&dbm, db_type(sc),
488                         sc->cache_config, APR_DBM_RWCREATE,
489                         SSL_DBM_FILE_MODE, spool);
490    if (rv != APR_SUCCESS)
491    {
492        ap_log_error(APLOG_MARK, APLOG_NOTICE, rv, s,
493                     "error opening cache '%s'",
494                     sc->cache_config);
495        apr_pool_destroy(spool);
496        return -1;
497    }
498
499    rv = apr_dbm_store(dbm, dbmkey, dbmval);
500    if (rv != APR_SUCCESS)
501    {
502        ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s,
503                     "error storing in cache '%s'",
504                     sc->cache_config);
505        apr_dbm_close(dbm);
506        apr_pool_destroy(spool);
507        return -1;
508    }
509
510    ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s,
511                 "stored %ld bytes of data (%ld byte key) in cache '%s'",
512                 dbmval.dsize, dbmkey.dsize, sc->cache_config);
513
514    apr_dbm_close(dbm);
515
516    apr_pool_destroy(spool);
517
518    return 0;
519}
520
521static int dbm_cache_store_session(void *baton, gnutls_datum_t key,
522                                   gnutls_datum_t data)
523{
524    mgs_handle_t *ctxt = baton;
525    gnutls_datum_t dbmkey;
526
527    if (mgs_session_id2dbm(ctxt->c, key.data, key.size, &dbmkey) < 0)
528        return -1;
529
530    apr_time_t expiry = apr_time_now() + ctxt->sc->cache_timeout;
531
532    return dbm_cache_store(ctxt->c->base_server, dbmkey, data, expiry);
533}
534
535static int dbm_cache_delete(void *baton, gnutls_datum_t key) {
536    apr_dbm_t *dbm;
537    gnutls_datum_t tmpkey;
538    mgs_handle_t *ctxt = baton;
539    apr_status_t rv;
540
541    if (mgs_session_id2dbm(ctxt->c, key.data, key.size, &tmpkey) < 0)
542        return -1;
543    apr_datum_t dbmkey = {(char*) tmpkey.data, tmpkey.size};
544
545    rv = apr_dbm_open_ex(&dbm, db_type(ctxt->sc),
546            ctxt->sc->cache_config, APR_DBM_RWCREATE,
547            SSL_DBM_FILE_MODE, ctxt->c->pool);
548    if (rv != APR_SUCCESS) {
549        ap_log_error(APLOG_MARK, APLOG_NOTICE, rv,
550                ctxt->c->base_server,
551                "[gnutls_cache] error opening cache '%s'",
552                ctxt->sc->cache_config);
553        return -1;
554    }
555
556    rv = apr_dbm_delete(dbm, dbmkey);
557
558    if (rv != APR_SUCCESS) {
559        ap_log_error(APLOG_MARK, APLOG_NOTICE, rv,
560                ctxt->c->base_server,
561                "[gnutls_cache] error deleting from cache '%s'",
562                ctxt->sc->cache_config);
563        apr_dbm_close(dbm);
564        return -1;
565    }
566
567    apr_dbm_close(dbm);
568
569    return 0;
570}
571
572static int dbm_cache_post_config(apr_pool_t * p, server_rec * s,
573        mgs_srvconf_rec * sc) {
574    apr_status_t rv;
575    apr_dbm_t *dbm;
576    const char *path1;
577    const char *path2;
578
579    rv = apr_dbm_open_ex(&dbm, db_type(sc), sc->cache_config,
580            APR_DBM_RWCREATE, SSL_DBM_FILE_MODE, p);
581
582    if (rv != APR_SUCCESS) {
583        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s,
584                "GnuTLS: Cannot create DBM Cache at `%s'",
585                sc->cache_config);
586        return rv;
587    }
588
589    apr_dbm_close(dbm);
590
591    apr_dbm_get_usednames_ex(p, db_type(sc), sc->cache_config, &path1,
592            &path2);
593
594    /* The Following Code takes logic directly from mod_ssl's DBM Cache */
595#if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE)
596    /* Running as Root */
597    if (path1 && geteuid() == 0) {
598        if (0 != chown(path1, ap_unixd_config.user_id, -1))
599            ap_log_error(APLOG_MARK, APLOG_NOTICE, -1, s,
600                         "GnuTLS: could not chown cache path1 `%s' to uid %d (errno: %d)",
601                         path1, ap_unixd_config.user_id, errno);
602        if (path2 != NULL) {
603            if (0 != chown(path2, ap_unixd_config.user_id, -1))
604                ap_log_error(APLOG_MARK, APLOG_NOTICE, -1, s,
605                             "GnuTLS: could not chown cache path2 `%s' to uid %d (errno: %d)",
606                             path2, ap_unixd_config.user_id, errno);
607        }
608    }
609#endif
610
611    return rv;
612}
613
614int mgs_cache_post_config(apr_pool_t * p, server_rec * s,
615        mgs_srvconf_rec * sc) {
616
617    /* if GnuTLSCache was never explicitly set: */
618    if (sc->cache_type == mgs_cache_unset)
619        sc->cache_type = mgs_cache_none;
620    /* if GnuTLSCacheTimeout was never explicitly set: */
621    if (sc->cache_timeout == -1)
622        sc->cache_timeout = apr_time_from_sec(300);
623
624    if (sc->cache_type == mgs_cache_dbm
625            || sc->cache_type == mgs_cache_gdbm) {
626        return dbm_cache_post_config(p, s, sc);
627    }
628    return 0;
629}
630
631#if HAVE_APR_MEMCACHE
632int mgs_cache_child_init(apr_pool_t * p,
633                         server_rec * s,
634                         mgs_srvconf_rec * sc)
635#else
636int mgs_cache_child_init(apr_pool_t * p __attribute__((unused)),
637                         server_rec * s __attribute__((unused)),
638                         mgs_srvconf_rec * sc)
639#endif
640{
641    if (sc->cache_type == mgs_cache_dbm
642            || sc->cache_type == mgs_cache_gdbm) {
643        return 0;
644    }
645#if HAVE_APR_MEMCACHE
646    else if (sc->cache_type == mgs_cache_memcache) {
647        return mc_cache_child_init(p, s, sc);
648    }
649#endif
650    return 0;
651}
652
653#include <assert.h>
654
655int mgs_cache_session_init(mgs_handle_t * ctxt) {
656    if (ctxt->sc->cache_type == mgs_cache_dbm
657            || ctxt->sc->cache_type == mgs_cache_gdbm) {
658        gnutls_db_set_retrieve_function(ctxt->session,
659                dbm_cache_fetch_session);
660        gnutls_db_set_remove_function(ctxt->session,
661                dbm_cache_delete);
662        gnutls_db_set_store_function(ctxt->session,
663                dbm_cache_store_session);
664        gnutls_db_set_ptr(ctxt->session, ctxt);
665    }
666#if HAVE_APR_MEMCACHE
667    else if (ctxt->sc->cache_type == mgs_cache_memcache) {
668        gnutls_db_set_retrieve_function(ctxt->session,
669                mc_cache_fetch);
670        gnutls_db_set_remove_function(ctxt->session,
671                mc_cache_delete);
672        gnutls_db_set_store_function(ctxt->session,
673                mc_cache_store);
674        gnutls_db_set_ptr(ctxt->session, ctxt);
675    }
676#endif
677
678    return 0;
679}
Note: See TracBrowser for help on using the repository browser.