root/usr/src/cmd/sendmail/libsm/mbdb.c
/*
 * Copyright (c) 2001-2003,2009 Sendmail, Inc. and its suppliers.
 *      All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 */

#include <sm/gen.h>
SM_RCSID("@(#)$Id: mbdb.c,v 1.41 2009/06/19 22:02:26 guenther Exp $")

#include <sys/param.h>

#include <ctype.h>
#include <errno.h>
#include <pwd.h>
#include <stdlib.h>
#include <setjmp.h>
#include <unistd.h>

#include <sm/limits.h>
#include <sm/conf.h>
#include <sm/assert.h>
#include <sm/bitops.h>
#include <sm/errstring.h>
#include <sm/heap.h>
#include <sm/mbdb.h>
#include <sm/string.h>
# ifdef EX_OK
#  undef EX_OK                  /* for SVr4.2 SMP */
# endif /* EX_OK */
#include <sm/sysexits.h>

#if LDAPMAP
# if _LDAP_EXAMPLE_
#  include <sm/ldap.h>
# endif /* _LDAP_EXAMPLE_ */
#endif /* LDAPMAP */

typedef struct
{
        char    *mbdb_typename;
        int     (*mbdb_initialize) __P((char *));
        int     (*mbdb_lookup) __P((char *name, SM_MBDB_T *user));
        void    (*mbdb_terminate) __P((void));
} SM_MBDB_TYPE_T;

static int      mbdb_pw_initialize __P((char *));
static int      mbdb_pw_lookup __P((char *name, SM_MBDB_T *user));
static void     mbdb_pw_terminate __P((void));

#if LDAPMAP
# if _LDAP_EXAMPLE_
static struct sm_ldap_struct LDAPLMAP;
static int      mbdb_ldap_initialize __P((char *));
static int      mbdb_ldap_lookup __P((char *name, SM_MBDB_T *user));
static void     mbdb_ldap_terminate __P((void));
# endif /* _LDAP_EXAMPLE_ */
#endif /* LDAPMAP */

static SM_MBDB_TYPE_T SmMbdbTypes[] =
{
        { "pw", mbdb_pw_initialize, mbdb_pw_lookup, mbdb_pw_terminate },
#if LDAPMAP
# if _LDAP_EXAMPLE_
        { "ldap", mbdb_ldap_initialize, mbdb_ldap_lookup, mbdb_ldap_terminate },
# endif /* _LDAP_EXAMPLE_ */
#endif /* LDAPMAP */
        { NULL, NULL, NULL, NULL }
};

static SM_MBDB_TYPE_T *SmMbdbType = &SmMbdbTypes[0];

/*
**  SM_MBDB_INITIALIZE -- specify which mailbox database to use
**
**      If this function is not called, then the "pw" implementation
**      is used by default; this implementation uses getpwnam().
**
**      Parameters:
**              mbdb -- Which mailbox database to use.
**                      The argument has the form "name" or "name.arg".
**                      "pw" means use getpwnam().
**
**      Results:
**              EX_OK on success, or an EX_* code on failure.
*/

int
sm_mbdb_initialize(mbdb)
        char *mbdb;
{
        size_t namelen;
        int err;
        char *name;
        char *arg;
        SM_MBDB_TYPE_T *t;

        SM_REQUIRE(mbdb != NULL);

        name = mbdb;
        arg = strchr(mbdb, '.');
        if (arg == NULL)
                namelen = strlen(name);
        else
        {
                namelen = arg - name;
                ++arg;
        }

        for (t = SmMbdbTypes; t->mbdb_typename != NULL; ++t)
        {
                if (strlen(t->mbdb_typename) == namelen &&
                    strncmp(name, t->mbdb_typename, namelen) == 0)
                {
                        err = EX_OK;
                        if (t->mbdb_initialize != NULL)
                                err = t->mbdb_initialize(arg);
                        if (err == EX_OK)
                                SmMbdbType = t;
                        return err;
                }
        }
        return EX_UNAVAILABLE;
}

/*
**  SM_MBDB_TERMINATE -- terminate connection to the mailbox database
**
**      Because this function closes any cached file descriptors that
**      are being held open for the connection to the mailbox database,
**      it should be called for security reasons prior to dropping privileges
**      and execing another process.
**
**      Parameters:
**              none.
**
**      Results:
**              none.
*/

void
sm_mbdb_terminate()
{
        if (SmMbdbType->mbdb_terminate != NULL)
                SmMbdbType->mbdb_terminate();
}

/*
**  SM_MBDB_LOOKUP -- look up a local mail recipient, given name
**
**      Parameters:
**              name -- name of local mail recipient
**              user -- pointer to structure to fill in on success
**
**      Results:
**              On success, fill in *user and return EX_OK.
**              If the user does not exist, return EX_NOUSER.
**              If a temporary failure (eg, a network failure) occurred,
**              return EX_TEMPFAIL.  Otherwise return EX_OSERR.
*/

int
sm_mbdb_lookup(name, user)
        char *name;
        SM_MBDB_T *user;
{
        int ret = EX_NOUSER;

        if (SmMbdbType->mbdb_lookup != NULL)
                ret = SmMbdbType->mbdb_lookup(name, user);
        return ret;
}

/*
**  SM_MBDB_FROMPW -- copy from struct pw to SM_MBDB_T
**
**      Parameters:
**              user -- destination user information structure
**              pw -- source passwd structure
**
**      Results:
**              none.
*/

void
sm_mbdb_frompw(user, pw)
        SM_MBDB_T *user;
        struct passwd *pw;
{
        SM_REQUIRE(user != NULL);
        (void) sm_strlcpy(user->mbdb_name, pw->pw_name,
                          sizeof(user->mbdb_name));
        user->mbdb_uid = pw->pw_uid;
        user->mbdb_gid = pw->pw_gid;
        sm_pwfullname(pw->pw_gecos, pw->pw_name, user->mbdb_fullname,
                      sizeof(user->mbdb_fullname));
        (void) sm_strlcpy(user->mbdb_homedir, pw->pw_dir,
                          sizeof(user->mbdb_homedir));
        (void) sm_strlcpy(user->mbdb_shell, pw->pw_shell,
                          sizeof(user->mbdb_shell));
}

/*
**  SM_PWFULLNAME -- build full name of user from pw_gecos field.
**
**      This routine interprets the strange entry that would appear
**      in the GECOS field of the password file.
**
**      Parameters:
**              gecos -- name to build.
**              user -- the login name of this user (for &).
**              buf -- place to put the result.
**              buflen -- length of buf.
**
**      Returns:
**              none.
*/

#if _FFR_HANDLE_ISO8859_GECOS
static char Latin1ToASCII[128] =
{
        32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
        32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33,
        99, 80, 36, 89, 124, 36, 34, 99, 97, 60, 45, 45, 114, 45, 111, 42,
        50, 51, 39, 117, 80, 46, 44, 49, 111, 62, 42, 42, 42, 63, 65, 65,
        65, 65, 65, 65, 65, 67, 69, 69, 69, 69, 73, 73, 73, 73, 68, 78, 79,
        79, 79, 79, 79, 88, 79, 85, 85, 85, 85, 89, 80, 66, 97, 97, 97, 97,
        97, 97, 97, 99, 101, 101, 101, 101, 105, 105, 105, 105, 100, 110,
        111, 111, 111, 111, 111, 47, 111, 117, 117, 117, 117, 121, 112, 121
};
#endif /* _FFR_HANDLE_ISO8859_GECOS */

void
sm_pwfullname(gecos, user, buf, buflen)
        register char *gecos;
        char *user;
        char *buf;
        size_t buflen;
{
        register char *p;
        register char *bp = buf;

        if (*gecos == '*')
                gecos++;

        /* copy gecos, interpolating & to be full name */
        for (p = gecos; *p != '\0' && *p != ',' && *p != ';' && *p != '%'; p++)
        {
                if (bp >= &buf[buflen - 1])
                {
                        /* buffer overflow -- just use login name */
                        (void) sm_strlcpy(buf, user, buflen);
                        return;
                }
                if (*p == '&')
                {
                        /* interpolate full name */
                        (void) sm_strlcpy(bp, user, buflen - (bp - buf));
                        *bp = toupper(*bp);
                        bp += strlen(bp);
                }
                else
                {
#if _FFR_HANDLE_ISO8859_GECOS
                        if ((unsigned char) *p >= 128)
                                *bp++ = Latin1ToASCII[(unsigned char) *p - 128];
                        else
#endif /* _FFR_HANDLE_ISO8859_GECOS */
                                *bp++ = *p;
                }
        }
        *bp = '\0';
}

/*
**  /etc/passwd implementation.
*/

/*
**  MBDB_PW_INITIALIZE -- initialize getpwnam() version
**
**      Parameters:
**              arg -- unused.
**
**      Results:
**              EX_OK.
*/

/* ARGSUSED0 */
static int
mbdb_pw_initialize(arg)
        char *arg;
{
        return EX_OK;
}

/*
**  MBDB_PW_LOOKUP -- look up a local mail recipient, given name
**
**      Parameters:
**              name -- name of local mail recipient
**              user -- pointer to structure to fill in on success
**
**      Results:
**              On success, fill in *user and return EX_OK.
**              Failure: EX_NOUSER.
*/

static int
mbdb_pw_lookup(name, user)
        char *name;
        SM_MBDB_T *user;
{
        struct passwd *pw;

#ifdef HESIOD
        /* DEC Hesiod getpwnam accepts numeric strings -- short circuit it */
        {
                char *p;

                for (p = name; *p != '\0'; p++)
                        if (!isascii(*p) || !isdigit(*p))
                                break;
                if (*p == '\0')
                        return EX_NOUSER;
        }
#endif /* HESIOD */

        errno = 0;
        pw = getpwnam(name);
        if (pw == NULL)
        {
#if 0
                /*
                **  getpwnam() isn't advertised as setting errno.
                **  In fact, under FreeBSD, non-root getpwnam() on
                **  non-existant users returns NULL with errno = EPERM.
                **  This test won't work.
                */
                switch (errno)
                {
                  case 0:
                        return EX_NOUSER;
                  case EIO:
                        return EX_OSERR;
                  default:
                        return EX_TEMPFAIL;
                }
#endif /* 0 */
                return EX_NOUSER;
        }

        sm_mbdb_frompw(user, pw);
        return EX_OK;
}

/*
**  MBDB_PW_TERMINATE -- terminate connection to the mailbox database
**
**      Parameters:
**              none.
**
**      Results:
**              none.
*/

static void
mbdb_pw_terminate()
{
        endpwent();
}

#if LDAPMAP
# if _LDAP_EXAMPLE_
/*
**  LDAP example implementation based on RFC 2307, "An Approach for Using
**  LDAP as a Network Information Service":
**
**      ( nisSchema.1.0 NAME 'uidNumber'
**        DESC 'An integer uniquely identifying a user in an
**              administrative domain'
**        EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )
**
**      ( nisSchema.1.1 NAME 'gidNumber'
**        DESC 'An integer uniquely identifying a group in an
**              administrative domain'
**        EQUALITY integerMatch SYNTAX 'INTEGER' SINGLE-VALUE )
**
**      ( nisSchema.1.2 NAME 'gecos'
**        DESC 'The GECOS field; the common name'
**        EQUALITY caseIgnoreIA5Match
**        SUBSTRINGS caseIgnoreIA5SubstringsMatch
**        SYNTAX 'IA5String' SINGLE-VALUE )
**
**      ( nisSchema.1.3 NAME 'homeDirectory'
**        DESC 'The absolute path to the home directory'
**        EQUALITY caseExactIA5Match
**        SYNTAX 'IA5String' SINGLE-VALUE )
**
**      ( nisSchema.1.4 NAME 'loginShell'
**        DESC 'The path to the login shell'
**        EQUALITY caseExactIA5Match
**        SYNTAX 'IA5String' SINGLE-VALUE )
**
**      ( nisSchema.2.0 NAME 'posixAccount' SUP top AUXILIARY
**        DESC 'Abstraction of an account with POSIX attributes'
**        MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
**        MAY ( userPassword $ loginShell $ gecos $ description ) )
**
*/

#  define MBDB_LDAP_LABEL               "MailboxDatabase"

#  ifndef MBDB_LDAP_FILTER
#   define MBDB_LDAP_FILTER             "(&(objectClass=posixAccount)(uid=%0))"
#  endif /* MBDB_LDAP_FILTER */

#  ifndef MBDB_DEFAULT_LDAP_BASEDN
#   define MBDB_DEFAULT_LDAP_BASEDN     NULL
#  endif /* MBDB_DEFAULT_LDAP_BASEDN */

#  ifndef MBDB_DEFAULT_LDAP_SERVER
#   define MBDB_DEFAULT_LDAP_SERVER     NULL
#  endif /* MBDB_DEFAULT_LDAP_SERVER */

/*
**  MBDB_LDAP_INITIALIZE -- initialize LDAP version
**
**      Parameters:
**              arg -- LDAP specification
**
**      Results:
**              EX_OK on success, or an EX_* code on failure.
*/

static int
mbdb_ldap_initialize(arg)
        char *arg;
{
        sm_ldap_clear(&LDAPLMAP);
        LDAPLMAP.ldap_base = MBDB_DEFAULT_LDAP_BASEDN;
        LDAPLMAP.ldap_host = MBDB_DEFAULT_LDAP_SERVER;
        LDAPLMAP.ldap_filter = MBDB_LDAP_FILTER;

        /* Only want one match */
        LDAPLMAP.ldap_sizelimit = 1;

        /* interpolate new ldap_base and ldap_host from arg if given */
        if (arg != NULL && *arg != '\0')
        {
                char *new;
                char *sep;
                size_t len;

                len = strlen(arg) + 1;
                new = sm_malloc(len);
                if (new == NULL)
                        return EX_TEMPFAIL;
                (void) sm_strlcpy(new, arg, len);
                sep = strrchr(new, '@');
                if (sep != NULL)
                {
                        *sep++ = '\0';
                        LDAPLMAP.ldap_host = sep;
                }
                LDAPLMAP.ldap_base = new;
        }
        return EX_OK;
}


/*
**  MBDB_LDAP_LOOKUP -- look up a local mail recipient, given name
**
**      Parameters:
**              name -- name of local mail recipient
**              user -- pointer to structure to fill in on success
**
**      Results:
**              On success, fill in *user and return EX_OK.
**              Failure: EX_NOUSER.
*/

#define NEED_FULLNAME   0x01
#define NEED_HOMEDIR    0x02
#define NEED_SHELL      0x04
#define NEED_UID        0x08
#define NEED_GID        0x10

static int
mbdb_ldap_lookup(name, user)
        char *name;
        SM_MBDB_T *user;
{
        int msgid;
        int need;
        int ret;
        int save_errno;
        LDAPMessage *entry;
        BerElement *ber;
        char *attr = NULL;

        if (strlen(name) >= sizeof(user->mbdb_name))
        {
                errno = EINVAL;
                return EX_NOUSER;
        }

        if (LDAPLMAP.ldap_filter == NULL)
        {
                /* map not initialized, but don't have arg here */
                errno = EFAULT;
                return EX_TEMPFAIL;
        }

        if (LDAPLMAP.ldap_pid != getpid())
        {
                /* re-open map in this child process */
                LDAPLMAP.ldap_ld = NULL;
        }

        if (LDAPLMAP.ldap_ld == NULL)
        {
                /* map not open, try to open now */
                if (!sm_ldap_start(MBDB_LDAP_LABEL, &LDAPLMAP))
                        return EX_TEMPFAIL;
        }

        sm_ldap_setopts(LDAPLMAP.ldap_ld, &LDAPLMAP);
        msgid = sm_ldap_search(&LDAPLMAP, name);
        if (msgid == -1)
        {
                save_errno = sm_ldap_geterrno(LDAPLMAP.ldap_ld) + E_LDAPBASE;
#  ifdef LDAP_SERVER_DOWN
                if (errno == LDAP_SERVER_DOWN)
                {
                        /* server disappeared, try reopen on next search */
                        sm_ldap_close(&LDAPLMAP);
                }
#  endif /* LDAP_SERVER_DOWN */
                errno = save_errno;
                return EX_TEMPFAIL;
        }

        /* Get results */
        ret = ldap_result(LDAPLMAP.ldap_ld, msgid, 1,
                          (LDAPLMAP.ldap_timeout.tv_sec == 0 ? NULL :
                           &(LDAPLMAP.ldap_timeout)),
                          &(LDAPLMAP.ldap_res));

        if (ret != LDAP_RES_SEARCH_RESULT &&
            ret != LDAP_RES_SEARCH_ENTRY)
        {
                if (ret == 0)
                        errno = ETIMEDOUT;
                else
                        errno = sm_ldap_geterrno(LDAPLMAP.ldap_ld);
                ret = EX_TEMPFAIL;
                goto abort;
        }

        entry = ldap_first_entry(LDAPLMAP.ldap_ld, LDAPLMAP.ldap_res);
        if (entry == NULL)
        {
                int rc;

                /*
                **  We may have gotten an LDAP_RES_SEARCH_RESULT response
                **  with an error inside it, so we have to extract that
                **  with ldap_parse_result().  This can happen when talking
                **  to an LDAP proxy whose backend has gone down.
                */

                save_errno = ldap_parse_result(LDAPLMAP.ldap_ld,
                                               LDAPLMAP.ldap_res, &rc, NULL,
                                               NULL, NULL, NULL, 0);
                if (save_errno == LDAP_SUCCESS)
                        save_errno = rc;
                if (save_errno == LDAP_SUCCESS)
                {
                        errno = ENOENT;
                        ret = EX_NOUSER;
                }
                else
                {
                        errno = save_errno;
                        ret = EX_TEMPFAIL;
                }
                goto abort;
        }

# if !defined(LDAP_VERSION_MAX) && !defined(LDAP_OPT_SIZELIMIT)
        /*
        **  Reset value to prevent lingering
        **  LDAP_DECODING_ERROR due to
        **  OpenLDAP 1.X's hack (see below)
        */

        LDAPLMAP.ldap_ld->ld_errno = LDAP_SUCCESS;
# endif /* !defined(LDAP_VERSION_MAX) !defined(LDAP_OPT_SIZELIMIT) */

        ret = EX_OK;
        need = NEED_FULLNAME|NEED_HOMEDIR|NEED_SHELL|NEED_UID|NEED_GID;
        for (attr = ldap_first_attribute(LDAPLMAP.ldap_ld, entry, &ber);
             attr != NULL;
             attr = ldap_next_attribute(LDAPLMAP.ldap_ld, entry, ber))
        {
                char **vals;

                vals = ldap_get_values(LDAPLMAP.ldap_ld, entry, attr);
                if (vals == NULL)
                {
                        errno = sm_ldap_geterrno(LDAPLMAP.ldap_ld);
                        if (errno == LDAP_SUCCESS)
                        {
                                ldap_memfree(attr);
                                continue;
                        }

                        /* Must be an error */
                        errno += E_LDAPBASE;
                        ret = EX_TEMPFAIL;
                        goto abort;
                }

# if !defined(LDAP_VERSION_MAX) && !defined(LDAP_OPT_SIZELIMIT)
                /*
                **  Reset value to prevent lingering
                **  LDAP_DECODING_ERROR due to
                **  OpenLDAP 1.X's hack (see below)
                */

                LDAPLMAP.ldap_ld->ld_errno = LDAP_SUCCESS;
# endif /* !defined(LDAP_VERSION_MAX) !defined(LDAP_OPT_SIZELIMIT) */

                if (vals[0] == NULL || vals[0][0] == '\0')
                        goto skip;

                if (strcasecmp(attr, "gecos") == 0)
                {
                        if (!bitset(NEED_FULLNAME, need) ||
                            strlen(vals[0]) >= sizeof(user->mbdb_fullname))
                                goto skip;

                        sm_pwfullname(vals[0], name, user->mbdb_fullname,
                                      sizeof(user->mbdb_fullname));
                        need &= ~NEED_FULLNAME;
                }
                else if (strcasecmp(attr, "homeDirectory") == 0)
                {
                        if (!bitset(NEED_HOMEDIR, need) ||
                            strlen(vals[0]) >= sizeof(user->mbdb_homedir))
                                goto skip;

                        (void) sm_strlcpy(user->mbdb_homedir, vals[0],
                                          sizeof(user->mbdb_homedir));
                        need &= ~NEED_HOMEDIR;
                }
                else if (strcasecmp(attr, "loginShell") == 0)
                {
                        if (!bitset(NEED_SHELL, need) ||
                            strlen(vals[0]) >= sizeof(user->mbdb_shell))
                                goto skip;

                        (void) sm_strlcpy(user->mbdb_shell, vals[0],
                                          sizeof(user->mbdb_shell));
                        need &= ~NEED_SHELL;
                }
                else if (strcasecmp(attr, "uidNumber") == 0)
                {
                        char *p;

                        if (!bitset(NEED_UID, need))
                                goto skip;

                        for (p = vals[0]; *p != '\0'; p++)
                        {
                                /* allow negative numbers */
                                if (p == vals[0] && *p == '-')
                                {
                                        /* but not simply '-' */
                                        if (*(p + 1) == '\0')
                                                goto skip;
                                }
                                else if (!isascii(*p) || !isdigit(*p))
                                        goto skip;
                        }
                        user->mbdb_uid = atoi(vals[0]);
                        need &= ~NEED_UID;
                }
                else if (strcasecmp(attr, "gidNumber") == 0)
                {
                        char *p;

                        if (!bitset(NEED_GID, need))
                                goto skip;

                        for (p = vals[0]; *p != '\0'; p++)
                        {
                                /* allow negative numbers */
                                if (p == vals[0] && *p == '-')
                                {
                                        /* but not simply '-' */
                                        if (*(p + 1) == '\0')
                                                goto skip;
                                }
                                else if (!isascii(*p) || !isdigit(*p))
                                        goto skip;
                        }
                        user->mbdb_gid = atoi(vals[0]);
                        need &= ~NEED_GID;
                }

skip:
                ldap_value_free(vals);
                ldap_memfree(attr);
        }

        errno = sm_ldap_geterrno(LDAPLMAP.ldap_ld);

        /*
        **  We check errno != LDAP_DECODING_ERROR since
        **  OpenLDAP 1.X has a very ugly *undocumented*
        **  hack of returning this error code from
        **  ldap_next_attribute() if the library freed the
        **  ber attribute.  See:
        **  http://www.openldap.org/lists/openldap-devel/9901/msg00064.html
        */

        if (errno != LDAP_SUCCESS &&
            errno != LDAP_DECODING_ERROR)
        {
                /* Must be an error */
                errno += E_LDAPBASE;
                ret = EX_TEMPFAIL;
                goto abort;
        }

 abort:
        save_errno = errno;
        if (attr != NULL)
        {
                ldap_memfree(attr);
                attr = NULL;
        }
        if (LDAPLMAP.ldap_res != NULL)
        {
                ldap_msgfree(LDAPLMAP.ldap_res);
                LDAPLMAP.ldap_res = NULL;
        }
        if (ret == EX_OK)
        {
                if (need == 0)
                {
                        (void) sm_strlcpy(user->mbdb_name, name,
                                          sizeof(user->mbdb_name));
                        save_errno = 0;
                }
                else
                {
                        ret = EX_NOUSER;
                        save_errno = EINVAL;
                }
        }
        errno = save_errno;
        return ret;
}

/*
**  MBDB_LDAP_TERMINATE -- terminate connection to the mailbox database
**
**      Parameters:
**              none.
**
**      Results:
**              none.
*/

static void
mbdb_ldap_terminate()
{
        sm_ldap_close(&LDAPLMAP);
}
# endif /* _LDAP_EXAMPLE_ */
#endif /* LDAPMAP */