/*
 * Copyright (C) 1994-2021 Altair Engineering, Inc.
 * For more information, contact Altair at www.altair.com.
 *
 * This file is part of both the OpenPBS software ("OpenPBS")
 * and the PBS Professional ("PBS Pro") software.
 *
 * Open Source License Information:
 *
 * OpenPBS is free software. You can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * OpenPBS 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 Affero General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Commercial License Information:
 *
 * PBS Pro is commercially licensed software that shares a common core with
 * the OpenPBS software.  For a copy of the commercial license terms and
 * conditions, go to: (http://www.pbspro.com/agreement.html) or contact the
 * Altair Legal Department.
 *
 * Altair's dual-license business model allows companies, individuals, and
 * organizations to create proprietary derivative works of OpenPBS and
 * distribute them - whether embedded or bundled with other software -
 * under a commercial license agreement.
 *
 * Use of Altair's trademarks, including but not limited to "PBS™",
 * "OpenPBS®", "PBS Professional®", and "PBS Pro™" and Altair's logos is
 * subject to Altair's trademark licensing policies.
 */

/**
 * @file    reply_send.c
 *
 * @brief
 * 		This file contains the routines used to send a reply to a client following
 * 		the processing of a request.  The following routines are provided here:
 *
 *	reply_send()  - the main routine, used by all reply senders
 *	reply_ack()   - send a basic no error acknowledgement
 *	req_reject()  - send a basic error return
 *	reply_text()  - send a return with a supplied text string
 *	reply_jobid() - used by several requests where the job id must be sent
 *	reply_free()  - free the substructure that might hang from a reply
 *	set_err_msg() - set a message relating to the error "code"
 *	dis_reply_write()	- reply is sent to a remote client
 *	reply_badattr()	- Create a reject (error) reply for a request including the name of the bad attribute/resource.
 *
 */

#include <pbs_config.h> /* the master config generated by configure */

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>
#include "libpbs.h"
#include "dis.h"
#include "log.h"
#include "pbs_error.h"
#include "server_limits.h"
#include "list_link.h"
#include "net_connect.h"
#include "attribute.h"
#include "credential.h"
#include "batch_request.h"
#include "work_task.h"
#include "pbs_nodes.h"
#include "svrfunc.h"
#include "tpp.h"

/* External Globals */

extern char *msg_daemonname;
extern char *msg_system;

#ifndef PBS_MOM
extern pbs_list_head task_list_event;
extern pbs_list_head task_list_immed;
extern char *resc_in_err;
#endif /* PBS_MOM */

#ifndef WIN32
extern volatile int reply_timedout; /* global to notify DIS routines reply took too long */
#endif
#define ERR_MSG_SIZE 256

/**
 * @brief
 * 		set a message relating to the error "code"
 *
 * @param code[in] - the specific error
 * @param msgbuf[out] - the buffer into which the message is placed
 * @param msglen[in] - length of above buffer
 */
static void
set_err_msg(int code, char *msgbuf, size_t msglen)
{
	char *msg = NULL;
	char *msg_tmp;

	/* subtract 1 from buffer length to insure room for null terminator */
	--msglen;

	/* see if there is an error message associated with the code */

	*msgbuf = '\0';
	if (code == PBSE_SYSTEM) {
		strncpy(msgbuf, msg_daemonname, msglen - 2);
		strcat(msgbuf, ": ");
		strncat(msgbuf, msg_system, msglen - strlen(msgbuf));
		msg_tmp = strerror(errno);

		if (msg_tmp)
			strncat(msgbuf, msg_tmp, msglen - strlen(msgbuf));
		else
			strncat(msgbuf, "Unknown error", msglen - strlen(msgbuf));

#ifndef PBS_MOM
	} else if ((
			   (code == PBSE_UNKRESC) ||
			   (code == PBSE_BADATVAL) ||
			   (code == PBSE_INVALSELECTRESC) ||
			   (code == PBSE_INVALJOBRESC) ||
			   (code == PBSE_INVALNODEPLACE) ||
			   (code == PBSE_DUPRESC) ||
			   (code == PBSE_INDIRECTHOP) ||
			   (code == PBSE_SAVE_ERR) ||
			   (code == PBSE_INDIRECTBT)) &&
		   (resc_in_err != NULL)) {
		strncpy(msgbuf, pbse_to_txt(code), msglen - 2);
		/* -2 is to make sure there is room for a colon and a space */
		strcat(msgbuf, ": ");
		strncat(msgbuf, resc_in_err, msglen - strlen(msgbuf));
		free(resc_in_err);
		resc_in_err = NULL;
		msg = NULL;
#endif
	} else if (code > PBSE_) {
		msg = pbse_to_txt(code);

	} else {
		msg = strerror(code);
	}

	if (msg) {
		(void) strncpy(msgbuf, msg, msglen);
	}
	msgbuf[msglen] = '\0';
}
#ifndef WIN32
/**
 * @brief
 * 		SIGALRM signal handler for dis_reply_write
 *
 * Set the volatile global variable reply_timedout
 * Record about the timeout in TCP reply.
 *
 * @param[in]	sig -  signal number
 *
 * @return	return void
 */
void
reply_alarm(int sig)
{
	reply_timedout = 1;
	log_event(PBSEVENT_DEBUG, PBS_EVENTCLASS_REQUEST, LOG_WARNING,
		  "dis_reply_write", "timeout attempting to send TCP reply");
}
#endif

/**
 * @brief
 * 		reply is to be sent to a remote client
 *
 * @param[in]	sfds - connection socket
 * @param[in]	preq - batch_request which contains the reply for the request
 *
 * @return	return code
 */
static int
dis_reply_write(int sfds, struct batch_request *preq)
{
	int rc;
	struct batch_reply *preply = &preq->rq_reply;
#ifndef WIN32
	struct sigaction act, oact;
	time_t old_tcp_timeout = pbs_tcp_timeout;
#endif

	if (preq->prot == PROT_TPP) {
		rc = encode_DIS_replyTPP(sfds, preq->tppcmd_msgid, preply);
	} else {
#ifndef WIN32
		reply_timedout = 0;
		/* set alarm to interrupt poll() etc. while flushing out data */
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		act.sa_handler = reply_alarm;
		if (sigaction(SIGALRM, &act, &oact) == -1)
			return (PBS_NET_RC_RETRY);
		alarm(PBS_DIS_TCP_TIMEOUT_REPLY);
		pbs_tcp_timeout = PBS_DIS_TCP_TIMEOUT_REPLY;
#endif
		/*
		 * clear pbs_tcp_errno - set on error in dis_flush when called
		 * either in encode_DIS_reply() or directly below.
		 */
		pbs_tcp_errno = 0;
		DIS_tcp_funcs(); /* setup for DIS over tcp */

		rc = encode_DIS_reply(sfds, preply);
	}

	if (rc == 0) {
		rc = dis_flush(sfds);
	}

#ifndef WIN32
	reply_timedout = 0; /* Resetting the value for next tcp connection */
	if (preq->prot == PROT_TCP) {
		alarm(0);
		(void) sigaction(SIGALRM, &oact, NULL); /* reset handler for SIGALRM */
	}
	pbs_tcp_timeout = old_tcp_timeout;
#endif
	if (rc) {
		char hn[PBS_MAXHOSTNAME + 1];

		if (get_connecthost(sfds, hn, PBS_MAXHOSTNAME) == -1)
			strcpy(hn, "??");
		(void) sprintf(log_buffer, "DIS reply failure, %d, to host %s, errno=%d", rc, hn, pbs_tcp_errno);
		/* if EAGAIN - then write was blocked and timed-out, note it */
		if (pbs_tcp_errno == EAGAIN)
			strcat(log_buffer, " write timed out");
		log_event(PBSEVENT_SYSTEM, PBS_EVENTCLASS_REQUEST, LOG_WARNING,
			  "dis_reply_write", log_buffer);
		close_client(sfds);
	}
	return rc;
}

int
reply_send_status_part(struct batch_request *preq)
{
	int rc = PBSE_SYSTEM;
	if (preq->rq_conn >= 0) {
		struct batch_reply *preply = &preq->rq_reply;
		preply->brp_is_part = 1;
		rc = dis_reply_write(preq->rq_conn, preq);
		if (rc != PBSE_NONE)
			return rc;
		reply_free(&preq->rq_reply);
		preply->brp_choice = BATCH_REPLY_CHOICE_Status;
		CLEAR_HEAD(preply->brp_un.brp_status);
		preply->brp_count = 0;
	}
	return rc;
}

/**
 * @brief
 * 		Send a reply to a batch request, reply either goes to a
 * 		remote client over the network:
 *		Encode the reply to a "presentation element",
 *		allocate the presenetation stream and attach to socket,
 *		write out reply, and free ps, pe, and isoreply structures.
 * 		Or the reply is for a request from the local server:
 *		locate the work task associated with the request and dispatch it
 *
 * @par Side-effects:
 *		The request (and reply) structures are freed.
 *
 * @param[in]	request	- batch request
 *
 * @return	error code
 * @retval	0	- success
 * @retval	!=0	- failure
 */
int
reply_send(struct batch_request *request)
{
#ifndef PBS_MOM
	struct work_task *ptask;
#endif /* PBS_MOM */
	int rc = 0;
	int sfds; /* socket */
	int rq_type;

	if (request == NULL)
		return 0;

	sfds = request->rq_conn;
	rq_type = request->rq_type;

	if (rq_type == PBS_BATCH_ModifyJob_Async || rq_type == PBS_BATCH_AsyrunJob) {
		free_br(request);
		return 0;
	}

	request->rq_reply.brp_is_part = 0;

	/* if this is a child request, just move the error to the parent */
	if (request->rq_parentbr) {
		if (((request->rq_parentbr->rq_reply.brp_choice == BATCH_REPLY_CHOICE_NULL) || (request->rq_parentbr->rq_reply.brp_choice == BATCH_REPLY_CHOICE_Delete)) && (request->rq_parentbr->rq_reply.brp_code == 0)) {
			request->rq_parentbr->rq_reply.brp_code = request->rq_reply.brp_code;
			request->rq_parentbr->rq_reply.brp_auxcode = request->rq_reply.brp_auxcode;
			if (request->rq_type == PBS_BATCH_DeleteJobList) {
				request->rq_parentbr->rq_reply.brp_count = request->rq_reply.brp_count;
				pbs_delstatfree(request->rq_parentbr->rq_reply.brp_un.brp_deletejoblist.brp_delstatc);
				request->rq_parentbr->rq_reply.brp_un.brp_deletejoblist.brp_delstatc = request->rq_reply.brp_un.brp_deletejoblist.brp_delstatc;
			}
			if (request->rq_reply.brp_choice == BATCH_REPLY_CHOICE_Text) {
				request->rq_parentbr->rq_reply.brp_choice =
					request->rq_reply.brp_choice;
				request->rq_parentbr->rq_reply.brp_un.brp_txt.brp_txtlen = request->rq_reply.brp_un.brp_txt.brp_txtlen;
				request->rq_parentbr->rq_reply.brp_un.brp_txt.brp_str =
					strdup(request->rq_reply.brp_un.brp_txt.brp_str);
				if (request->rq_parentbr->rq_reply.brp_un.brp_txt.brp_str == NULL) {
					log_err(-1, "reply_send", "Unable to allocate Memory!\n");
					return (PBSE_SYSTEM);
				}
			}
		}
	} else if (request->rq_refct > 0) {
		/* waiting on sister (subjob) requests, will send when */
		/* last one decrecments the reference count to zero    */
		return 0;
	} else if (sfds == PBS_LOCAL_CONNECTION) {

#ifndef PBS_MOM
		/*
		 * reply stays local, find work task and move it to
		 * the immediate list for dispatching.
		 *
		 * Special Note: In this instance the function that utimately
		 * gets dispatched by the work_task entry has the RESPONSIBILITY
		 * for freeing the batch_request structure.
		 */

		ptask = find_work_task(WORK_Deferred_Local, request, NULL);
		return convert_work_task(ptask, WORK_Immed);

		/* Uh Oh, should have found a task and didn't */

		log_err(-1, __func__, "did not find work task for local request");
#endif /* PBS_MOM */
		rc = PBSE_SYSTEM;

	} else if (sfds >= 0) {

		/*
		 * Otherwise, the reply is to be sent to a remote client
		 */
		if (rc == PBSE_NONE) {
			rc = dis_reply_write(sfds, request);
		}
	}

	free_br(request);
	return (rc);
}

/**
 * @brief
 * 		Send a normal acknowledgement reply to a request
 *
 * @param[in,out]	preq	- request structure
 *
 * @par Side-effects:
 *		Always frees the request structure.
 *
 */
void
reply_ack(struct batch_request *preq)
{
	int rq_type;

	if (preq == NULL)
		return;

	rq_type = preq->rq_type;
	if (rq_type == PBS_BATCH_ModifyJob_Async || rq_type == PBS_BATCH_AsyrunJob) {
		free_br(preq);
		return;
	}

	if (preq->prot == PROT_TPP && preq->tpp_ack == 0) {
		free_br(preq);
		return;
	}

	if (preq->rq_type != PBS_BATCH_DeleteJobList) {
		if (preq->rq_reply.brp_choice != BATCH_REPLY_CHOICE_NULL)
			/* in case another reply was being built up, clean it out */
			reply_free(&preq->rq_reply);
		preq->rq_reply.brp_choice = BATCH_REPLY_CHOICE_NULL;
	}

	preq->rq_reply.brp_code = PBSE_NONE;
	preq->rq_reply.brp_auxcode = 0;

	(void) reply_send(preq);
}

/**
 * @brief
 * 		Free any sub-structures that might hang from the basic
 * 		batch_reply structure, the reply structure itself IS NOT FREED.
 *
 * @param[in]	prep	- basic batch_reply structure
 */
void
reply_free(struct batch_reply *prep)
{
	struct brp_status *pstat;
	struct brp_status *pstatx;
	struct brp_select *psel;
	struct brp_select *pselx;
	struct batch_deljob_status *pdelstat;
	struct batch_deljob_status *pdelstatx;

	if (prep->brp_choice == BATCH_REPLY_CHOICE_Text) {
		if (prep->brp_un.brp_txt.brp_str) {
			(void) free(prep->brp_un.brp_txt.brp_str);
			prep->brp_un.brp_txt.brp_str = NULL;
			prep->brp_un.brp_txt.brp_txtlen = 0;
		}

	} else if (prep->brp_choice == BATCH_REPLY_CHOICE_Select) {
		psel = prep->brp_un.brp_select;
		while (psel) {
			pselx = psel->brp_next;
			(void) free(psel);
			psel = pselx;
		}

	} else if (prep->brp_choice == BATCH_REPLY_CHOICE_Status) {
		pstat = (struct brp_status *) GET_NEXT(prep->brp_un.brp_status);
		while (pstat) {
			pstatx = (struct brp_status *) GET_NEXT(pstat->brp_stlink);
			free_attrlist(&pstat->brp_attr);
			(void) free(pstat);
			pstat = pstatx;
		}

	} else if (prep->brp_choice == BATCH_REPLY_CHOICE_Delete) {
		pdelstat = prep->brp_un.brp_deletejoblist.brp_delstatc;
		while (pdelstat) {
			pdelstatx = pdelstat->next;
			if (pdelstat->name)
				free(pdelstat->name);
			free(pdelstat);
			pdelstat = pdelstatx;
		}

	} else if (prep->brp_choice == BATCH_REPLY_CHOICE_RescQuery) {
		(void) free(prep->brp_un.brp_rescq.brq_avail);
		(void) free(prep->brp_un.brp_rescq.brq_alloc);
		(void) free(prep->brp_un.brp_rescq.brq_resvd);
		(void) free(prep->brp_un.brp_rescq.brq_down);
	}
	prep->brp_choice = BATCH_REPLY_CHOICE_NULL;
}

/**
 * @brief
 * 		Create a reject (error) reply for a request, then send the reply.
 *
 * @param[in]	code	- PBS error code indicating the reason the rejection
 * 							is taking place.  If this is PBSE_NONE, no log message is output.
 * @param[in]	aux	- Auxiliary error code
 * @param[in,out]	preq	- Pointer to batch request
 *
 * @par Side-effects:
 *		Always frees the request structure.
 */
void
req_reject(int code, int aux, struct batch_request *preq)
{
	int evt_type;
	char msgbuf[ERR_MSG_SIZE];
	int rq_type;

	if (preq == NULL)
		return;

	rq_type = preq->rq_type;
	if (rq_type == PBS_BATCH_ModifyJob_Async || rq_type == PBS_BATCH_AsyrunJob) {
		free_br(preq);
		return;
	}

	if (code != PBSE_NONE) {
		evt_type = PBSEVENT_DEBUG;
		if (code == PBSE_BADHOST)
			evt_type |= PBSEVENT_SECURITY;
		sprintf(log_buffer,
			"Reject reply code=%d, aux=%d, type=%d, from %s@%s",
			code, aux, preq->rq_type, preq->rq_user, preq->rq_host);
		log_event(evt_type, PBS_EVENTCLASS_REQUEST, LOG_INFO,
			  "req_reject", log_buffer);
	}
	set_err_msg(code, msgbuf, ERR_MSG_SIZE);

	if (preq->rq_type != PBS_BATCH_DeleteJobList) {
		if (preq->rq_reply.brp_choice != BATCH_REPLY_CHOICE_NULL) {
			/* in case another reply was being built up, clean it out */
			reply_free(&preq->rq_reply);
		}

		if (*msgbuf != '\0') {
			preq->rq_reply.brp_choice = BATCH_REPLY_CHOICE_Text;
			if ((preq->rq_reply.brp_un.brp_txt.brp_str = strdup(msgbuf)) == NULL) {
				log_err(-1, "req_reject", "Unable to allocate Memory!\n");
				return;
			}
			preq->rq_reply.brp_un.brp_txt.brp_txtlen = strlen(msgbuf);
		} else {
			preq->rq_reply.brp_choice = BATCH_REPLY_CHOICE_NULL;
		}
	}

	preq->rq_reply.brp_code = code;
	preq->rq_reply.brp_auxcode = aux;

	(void) reply_send(preq);
}

/**
 * @brief
 * 		Create a reject (error) reply for a request including
 * 		the name of the bad attribute/resource.
 *
 * @param[in]	code	PBS error code indicating the reason the rejection
 * 			is taking place.  If this is PBSE_NONE, no log message is output.
 * @param[in]	aux	Auxiliary error code
 * @param[in,out] pal	external form of attributes.
 * @param[in]	preq	Pointer to batch request
 */
void
reply_badattr(int code, int aux, svrattrl *pal,
	      struct batch_request *preq)
{
	int i = 1;
	size_t len;
	char msgbuf[ERR_MSG_SIZE];

	if (preq == NULL)
		return;

	if (preq->rq_type == PBS_BATCH_ModifyJob_Async) {
		free_br(preq);
		return;
	}

#ifdef NAS /* localmod 005 */
	set_err_msg(code, msgbuf, sizeof(msgbuf));
#else
	set_err_msg(code, msgbuf, ERR_MSG_SIZE);
#endif /* localmod 005 */

	while (pal) {
		if (i == aux) {
			/* append attribute info only if it fits in msgbuf */
			/* add one for space between msg and attribute name */
			len = strlen(msgbuf) + 1 + strlen(pal->al_name);
			if (pal->al_resc)
				/* add one for dot between attribute and resource */
				len += 1 + strlen(pal->al_resc);

#ifdef NAS /* localmod 005 */
			if (len < sizeof(msgbuf)) {
#else
			if (len < ERR_MSG_SIZE) {
#endif /* localmod 005 */
				(void) strcat(msgbuf, " ");
				(void) strcat(msgbuf, pal->al_name);
				if (pal->al_resc) {
					(void) strcat(msgbuf, ".");
					(void) strcat(msgbuf, pal->al_resc);
				}
			}
			break;
		}
		pal = (svrattrl *) GET_NEXT(pal->al_link);
		++i;
	}

	(void) reply_text(preq, code, msgbuf);
}

/**
 * @brief
 * 		Return a reply with a supplied text string.
 *
 * @param[out]	preq	- Pointer to batch request
 * @param[in]	code	- PBS error code indicating the reason the rejection
 * 							is taking place.  If this is PBSE_NONE, no log message is output.
 * @param[in]	text	- text string for the reply.
 *
 * @return	error code
 */
int
reply_text(struct batch_request *preq, int code, char *text)
{
	if (preq == NULL)
		return 0;

	if (preq->rq_type == PBS_BATCH_ModifyJob_Async) {
		free_br(preq);
		return 0;
	}

	if (preq->rq_reply.brp_choice != BATCH_REPLY_CHOICE_NULL)
		/* in case another reply was being built up, clean it out */
		reply_free(&preq->rq_reply);

	preq->rq_reply.brp_code = code;
	preq->rq_reply.brp_auxcode = 0;
	if (text && *text) {
		preq->rq_reply.brp_choice = BATCH_REPLY_CHOICE_Text;
		if ((preq->rq_reply.brp_un.brp_txt.brp_str = strdup(text)) == NULL)
			return 0;
		preq->rq_reply.brp_un.brp_txt.brp_txtlen = strlen(text);
	} else {
		preq->rq_reply.brp_choice = BATCH_REPLY_CHOICE_NULL;
	}
	return reply_send(preq);
}

/**
 * @brief
 * 		Return a reply with the job id.
 *
 * @see req_queuejob()
 * @see req_commit()
 *
 * @par Side-effects:
 *		Always frees the request structure.
 *
 * @param[out]	preq	- Pointer to batch request
 * @param[in]	jobid	- job id.
 * @param[in]	which	- the union discriminator
 */
int
reply_jobid(struct batch_request *preq, char *jobid, int which)
{
	if (preq->rq_reply.brp_choice != BATCH_REPLY_CHOICE_NULL)
		/* in case another reply was being built up, clean it out */
		reply_free(&preq->rq_reply);

	preq->rq_reply.brp_code = 0;
	preq->rq_reply.brp_auxcode = 0;
	preq->rq_reply.brp_choice = which;
	(void) strncpy(preq->rq_reply.brp_un.brp_jid, jobid, PBS_MAXSVRJOBID);
	return (reply_send(preq));
}
