/*****************************************************************************\
 *  stepd_proxy.c
 *****************************************************************************
 *  Copyright (C) SchedMD LLC.
 *
 *  This file is part of Slurm, a resource management program.
 *  For details, see <https://slurm.schedmd.com/>.
 *  Please also read the included file: DISCLAIMER.
 *
 *  Slurm 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.
 *
 *  In addition, as a special exception, the copyright holders give permission
 *  to link the code of portions of this program with the OpenSSL library under
 *  certain conditions as described in each individual source file, and
 *  distribute linked combinations including the two. You must obey the GNU
 *  General Public License in all respects for all of the code used other than
 *  OpenSSL. If you modify file(s) with this exception, you may extend this
 *  exception to your version of the file(s), but you are not obligated to do
 *  so. If you do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source files in
 *  the program, then also delete it here.
 *
 *  Slurm 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 Slurm; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
\*****************************************************************************/

#include <sys/un.h>

#include "src/common/fd.h"
#include "src/common/pack.h"
#include "src/common/read_config.h"
#include "src/common/slurm_protocol_api.h"
#include "src/common/slurm_protocol_pack.h"
#include "src/common/stepd_api.h"
#include "src/common/stepd_proxy.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"

#include "src/conmgr/conmgr.h"

#include "src/interfaces/conn.h"

static char *slurmd_spooldir = NULL;

static int _slurmd_pack_msg_to_stepd(slurm_msg_t *resp, buf_t *out)
{
	uint32_t length_position, end_position;

	/* save position of length for later */
	length_position = get_buf_offset(out);
	pack32(0, out);

	pack16(resp->msg_type, out);
	if (pack_msg(resp, out) != SLURM_SUCCESS) {
		FREE_NULL_BUFFER(out);
		return SLURM_ERROR;
	}

	/* write length then reset out to end of message */
	end_position = get_buf_offset(out);
	set_buf_offset(out, length_position);
	pack32((end_position - length_position - sizeof(uint32_t)), out);
	set_buf_offset(out, end_position);

	return SLURM_SUCCESS;
}

static int _slurmd_send_resp_to_stepd(conmgr_fd_t *con, slurm_msg_t *resp)
{
	buf_t *out = init_buf(BUF_SIZE);
	int rc = SLURM_SUCCESS;

	if (_slurmd_pack_msg_to_stepd(resp, out)) {
		error("%s: Failed to pack response to slurmstepd", __func__);
		rc = SLURM_ERROR;
		goto cleanup;
	}
	if (conmgr_fd_xfer_out_buffer(con, out)) {
		error("%s: Failed to transfer buffer for response to slurmstepd",
		      __func__);
		rc = SLURM_ERROR;
		goto cleanup;
	}

cleanup:
	FREE_NULL_BUFFER(out);
	return rc;
}

static int _slurmd_send_rc_to_stepd(conmgr_fd_t *con, int rc,
				    uint16_t protocol_version)
{
	slurm_msg_t resp;
	return_code_msg_t rc_msg = { 0 };

	/*
	 * It's possible we didn't even unpack slurmstepd's protocol version.
	 * In that case, just try to use slurmd's protocol version.
	 */
	if (!protocol_version)
		protocol_version = SLURM_PROTOCOL_VERSION;

	slurm_msg_t_init(&resp);
	resp.protocol_version = protocol_version;
	resp.msg_type = RESPONSE_SLURM_RC;
	rc_msg.return_code = rc;
	resp.data = &rc_msg;

	return _slurmd_send_resp_to_stepd(con, &resp);
}

static int _slurmd_send_recv_msg(conmgr_fd_t *con, slurm_msg_t *req,
				 slurm_msg_t *resp, int timeout,
				 uint16_t proxy_type)
{
	int rc = SLURM_SUCCESS;

	switch (proxy_type) {
	case PROXY_TO_CTLD_SEND_ONLY:
		rc = slurm_send_only_controller_msg(req, working_cluster_rec);
		break;
	case PROXY_TO_CTLD_SEND_RECV:
		rc = slurm_send_recv_controller_msg(req, resp,
						    working_cluster_rec);
		break;
	case PROXY_TO_NODE_SEND_RECV:
		rc = slurm_send_recv_node_msg(req, resp, timeout);
		break;
	case PROXY_TO_NODE_SEND_ONLY:
		rc = slurm_send_only_node_msg(req);
		break;
	default:
		rc = SLURM_ERROR;
		error("%s: Unknown proxy type %u", __func__, proxy_type);
		break;
	}

	if (rc) {
		error("%s: Failed to send/recv slurmstepd message %s using proxy_type %s: %m",
		      __func__, rpc_num2string(req->msg_type),
		      rpc_num2string(proxy_type));
	}

	return rc;
}

static int _on_data_local_socket(conmgr_callback_args_t conmgr_args, void *arg)
{
	conmgr_fd_t *con = conmgr_args.con;
	buf_t *in = NULL;
	uint32_t length, timeout, r_uid;
	uint16_t msg_type, protocol_version, proxy_type;
	int rc = SLURM_SUCCESS;
	slurm_addr_t req_address = { 0 };
	slurm_msg_t req = SLURM_MSG_INITIALIZER, resp = SLURM_MSG_INITIALIZER;
	return_code_msg_t *rc_msg;
	char *req_tls_cert = NULL;

	uid_t uid = SLURM_AUTH_NOBODY;
	gid_t gid = SLURM_AUTH_NOBODY;
	pid_t pid = 0;

	if (!(in = conmgr_fd_shadow_in_buffer(con))) {
		error("%s: conmgr_fd_shadow_in_buffer() failed", __func__);
		rc = ESLURMD_STEPD_PROXY_FAILED;
		goto unpack_error;
	}

	safe_unpack16(&protocol_version, in);
	safe_unpack32(&length, in);
	safe_unpack16(&msg_type, in);
	safe_unpack32(&timeout, in);
	safe_unpack16(&proxy_type, in);

	switch (proxy_type) {
	case PROXY_TO_NODE_SEND_RECV:
	case PROXY_TO_NODE_SEND_ONLY:
		if (slurm_unpack_addr_no_alloc(&req_address, in))
			goto unpack_error;
		safe_unpackstr(&req_tls_cert, in);
		break;
	default:
		/* don't need address for ctld messages */
		break;
	}
	safe_unpack32(&r_uid, in);

	if (conmgr_get_fd_auth_creds(con, &uid, &gid, &pid)) {
		error("%s: conmgr_get_fd_auth_creds() failed", __func__);
		rc = ESLURMD_STEPD_PROXY_FAILED;
		goto unpack_error;
	}

	if (uid != slurm_conf.slurmd_user_id) {
		error("%s: uid %u does not match slurmd user %u",
		      __func__, uid, slurm_conf.slurmd_user_id);
		rc = SLURM_PROTOCOL_AUTHENTICATION_ERROR;
		goto unpack_error;
	}

	if (size_buf(in) < (length + sizeof(uint16_t))) {
		log_flag(NET, "incomplete message, only %u bytes available of %u bytes",
			 size_buf(in), length);
		FREE_NULL_BUFFER(in);
		xfree(req_tls_cert);
		/* Do not close connection */
		return SLURM_SUCCESS;
	}
	conmgr_fd_mark_consumed_in_buffer(con, length);

	req.protocol_version = protocol_version;
	req.msg_type = msg_type;
	req.address = req_address;
	req.tls_cert = req_tls_cert;
	slurm_msg_set_r_uid(&req, r_uid);

	if (unpack_msg(&req, in) != SLURM_SUCCESS) {
		error("%s: Failed to unpack message from slurmstepd to relay to slurmctld",
		      __func__);
		rc = ESLURMD_STEPD_PROXY_FAILED;
		goto unpack_error;
	}

	if (_slurmd_send_recv_msg(con, &req, &resp, timeout, proxy_type)) {
		rc = ESLURMD_STEPD_PROXY_FAILED;
		goto unpack_error;
	}

	/*
	 * Send success rc back to slurmstepd for SEND_ONLY messages so
	 * slurmstepd knows its message was successfully sent.
	 */
	switch (proxy_type) {
	case PROXY_TO_NODE_SEND_ONLY:
	case PROXY_TO_CTLD_SEND_ONLY:
		rc_msg = xmalloc(sizeof(*rc_msg));
		resp.protocol_version = protocol_version;
		resp.msg_type = RESPONSE_SLURM_RC;
		rc_msg->return_code = SLURM_SUCCESS;
		resp.data = rc_msg;
		break;
	}

	_slurmd_send_resp_to_stepd(con, &resp);

unpack_error:
	/*
	 * attempt to send rc back to slurmstepd so that slurmstepd knows an
	 * error occurred and its message was not actually sent
	 */
	if (rc && _slurmd_send_rc_to_stepd(con, rc, protocol_version)) {
		error("%s: Failed to send rc to slurmstepd saying that the proxy failed",
		      __func__);
	}

	xfree(req_tls_cert);
	slurm_free_msg_data(req.msg_type, req.data);
	slurm_free_msg_data(resp.msg_type, resp.data);

	conmgr_queue_close_fd(con);
	FREE_NULL_BUFFER(in);

	return rc;
}

extern void stepd_proxy_slurmd_init(char *spooldir)
{
	static const conmgr_events_t events = {
		.on_data = _on_data_local_socket,
	};
	static char *path = NULL;
	int rc;

	if (!path)
		xstrfmtcat(path, "unix:%s/slurmd.socket", spooldir);

	if ((rc = conmgr_create_listen_socket(NULL, CON_TYPE_RAW, CON_FLAG_NONE,
					      path, &events, NULL)))
		fatal("%s: [%s] unable to create socket: %s",
		      __func__, path, slurm_strerror(rc));
}

extern void stepd_proxy_stepd_init(char *spooldir)
{
	slurmd_spooldir = xstrdup(spooldir);
}

static int _stepd_connect_to_slurmd(void)
{
	struct sockaddr_un slurmd_addr = { .sun_family = AF_UNIX };
	size_t len;
	int fd;

	(void) snprintf(slurmd_addr.sun_path, sizeof(slurmd_addr.sun_path),
			"%s/slurmd.socket", slurmd_spooldir);

	len = strlen(slurmd_addr.sun_path) + 1 + sizeof(slurmd_addr.sun_family);

	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
		error("%s: socket() failed: %m", __func__);
		return -1;
	}

	if (connect(fd, (struct sockaddr *) &slurmd_addr, len) < 0) {
		error("%s: connect() failed for %s: %m",
		      __func__, slurmd_addr.sun_path);
		close(fd);
		return -1;
	}

	log_flag(NET, "%s: Opened connection to slurmd listening socket at '%s'",
		 __func__, slurmd_addr.sun_path);

	return fd;
}

static int _stepd_send_to_slurmd(int fd, slurm_msg_t *req, int timeout,
				 uint16_t proxy_type)
{
	uint32_t length_position, end_position;
	buf_t *buffer = init_buf(BUF_SIZE);

	pack16(SLURM_PROTOCOL_VERSION, buffer);

	/* save position of length for later */
	length_position = get_buf_offset(buffer);
	pack32(0, buffer);

	pack16(req->msg_type, buffer);
	pack32(timeout, buffer);
	pack16(proxy_type, buffer);

	switch (proxy_type) {
	case PROXY_TO_NODE_SEND_RECV:
	case PROXY_TO_NODE_SEND_ONLY:
		slurm_pack_addr(&req->address, buffer);
		packstr(req->tls_cert, buffer);
		break;
	default:
		/* don't need address for ctld messages */
		break;
	}
	pack32(req->restrict_uid, buffer);

	if (pack_msg(req, buffer) != SLURM_SUCCESS) {
		error("%s: could not pack req", __func__);
		goto pack_error;
	}

	/* write length then reset buffer to end of message */
	end_position = get_buf_offset(buffer);
	set_buf_offset(buffer, length_position);
	pack32(end_position - length_position, buffer);
	set_buf_offset(buffer, end_position);

	/* send to slurmd */
	safe_write(fd, get_buf_data(buffer), get_buf_offset(buffer));
	FREE_NULL_BUFFER(buffer);

	log_flag(NET, "%s: sent message %s using proxy_type %s (via slurmd)",
		 __func__, rpc_num2string(req->msg_type),
		 rpc_num2string(proxy_type));

	return SLURM_SUCCESS;

rwfail:
pack_error:
	FREE_NULL_BUFFER(buffer);
	return SLURM_ERROR;
}

static int _stepd_recv_from_slurmd(int fd, slurm_msg_t *resp)
{
	uint32_t len;
	buf_t *buffer = NULL;

	/* read response from slurmd */
	safe_read(fd, &len, sizeof(uint32_t));
	if (!(len = ntohl(len)))
		goto rwfail;
	buffer = init_buf(len);
	safe_read(fd, buffer->head, len);

	slurm_msg_t_init(resp);

	safe_unpack16(&resp->msg_type, buffer);
	if (unpack_msg(resp, buffer) != SLURM_SUCCESS) {
		error("%s: could not unpack resp for %s message",
		      __func__, rpc_num2string(resp->msg_type));
		goto unpack_error;
	}
	FREE_NULL_BUFFER(buffer);

	log_flag(NET, "%s: received message %s (via slurmd)",
		 __func__, rpc_num2string(resp->msg_type));

	return SLURM_SUCCESS;
rwfail:
unpack_error:
	FREE_NULL_BUFFER(buffer);
	return SLURM_ERROR;
}

static int _stepd_send_recv_msg(slurm_msg_t *req, slurm_msg_t *resp,
				int timeout, uint16_t proxy_type)
{
	int fd;

	xassert(req);

	if ((fd = _stepd_connect_to_slurmd()) < 0) {
		error("%s: failed to connect to slurmd socket", __func__);
		return SLURM_ERROR;
	}

	if (_stepd_send_to_slurmd(fd, req, timeout, proxy_type)) {
		error("%s: failed to send %s message to slurmd using proxy_type %s",
		      __func__, rpc_num2string(req->msg_type),
		      rpc_num2string(proxy_type));
		close(fd);
		return SLURM_ERROR;
	}

	if (_stepd_recv_from_slurmd(fd, resp)) {
		error("%s: failed to receive response from slurmd proxy for %s message", __func__,
		      rpc_num2string(req->msg_type));
		close(fd);
		return SLURM_ERROR;
	}
	close(fd);

	/* Check if slurmd hit any errors trying to send our request message */
	if (resp->msg_type == RESPONSE_SLURM_RC) {
		switch (((return_code_msg_t *) resp->data)->return_code) {
		case ESLURMD_STEPD_PROXY_FAILED:
			error("%s: slurmd was unable to proxy request message to its final destination",
			      __func__);
			return SLURM_ERROR;
		case SLURM_PROTOCOL_AUTHENTICATION_ERROR:
			error("%s: slurmd was unable to authenticate message we sent",
			      __func__);
			return SLURM_ERROR;
		default:
			/* No proxy related errors */
			break;
		}
	}

	return SLURM_SUCCESS;
}

extern int stepd_proxy_send_only_ctld_msg(slurm_msg_t *req)
{
	int rc;
	/*
	 * need response message to see if slurmd successfully sent message to
	 * its final destination.
	 */
	slurm_msg_t resp;

	xassert(running_in_slurmstepd());

	slurm_msg_t_init(&resp);
	rc = _stepd_send_recv_msg(req, &resp, 0, PROXY_TO_CTLD_SEND_ONLY);
	slurm_free_msg_data(resp.msg_type, resp.data);

	return rc;
}

extern int stepd_proxy_send_recv_ctld_msg(slurm_msg_t *req, slurm_msg_t *resp)
{
	xassert(running_in_slurmstepd());
	return _stepd_send_recv_msg(req, resp, 0, PROXY_TO_CTLD_SEND_RECV);
}

extern int stepd_proxy_send_only_node_msg(slurm_msg_t *req)
{
	int rc;
	/*
	 * need response message to see if  slurmd successfully sent message to
	 * its final destination.
	 */
	slurm_msg_t resp;

	xassert(running_in_slurmstepd());

	slurm_msg_t_init(&resp);
	rc = _stepd_send_recv_msg(req, &resp, 0, PROXY_TO_NODE_SEND_ONLY);
	slurm_free_msg_data(resp.msg_type, resp.data);

	return rc;
}

extern int stepd_proxy_send_recv_node_msg(slurm_msg_t *req, slurm_msg_t *resp,
					  int timeout)
{
	xassert(running_in_slurmstepd());
	return _stepd_send_recv_msg(req, resp, timeout,
				    PROXY_TO_NODE_SEND_RECV);
}

static int _slurmd_send_msg_to_stepd(int fd, slurm_msg_t *req)
{
	int req_msg_type = req->msg_type;
	uint32_t buf_size;

	safe_write(fd, &req_msg_type, sizeof(int));

	buf_size = get_buf_offset(req->buffer) - req->body_offset;

	safe_write(fd, &req->protocol_version, sizeof(uint16_t));
	safe_write(fd, &buf_size, sizeof(uint32_t));
	safe_write(fd, &req->buffer->head[req->body_offset], buf_size);

	return SLURM_SUCCESS;

rwfail:
	error("%s: Failed to write to stepd: %m", __func__);
	return SLURM_ERROR;
}

static int _slurmd_recv_msg_from_stepd(int fd, buf_t **resp_buf)
{
	uint32_t data_size;
	char *data = NULL;
	buf_t *buf;

	/* see _stepd_send_resp_to_slurmd() */
	safe_read(fd, &data_size, sizeof(uint32_t));
	data_size = ntohl(data_size);
	data = xmalloc(data_size);
	safe_read(fd, data, data_size);

	buf = create_buf(data, data_size);

	*resp_buf = buf;

	return SLURM_SUCCESS;
rwfail:
	error("%s: Failed to read from stepd: %m", __func__);
	return SLURM_ERROR;
}

extern int stepd_proxy_send_recv_to_stepd(slurm_msg_t *req, buf_t **resp_buf,
					  slurm_step_id_t *step_id,
					  int stepd_fd, bool reply)
{
	xassert(running_in_slurmd());

	fd_set_nonblocking(stepd_fd);

	if (_slurmd_send_msg_to_stepd(stepd_fd, req)) {
		error("%s: Failed to send msg to stepd", __func__);
		return SLURM_ERROR;
	}

	if (!reply)
		return SLURM_SUCCESS;

	if (_slurmd_recv_msg_from_stepd(stepd_fd, resp_buf)) {
		error("%s: Failed to receive response from stepd", __func__);
		return SLURM_ERROR;
	}

	return SLURM_SUCCESS;
}

static int _stepd_send_resp_to_slurmd(int fd, uint32_t msglen,
				      msg_bufs_t *buffers)
{
	xassert(buffers);

	/* see _slurmd_recv_msg_from_stepd() */
	safe_write(fd, &msglen, sizeof(msglen));
	safe_write(fd, get_buf_data(buffers->header),
		   get_buf_offset(buffers->header));
	/* No auth, SLURM_NO_AUTH_CRED is set */
	safe_write(fd, get_buf_data(buffers->body),
		   get_buf_offset(buffers->body));

	return SLURM_SUCCESS;
rwfail:
	return SLURM_ERROR;
}

extern int stepd_proxy_send_resp_to_slurmd(int fd, slurm_msg_t *source_msg,
					   slurm_msg_type_t msg_type,
					   void *data)
{
	msg_bufs_t buffers = { 0 };
	uint32_t msglen = 0;
	int rc;
	slurm_msg_t resp_msg;

	xassert(running_in_slurmstepd());

	slurm_resp_msg_init(&resp_msg, source_msg, msg_type, data);

	if (slurm_buffers_pack_msg(&resp_msg, &buffers, true)) {
		rc = SLURM_ERROR;
		goto end;
	}

	msglen = get_buf_offset(buffers.body) + get_buf_offset(buffers.header);
	/* No auth, SLURM_NO_AUTH_CRED is set */

	msglen = htonl(msglen);

	rc = _stepd_send_resp_to_slurmd(fd, msglen, &buffers);

end:
	FREE_NULL_BUFFER(buffers.auth);
	FREE_NULL_BUFFER(buffers.body);
	FREE_NULL_BUFFER(buffers.header);

	return rc;
}
