/*
 * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

/*
 *  PROPRIETARY/CONFIDENTIAL.  Use of this product is subject to license terms.
 *  Copyright  1999 Sun Microsystems, Inc. Some preexisting portions Copyright
 *  1999 Netscape Communications Corp. All rights reserved.
 */

/*
 * auth_msql.c: Sample htaccess-based NSAPI authentication module using
 *              the mSQL 2.0.x database.
 *
 * Loosely based on Apache mod_auth_msql module by Dirk.vanGulik@jrc.it
 */

#ifdef XP_WIN32
#define NSAPI_PUBLIC __declspec(dllexport)
#else /* !XP_WIN32 */
#define NSAPI_PUBLIC
#endif /* !XP_WIN32 */

/* The following is the standard header for SAFs.  It is used to
 * get the data structures and prototypes needed to declare and use SAFs.
 */

#include "nsapi.h"
#include "htmodule.h"

#include <msql.h>

static CRITICAL auth_crit = NULL;

#define MAX_QUERY_SIZE 512

/* ------------------ htaccess mSQL Authentication Module -----------------

   Usage:
   At the end of init.conf, after loading the htaccess plug-in:
      Init fn=load-modules shlib=auth_msql.<ext> \
         funcs="msql-module-init,msql-check-auth,msql-check-group"

   And after htaccess-init:
      Init fn=msql-module-init

   <ext> = so on UNIX
   <ext> = dll on NT.

   Compiling:
   It needs to be able to find msql.h, by default found in 
   /usr/local/Hughes/include/

   It needs to be linked with -lmsql.  This library is by default found in
   /usr/local/Hughes/lib

   This module allows access control using the public domain
   mSQL database; a fast but limted SQL engine which can be
   contacted over an internal unix domain protocol as well as
   over normal inter-machine tcp/ip socket communication.
  
   An example table could be:
  
   create table users (
          uid  char(255),
          passwd  char(32),
        [ group   char(32) ]
          ) \g
  
   To be sure that the uid is unique, add an index using:

   create unique index idx1 on users (uid) \g

   If more than one entry for a user is returned then the user is denied
   access.

   Here is a list of possible directives:

   Auth_MSQLhost localhost
  or
   Auth_MSQLhost msql.mcom.com
  
                        Setting this directive to localhost causes the
                        server to connect locally rather than over the
                        network.
  
   Auth_MSQLdatabase    www
  
                        The name of the database on the above machine,
                        which contains *both* the tables for group and
                        for user/passwords. Currently it is not possible
                        to have these split over two databases. Make
                        sure that the msql.acl (access control file) of
                        mSQL does indeed allow the effective uid of the
                        web server read access to this database. Check the
                        [server-root]/config/init.conf file for this uid.
  
   Auth_MSQLpwd_table   users

                        Table in which the users/passwords are stored.

   Auth_MSQLuid_field   uid
   Auth_MSQLpwd_field   passwd

                        These two fields describe the structure of the table
                        so the plugin can create SELECT statements.


   Auth_MSQLgrp_table   users
  or
   Auth_MSQLgrp_table   groups

                        Group authentication may be done either using a 
                        separate table or a field within the users table.

   Auth_MSQLgrp_field   group

                        The field used for group access.

   Auth_MSQL_nopasswd           off
   Auth_MSQL_Authoritative      on
   Auth_MSQL_EncryptedPasswords on

                        These directives are silently ignored.

 */


/* ---------------------- Start mSQL-specific code ------------------------ */

/* Make a connection to the database, issue a query and return the results.
 * This currently opens a new connection upon each request. It would be
 * easy to maintain an open connection to the database too.  There are other
 * considerations such as number of connections the database supports, what
 * do to when the connection is lost, close the connection upon server shutdown,
 * etc.
*/
char * do_msql_query(Session *sn, Request *rq, char * msqlhost, char * database, char * query) {
    int rv = 0;

    static int sock = -1;
    m_result * results;
    m_row row;
    char * entry = NULL;

    if ((msqlhost) && (!(strcasecmp(msqlhost,"localhost"))))
        msqlhost=NULL;

    /* Make this single-threaded so requests don't get mixed up */
//    crit_enter(auth_crit);

    if (sock == -1)
        if ((sock=msqlConnect(msqlhost)) == -1) {
            log_error(LOG_FAILURE, "msql-check-auth", sn, rq, 
                  "Could not connect to mSQL database %s (%s)", 
                  database, msqlhost ? msqlhost : "localhost", 
                  msqlErrMsg);
            goto done;
        }

    if (msqlSelectDB(sock,database) == -1 ) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, 
            "Could not select mSQL table \'%s\' on host \'%s\'(%s)",
            database, msqlhost ? msqlhost : "localhost", msqlErrMsg);
        goto done;
    }

    if (msqlQuery(sock,query) == -1 ) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, 
            "Could not query mSQL database '%s' on host '%s' (%s) with query [%s]",
            database, msqlhost ? msqlhost : "localhost", query, msqlErrMsg);
        goto done;
    }

    if (!(results=msqlStoreResult())) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, 
            "Could not get the results from mSQL database \'%s\' on \'%s\' (%s) with query [%s]",
            database, msqlhost ? msqlhost : "localhost", query);
        goto done;
    }

    if (msqlNumRows(results) > 1) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, 
        "Too many matches returned from mSQL query on database \'%s\' on \'%s\' (%s) with query [%s]",
            database, msqlhost ? msqlhost : "localhost", query);
        goto done;
    }

    /* nothing returned so exit quickly and return NULL */
    if (msqlNumRows(results) < 1)  
        goto done;

    if ((row=msqlFetchRow(results)) != NULL) {
        /* copy the first matching field value */
        if (!(entry=MALLOC(strlen(row[0])+1))) {
            log_error(LOG_FAILURE, "msql-check-auth", sn, rq, 
                    "Could not allocate memory for mSQL query result %s (%s) "
                    "with [%s]", database, msqlErrMsg, query);
        } else {
            strcpy(entry,row[0]);
        }
    }

    /* ignore errors, functions are voids anyway. */
    msqlFreeResult(results);

done:

    msqlClose(sock);
    sock=-1;

//    crit_exit(auth_crit);
    return entry;
}

/* Get the user's password from the mSQL file and check to see if it is
 * correct.  Only supports crypt() passwords.
 */
#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int msql_check_auth(pblock *param, Session *sn, Request *rq)
{
    char *msqlhost = pblock_findval("Auth_MSQLhost", rq->vars);
    char *database = pblock_findval("Auth_MSQLdatabase", rq->vars);
    char *pwd_table = pblock_findval("Auth_MSQLpwd_table", rq->vars);
    char *uid_field = pblock_findval("Auth_MSQLuid_field", rq->vars);
    char *passwd_field = pblock_findval("Auth_MSQLpasswd_field", rq->vars);

    char *user = pblock_findval("user", param);
    char *pw = pblock_findval("pw", param);

    char query[MAX_QUERY_SIZE];
    char * password;

    int rv = 0;

    if (!database) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "Database not specified.");
        return REQ_NOACTION;
    }

    if (!msqlhost) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "Host not specified.");
        return REQ_NOACTION;
    }

    if (!pwd_table) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "Password table not specified.");
        return REQ_NOACTION;
    }

    if (!uid_field) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "UID field not specified.");
        return REQ_NOACTION;
    }

    util_snprintf(query, sizeof(query),                 
       "select %s from %s where %s='%s'",
        passwd_field,
        pwd_table,
        uid_field,
        user
    );

    if (!(password = do_msql_query(sn, rq, msqlhost, database, query))) {

        log_error(LOG_SECURITY, "msql-check-auth", sn, rq,
              "user %s does not exist in database %s",
              user, database);
        return REQ_NOACTION;
    }

    /* Ensure that we are thread-safe */
    crit_enter(auth_crit);
    rv = strcmp(password, (char *)crypt((char *)pw,(char *)password));
    crit_exit(auth_crit);

    if (rv != 0) {
        log_error(LOG_SECURITY, "msql-check-auth", sn, rq,
              "user %s password does not match password in database %s",
              user, database);
        rv = REQ_NOACTION;
    } else
        rv = REQ_PROCEED;

    if (password) FREE(password);

    return rv;
}


/* There are 2 ways to do group authentication:
 *
 *   1. Use the group field in the user table.  This limits a user to only
 *      one group.
 *
 *   2. Use a group table and have separate entries for each user for each
 *      group they are in.
 *
 */

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int msql_check_group(pblock *param, Session *sn, Request *rq)
{
    char *msqlhost = pblock_findval("Auth_MSQLhost", rq->vars);
    char *database = pblock_findval("Auth_MSQLdatabase", rq->vars);
    char *group_table = pblock_findval("Auth_MSQLgrp_table", rq->vars);
    char *group_field = pblock_findval("Auth_MSQLgrp_field", rq->vars);
    char *uid_field = pblock_findval("Auth_MSQLuid_field", rq->vars);
    char *user = pblock_findval("user", param);
    char *group = pblock_findval("group", param);

    char query[MAX_QUERY_SIZE];

    char *groups, *x;

    int rv = 0;

    if (!database) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "Database not specified.");
        return REQ_NOACTION;
    }

    if (!msqlhost) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "Host not specified.");
        return REQ_NOACTION;
    }

    /* If no group table is set, assume the same table as the users */
    if (!group_table)
        group_table = pblock_findval("Auth_MSQLpwd_table", rq->vars);

    if (!group_field) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "Group field not specified.");
        return REQ_NOACTION;
    }

    if (!uid_field) {
        log_error(LOG_FAILURE, "msql-check-auth", sn, rq, "UID field not specified.");
        return REQ_NOACTION;
    }

    util_snprintf(query, sizeof(query),
            "select %s from %s where %s='%s' and %s='%s'",
            group_field,
            group_table,
            uid_field,user,
            group_field, group
    );

    if (!(groups = do_msql_query(sn, rq, msqlhost, database, query))) {
        log_error(LOG_SECURITY, "msql-check-auth", sn, rq,
              "user %s is not in group %s in database %s",
              user, group, database);
        return REQ_NOACTION;
    }

    if (groups) FREE(groups);

    /* just returning something is enough to be a success */

    return REQ_PROCEED;
}

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int set_msqlhost(pblock *param, Session *sn, Request *rq)
{
    char * host = pblock_findval("arg1", param);
    pblock_nvinsert("Auth_MSQLhost", host, rq->vars);

    return REQ_PROCEED;
}

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int set_msqldatabase(pblock *param, Session *sn, Request *rq)
{
    char * db = pblock_findval("arg1", param);
    pblock_nvinsert("Auth_MSQLdatabase", db, rq->vars);

    return REQ_PROCEED;
}

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int set_msqltable(pblock *param, Session *sn, Request *rq)
{
    char * table = pblock_findval("arg1", param);
    pblock_nvinsert("Auth_MSQLpwd_table", table, rq->vars);

    return REQ_PROCEED;
}

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int set_msqluidfield(pblock *param, Session *sn, Request *rq)
{
    char * field = pblock_findval("arg1", param);
    pblock_nvinsert("Auth_MSQLuid_field", field, rq->vars);

    return REQ_PROCEED;
}

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int set_msqlpasswdfield(pblock *param, Session *sn, Request *rq)
{
    char * field = pblock_findval("arg1", param);
    pblock_nvinsert("Auth_MSQLpasswd_field", field, rq->vars);

    return REQ_PROCEED;
}

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int set_msqlgrptable(pblock *param, Session *sn, Request *rq)
{
    char * field = pblock_findval("arg1", param);
    pblock_nvinsert("Auth_MSQLgrp_table", field, rq->vars);

    return REQ_PROCEED;
}

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int set_msqlgrpfield(pblock *param, Session *sn, Request *rq)
{
    char * field = pblock_findval("arg1", param);
    pblock_nvinsert("Auth_MSQLgrp_field", field, rq->vars);

    return REQ_PROCEED;
}

/* ---------------------- End mSQL-specific code ------------------------ */

/* ---------------- Start generic htaccess module code ------------------ */

/* This function should be included in any htaccess-based authentication
 * module.  It handles registering the directive with the main htaccess
 * plug-in.
 */
static int
register_directive(char * directive, char * function, int argtype, int authtype,
         char * check_fn, Session *sn, Request *rq, NSAPI_PUBLIC int fn)
{
    Request * nrq = rq;
    FuncPtr pFunc = NULL;
    pblock *npb;
    int rv;
    char a[15];

    /* Insert the variables into the pblock */
    npb = pblock_create(6);
    pblock_nvinsert("fn", "htaccess-register", npb);
    pblock_nvinsert("directive", directive, npb);
    util_itoa(argtype, a);
    pblock_nvinsert("argtype", a, npb);
    util_itoa(authtype, a);
    pblock_nvinsert("authtype", a, npb);
    pblock_nvinsert("function", function, npb);
    pblock_nvinsert("check_fn", check_fn, npb);
    
    if ((pFunc = func_find("htaccess-register")) == NULL)
    {
        log_error(LOG_FAILURE, "module", sn, rq, "htaccess-register() not found.");
        return REQ_ABORTED;
    } else
        rv = func_exec(npb, sn, nrq);

    pblock_free(npb);
    func_insert(function, (FuncPtr) fn);

    if (rv !=  REQ_PROCEED)
        return REQ_ABORTED;

    return REQ_PROCEED ;
}

/* Free up the thread-lock. */
#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC void cleanup(void *parameter)
{
    if (auth_crit)
        crit_terminate(auth_crit);
}

/* This function needs to be called before the htaccess module can be used.
 * This handles registering each directive, the number of arguments it takes
 * and its underlying function with the main htaccess plug-in.
 *
 */
#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int msql_module_init(pblock *param, Session *sn, Request *rq)
{
    auth_crit = crit_init();

    if (auth_crit == 0) {
        log_error(LOG_FAILURE, "msql-module-init", sn, rq, "Unable to obtain crypt critical lock.");
        return REQ_ABORTED;
    }

    daemon_atrestart(cleanup, NULL);

    if (!(register_directive("Auth_MSQLhost", "set-msqlhost", PASS1, USER_AUTH, "msql-check-auth", sn, rq, (NSAPI_PUBLIC int) set_msqlhost)) == REQ_PROCEED)
        return REQ_ABORTED;
    if (!(register_directive("Auth_MSQLdatabase", "set-msqldatabase", PASS1, USER_AUTH, "msql-check-auth", sn, rq, (NSAPI_PUBLIC int) set_msqldatabase)) == REQ_PROCEED)
        return REQ_ABORTED;
    if (!(register_directive("Auth_MSQLpwd_table", "set-msqltable", PASS1, USER_AUTH, "msql-check-auth", sn, rq, (NSAPI_PUBLIC int) set_msqltable)) == REQ_PROCEED)
        return REQ_ABORTED;
    if (!(register_directive("Auth_MSQLuid_field", "set-msqluidfield", PASS1, USER_AUTH, "msql-check-auth", sn, rq, (NSAPI_PUBLIC int) set_msqluidfield)) == REQ_PROCEED)
        return REQ_ABORTED;
    if (!(register_directive("Auth_MSQLpasswd_field", "set-msqlpasswdfield", PASS1, USER_AUTH, "msql-check-auth", sn, rq, (NSAPI_PUBLIC int) set_msqlpasswdfield)) == REQ_PROCEED)
        return REQ_ABORTED;
    if (!(register_directive("Auth_MSQLgrp_table", "set-msqlgrptable", PASS1, GROUP_AUTH, "msql-check-group", sn, rq, (NSAPI_PUBLIC int) set_msqlgrptable)) == REQ_PROCEED)
        return REQ_ABORTED;
    if (!(register_directive("Auth_MSQLgrp_field", "set-msqlgrpfield", PASS1, GROUP_AUTH, "msql-check-group", sn, rq, (NSAPI_PUBLIC int) set_msqlgrpfield)) == REQ_PROCEED)
        return REQ_ABORTED;

    return REQ_PROCEED;
}
