--- /dev/null
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krb5/ccache/cc_dir.c - Directory-based credential cache collection */
+/*
+ * Copyright (C) 2011 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Export of this software from the United States of America may
+ * require a specific license from the United States Government.
+ * It is the responsibility of any person or organization contemplating
+ * export to obtain such a license before exporting.
+ *
+ * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
+ * distribute this software and its documentation for any purpose and
+ * without fee is hereby granted, provided that the above copyright
+ * notice appear in all copies and that both that copyright notice and
+ * this permission notice appear in supporting documentation, and that
+ * the name of M.I.T. not be used in advertising or publicity pertaining
+ * to distribution of the software without specific, written prior
+ * permission. Furthermore if you modify this software you must label
+ * your software as modified software and not distribute it in such a
+ * fashion that it might be confused with the original M.I.T. software.
+ * M.I.T. makes no representations about the suitability of
+ * this software for any purpose. It is provided "as is" without express
+ * or implied warranty.
+ */
+
+/*
+ * This credential cache type represents a set of file-based caches with a
+ * switchable primary cache. An alternate form of the type represents a
+ * subsidiary file cache within the directory.
+ *
+ * A cache name of the form DIR:dirname identifies a directory containing the
+ * cache set. Resolving a name of this form results in dirname's primary
+ * cache. If a context's default cache is of this form, the global cache
+ * collection will contain dirname's cache set, and new unique caches of type
+ * DIR will be created within dirname.
+ *
+ * A cache name of the form DIR::filepath represents a single cache within the
+ * directory. Switching to a ccache of this type causes the directory's
+ * primary cache to be set to the named cache.
+ *
+ * Within the directory, cache names begin with 'tkt'. The file "primary"
+ * contains a single line naming the primary cache. The directory must already
+ * exist when the DIR ccache is resolved, but the primary file will be created
+ * automatically if it does not exist.
+ */
+
+#include "k5-int.h"
+#include "cc-int.h"
+
+#if HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#if HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+/* This is Unix-only for now. To work on Windows, we will need opendir/readdir
+ * replacements and possibly more flexible newline handling. */
+#ifndef _WIN32
+
+#include <dirent.h>
+
+extern const krb5_cc_ops krb5_dcc_ops;
+extern const krb5_cc_ops krb5_fcc_ops;
+
+/* Fields are not modified after creation, so no lock is necessary. */
+typedef struct dcc_data_st {
+ char *residual; /* dirname or :filename */
+ krb5_ccache fcc; /* File cache for actual cache ops */
+} dcc_data;
+
+static inline krb5_boolean
+filename_is_cache(const char *filename)
+{
+ return (strncmp(filename, "tkt", 3) == 0);
+}
+
+/* Compose the pathname of the primary file within a cache directory. */
+static inline krb5_error_code
+primary_pathname(const char *dirname, char **path_out)
+{
+ return k5_path_join(dirname, "primary", path_out);
+}
+
+/* Compose a residual string for a subsidiary path with the specified directory
+ * name and filename. */
+static krb5_error_code
+subsidiary_residual(const char *dirname, const char *filename, char **out)
+{
+ krb5_error_code ret;
+ char *path, *residual;
+
+ *out = NULL;
+ ret = k5_path_join(dirname, filename, &path);
+ if (ret)
+ return ret;
+ ret = asprintf(&residual, ":%s", path);
+ free(path);
+ if (ret < 0)
+ return ENOMEM;
+ *out = residual;
+ return 0;
+}
+
+static inline krb5_error_code
+split_path(krb5_context context, const char *path, char **dirname_out,
+ char **filename_out)
+{
+ krb5_error_code ret;
+ char *dirname, *filename;
+
+ *dirname_out = NULL;
+ *filename_out = NULL;
+ ret = k5_path_split(path, &dirname, &filename);
+ if (ret)
+ return ret;
+
+ if (*dirname == '\0') {
+ ret = KRB5_CC_BADNAME;
+ krb5_set_error_message(context, ret,
+ _("Subsidiary cache path %s has no parent "
+ "directory"), path);
+ goto error;
+ }
+ if (!filename_is_cache(filename)) {
+ ret = KRB5_CC_BADNAME;
+ krb5_set_error_message(context, ret,
+ _("Subsidiary cache path %s filename does not "
+ "begin with \"tkt\""), path);
+ goto error;
+ }
+
+ *dirname_out = dirname;
+ *filename_out = filename;
+ return 0;
+
+error:
+ free(dirname);
+ free(filename);
+ return ret;
+}
+
+/* Read the primary file and compose the residual string for the primary
+ * subsidiary cache file. */
+static krb5_error_code
+read_primary_file(krb5_context context, const char *primary_path,
+ const char *dirname, char **residual_out)
+{
+ FILE *fp;
+ char buf[64], *ret;
+ size_t len;
+
+ *residual_out = NULL;
+
+ /* Open the file and read its first line. */
+ fp = fopen(primary_path, "r");
+ if (fp == NULL)
+ return ENOENT;
+ ret = fgets(buf, sizeof(buf), fp);
+ fclose(fp);
+ if (ret == NULL)
+ return KRB5_CC_IO;
+ len = strlen(buf);
+
+ /* Check if line is too long, doesn't look like a subsidiary cache
+ * filename, or isn't a single-component filename. */
+ if (buf[len - 1] != '\n' || !filename_is_cache(buf) ||
+ strchr(buf, '/') || strchr(buf, '\\')) {
+ krb5_set_error_message(context, KRB5_CC_FORMAT,
+ _("%s contains invalid filename"),
+ primary_path);
+ return KRB5_CC_FORMAT;
+ }
+ buf[len - 1] = '\0';
+
+ return subsidiary_residual(dirname, buf, residual_out);
+}
+
+/* Create or update the primary file with a line containing contents. */
+static krb5_error_code
+write_primary_file(const char *primary_path, const char *contents)
+{
+ krb5_error_code ret = KRB5_CC_IO;
+ char *newpath = NULL;
+ FILE *fp = NULL;
+ int fd = -1;
+
+ if (asprintf(&newpath, "%s.XXXXXX", primary_path) < 0)
+ return ENOMEM;
+ fd = mkstemp(newpath);
+ if (fd < 0)
+ goto cleanup;
+#ifdef HAVE_CHMOD
+ chmod(newpath, S_IRUSR | S_IWUSR);
+#endif
+ fp = fdopen(fd, "w");
+ if (fp == NULL)
+ goto cleanup;
+ fd = -1;
+ if (fprintf(fp, "%s\n", contents) < 0)
+ goto cleanup;
+ if (fclose(fp) == EOF)
+ goto cleanup;
+ fp = NULL;
+ if (rename(newpath, primary_path) != 0)
+ goto cleanup;
+ ret = 0;
+
+cleanup:
+ if (fd >= 0)
+ close(fd);
+ if (fp != NULL)
+ fclose(fp);
+ free(newpath);
+ return ret;
+}
+
+/* Verify that a cache directory path exists as a directory. */
+static krb5_error_code
+verify_dir(krb5_context context, const char *dirname)
+{
+ struct stat st;
+
+ if (stat(dirname, &st) < 0) {
+ krb5_set_error_message(context, KRB5_FCC_NOFILE,
+ _("Credential cache directory %s does not "
+ "exist"), dirname);
+ return KRB5_FCC_NOFILE;
+ }
+ if (!S_ISDIR(st.st_mode)) {
+ krb5_set_error_message(context, KRB5_CC_FORMAT,
+ _("Credential cache directory %s exists but is"
+ "not a directory"), dirname);
+ return KRB5_CC_FORMAT;
+ }
+ return 0;
+}
+
+/*
+ * If the default ccache name for context is a directory collection, set
+ * *dirname_out to the directory name for that collection. Otherwise set
+ * *dirname_out to NULL.
+ */
+static krb5_error_code
+get_context_default_dir(krb5_context context, char **dirname_out)
+{
+ const char *defname;
+ char *dirname;
+
+ *dirname_out = NULL;
+ defname = krb5_cc_default_name(context);
+ if (defname == NULL)
+ return 0;
+ if (strncmp(defname, "DIR:", 4) != 0 ||
+ defname[4] == ':' || defname[4] == '\0')
+ return 0;
+ dirname = strdup(defname + 4);
+ if (dirname == NULL)
+ return ENOMEM;
+ *dirname_out = dirname;
+ return 0;
+}
+
+static const char * KRB5_CALLCONV
+dcc_get_name(krb5_context context, krb5_ccache cache)
+{
+ dcc_data *data = cache->data;
+
+ return data->residual;
+}
+
+/* Construct a cache object given a residual string and file ccache. Take
+ * ownership of fcc on success. */
+static krb5_error_code
+make_cache(const char *residual, krb5_ccache fcc, krb5_ccache *cache_out)
+{
+ krb5_ccache cache = NULL;
+ dcc_data *data = NULL;
+ char *residual_copy = NULL;
+
+ cache = malloc(sizeof(*cache));
+ if (cache == NULL)
+ goto oom;
+ data = malloc(sizeof(*data));
+ if (data == NULL)
+ goto oom;
+ residual_copy = strdup(residual);
+ if (residual_copy == NULL)
+ goto oom;
+
+ data->residual = residual_copy;
+ data->fcc = fcc;
+ cache->ops = &krb5_dcc_ops;
+ cache->data = data;
+ cache->magic = KV5M_CCACHE;
+ *cache_out = cache;
+ return 0;
+
+oom:
+ free(cache);
+ free(data);
+ free(residual_copy);
+ return ENOMEM;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_resolve(krb5_context context, krb5_ccache *cache_out, const char *residual)
+{
+ krb5_error_code ret;
+ krb5_ccache fcc;
+ char *primary_path = NULL, *sresidual = NULL, *dirname, *filename;
+
+ *cache_out = NULL;
+
+ if (*residual == ':') {
+ /* This is a subsidiary cache within the directory. */
+ ret = split_path(context, residual + 1, &dirname, &filename);
+ if (ret)
+ return ret;
+
+ ret = verify_dir(context, dirname);
+ free(dirname);
+ free(filename);
+ if (ret)
+ return ret;
+ } else {
+ /* This is the directory itself; resolve to the primary cache. */
+ ret = verify_dir(context, residual);
+ if (ret)
+ return ret;
+
+ ret = primary_pathname(residual, &primary_path);
+ if (ret)
+ goto cleanup;
+
+ ret = read_primary_file(context, primary_path, residual, &sresidual);
+ if (ret == ENOENT) {
+ /* Create an initial primary file. */
+ ret = write_primary_file(primary_path, "tkt");
+ if (ret)
+ goto cleanup;
+ ret = subsidiary_residual(residual, "tkt", &sresidual);
+ if (ret)
+ goto cleanup;
+ }
+ residual = sresidual;
+ }
+
+ ret = krb5_fcc_ops.resolve(context, &fcc, residual + 1);
+ if (ret)
+ goto cleanup;
+ ret = make_cache(residual, fcc, cache_out);
+ if (ret)
+ krb5_fcc_ops.close(context, fcc);
+
+cleanup:
+ free(primary_path);
+ free(sresidual);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_gen_new(krb5_context context, krb5_ccache *cache_out)
+{
+ krb5_error_code ret;
+ char *dirname = NULL, *template = NULL, *residual = NULL;
+ krb5_ccache fcc;
+
+ *cache_out = NULL;
+ ret = get_context_default_dir(context, &dirname);
+ if (ret)
+ return ret;
+ if (dirname == NULL) {
+ krb5_set_error_message(context, KRB5_DCC_CANNOT_CREATE,
+ _("Can't create new subsidiary cache because "
+ "default cache is not a directory "
+ "collection"));
+ return KRB5_DCC_CANNOT_CREATE;
+ }
+ ret = k5_path_join(dirname, "tktXXXXXX", &template);
+ if (ret)
+ goto cleanup;
+ ret = krb5int_fcc_new_unique(context, template, &fcc);
+ if (ret)
+ goto cleanup;
+ if (asprintf(&residual, ":%s", template) < 0) {
+ ret = ENOMEM;
+ goto cleanup;
+ }
+ ret = make_cache(residual, fcc, cache_out);
+ if (ret)
+ krb5_fcc_ops.destroy(context, fcc);
+
+cleanup:
+ free(dirname);
+ free(template);
+ free(residual);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_init(krb5_context context, krb5_ccache cache, krb5_principal princ)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.init(context, data->fcc, princ);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_destroy(krb5_context context, krb5_ccache cache)
+{
+ dcc_data *data = cache->data;
+ krb5_error_code ret;
+
+ ret = krb5_fcc_ops.destroy(context, data->fcc);
+ free(data->residual);
+ free(data);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_close(krb5_context context, krb5_ccache cache)
+{
+ dcc_data *data = cache->data;
+ krb5_error_code ret;
+
+ ret = krb5_fcc_ops.close(context, data->fcc);
+ free(data->residual);
+ free(data);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_store(krb5_context context, krb5_ccache cache, krb5_creds *creds)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.store(context, data->fcc, creds);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_retrieve(krb5_context context, krb5_ccache cache, krb5_flags flags,
+ krb5_creds *mcreds, krb5_creds *creds)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.retrieve(context, data->fcc, flags, mcreds,
+ creds);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_get_princ(krb5_context context, krb5_ccache cache,
+ krb5_principal *princ_out)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.get_princ(context, data->fcc, princ_out);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_get_first(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.get_first(context, data->fcc, cursor);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_get_next(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor,
+ krb5_creds *creds)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.get_next(context, data->fcc, cursor, creds);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_end_get(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.end_get(context, data->fcc, cursor);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_remove_cred(krb5_context context, krb5_ccache cache, krb5_flags flags,
+ krb5_creds *creds)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.remove_cred(context, data->fcc, flags, creds);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_set_flags(krb5_context context, krb5_ccache cache, krb5_flags flags)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.set_flags(context, data->fcc, flags);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_get_flags(krb5_context context, krb5_ccache cache, krb5_flags *flags_out)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.get_flags(context, data->fcc, flags_out);
+}
+
+struct dcc_ptcursor_data {
+ char *primary;
+ char *dirname;
+ DIR *dir;
+ krb5_boolean first;
+};
+
+/* Construct a cursor, taking ownership of dirname, primary, and dir on
+ * success. */
+static krb5_error_code
+make_cursor(char *dirname, char *primary, DIR *dir,
+ krb5_cc_ptcursor *cursor_out)
+{
+ krb5_cc_ptcursor cursor;
+ struct dcc_ptcursor_data *data;
+
+ *cursor_out = NULL;
+
+ data = malloc(sizeof(*data));
+ if (data == NULL)
+ return ENOMEM;
+ cursor = malloc(sizeof(*cursor));
+ if (cursor == NULL) {
+ free(data);
+ return ENOMEM;
+ }
+
+ data->dirname = dirname;
+ data->primary = primary;
+ data->dir = dir;
+ data->first = TRUE;
+ cursor->ops = &krb5_dcc_ops;
+ cursor->data = data;
+ *cursor_out = cursor;
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_ptcursor_new(krb5_context context, krb5_cc_ptcursor *cursor_out)
+{
+ krb5_error_code ret;
+ char *dirname = NULL, *primary_path = NULL, *primary = NULL;
+ DIR *dir = NULL;
+
+ *cursor_out = NULL;
+
+ /* Open the directory for the context's default cache. */
+ ret = get_context_default_dir(context, &dirname);
+ if (ret || dirname == NULL)
+ goto cleanup;
+ dir = opendir(dirname);
+ if (dir == NULL)
+ goto cleanup;
+
+ /* Fetch the primary cache name if possible. */
+ ret = primary_pathname(dirname, &primary_path);
+ if (ret)
+ goto cleanup;
+ ret = read_primary_file(context, primary_path, dirname, &primary);
+ if (ret)
+ krb5_clear_error_message(context);
+
+ ret = make_cursor(dirname, primary, dir, cursor_out);
+ if (ret)
+ goto cleanup;
+ dirname = primary = NULL;
+ dir = NULL;
+
+cleanup:
+ free(dirname);
+ free(primary_path);
+ free(primary);
+ if (dir)
+ closedir(dir);
+ /* Return an empty cursor if we fail for any reason. */
+ if (*cursor_out == NULL)
+ return make_cursor(NULL, NULL, NULL, cursor_out);
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_ptcursor_next(krb5_context context, krb5_cc_ptcursor cursor,
+ krb5_ccache *cache_out)
+{
+ struct dcc_ptcursor_data *data = cursor->data;
+ struct dirent *ent;
+ char *residual;
+ krb5_error_code ret;
+ struct stat sb;
+
+ *cache_out = NULL;
+ if (data->dir == NULL) /* Empty cursor */
+ return 0;
+
+ /* Return the primary cache if we haven't yet. */
+ if (data->first) {
+ data->first = FALSE;
+ if (data->primary != NULL && stat(data->primary + 1, &sb) == 0)
+ return dcc_resolve(context, cache_out, data->primary);
+ }
+
+ /* Look for the next filename of the correct form, without repeating the
+ * primary cache. */
+ while ((ent = readdir(data->dir)) != NULL) {
+ if (!filename_is_cache(ent->d_name))
+ continue;
+ ret = subsidiary_residual(data->dirname, ent->d_name, &residual);
+ if (ret)
+ return ret;
+ if (data->primary != NULL && strcmp(residual, data->primary) == 0) {
+ free(residual);
+ continue;
+ }
+ ret = dcc_resolve(context, cache_out, residual);
+ free(residual);
+ return ret;
+ }
+
+ /* We exhausted the directory without finding a cache to yield. */
+ free(data->dir);
+ data->dir = NULL;
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_ptcursor_free(krb5_context context, krb5_cc_ptcursor *cursor)
+{
+ struct dcc_ptcursor_data *data = (*cursor)->data;
+
+ if (data->dir)
+ closedir(data->dir);
+ free(data->dirname);
+ free(data->primary);
+ free(data);
+ free(*cursor);
+ *cursor = NULL;
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_lastchange(krb5_context context, krb5_ccache cache,
+ krb5_timestamp *time_out)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.lastchange(context, data->fcc, time_out);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_lock(krb5_context context, krb5_ccache cache)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.lock(context, data->fcc);
+}
+
+static krb5_error_code KRB5_CALLCONV
+dcc_unlock(krb5_context context, krb5_ccache cache)
+{
+ dcc_data *data = cache->data;
+
+ return krb5_fcc_ops.unlock(context, data->fcc);
+}
+
+const krb5_cc_ops krb5_dcc_ops = {
+ 0,
+ "DIR",
+ dcc_get_name,
+ dcc_resolve,
+ dcc_gen_new,
+ dcc_init,
+ dcc_destroy,
+ dcc_close,
+ dcc_store,
+ dcc_retrieve,
+ dcc_get_princ,
+ dcc_get_first,
+ dcc_get_next,
+ dcc_end_get,
+ dcc_remove_cred,
+ dcc_set_flags,
+ dcc_get_flags,
+ dcc_ptcursor_new,
+ dcc_ptcursor_next,
+ dcc_ptcursor_free,
+ NULL, /* move */
+ dcc_lastchange,
+ NULL, /* wasdefault */
+ dcc_lock,
+ dcc_unlock,
+};
+
+#endif /* not _WIN32 */