/*
 *
 *
 *    auth.cc
 *
 *    C++ source code for class my::auth::pam member functions and/or static member
 *    data.
 *
 *    =========================================================================
 *
 *    Copyright (C) 1999-2013 by Matt Roberts, KK5JY.
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General Public
 *    License along with this library; if not, write to the Free Software
 *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *    =========================================================================
 *
 *
 */

#include <my++/auth.h>
#include <my++/stype.h>
#include <sys/types.h>

#include <cstdlib>
#include <cstring>


bool my::auth::pam::base_session::set_string_item (int item, const std::string &s) {
	rtn = pam_set_item (pamh, item, (void*)s.c_str());
	if (rtn != success)
		throw base_session::exception(pamh, rtn);
	return true;
}

bool my::auth::pam::base_session::get_string_item (int item, std::string &s) {
	void *value;

	#ifdef LINUX
	rtn = pam_get_item (pamh, item, (const void**)&value);
	#else
	rtn = pam_get_item (pamh, item, &value);
	#endif

	if (rtn != success)
		throw base_session::exception(pamh, rtn);
	s = (char*)(value);
	return true;
}

bool my::auth::pam::base_session::set_credentials (const flags f) {
	rtn = pam_setcred (pamh, f);
	if (rtn != success)
		throw base_session::exception(pamh, rtn);
	return true;
}

bool my::auth::pam::base_session::set_env (const std::string &name, const std::string &val) {
	std::string arg = name;
	arg += "=";
	arg += val;
	rtn = pam_putenv (pamh, arg.c_str());
	if (rtn != success)
		throw base_session::exception(pamh, rtn);
	return true;
}

bool my::auth::pam::base_session::get_env (const std::string &name, std::string &val) {
	char buffer [128];
	buffer[0] = 0;
	if (!pam_getenv (pamh, buffer))
		throw base_session::exception(pamh, rtn);
	for (char *cp = buffer; *cp != 0; ++cp) {
		switch (*cp) {
			case 0:
				val = "";
				return true;
			case '=':
				val = (cp + 1);
				return true;
			default:
				break;
		}
	}

	return true;
}

bool my::auth::pam::base_session::start (const std::string &user) {
	/* start the PAM sequence */
	if (user.size())
		rtn = pam_start (app_name.c_str(), user.c_str(), &conv, &pamh);
	else
		rtn = pam_start (app_name.c_str(), 0, &conv, &pamh);

	/* handle unexpected failure */
	if (rtn != success)
		throw base_session::exception (pamh, rtn);
	started = true;
	return true;
}

bool my::auth::pam::base_session::authenticate (const my::auth::pam::base_session::flags f) {
	/* authenticate the user */
	rtn = pam_authenticate (pamh, f);

	/* handle unexpected failure */
	if (rtn != success) {
		switch (rtn) {
			case PAM_ABORT:
			case PAM_MAXTRIES:
				throw base_session::exception (pamh, rtn);
			default:
				return false;
		}
	}

	return true;
}

bool my::auth::pam::base_session::change_token (const my::auth::pam::base_session::flags f) {
	rtn = pam_chauthtok (pamh, f);
	if (rtn != success)
		throw base_session::exception (pamh, rtn);
	return (rtn != 0);
}

bool my::auth::pam::base_session::manage_account (const my::auth::pam::base_session::flags f) {
	/* do account management */
	rtn = pam_acct_mgmt (pamh, f);

	/* handle unexpected failure */
	if (rtn != success) {
		switch (rtn) {
			case token_required:
				try {
					return change_token ((flags)((f & silent) | change_expired_token));
				} catch (...) {
					return false;
				}
			case account_expired:
			case auth_error:
			case permission_denied:
			case unknown_user:
			default:
				throw base_session::exception (pamh, rtn);
		}
	}

	return true;
}

bool my::auth::pam::base_session::open_session (const my::auth::pam::base_session::flags f) {
	rtn = pam_open_session (pamh, f);
	if (rtn != success)
		throw base_session::exception (pamh, rtn);
	session_open = true;
	return true;
}

bool my::auth::pam::base_session::close_session (const my::auth::pam::base_session::flags f) {
	rtn = pam_close_session (pamh, f);
	if (rtn != success)
		throw base_session::exception (pamh, rtn);
	session_open = false;
	return true;
}

bool my::auth::pam::base_session::end (void) {
	rtn = pam_end(pamh, rtn);
	started = false;
	if (rtn != success)
		throw base_session::exception(pamh, rtn);
	return true;
}

int my::auth::pam::base_session::int_conv (int num_msg, const pam_message **msg,
		pam_response **resp, void *appdata_ptr) {
	/* these are the arguments passed to the function */
	cargs *args = (cargs*)(appdata_ptr);

	/* pointer to responses */
	pam_response *reply = 0;

	/* other local vars */
	int i;

	/* create a message list */
	message_list ml;

	for (i = 0; i < num_msg; ++i) {
		switch (msg[i]->msg_style) {
			case PAM_PROMPT_ECHO_OFF:
				ml.push_back(message(msg[i]->msg, message::prompt_noecho));
				break;
			case PAM_PROMPT_ECHO_ON:
				ml.push_back(message(msg[i]->msg, message::prompt_echo));
				break;
			case PAM_ERROR_MSG:
				ml.push_back(message(msg[i]->msg, message::error_message));
				break;
			case PAM_TEXT_INFO:
				ml.push_back(message(msg[i]->msg, message::info_message));
				break;
#ifdef PAM_BINARY_MSG
			case PAM_BINARY_PROMPT:
				ml.push_back(message(msg[i]->msg, message::binary_prompt));
				break;
#endif
#ifdef PAM_BINARY_MSG
			case PAM_BINARY_MSG:
				ml.push_back(message(msg[i]->msg, message::binary_message));
				break;
#endif
			default:
				return PAM_CONV_ERR;
		}
	}

	/* allocate the response array */
	reply = (pam_response *) calloc(num_msg, sizeof(pam_response));

	/* if memory allocation error...*/
	if (!reply) {
		*resp = 0;
		return PAM_CONV_ERR;
	}

	/*
	 *  call the virtual 'conversation' function, with the message list 'ml'
	 *  as an argument, and then deal with its success
	 */
	if (args->obj->conversation(ml)) {
		/* copy response codes, and return */
		for (i = 0; i < num_msg; ++i) {
			if (ml[i].resp.size()) {
				reply[i].resp = (char*)calloc(ml[i].resp.size() + 1, sizeof(char));
				strcpy (reply[i].resp, ml[i].resp.c_str());
			} else {
				reply[i].resp = 0;
			}

			reply[i].resp_retcode = 0; /* PAM_SUCCESS */
		}

		/* pass back the reply */
		*resp = reply;

		/* return success */
		return PAM_SUCCESS;
	}

	/* if we get this far, we have failed */
	resp = 0;
	free(reply);
	return PAM_CONV_ERR;
}

my::auth::pam::base_session::base_session (const std::string &an)
	: app_name(an) {
		/* setup the private data */
		pamh = 0;
		args.obj = this;
		args.real_args = 0;
		#ifdef LINUX
		conv.conv = int_conv;
		#else
		conv.conv = (int (*)(int, pam_message**, pam_response**, void*)) int_conv;
		#endif
		conv.appdata_ptr = (void*)(&args);
		rtn = success;
		started = false;
		session_open = false;
	}

my::auth::pam::base_session::~base_session (void) throw () {
	/* TODO - modify this so that it can throw exceptions */
	if (session_open) {
		try {
			close_session();
		} catch (...) {
			/* do nothing */
		}
	}

	if (started) {
		try {
			end();
		} catch (...) {
			/* do nothing */
		}
	}
}


bool my::auth::pam::ttysession::doAuth (void) {
	try {
		/* start the authentication sequence */
		if (!start(user)) return false;

		/* set some parameters for the conversation */
		set_user_prompt ("Username: ");

		/* authenticate the user */
		if (!authenticate()) return false;

		/* do account management if needed */
		if (!manage_account()) {
			/* change the user's auth token if needed */
			if (!change_token())
				return false;
		}

		/* open the user's session */
		if (!open_session()) return false;
	} catch (const ttysession::exception &e) {
		try {
			end();
		} catch (...) {
		}

		/* rethrow the exception */
		throw;
	}

	return true;
}


void write_wrapper(int fileno, const char *data, int length) {
	int count = 0;
	while (count != length) {
		int out = write(fileno, data, length);
		if (out <= 0) {
			return;
		}
		count += out;
	}
}


bool my::auth::pam::ttysession::conversation (my::auth::pam::base_session::message_list &ml) {
	char buffer [128];
	termios old_ios, new_ios;
	int fd = STDIN_FILENO;
	int n = 0;
	bool reset_ios = false;

	for (message_list::iterator mi = ml.begin(); mi != ml.end(); ++mi) {
		switch (mi->style) {
			case message::prompt_noecho:
				if (isatty(fd)) {
					if (tcgetattr(STDIN_FILENO, &old_ios) != 0)
						return false;
					memcpy(&new_ios, &old_ios, sizeof(new_ios));
					new_ios.c_lflag &= ~(ECHO);
					tcsetattr(fd, TCSAFLUSH, &new_ios);
					reset_ios = true;
				}
			case message::prompt_echo:
				write_wrapper (STDOUT_FILENO, mi->text.c_str(), mi->text.length());
				n = read (fd, buffer, sizeof(buffer));

				if (n >= 0)
					buffer[n] = 0;
				else
					buffer[0] = 0;

				for (n = strlen(buffer) - 1; n >= 0; ++n) {
					if (buffer[n] == '\n')
						buffer[n] = 0;
					else if (buffer[n] == '\r')
						buffer[n] = 0;
					else break;
				}

				if (reset_ios) {
					tcsetattr(fd, TCSAFLUSH, &old_ios);
					write_wrapper (STDOUT_FILENO, "\n", 1);
				}
				break;
			case message::info_message:
			case message::error_message:
				write_wrapper (STDOUT_FILENO, mi->text.c_str(), mi->text.length());
			default:
				break;
		}

		mi->setResponse (buffer);
	}

	return true;
}
