/*
 *
 * Copyright (C) 2006 iptelorg GmbH
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * Kamailio 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
 *
 * Kamailio 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 Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "../../core/sr_module.h"
#include "../../core/ut.h"
#include "../../core/dprint.h"
#include "../../core/pt.h"
#include "../../core/cfg/cfg_struct.h"
#include "ctrl_socks.h"
#include "io_listener.h"

#include <sys/types.h>
#include <sys/socket.h> /* socketpair */
#include <unistd.h>		/* close, fork, getpid */
#include <stdio.h>		/* snprintf */
#include <string.h>		/* strerror */
#include <errno.h>

MODULE_VERSION

#include "ctl_defaults.h"
#include "binrpc_run.h"
#ifdef USE_FIFO
#include "fifo_server.h"
#endif

static int mod_init(void);
static int mod_child(int rank);
static void mod_destroy(void);

static cmd_export_t cmds[] = {{0, 0, 0, 0, 0, 0}};


static int usock_mode = 0600; /* permissions, default rw-------*/
static int usock_uid = -1;	  /* username and group for the unix sockets*/
static int usock_gid = -1;

/* if set try to automatically convert values to the requested type in
   rpc->scan (default: not set) */
extern int autoconvert;
extern int binrpc_max_body_size;
extern int binrpc_struct_max_body_size;
extern int binrpc_buffer_size;

static int add_binrpc_socket(modparam_t type, void *val);
#ifdef USE_FIFO
static int add_fifo_socket(modparam_t type, void *val);
#endif
static int fix_user(modparam_t type, void *val);
static int fix_group(modparam_t type, void *val);


static void ctrl_listen_ls_rpc(rpc_t *rpc, void *ctx);
void io_listen_who_rpc(rpc_t *rpc, void *ctx);
void io_listen_conn_rpc(rpc_t *rpc, void *ctx);


static char *io_listen_who_doc[] = {"list open connections", 0};
static char *io_listen_conn_doc[] = {"returns number of open connections", 0};
static char *ctl_listen_ls_doc[] = {"list ctl listen sockets", 0};

static rpc_export_t ctl_rpc[] = {
		{"ctl.who", io_listen_who_rpc, (const char **)io_listen_who_doc, 0},
		{"ctl.connections", io_listen_conn_rpc,
				(const char **)io_listen_conn_doc, 0},
		{"ctl.listen", ctrl_listen_ls_rpc, (const char **)ctl_listen_ls_doc, 0},
		{0, 0, 0, 0}};

static param_export_t params[] = {
#ifdef USE_FIFO
		{"fifo", PARAM_STRING | PARAM_USE_FUNC, (void *)add_fifo_socket},
#endif
		{"binrpc", PARAM_STRING | PARAM_USE_FUNC, (void *)add_binrpc_socket},
		{"mode", PARAM_INT, &usock_mode},
		{"user", PARAM_STRING | PARAM_USE_FUNC, fix_user},
		{"group", PARAM_STRING | PARAM_USE_FUNC, fix_group},
		{"autoconversion", PARAM_INT, &autoconvert},
		{"binrpc_max_body_size", PARAM_INT, &binrpc_max_body_size},
		{"binrpc_struct_max_body_size", PARAM_INT,
				&binrpc_struct_max_body_size},
		{"binrpc_buffer_size", PARAM_INT, &binrpc_buffer_size},
		{0, 0, 0}}; /* no params */

struct module_exports exports = {
		"ctl",			 /* module name */
		DEFAULT_DLFLAGS, /* dlopen flags */
		cmds,			 /* cmd (cfg function) exports */
		params,			 /* param exports */
		ctl_rpc,		 /* RPC method exports */
		0,				 /* pseudo-variables exports */
		0,				 /* response handling function */
		mod_init,		 /* module init function */
		mod_child,		 /* per-child init function */
		mod_destroy		 /* module destroy function */
};


static struct id_list *listen_lst = 0; /* binrpc sockets name list */
static struct ctrl_socket *ctrl_sock_lst = 0;
static int fd_no = 0; /* number of fd used */


static int add_binrpc_socket(modparam_t type, void *val)
{
	char *s;
	struct id_list *id;

	if((type & PARAM_STRING) == 0) {
		LOG(L_CRIT, "BUG: ctl: add_binrpc_socket: bad parameter type %d\n",
				type);
		goto error;
	}
	s = (char *)val;
	id = parse_listen_id(s, strlen(s), UDP_SOCK); /* default udp proto */
	if(id == 0) {
		LOG(L_ERR, "ERROR: ctl: bad listen socket: \"%s\"\n", s);
		goto error;
	}
	id->data_proto = P_BINRPC;
	id->next = listen_lst;
	listen_lst = id;
	return 0;
error:
	return -1;
}


#ifdef USE_FIFO
static int add_fifo_socket(modparam_t type, void *val)
{
	char *s;
	struct id_list *id;

	if((type & PARAM_STRING) == 0) {
		LOG(L_CRIT, "BUG: ctl: add_fifo: bad parameter type %d\n", type);
		goto error;
	}
	s = (char *)val;
	id = parse_listen_id(s, strlen(s), FIFO_SOCK); /* default udp proto */
	if(id == 0) {
		LOG(L_ERR, "ERROR: ctl: bad fifo: \"%s\"\n", s);
		goto error;
	}
	id->data_proto = P_FIFO;
	id->next = listen_lst;
	listen_lst = id;
	return 0;
error:
	return -1;
}
#endif


static int fix_user(modparam_t type, void *val)
{
	char *s;

	if((type & PARAM_STRING) == 0) {
		LOG(L_CRIT, "BUG: ctl: fix_user: bad parameter type %d\n", type);
		goto error;
	}
	s = (char *)val;
	if(user2uid(&usock_uid, 0, s) < 0) {
		LOG(L_ERR, "ERROR: ctl: bad user name/uid number %s\n", s);
		goto error;
	}
	return 0;
error:
	return -1;
}


static int fix_group(modparam_t type, void *val)
{
	char *s;

	if((type & PARAM_STRING) == 0) {
		LOG(L_CRIT, "BUG: ctl: fix_group: bad parameter type %d\n", type);
		goto error;
	}
	s = (char *)val;
	if(group2gid(&usock_gid, s) < 0) {
		LOG(L_ERR, "ERROR: ctl: bad group name/gid number %s\n", s);
		goto error;
	}
	return 0;
error:
	return -1;
}

#define CTL_SOCKET_PATH_SIZE 128

static int mod_init(void)
{
	struct id_list *l;
	char ctl_socket_path[CTL_SOCKET_PATH_SIZE];
	int len;

	binrpc_callbacks_init();

	if(binrpc_max_body_size <= 0)
		binrpc_max_body_size = 4;
	if(binrpc_struct_max_body_size <= 0)
		binrpc_struct_max_body_size = 1;
	binrpc_max_body_size *= 1024;
	binrpc_struct_max_body_size *= 1024;

	if(listen_lst == 0) {
		if(strcmp(runtime_dir, RUN_DIR) == 0) {
			add_binrpc_socket(PARAM_STRING, DEFAULT_CTL_SOCKET);
		} else {
			len = sizeof(DEFAULT_CTL_SOCKET_PROTO)
				  + sizeof(DEFAULT_CTL_SOCKET_NAME) + strlen(runtime_dir) + 4;
			if(len > CTL_SOCKET_PATH_SIZE) {
				LM_ERR("ctl socket path is too big\n");
				return -1;
			}
			snprintf(ctl_socket_path, len, "%s%s/%s", DEFAULT_CTL_SOCKET_PROTO,
					runtime_dir, DEFAULT_CTL_SOCKET_NAME);
			add_binrpc_socket(PARAM_STRING, ctl_socket_path);
		}
	}
	DBG("listening on:\n");
	for(l = listen_lst; l; l = l->next) {
		fd_no++;
		switch(l->proto) {
			case UNIXD_SOCK:
				DBG("        [%s:unix dgram]  %s\n",
						payload_proto_name(l->data_proto), l->name);
				break;
			case UNIXS_SOCK:
				DBG("        [%s:unix stream] %s\n",
						payload_proto_name(l->data_proto), l->name);
				break;
			case UDP_SOCK:
				DBG("        [%s:udp]         %s:%d\n",
						payload_proto_name(l->data_proto), l->name,
						l->port ? l->port : DEFAULT_CTL_PORT);
				break;
			case TCP_SOCK:
				DBG("        [%s:tcp]         %s:%d\n",
						payload_proto_name(l->data_proto), l->name,
						l->port ? l->port : DEFAULT_CTL_PORT);
				break;
#ifdef USE_FIFO
			case FIFO_SOCK:
				DBG("        [%s:fifo]         %s\n",
						payload_proto_name(l->data_proto), l->name);
				fd_no++; /* fifos use 2 fds */
				break;
#endif
			default:
				LOG(L_CRIT, "BUG: ctrl: listen protocol %d not supported\n",
						l->proto);
				goto error;
		}
	}
	/* get the uid/gid from core if not set for the module */
	if(usock_uid == -1 && sock_uid != -1)
		usock_uid = sock_uid;
	if(usock_gid == -1 && sock_gid != -1)
		usock_gid = sock_gid;
	/* open socket now, before suid */
	if(init_ctrl_sockets(&ctrl_sock_lst, listen_lst, DEFAULT_CTL_PORT,
			   usock_mode, usock_uid, usock_gid)
			< 0) {
		LOG(L_ERR, "ERROR: ctl: mod_init: init ctrl. sockets failed\n");
		goto error;
	}
	if(ctrl_sock_lst) {
		/* we will fork */
		register_procs(1); /* we will be creating an extra process */
		register_fds(fd_no);
		/* The child process will keep updating its local configuration */
		cfg_register_child(1);
	}
#ifdef USE_FIFO
	fifo_rpc_init();
#endif
	return 0;
error:
	return -1;
}


static int mod_child(int rank)
{
	int pid;
	struct ctrl_socket *cs;
	static int rpc_handler = 0;

	/* do nothing from PROC_INIT, is the same as PROC_MAIN */
	if(rank == PROC_INIT)
		return 0;
	/* we want to fork(), but only from one process */
	if((rank == PROC_MAIN) && (ctrl_sock_lst)) { /* FIXME: no fork ?? */
		DBG("ctl: mod_child(%d), ctrl_sock_lst=%p\n", rank, ctrl_sock_lst);
		/* fork, but make sure we know not to close our own sockets when
		 * ctl child_init will be called for the new child */
		rpc_handler = 1;
		/* child should start with a correct estimated used fds number*/
		register_fds(MAX_IO_READ_CONNECTIONS);
		pid = fork_process(PROC_RPC, "ctl handler", 1);
		DBG("ctl: mod_child(%d), fork_process=%d, csl=%p\n", rank, pid,
				ctrl_sock_lst);
		if(pid < 0) {
			goto error;
		}
		if(pid == 0) { /* child */
			_ksr_is_main = 0;
			DBG("ctl: %d io_listen_loop(%d, %p)\n", rank, fd_no, ctrl_sock_lst);
			io_listen_loop(fd_no, ctrl_sock_lst);
		} else { /* parent */
			/* not used in parent */
			register_fds(-MAX_IO_READ_CONNECTIONS);
			rpc_handler = 0;
		}
	}
	if(rank != PROC_RPC || !rpc_handler) {
		/* close all the opened fds, we don't need them here */
		for(cs = ctrl_sock_lst; cs; cs = cs->next) {
			if(cs->fd >= 0)
				close(cs->fd);
			cs->fd = -1;
			if(cs->write_fd != -1) {
				close(cs->write_fd);
				cs->write_fd = -1;
			}
		}
		if(rank != PROC_MAIN) { /* we need the lists in main for on_exit cleanup,
								 see mod_destroy */
			/* free memory, we don't need the lists anymore */
			free_ctrl_socket_list(ctrl_sock_lst);
			ctrl_sock_lst = 0;
			free_id_list(listen_lst);
			listen_lst = 0;
		}
	}
	return 0;
error:
	return -1;
}


static void mod_destroy(void)
{
	struct ctrl_socket *cs;

	/* close all the opened fds & unlink the files */
	for(cs = ctrl_sock_lst; cs; cs = cs->next) {
		switch(cs->transport) {
			case UNIXS_SOCK:
			case UNIXD_SOCK:
				if(cs->fd >= 0)
					close(cs->fd);
				cs->fd = -1;
				if(cs->write_fd != -1) {
					close(cs->write_fd);
					cs->write_fd = -1;
				}
				if(cs->name) {
					if(unlink(cs->name) < 0) {
						LOG(L_ERR,
								"ERROR: ctl: could not delete unix"
								" socket %s: %s (%d)\n",
								cs->name, strerror(errno), errno);
					}
				}
				break;
#ifdef USE_FIFO
			case FIFO_SOCK:
				destroy_fifo(cs->fd, cs->write_fd, cs->name);
				break;
#endif
			default:
				if(cs->fd >= 0)
					close(cs->fd);
				cs->fd = -1;
				if(cs->write_fd != -1) {
					close(cs->write_fd);
					cs->write_fd = -1;
				}
		}
	}
	if(listen_lst) {
		free_id_list(listen_lst);
		listen_lst = 0;
	}
	if(ctrl_sock_lst) {
		free_ctrl_socket_list(ctrl_sock_lst);
		ctrl_sock_lst = 0;
	}
}


static void ctrl_listen_ls_rpc(rpc_t *rpc, void *ctx)
{
	struct ctrl_socket *cs;

	for(cs = ctrl_sock_lst; cs; cs = cs->next) {
		rpc->add(ctx, "ssss", payload_proto_name(cs->p_proto),
				socket_proto_name(cs->transport), cs->name,
				(cs->port) ? int2str(cs->port, 0) : "");
	}
}
