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

/*****************************************************************************
 * COPYRIGHT AND PERMISSION NOTICE
 * 
 * Copyright (c) 2001-2003 The Queen in Right of Canada
 * 
 * All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, and/or sell
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, provided that the above copyright notice(s) and this
 * permission notice appear in all copies of the Software and that both the
 * above copyright notice(s) and this permission notice appear in supporting
 * documentation.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE 
 * BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 
 * SOFTWARE.
 * 
 * Except as contained in this notice, the name of a copyright holder shall not
 * be used in advertising or otherwise to promote the sale, use or other
 * dealings in this Software without prior written authorization of the
 * copyright holder.
 ***************************************************************************/

/*
 * Support functions for PAM processing and interacting with pamd
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2013\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: pamlib.c 2669 2013-10-22 17:03:44Z brachman $";
#endif

#include "auth.h"

#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

static const char *log_module_name = "pamlib";

/*
 * Read a block from FP into INBUF and parse it into name/value pairs in KWVP.
 * Return 1 and parse the input if something is read, return 0 if nothing was
 * read, or return -1 if an error occurred.
 */
int
pamd_get_block(FILE *fp, Ds *inbuf, Kwv **kwvp)
{
  char *p, *q;
  Kwv *kwv;
  Kwv_conf kwv_conf = {
	"=", NULL, " ", KWV_CONF_DEFAULT, NULL, 8, NULL, NULL
  };

  ds_reset(inbuf);
  *kwvp = NULL;

  if (feof(fp)) {
	log_msg((LOG_TRACE_LEVEL, "pamd_get_block: eof"));
	return(0);
  }

  if (ferror(fp)) {
	log_err((LOG_ERROR_LEVEL, "pamd_get_block: error"));
	return(-1);
  }

  while ((p = ds_agets(inbuf, fp)) != NULL) {
	/* Was a blank line read?  That signals the end of the block. */
#ifdef NOTDEF
	if (*p == '\r' && *(p + 1) == '\n')
	  break;
	if (*p == '\n')
	  break;
	if ((q = strchr(p, (int) '\n')) == NULL)
	  return(-1);
#else
	if (*p == '\0')
	  break;
#endif
	log_msg((LOG_TRACE_LEVEL, "Read: \"%s\"", p));
	if (feof(fp)) {
	  log_msg((LOG_TRACE_LEVEL, "pamd_get_block: eof"));
	  return(-1);
	}
  }

  if (ferror(fp)) {
	log_err((LOG_ERROR_LEVEL, "pamd_get_block: error"));
	return(-1);
  }

#ifdef DEBUG
  log_msg((LOG_TRACE_LEVEL | LOG_SENSITIVE_FLAG, "inbuf=%s", ds_buf(inbuf)));
#endif

  /* If nothing was read, return. */
  p = ds_buf(inbuf);
#ifdef NOTDEF
  if (p == NULL || *p == '\n' || *p == '\0' || (*p == '\r' && *(p + 1) == '\n'))
	return(0);
#else
  if (p == NULL || *p == '\0') {
	log_msg((LOG_TRACE_LEVEL, "pamd_get_block: end of block"));
	return(0);
  }
#endif

  if ((kwv = kwv_make_new(p, &kwv_conf)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Error parsing block"));
	return(-1);
  }
  else {
	int i;
	Kwv_iter *iter;
	Kwv_pair *v;

	log_msg((LOG_DEBUG_LEVEL, "Parsed block into %d %s:",
			 kwv_count(kwv, NULL),
			 pluralize("variable", "s", kwv_count(kwv, NULL))));

	iter = kwv_iter_begin(kwv, NULL);
	i = 1;
	for (v = kwv_iter_first(iter); v != NULL; v = kwv_iter_next(iter))
	  log_msg((LOG_DEBUG_LEVEL, "  %2d. %s=\"%s\"", i++, v->name, v->val));
	kwv_iter_end(iter);
  }

  kwv_set_mode(kwv, "+c+i");
  *kwvp = kwv;

  return(1);
}

/*
 * Find the port that pamd should listen to for session requests.
 * PORTNAME can be a port number or a service name; if it is NULL,
 * the PAMD_PORT directive can specify the port number/service name and
 * failing that, the compile-time symbol PAMD_SERVICE_NAME is used.
 *
 * Return that port number, or zero (a valid but reserved port number) and
 * optionally an error message.
 */
in_port_t
pam_get_pamd_port(char *portname, char **errmsg)
{
  in_port_t port;

  port = 0;
  if (portname == NULL && (portname = conf_val(CONF_PAMD_PORT)) == NULL)
	portname = PAMD_SERVICE_NAME;

  if (is_digit_string(portname)) {
	/* Must be a port number. */
	if (strnum(portname, STRNUM_IN_PORT_T, &port) == -1) {
	  if (errmsg != NULL)
		*errmsg = "An invalid pamd port number is configured";
	  return(0);
	}
  }
  else {
	struct servent *serv;

	/* Must be a service name. */
	if ((serv = getservbyname(portname, "tcp")) == NULL) {
	  if (errmsg != NULL)
		*errmsg = ds_xprintf("Can't find pamd service \"%s\"", portname);
	  return(0);
	}
	port = htons(serv->s_port);
	endservent();
  }

  return(port);
}

/*
 * A PAMD transaction identifier looks like, relative to the pamd server:
 * <ip_addr>:<port>:<pid>:<unique>
 * i.e., <port> is the port number that pamd listens to for this client's
 * responses after the initial request to the "public" port.
 */
Pam_auth_tid *
pam_new_tid(char *reply_addr, char *reply_port)
{
  Pam_auth_tid *tid;

  tid = ALLOC(Pam_auth_tid);
  tid->host = strdup(reply_addr);
  strnum(reply_port, STRNUM_IN_PORT_T, &tid->port);
  tid->pid = getpid();
  if ((tid->unique = crypto_make_random_string(NULL, 8)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "crypto_make_random_string() failed"));
	return(NULL);
  }

  tid->str = ds_xprintf("%s:%u:%u:%s",
						tid->host, tid->port, tid->pid, tid->unique);

  log_msg((LOG_TRACE_LEVEL, "New PAM tid=\"%s\"", tid->str));

  return(tid);
}

Pam_auth_tid *
pam_parse_tid(char *str)
{
  char *p, *q;
  Pam_auth_tid *tid;

  tid = ALLOC(Pam_auth_tid);

  /* Keep the original string */
  tid->str = strdup(str);

  /* Make a copy and carve it up. */
  tid->host = strdup(str);
  if ((p = strchr(tid->host, (int) ':')) == NULL)
	return(NULL);
  *p++ = '\0';

  if ((q = strchr(p, (int) ':')) == NULL)
	return(NULL);
  *q++ = '\0';
  if (strnum(p, STRNUM_IN_PORT_T, &tid->port) == -1 || tid->port == 0)
	return(NULL);

  p = q;
  if ((q = strchr(p, (int) ':')) == NULL)
	return(NULL);
  *q++ = '\0';
  if (strnum(p, STRNUM_PID_T, &tid->pid) == -1)
	return(NULL);

  tid->unique = q;

  return(tid);
}

/*
 * This does most of the work for local_pam_authenticate.
 * It sends a block to pamd (a series of lines of text, terminated by a
 * blank line), then waits for one or more response blocks (or times out).
 */
int
pam_auth(Kwv *kwv_trans, char *username, Pam_auth_tid **tidp, char *hostname,
		 in_port_t port, char **mapped_username, char **lifetime,
		 char **xml, Auth_prompts **prompts)
{
  int i, read_fd, write_fd, st;
  char *p, *result;
  char *transid, *pam_username;
  FILE *fp_in, *fp_out;
  struct timeval timeout;
  Auth_prompt *pr, *pr_head;
  Ds ds, *inbuf, *outbuf;
  Http *h;
  Kwv *kwv, *kwv_head;
  Kwv_iter *iter;
  Kwv_pair *v;
  Pam_auth_tid *new_tid, *tid;
  Net_connection_type ct;

  log_msg((LOG_TRACE_LEVEL, "pam_auth begins, args:"));
  iter = kwv_iter_begin(kwv_trans, NULL);
  for (v = kwv_iter_first(iter); v != NULL; v = kwv_iter_next(iter))
	log_msg((LOG_TRACE_LEVEL, "  %s=\"%s\"", v->name, v->val));
  kwv_iter_end(iter);

  if (xml != NULL)
	*xml = NULL;
  if (prompts != NULL)
	*prompts = NULL;

  read_fd = write_fd = -1;
  kwv = kwv_head = NULL;
  st = 0;
  h = NULL;

  ct = NET_TCP;
  if (tidp != NULL && *tidp != NULL) {
	if (hostname == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "No hostname provided"));
	  return(-1);
	}
	if (port == 0) {
	  log_msg((LOG_ERROR_LEVEL, "No port provided"));
	  return(-1);
	}
  }

  if (ct == NET_SSL || ct == NET_SSL_VERIFY) {
	/* XXX UNTESTED... */
	/* This assumes that pamd has an SSL wrapper. */
	if (net_connect_to_server_ssl(hostname, port, ct,
								  conf_val(CONF_SSL_PROG),
								  conf_val(CONF_SSL_PROG_ARGS),
								  conf_val(CONF_SSL_PROG_CLIENT_CRT),
								  conf_val(CONF_SSL_PROG_CA_CRT),
								  &read_fd, &write_fd) == -1) {
	  log_msg((LOG_ERROR_LEVEL, "SSL connection to %s failed", hostname));
	  st = -1;
	  goto done;
	}
	log_msg((LOG_DEBUG_LEVEL, "SSL connection to %s:%u", hostname, port));
  }
  else if (ct == NET_TCP) {
	if (net_connect_to_server(hostname, port, &read_fd, &write_fd) == -1) {
	  log_msg((LOG_ERROR_LEVEL, "Connect to %s:%u failed", hostname, port));
	  st = -1;
	  goto done;
	}
	log_msg((LOG_DEBUG_LEVEL, "Connected to %s:%u", hostname, port));
  }

  /* XXX NET_HTTP is not supported yet. */
  outbuf = ds_init(NULL);
  if (ct == NET_HTTP) {
	int argnum, status_code;
	char *cookies[2], *errmsg, *url;
	Credentials *admin_cr;
	Ds *ds = ds_init(NULL);
	Dsvec *dsv;
	Http_params *params;

	url = "";

	argnum = 0;
	dsv = dsvec_init_size(NULL, sizeof(Http_params), 5);

	if (username != NULL) {
	  params = http_param(dsv, "USERNAME", username, NULL, 0);
	  argnum++;
	}

	if (tidp != NULL && *tidp != NULL) {
	  tid = *tidp;
	  params = http_param(dsv, "TRANSID", tid->str, NULL, 0);
	  argnum++;

	  for (i = 0; i < kwv_count(kwv_trans, NULL); i++) {
		ds_sprintf(ds, 0, "%s%d", AUTH_PROMPT_VAR_PREFIX, i + 1);
		if ((p = kwv_lookup_value(kwv_trans, ds_buf(ds))) != NULL) {
		  params = http_param(dsv, ds_buf(ds), p, NULL, 0);
		  argnum++;
		}
		ds_reset_buf(ds);
	  }
	}

	admin_cr = make_admin_credentials();
	credentials_to_auth_cookies(admin_cr, &cookies[0]);
	cookies[1] = NULL;

	errmsg = NULL;
	h = http_invoke_stream(url, HTTP_POST_METHOD,
						   ssl_verify ?
						   HTTP_SSL_ON_VERIFY : HTTP_SSL_URL_SCHEME,
						   argnum, (Http_params *) dsvec_base(dsv), NULL,
						   cookies, &fp_in, &status_code, NULL, &errmsg);
	if (h == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Could not invoke pamd: %s", errmsg));
	  st = -1;
	  goto done;
	}
	read_fd = fileno(fp_in);
  }
  else {
	char *cookie;
	Credentials *admin_cr;

	h = NULL;
	if (read_fd == -1 || (fp_in = fdopen(read_fd, "r")) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "No read descriptor available"));
	  st = -1;
	  goto done;
	}
	if (write_fd == -1 || (fp_out = fdopen(write_fd, "w")) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "No write descriptor available"));
	  st = -1;
	  goto done;
	}

	if (tidp != NULL && *tidp != NULL) {
	  Ds *ds = ds_init(NULL);

	  /* If there's information to pass, do that first. */
	  tid = *tidp;
	  log_msg((LOG_DEBUG_LEVEL, "Sending: TRANSID=\"%s\"\n", tid->str));
	  ds_asprintf(outbuf, "TRANSID=\"%s\"\n", tid->str);
	  for (i = 0; i < kwv_count(kwv_trans, NULL); i++) {
		ds_sprintf(ds, 0, "%s%d", AUTH_PROMPT_VAR_PREFIX, i + 1);
		if ((p = kwv_lookup_value(kwv_trans, ds_buf(ds))) != NULL) {
		  log_msg((LOG_DEBUG_LEVEL, "Sending: %s=\"%s\"\n", ds_buf(ds), p));
		  ds_asprintf(outbuf, "%s=\"%s\"\n", ds_buf(ds), p);
		}
	  }
	  ds_free(ds);
	}
	else if (username == NULL || *username == '\0')
	  log_msg((LOG_TRACE_LEVEL, "No username argument was provided"));
	else if (username != NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Sending: username=\"%s\"", username));
	  ds_asprintf(outbuf, "USERNAME=\"%s\"\n", username);
	}

	log_msg((LOG_TRACE_LEVEL, "Preparing credentials cookie..."));
	admin_cr = make_admin_credentials();
	credentials_to_auth_cookies(admin_cr, &cookie);
	log_msg((LOG_DEBUG_LEVEL, "Sending: CREDENTIALS=\"%s\"\n", cookie));
	ds_asprintf(outbuf, "CREDENTIALS=\"%s\"\n", cookie);
	ds_asprintf(outbuf, "\n");
	if (net_write(write_fd, ds_buf(outbuf), ds_len(outbuf)) == -1) {
	  log_err((LOG_ERROR_LEVEL, "net_write"));
	  st = -1;
	  goto done;
	}
	ds_free(outbuf);
	log_msg((LOG_DEBUG_LEVEL, "Finished sending"));

	fflush(fp_out);
	shutdown(write_fd, SHUT_WR);

	timeout.tv_sec = PAMD_PAMD_TIMEOUT_SECS;
	timeout.tv_usec = 0;
	log_msg((LOG_DEBUG_LEVEL, "Waiting %ld secs for reply", timeout.tv_sec));
	if (net_input_or_timeout(read_fd, &timeout) != 1) {
	  log_msg((LOG_ERROR_LEVEL, "Timeout"));
	  st = -1;
	  goto done;
	}
  }

  inbuf = ds_init(NULL);
  inbuf->clear_flag = 1;
  inbuf->delnl_flag = 1;
  inbuf->crnl_flag = 1;

  if ((st = pamd_get_block(fp_in, inbuf, &kwv_head)) <= 0) {
	log_msg((LOG_ERROR_LEVEL, "pamd_get_block failed, st=%d", st));
	st = -1;
	goto done;
  }

  if ((pam_username = kwv_lookup_value(kwv_head, "username")) != NULL)
	*mapped_username = strdup(pam_username);

  /* If pamd gives us a result, then we're done. */
  if ((result = kwv_lookup_value(kwv_head, "result")) != NULL) {
	if (streq(result, "ok"))
	  st = 0;
	else
	  st = -1;

	if (pam_username == NULL && st == 0) {
	  log_msg((LOG_ERROR_LEVEL, "No pam_username given"));
	  st = -1;
	}

	goto done;
  }

  /* We didn't get a result, so there must be more prompts. */
  if ((transid = kwv_lookup_value(kwv_head, "transid")) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Missing transid"));
	st = -1;
	goto done;
  }
  if ((new_tid = pam_parse_tid(transid)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Invalid transid: parse error"));
	st = -1;
	goto done;
  }
  if (tidp != NULL)
	*tidp = new_tid;

  ds_init(&ds);
  ds_asprintf(&ds, "<prompts transid=\"%s\">\n", new_tid->str);

  pr_head = pr = NULL;
  while (pamd_get_block(fp_in, inbuf, &kwv) == 1) {
	if ((p = kwv_lookup_value(kwv, "type")) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "No 'type' value"));
	  st = -1;
	  goto done;
	}

	if (pr_head == NULL)
	  pr_head = pr = ALLOC(Auth_prompt);
	else {
	  pr->next = ALLOC(Auth_prompt);
	  pr = pr->next;
	}
	pr->label = NULL;
	pr->varname = NULL;
	pr->next = NULL;

	ds_asprintf(&ds, "<prompt type=\"%s\"", p);
	pr->type = strdup(p);

	if ((p = kwv_lookup_value(kwv, "label")) != NULL) {
	  ds_asprintf(&ds, " label=\"%s\"", p);
	  pr->label = strdup(p);
	}

	if ((p = kwv_lookup_value(kwv, "varname")) != NULL) {
		ds_asprintf(&ds, " varname=\"%s\"", p);
		pr->varname = strdup(p);
	}
	ds_asprintf(&ds, "/>\n");
  }
  ds_asprintf(&ds, "</prompts>\n");

  if (xml != NULL)
	*xml = ds_buf(&ds);
  if (prompts != NULL) {
	*prompts = ALLOC(Auth_prompts);
	(*prompts)->transid = new_tid->str;
	(*prompts)->head = pr_head;
  }

 done:
  if (h != NULL)
	http_close(h);
  if (read_fd != -1)
	close(read_fd);
  if (write_fd != -1)
	close(write_fd);
  if (kwv_head != NULL)
	kwv_free(kwv_head);
  if (kwv != NULL)
	kwv_free(kwv);

  return(st);
}

int
make_xml_prompts(Auth_prompts *prompts, int html, char **xml)
{
  char *gt, *lt;
  Auth_prompt *pr;
  Ds ds;

  if (html) {
	gt = "&gt;";
	lt = "&lt;";
  }
  else {
	gt = ">";
	lt = "<";
  }

  ds_init(&ds);
  ds_asprintf(&ds, "%sprompts transid=\"%s\"%s\n", lt, prompts->transid, gt);

  for (pr = prompts->head; pr != NULL; pr = pr->next) {
	ds_asprintf(&ds, "%sprompt type=\"%s\"", lt, pr->type);
	if (pr->label != NULL)
	  ds_asprintf(&ds, " label=\"%s\"", pr->label);
	if (pr->varname != NULL)
	  ds_asprintf(&ds, " varname=\"%s\"", pr->varname);
	ds_asprintf(&ds, "/%s\n", gt);
  }
  ds_asprintf(&ds, "%s/prompts%s\n", lt, gt);

  *xml = ds_buf(&ds);
  return(ds_len(&ds));
}

int
make_json_prompts(Auth_prompts *prompts, int html, char **json)
{
  Auth_prompt *pr;
  Ds ds;

  ds_init(&ds);
  ds_asprintf(&ds, "{ \"prompts\":");
  ds_asprintf(&ds, " { \"transid\":\"%s\"", prompts->transid);
  if (prompts->head != NULL)
	ds_asprintf(&ds, ",\n \"prompt\": [ ");

  for (pr = prompts->head; pr != NULL; pr = pr->next) {
	if (pr != prompts->head)
	  ds_asprintf(&ds, ",\n");

	ds_asprintf(&ds, "{ \"type\":\"%s\"", pr->type);
	if (pr->label != NULL)
	  ds_asprintf(&ds, ", \"label\":\"%s\"", pr->label);
	if (pr->varname != NULL)
	  ds_asprintf(&ds, ", \"varname\":\"%s\"", pr->varname);
	ds_asprintf(&ds, " }\n");
  }
if (prompts->head != NULL)
  ds_asprintf(&ds, "] } }");

  *json = ds_buf(&ds);
  return(ds_len(&ds));
}

char *
make_pam_prompt_form(Auth_prompts *prompts, char *jurisdiction,
					 char *action_url, char *action_query, char *inputs)
{
  char *p;
  Auth_prompt *pr;
  Ds ds;

  printf("\n<div class=\"prompt_form\">\n");

  ds_init(&ds);
  ds_asprintf(&ds, "<form method=\"post\" action=\"%s", action_url);
  if (action_query != NULL && action_query[0] != '\0')
	ds_asprintf(&ds, "?%s", action_query);
  ds_asprintf(&ds, "\">\n");

  ds_asprintf(&ds, "<table>\n");
  for (pr = prompts->head; pr != NULL; pr = pr->next) {
	ds_asprintf(&ds, "<tr>");
	if (pr->label != NULL) {
	  ds_asprintf(&ds, "<td class=\"prompt_label\"");
	  if (streq(pr->type, "error"))
		ds_asprintf(&ds, " width=\"90%%\"><em>%s</em></td>\n", pr->label);
	  else
		ds_asprintf(&ds, " width=\"50%%\">%s</td>\n", pr->label);
	}
	else
	  ds_asprintf(&ds, " width=\"50%%\">&nbsp;</td>\n");

	if (streq(pr->type, "text") || streq(pr->type, "password"))
	  ds_asprintf(&ds, "<td><input type=\"%s\" name=\"%s\"/></td>",
				  pr->type, pr->varname);
	else
	  ds_asprintf(&ds, "<td>&nbsp;</td>");
	ds_asprintf(&ds, "\n</tr>\n");
  }
  ds_asprintf(&ds, "</table>\n");

  ds_asprintf(&ds, "<input type=\"hidden\" name=\"DACS_JURISDICTION\"");
  ds_asprintf(&ds, " value=\"%s\"/>\n", jurisdiction);
  ds_asprintf(&ds, "<input type=\"hidden\" name=\"AUTH_TRANSID\"");
  ds_asprintf(&ds, " value=\"%s\"/>\n", prompts->transid);
  if (inputs != NULL)
	ds_asprintf(&ds, "%s\n", inputs);

  p = var_ns_get_value(dacs_conf->conf_var_ns, "Conf",
					   "prompt_submit_label");
  ds_asprintf(&ds, "<input type=\"submit\" value=\" %s \"/>\n",
			  (p != NULL) ? p : "Submit");

  ds_asprintf(&ds, "</form>\n");

  ds_asprintf(&ds, "</div>\n");

  return(ds_buf(&ds));
}
