/*
 * Copyright (c) 2003-2012
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/*
 * Three Apache modules are widely-used to perform web-server local
 * authentication: mod_auth, mod_auth_digest, and mod_auth_dbm.
 * o mod_auth uses a plaintext flat-file that is administered by htpasswd.
 * o mod_auth_digest also uses a plaintext flat-file; it is administered by
 *   htdigest.
 * o mod_auth_dbm uses one of the DBM-type libraries for storage; they are
 *   administered by dbmmanage or htdbm and can use sdbm, gdbm, ndbm, or db.
 * Password hashes can be computed in a variety of ways by htpasswd and
 * dbmmanage: MD5, crypt(3), SHA1, or plaintext (no hash)
 *
 * The purpose of this module is to authenticate against the information
 * maintained by these Apache modules to avoid duplication and to let
 * DACS use them directly rather than through local_native_auth.
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2012\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: local_apache_auth.c 2586 2012-03-15 16:21:40Z brachman $";
#endif

#include "dacs.h"

static const char *log_module_name = "local_apache_auth";

/*
 * Look up USERNAME in an Apache htpasswd flat-file, PATH.
 * Return 0 if found, -1 otherwise.
 */
static int
find_htpasswd_user(char *path, char *username, char **value)
{
  char *val;
  Ds *ds;

  val = NULL;

#ifdef NOTDEF
  {
	Kwv *kwv;
	Kwv_conf conf = {
	  ":", NULL, NULL, KWV_CONF_DEFAULT, NULL, 16, NULL, NULL
	};

	/*
	 * XXX If the password file has many entries (on the order of tens of
	 * thousands or more), the Kwv structure will prove to be very slow because
	 * it was not intended for that kind of use.  Such files should really
	 * be converted to one of the dbm formats, but this code can be much
	 * more efficient if we accept that we cannot rule out very large files.
	 */
	if ((ds = ds_load_file(NULL, path)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Unable to load \"%s\"", path));
	  return(-1);
	}

	if ((kwv = kwv_make_new(ds_buf(ds), &conf)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Error parsing \"%s\"", path));
	  return(-1);
	}

	val = kwv_lookup_value(kwv, username);
  }
#else
  {
	char *line, *p;
	FILE *fp;

	if ((fp = fopen(path, "r")) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Unable to open \"%s\"", path));
	  return(-1);
	}

	ds = ds_init(NULL);
	ds->delnl_flag = 1;
	while ((line = ds_gets(ds, fp)) != NULL) {
	  if ((p = strchr(line, (int) ':')) == NULL) {
		/* Huh?  Bad format. */
		log_msg((LOG_ERROR_LEVEL, "Invalid password file \"%s\"", path));
		break;
	  }

	  *p++ = '\0';
	  if (streq(username, line)) {
		val = p;
		break;
	  }
	}

	fclose(fp);
  }
#endif

  if (val == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Username \"%s\" not found", username));
	return(-1);
  }

  *value = val;

  return(0);
}

/*
 * Look up USERNAME in an Apache htdigest flat-file, PATH.
 * Return 0 if found, -1 otherwise.
 */
static int
find_htdigest_user(char *path, char *username, char *realm, char **value)
{
  char *line, *r, *u, *val;
  Ds ds;
  Dsvec *fields;
  FILE *fp;

  if ((fp = fopen(path, "r")) == NULL) {
	log_err((LOG_ERROR_LEVEL, "Unable to load \"%s\"", path));
	return(-1);
  }

  ds_init(&ds);
  ds.delnl_flag = 1;
  val = NULL;
  while ((line = ds_agets(&ds, fp)) != NULL) {
	if ((fields = strsplit(line, ":", 3)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Error parsing \"%s\"", path));
	  fclose(fp);
	  return(-1);
	}
	u = dsvec_ptr(fields, 0, char *);
	r = dsvec_ptr(fields, 1, char *);
	val = dsvec_ptr(fields, 2, char *);
	if (u == NULL || r == NULL || val == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Error parsing \"%s\"", path));
	  fclose(fp);
	  return(-1);
	}
	if (streq(username, u) && streq(realm, r)) {
	  *value = val;
	  fclose(fp);
	  return(0);
	} 
  }

  fclose(fp);

  return(-1);
}

/*
 * Authenticate against an Apache module's Basic or Digest auth files.
 * Return 0 if authentication succeeds, -1 otherwise.
 */
int
local_apache_auth(char *username, char *password, char *aux,
				  Http_auth_authorization *aa, Apache_module module,
				  Apache_dbm dbm, char *path)
{
  int st;
  char *hashval, *p, *uri, *value;
  size_t len;
  Apache_hash hash;
  Vfs_conf *conf;
  Vfs_handle *h;

  if (username == NULL)
	return(-1);

  conf = vfs_conf(NULL);
  conf->null_flag = 0;
  conf->create_flag = 0;
  vfs_conf(conf);

  value = NULL;
  if (module == MOD_AUTH_DBM) {
	/*
	 * XXX Like MOD_AUTH authentication, except lookup the username in
	 * a DBM-like database.
	 */
	switch (dbm) {
	case DBM_DB:
	  if (!vfs_enabled("db")) {
		log_msg((LOG_ERROR_LEVEL, "DBM-type db unavailable"));
		return(-1);
	  }
	  uri = ds_xprintf("dacs-db:%s", path);
	  break;

	case DBM_SDBM:
	  if (!vfs_enabled("sdbm")) {
		log_msg((LOG_ERROR_LEVEL, "DBM-type sdbm unavailable"));
		return(-1);
	  }
	  uri = ds_xprintf("dacs-sdbm:%s", path);
	  break;

	case DBM_GDBM:
	  if (!vfs_enabled("gdbm")) {
		log_msg((LOG_ERROR_LEVEL, "DBM-type gdbm unavailable"));
		return(-1);
	  }
	  uri = ds_xprintf("dacs-gdbm:%s", path);
	  break;

	case DBM_NDBM:
	  if (!vfs_enabled("ndbm")) {
		log_msg((LOG_ERROR_LEVEL, "DBM-type ndbm unavailable"));
		return(-1);
	  }
	  uri = ds_xprintf("dacs-ndbm:%s", path);
	  break;

	default:
	  log_msg((LOG_ERROR_LEVEL, "Unrecognized DBM-type"));
	  return(-1);
	}

	if ((h = vfs_open_uri(uri)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Error opening DBM \"%s\"", path));
	  return(-1);
	}

	st = vfs_get(h, username, (void **) &value, &len);
	vfs_close(h);
	if (st == -1) {
	  log_msg((LOG_ERROR_LEVEL, "Username \"%s\" not found", username));
	  return(-1);
	}
  }
  else if (module == MOD_AUTH) {
	if (find_htpasswd_user(path, username, &value) == -1)
	  return(-1);
  }
  else if (module == MOD_AUTH_DIGEST) {
	char *stored_password;

	/* XXX Verify RFC 2617 Digest authentication */
	if (aa == NULL || aa->realm == NULL)
	  return(-1);
	if (find_htdigest_user(path, username, aa->realm, &stored_password) == -1)
	  return(-1);

	if (aa->authorization == NULL)
	  return(http_digest_check(aa, stored_password));

	aa->password = stored_password;
	return(http_digest_auth(username, aa));
  }

  if (module == MOD_AUTH_DBM || module == MOD_AUTH) {
	if (password == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "No password is available"));
	  return(-1);
	}

	if ((p = strprefix(value, "$apr1$")) != NULL) {
	  char *salt;

	  hash = HASH_MD5;
	  crypt_md5_magic("$apr1$");
	  salt = ds_xprintf("$apr1$%s", p);
	  hashval = crypt_md5(password, salt);
	  if (streq(hashval, value))
		return(0);
	  return(-1);
	}
	else if (strprefix(value, "{SHA}") != NULL) {
	  unsigned int outlen;
	  unsigned char *outp;

	  hash = HASH_SHA1;
	  outp = crypto_digest("SHA1", password, strlen(password), NULL, &outlen);
	  mime_encode_base64(outp, outlen, &hashval);
	  if (streq(hashval, value + 5))
		return(0);
	  return(-1);
	}
	else if (strlen(value) == 13 && !streq(value, password)) {
	  hash = HASH_CRYPT;
	  hashval = crypt(password, value);
	  if (streq(hashval, value))
		return(0);
	  return(-1);
	}
	else {
	  hash = HASH_PLAIN;
	  if (streq(value, password))
		return(0);
	  return(-1);
	}
  }

  log_msg((LOG_ERROR_LEVEL, "Implementation bug!"));
  return(-1);
}

#ifdef PROG
int
main(int argc, char **argv)
{
  int emitted_dtd, i, st;
  char *errmsg, *jurisdiction, *username, *password, *aux;
  char *auth_file, *auth_module, *dbm_type;
  Apache_module mod;
  Auth_reply_ok ok;
  Apache_dbm dbm;
  Http_auth_authorization *aa;
  Kwv *kwv;

  emitted_dtd = 0;
  errmsg = "internal";
  username = password = aux = jurisdiction = NULL;
  auth_file = NULL;
  auth_module = NULL;
  dbm = DBM_NONE;
  dbm_type = NULL;
  aa = NULL;

  if (dacs_init(DACS_LOCAL_SERVICE, &argc, &argv, &kwv, &errmsg) == -1) {
	/* If we fail here, we may not have a DTD with which to reply... */
  fail:
	if (password != NULL)
	  strzap(password);
	if (aux != NULL)
	  strzap(aux);
	if (emitted_dtd) {
	  printf("%s\n", make_xml_auth_reply_failed(NULL, NULL));
	  emit_xml_trailer(stdout);
	}
	if (errmsg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Failed: reason=%s", errmsg));

	exit(1);
  }

  /* This must go after initialization. */
  emitted_dtd = emit_xml_header(stdout, "auth_reply");

  if (argc > 1) {
	errmsg = "Usage: unrecognized parameter";
	goto fail;
  }

  for (i = 0; i < kwv->nused; i++) {
	if (streq(kwv->pairs[i]->name, "USERNAME") && username == NULL)
	  username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "PASSWORD") && password == NULL)
	  password = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUXILIARY") && aux == NULL)
	  aux = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUTH_MODULE") && auth_module == NULL)
	  auth_module = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUTH_FILE") && auth_file == NULL)
	  auth_file = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DBM_TYPE") && dbm_type == NULL)
	  dbm_type = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_JURISDICTION")
			 && jurisdiction == NULL)
	  jurisdiction = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_VERSION"))
	  ;
	else
	  log_msg((LOG_TRACE_LEVEL, "Parameter: '%s'", kwv->pairs[i]->name));
  }

  /* Verify that we're truly responsible for DACS_JURISDICTION */
  if (dacs_verify_jurisdiction(jurisdiction) == -1) {
	errmsg = "Missing or incorrect DACS_JURISDICTION";
	goto fail;
  }

  if (auth_module == NULL || *auth_module == '\0') {
	errmsg = "Missing AUTH_MODULE";
	goto fail;
  }
  if ((mod = lookup_module(auth_module)) == MOD_AUTH_NONE) {
	errmsg = ds_xprintf("Unrecognized AUTH_MODULE: \"%s\"", auth_module);
	goto fail;
  }

  if (mod == MOD_AUTH_DBM) {
	if (dbm_type == NULL || *dbm_type == '\0') {
	  errmsg = "DBM_TYPE is required with mod_auth_dbm";
	  goto fail;
	}
	if ((dbm = lookup_dbm_type(dbm_type)) == DBM_NONE) {
	  errmsg = "Unrecognized DBM_TYPE";
	  goto fail;
	}
  }

  if (auth_file == NULL || *auth_file == '\0') {
	errmsg = "Missing AUTH_FILE";
	goto fail;
  }

  st = local_apache_auth(username, password, aux, aa, mod, dbm, auth_file);
  if (st != 0)
	goto fail;

  if (password != NULL)
	strzap(password);
  if (aux != NULL)
	strzap(aux);

  ok.username = username;
  /* If this wasn't specified, dacs_authenticate will use the default. */
  ok.lifetime = kwv_lookup_value(kwv, "CREDENTIALS_LIFETIME_SECS");
  ok.roles_reply = NULL;
  printf("%s\n", make_xml_auth_reply_ok(&ok));

  emit_xml_trailer(stdout);
  exit(0);
}
#endif
