/*
 * Copyright (C) 2013-2020 Nikos Mavrogiannopoulos
 * Copyright (C) 2014, 2015 Red Hat, Inc.
 *
 * This program 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, see <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>
#include <signal.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <gnutls/gnutls.h>
#include <gnutls/crypto.h>
#include <tlslib.h>
#include <script-list.h>
#include <ip-lease.h>
#include <proc-search.h>
#include "str.h"

#include <vpn.h>
#include <tun.h>
#include <main.h>
#include <ccan/list/list.h>
#include <common.h>

/* Puts the provided PIN into the config's cgroup */
void put_into_cgroup(main_server_st *s, const char *_cgroup, pid_t pid)
{
#ifdef __linux__
	char *name, *p, *savep;
	char cgroup[128];
	char file[_POSIX_PATH_MAX];
	FILE *fd;

	if (_cgroup == NULL)
		return;

	/* format: cpu,memory:cgroup-name */
	strlcpy(cgroup, _cgroup, sizeof(cgroup));

	name = strchr(cgroup, ':');
	if (name == NULL) {
		mslog(s, NULL, LOG_ERR, "error parsing cgroup name: %s",
		      cgroup);
		return;
	}
	name[0] = 0;
	name++;

	p = strtok_r(cgroup, ",", &savep);
	while (p != NULL) {
		mslog(s, NULL, LOG_DEBUG,
		      "putting process %u to cgroup '%s:%s'", (unsigned int)pid,
		      p, name);

		snprintf(file, sizeof(file), "/sys/fs/cgroup/%s/%s/tasks", p,
			 name);

		fd = fopen(file, "w");
		if (fd == NULL) {
			mslog(s, NULL, LOG_ERR, "cannot open: %s", file);
			return;
		}

		if (fprintf(fd, "%u", (unsigned int)pid) <= 0) {
			mslog(s, NULL, LOG_ERR, "could not write to: %s", file);
		}
		fclose(fd);
		p = strtok_r(NULL, ",", &savep);
	}

	return;
#else
	if (_cgroup != NULL)
		mslog(s, NULL, LOG_WARNING,
		      "Ignoring cgroup option as it is not supported on this system");
#endif
}

int send_cookie_auth_reply(main_server_st *s, struct proc_st *proc, AUTHREP r)
{
	AuthCookieReplyMsg msg = AUTH_COOKIE_REPLY_MSG__INIT;
	int ret;

	if (r == AUTH__REP__OK && proc->tun_lease.name[0] != 0) {
		char ipv6[MAX_IP_STR];
		char ipv4[MAX_IP_STR];
		char ipv6_local[MAX_IP_STR];
		char ipv4_local[MAX_IP_STR];

		/* fill message */
		msg.reply = AUTH__REP__OK;

		msg.has_session_id = 1;
		msg.session_id.data = proc->dtls_session_id;
		msg.session_id.len = sizeof(proc->dtls_session_id);

		msg.sid.data = proc->sid;
		msg.sid.len = sizeof(proc->sid);
		msg.session_start_time = proc->session_start_time;

		msg.vname = proc->tun_lease.name;
		msg.user_name = proc->username;
		msg.group_name = proc->groupname;

		if (proc->ipv4 && proc->ipv4->rip_len > 0) {
			msg.ipv4 = human_addr2(
				(struct sockaddr *)&proc->ipv4->rip,
				proc->ipv4->rip_len, ipv4, sizeof(ipv4), 0);
			msg.ipv4_local =
				human_addr2((struct sockaddr *)&proc->ipv4->lip,
					    proc->ipv4->lip_len, ipv4_local,
					    sizeof(ipv4_local), 0);
		}

		if (proc->ipv6 && proc->ipv6->rip_len > 0) {
			msg.ipv6 = human_addr2(
				(struct sockaddr *)&proc->ipv6->rip,
				proc->ipv6->rip_len, ipv6, sizeof(ipv6), 0);
			msg.ipv6_local =
				human_addr2((struct sockaddr *)&proc->ipv6->lip,
					    proc->ipv6->lip_len, ipv6_local,
					    sizeof(ipv6_local), 0);
		}

		msg.config = proc->config;
		msg.secmod_addr.data =
			(uint8_t *)&s
				->sec_mod_instances[proc->sec_mod_instance_index]
				.secmod_addr;
		msg.secmod_addr.len =
			s->sec_mod_instances[proc->sec_mod_instance_index]
				.secmod_addr_len;

		ret = send_socket_msg_to_worker(
			s, proc, AUTH_COOKIE_REP, proc->tun_lease.fd, &msg,
			(pack_size_func)auth_cookie_reply_msg__get_packed_size,
			(pack_func)auth_cookie_reply_msg__pack);
	} else {
		msg.reply = AUTH__REP__FAILED;

		ret = send_msg_to_worker(
			s, proc, AUTH_COOKIE_REP, &msg,
			(pack_size_func)auth_cookie_reply_msg__get_packed_size,
			(pack_func)auth_cookie_reply_msg__pack);
	}

	if (ret < 0) {
		int e = errno;

		mslog(s, proc, LOG_ERR, "send_msg: %s", strerror(e));
		return ret;
	}

	return 0;
}

int handle_auth_cookie_req(sec_mod_instance_st *sec_mod_instance,
			   struct proc_st *proc,
			   const AuthCookieRequestMsg *req)
{
	main_server_st *s = sec_mod_instance->server;
	int ret;
	struct proc_st *old_proc;

	if (req->cookie.data == NULL || req->cookie.len != sizeof(proc->sid))
		return -1;

	/* generate a new DTLS session ID for each connection, to allow
	 * openconnect of distinguishing when the DTLS key has switched. */
	ret = gnutls_rnd(GNUTLS_RND_NONCE, proc->dtls_session_id,
			 sizeof(proc->dtls_session_id));
	if (ret < 0)
		return -1;
	proc->dtls_session_id_size = sizeof(proc->dtls_session_id);

	/* check for a user with the same sid as in the cookie */
	old_proc = proc_search_sid(s, req->cookie.data);
	if (old_proc != NULL) {
		if (old_proc->invalidated != 0) {
			mslog(s, proc, LOG_ERR,
			      "the reused session has been invalidated");
			return -1;
		}
	}

	/* loads sup config and basic proc info (e.g., username) */
	ret = session_open(sec_mod_instance, proc, req->cookie.data,
			   req->cookie.len);
	if (ret < 0) {
		mslog(s, proc, LOG_INFO, "could not open session");
		return -1;
	}

	/* Put into right cgroup */
	if (proc->config->cgroup != NULL) {
		put_into_cgroup(s, proc->config->cgroup, proc->pid);
	}

	/* disconnect and reuse previous session's IPs*/
	if (old_proc != NULL) {
		if (strcmp(proc->username, old_proc->username) != 0) {
			mslog(s, old_proc, LOG_ERR,
			      "the user of the new session doesn't match the old (new: %s)",
			      proc->username);
			return -1;
		}

		mslog(s, old_proc, LOG_INFO,
		      "disconnecting previous user session due to session reuse");

		/* steal its leases */
		steal_ip_leases(old_proc, proc);

		if (old_proc->pid > 0) {
			kill_proc(old_proc);
		}
		mslog(s, proc, LOG_DEBUG, "reusing session");
	} else {
		mslog(s, proc, LOG_INFO, "new user session");
	}

	/* update the SID */
	memcpy(proc->sid, req->cookie.data, req->cookie.len);
	/* this also hints to call session_close() */
	proc->active_sid = 1;

	if (req->hostname != NULL)
		strlcpy(proc->hostname, req->hostname, sizeof(proc->hostname));

	/* add the links to proc hash */
	if (proc_table_add(s, proc) < 0) {
		mslog(s, proc, LOG_ERR, "failed to add proc hashes");
		return -1;
	}

	return 0;
}

/* Checks for multiple users.
 *
 * It returns a negative error code if more than the maximum allowed
 * users are found.
 *
 * In addition this function will also check whether the cookie
 * used had been reused before, and then disconnect the old session
 * (cookies are unique).
 */
int check_multiple_users(main_server_st *s, struct proc_st *proc)
{
	struct proc_st *ctmp = NULL, *cpos;
	unsigned int entries = 1; /* that one */
	unsigned int max;

	max = proc->config->max_same_clients;

	if (max == 0)
		return 0;

	list_for_each_safe(&s->proc_list.head, ctmp, cpos, list)
	{
		if (ctmp != proc && ctmp->pid != -1) {
			if (!ctmp->pid_killed &&
			    strcmp(proc->username, ctmp->username) == 0) {
				entries++;

				if (entries > max)
					return -1;
			}
		}
	}

	return 0;
}
