/*
 *   This program is is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or (at
 *   your option) any later version.
 *
 *   This program 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: 409ff0b8019ba03faeb797e7317911ea916c7238 $
 * @file rlm_smsotp.c
 * @brief Supports OTP authentication using SMS.
 *
 * @copyright 2000,2006  The FreeRADIUS server project
 * @copyright 2009  Siemens AG, Holger Wolff holger.wolff@siemens.com
 */
RCSID("$Id: 409ff0b8019ba03faeb797e7317911ea916c7238 $")

#include <freeradius-devel/radiusd.h>
#include <freeradius-devel/modules.h>
#include <sys/un.h>

typedef struct rlm_smsotp_t {
	char const	*socket;
	char const	*challenge;
	char const	*authtype;
	fr_connection_pool_t *pool;
} rlm_smsotp_t;

static const CONF_PARSER module_config[] = {
	{ "socket", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_smsotp_t, socket), "/var/run/smsotp_socket" },
	{ "challenge_message", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_smsotp_t, challenge), "Enter Mobile PIN" },
	{ "challenge_type", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_smsotp_t, authtype), "smsotp-reply" },
	CONF_PARSER_TERMINATOR
};

static int _mod_conn_free(int *fdp)
{
	close(*fdp);
	return 0;
}

static void *mod_conn_create(TALLOC_CTX *ctx, void *instance)
{
	int fd;
	struct sockaddr_un sa;
	rlm_smsotp_t *inst = instance;
	socklen_t socklen = sizeof(sa);
	int *fdp;

	sa.sun_family = AF_UNIX;
	strlcpy(sa.sun_path, inst->socket, sizeof(sa.sun_path));

	fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (fd < 0) {
		ERROR("Failed opening SMSOTP file %s: %s",
		       inst->socket, fr_syserror(errno));
		return NULL;
	}

	if (connect(fd, (struct sockaddr *) &sa, socklen) < -1) {
		close(fd);
		ERROR("Failed connecting to SMSOTP file %s: %s",
		       inst->socket, fr_syserror(errno));
		return NULL;
	}

	fdp = talloc_zero(ctx, int);
	talloc_set_destructor(fdp, _mod_conn_free);
	*fdp = fd;

	return fdp;
}

/*
 * Full read with logging, and close on failure.
 * Returns nread on success, 0 on EOF, -1 on other failures.
 */
static size_t read_all(int *fdp, char *buf, size_t len)
{
	ssize_t n;
	size_t total = 0;

	fd_set fds;
	struct timeval tv;
	int retval;

	FD_ZERO(&fds);
	FD_SET(*fdp, &fds);
	tv.tv_sec = 0;
	tv.tv_usec = 0;

	while (total < len) {
		n = read(*fdp, &buf[total], len - total);
		if (n < 0) {
			if (errno == EINTR) {
				continue;
			}
			return -1;
		}

		/*
		 *	Socket was closed.  Don't try to re-open it.
		 */
		if (n == 0) return 0;
		total += n;

		/*
		 *	Check if there's more data.  If not, return
		 *	now.
		 */
		retval = select(1, &fds, NULL, NULL, &tv);
		if (!retval) {
			buf[total]= '\0';
			break;
		}
	}

	return total;
}


/*
 *	Write all of the data, taking care of EINTR, etc.
 */
static int write_all(int *fdp, char const *buf, size_t len)
{
	size_t left = len;
	ssize_t n;

	while (left) {
		n = write(*fdp, &buf[len - left], left);
		if (n < 0) {
			if ((errno == EINTR) || (errno == EPIPE)) {
				continue;
			}
			return -1;
		}
		left -= n;
	}

	return 0;
}


/*
 *	Do any per-module initialization that is separate to each
 *	configured instance of the module.  e.g. set up connections
 *	to external databases, read configuration files, set up
 *	dictionary entries, etc.
 *
 *	If configuration information is given in the config section
 *	that must be referenced in later calls, store a handle to it
 *	in *instance otherwise put a null pointer there.
 */
static int mod_instantiate(CONF_SECTION *conf, void *instance)
{
	rlm_smsotp_t *inst = instance;
	struct sockaddr_un sa;
	if (strlen(inst->socket) > (sizeof(sa.sun_path) - 1)) {
		cf_log_err_cs(conf, "Socket filename is too long");
		return -1;
	}

	/*
	 *	Initialize the socket pool.
	 */
	inst->pool = fr_connection_pool_module_init(conf, inst, mod_conn_create, NULL, NULL);
	if (!inst->pool) {
		return -1;
	}

	return 0;
}

/*
 *	Authenticate the user with the given password.
 */
static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request)
{
	rlm_smsotp_t *inst = instance;
	VALUE_PAIR *state;
	int bufsize;
	int *fdp;
	rlm_rcode_t rcode = RLM_MODULE_FAIL;
	char buffer[1000];
	char output[1000];

	fdp = fr_connection_get(inst->pool);
	if (!fdp) return RLM_MODULE_FAIL;

	/* Get greeting */
	bufsize = read_all(fdp, buffer, sizeof(buffer));
	if (bufsize == 0) {
		REDEBUG("No data available from socket - other end closed the connection");
		goto done;
	}
	if (bufsize < 0) {
		REDEBUG("Failed reading from socket: %s", fr_syserror(errno));
		goto done;
	}

	/*
	 *  Look for the 'state' attribute.
	 */
#define WRITE_ALL(_a,_b,_c) if (write_all(_a,_b,_c) < 0) goto done;
	state = fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY);
	if (state) {
		RDEBUG("Found reply to access challenge");

		/* send username */
		snprintf(output, sizeof(output), "check otp for %s\n",
			 request->username->vp_strvalue);
		WRITE_ALL(fdp, output, strlen(output));

		(void) read_all(fdp, buffer, sizeof(buffer));

		/* send password */
		snprintf(output, sizeof(output), "user otp is %s\n",
			 request->password->vp_strvalue);
		WRITE_ALL(fdp, output, strlen(output));

		(void) read_all(fdp, buffer, sizeof(buffer));

		/* set uuid */
		snprintf(output, sizeof(output), "otp id is %s\n",
			 state->vp_strvalue);
		WRITE_ALL(fdp, output, strlen(output));

		(void) read_all(fdp, buffer, sizeof(buffer));

		/* now check the otp */
		WRITE_ALL(fdp, "get check result\n", 17);

		(void) read_all(fdp, buffer, sizeof(buffer));

		/* end the sesssion */
		WRITE_ALL(fdp, "quit\n", 5);

		RDEBUG("answer is %s", buffer);
		if (strcmp(buffer,"OK") == 0) {
			rcode = RLM_MODULE_OK;
		}

		goto done;
	}

	RDEBUG("Generating OTP");

	/* set username */
	snprintf(output, sizeof(output), "generate otp for %s\n",
		 request->username->vp_strvalue);
	WRITE_ALL(fdp, output, strlen(output));

	(void) read_all(fdp, buffer, sizeof(buffer));

	/* end the sesssion */
	WRITE_ALL(fdp, "quit\n", 5);

	RDEBUG("Unique ID is %s", buffer);

	/* check the return string */
	if (strcmp(buffer,"FAILED") == 0) { /* smsotp script returns a error */
		goto done;
	}

	/*
	 *	Create the challenge, and add it to the reply.
	 */

	pair_make_reply("Reply-Message", inst->challenge, T_OP_EQ);
	pair_make_reply("State", buffer, T_OP_EQ);

	/*
	 *  Mark the packet as an Access-Challenge packet.
	 *
	 *  The server will take care of sending it to the user.
	 */
	request->reply->code = PW_CODE_ACCESS_CHALLENGE;
	DEBUG("rlm_smsotp: Sending Access-Challenge");

	rcode = RLM_MODULE_HANDLED;

done:
	fr_connection_release(inst->pool, fdp);
	return rcode;
}

/*
 *	Find the named user in this modules database.  Create the set
 *	of attribute-value pairs to check and reply with for this user
 *	from the database. The authentication code only needs to check
 *	the password, the rest is done here.
 */
static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request)
{
	VALUE_PAIR *state;
	rlm_smsotp_t *inst = instance;

	/*
	 *  Look for the 'state' attribute.
	 */
	state = fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY);
	if (state != NULL) {
		DEBUG("rlm_smsotp: Found reply to access challenge (AUTZ), Adding Auth-Type '%s'",inst->authtype);

		fr_pair_delete_by_num(&request->config, PW_AUTH_TYPE, 0, TAG_ANY); /* delete old auth-type */
		pair_make_config("Auth-Type", inst->authtype, T_OP_SET);
	}

	return RLM_MODULE_OK;
}


/*
 *	The module name should be the only globally exported symbol.
 *	That is, everything else should be 'static'.
 *
 *	If the module needs to temporarily modify it's instantiation
 *	data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
 *	The server will then take care of ensuring that the module
 *	is single-threaded.
 */
extern module_t rlm_smsotp;
module_t rlm_smsotp = {
	.magic		= RLM_MODULE_INIT,
	.name		= "smsotp",
	.type		= RLM_TYPE_THREAD_SAFE,
	.inst_size	= sizeof(rlm_smsotp_t),
	.config		= module_config,
	.instantiate	= mod_instantiate,
	.methods = {
		[MOD_AUTHENTICATE]	= mod_authenticate,
		[MOD_AUTHORIZE]		= mod_authorize
	},
};
