/*-------------------------------------------------------------------------
 *
 * fe-cancel.c
 *	  functions related to query cancellation
 *
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/interfaces/libpq/fe-cancel.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres_fe.h"

#include <unistd.h>

#include "libpq-fe.h"
#include "libpq-int.h"
#include "port/pg_bswap.h"


/*
 * pg_cancel_conn (backing struct for PGcancelConn) is a wrapper around a
 * PGconn to send cancellations using PQcancelBlocking and PQcancelStart.
 * This isn't just a typedef because we want the compiler to complain when a
 * PGconn is passed to a function that expects a PGcancelConn, and vice versa.
 */
struct pg_cancel_conn
{
	PGconn		conn;
};

/*
 * pg_cancel (backing struct for PGcancel) stores all data necessary to send a
 * cancel request.
 */
struct pg_cancel
{
	SockAddr	raddr;			/* Remote address */
	int			be_pid;			/* PID of to-be-canceled backend */
	int			pgtcp_user_timeout; /* tcp user timeout */
	int			keepalives;		/* use TCP keepalives? */
	int			keepalives_idle;	/* time between TCP keepalives */
	int			keepalives_interval;	/* time between TCP keepalive
										 * retransmits */
	int			keepalives_count;	/* maximum number of TCP keepalive
									 * retransmits */

	/* Pre-constructed cancel request packet starts here */
	int32		cancel_pkt_len; /* in network byte order */
	char		cancel_req[FLEXIBLE_ARRAY_MEMBER];	/* CancelRequestPacket */
};


/*
 *		PQcancelCreate
 *
 * Create and return a PGcancelConn, which can be used to securely cancel a
 * query on the given connection.
 *
 * This requires either following the non-blocking flow through
 * PQcancelStart() and PQcancelPoll(), or the blocking PQcancelBlocking().
 */
PGcancelConn *
PQcancelCreate(PGconn *conn)
{
	PGconn	   *cancelConn = pqMakeEmptyPGconn();
	pg_conn_host originalHost;

	if (cancelConn == NULL)
		return NULL;

	/* Check we have an open connection */
	if (!conn)
	{
		libpq_append_conn_error(cancelConn, "connection pointer is NULL");
		return (PGcancelConn *) cancelConn;
	}

	if (conn->sock == PGINVALID_SOCKET)
	{
		libpq_append_conn_error(cancelConn, "connection not open");
		return (PGcancelConn *) cancelConn;
	}

	/* Check that we have received a cancellation key */
	if (conn->be_cancel_key_len == 0)
	{
		libpq_append_conn_error(cancelConn, "no cancellation key received");
		return (PGcancelConn *) cancelConn;
	}

	/*
	 * Indicate that this connection is used to send a cancellation
	 */
	cancelConn->cancelRequest = true;

	if (!pqCopyPGconn(conn, cancelConn))
		return (PGcancelConn *) cancelConn;

	/*
	 * Compute derived options
	 */
	if (!pqConnectOptions2(cancelConn))
		return (PGcancelConn *) cancelConn;

	/*
	 * Copy cancellation token data from the original connection
	 */
	cancelConn->be_pid = conn->be_pid;
	if (conn->be_cancel_key != NULL)
	{
		cancelConn->be_cancel_key = malloc(conn->be_cancel_key_len);
		if (cancelConn->be_cancel_key == NULL)
			goto oom_error;
		memcpy(cancelConn->be_cancel_key, conn->be_cancel_key, conn->be_cancel_key_len);
	}
	cancelConn->be_cancel_key_len = conn->be_cancel_key_len;
	cancelConn->pversion = conn->pversion;

	/*
	 * Cancel requests should not iterate over all possible hosts. The request
	 * needs to be sent to the exact host and address that the original
	 * connection used. So we manually create the host and address arrays with
	 * a single element after freeing the host array that we generated from
	 * the connection options.
	 */
	pqReleaseConnHosts(cancelConn);
	cancelConn->nconnhost = 1;
	cancelConn->naddr = 1;

	cancelConn->connhost = calloc(cancelConn->nconnhost, sizeof(pg_conn_host));
	if (!cancelConn->connhost)
		goto oom_error;

	originalHost = conn->connhost[conn->whichhost];
	if (originalHost.host)
	{
		cancelConn->connhost[0].host = strdup(originalHost.host);
		if (!cancelConn->connhost[0].host)
			goto oom_error;
	}
	if (originalHost.hostaddr)
	{
		cancelConn->connhost[0].hostaddr = strdup(originalHost.hostaddr);
		if (!cancelConn->connhost[0].hostaddr)
			goto oom_error;
	}
	if (originalHost.port)
	{
		cancelConn->connhost[0].port = strdup(originalHost.port);
		if (!cancelConn->connhost[0].port)
			goto oom_error;
	}
	if (originalHost.password)
	{
		cancelConn->connhost[0].password = strdup(originalHost.password);
		if (!cancelConn->connhost[0].password)
			goto oom_error;
	}

	cancelConn->addr = calloc(cancelConn->naddr, sizeof(AddrInfo));
	if (!cancelConn->addr)
		goto oom_error;

	cancelConn->addr[0].addr = conn->raddr;
	cancelConn->addr[0].family = conn->raddr.addr.ss_family;

	cancelConn->status = CONNECTION_ALLOCATED;
	return (PGcancelConn *) cancelConn;

oom_error:
	cancelConn->status = CONNECTION_BAD;
	libpq_append_conn_error(cancelConn, "out of memory");
	return (PGcancelConn *) cancelConn;
}


/*
 *		PQcancelBlocking
 *
 * Send a cancellation request in a blocking fashion.
 * Returns 1 if successful 0 if not.
 */
int
PQcancelBlocking(PGcancelConn *cancelConn)
{
	if (!PQcancelStart(cancelConn))
		return 0;
	return pqConnectDBComplete(&cancelConn->conn);
}

/*
 *		PQcancelStart
 *
 * Starts sending a cancellation request in a non-blocking fashion. Returns
 * 1 if successful 0 if not.
 */
int
PQcancelStart(PGcancelConn *cancelConn)
{
	if (!cancelConn || cancelConn->conn.status == CONNECTION_BAD)
		return 0;

	if (cancelConn->conn.status != CONNECTION_ALLOCATED)
	{
		libpq_append_conn_error(&cancelConn->conn,
								"cancel request is already being sent on this connection");
		cancelConn->conn.status = CONNECTION_BAD;
		return 0;
	}

	return pqConnectDBStart(&cancelConn->conn);
}

/*
 *		PQcancelPoll
 *
 * Poll a cancel connection. For usage details see PQconnectPoll.
 */
PostgresPollingStatusType
PQcancelPoll(PGcancelConn *cancelConn)
{
	PGconn	   *conn = &cancelConn->conn;
	int			n;

	/*
	 * We leave most of the connection establishment to PQconnectPoll, since
	 * it's very similar to normal connection establishment. But once we get
	 * to the CONNECTION_AWAITING_RESPONSE we need to start doing our own
	 * thing.
	 */
	if (conn->status != CONNECTION_AWAITING_RESPONSE)
	{
		return PQconnectPoll(conn);
	}

	/*
	 * At this point we are waiting on the server to close the connection,
	 * which is its way of communicating that the cancel has been handled.
	 */

	n = pqReadData(conn);

	if (n == 0)
		return PGRES_POLLING_READING;

#ifndef WIN32

	/*
	 * If we receive an error report it, but only if errno is non-zero.
	 * Otherwise we assume it's an EOF, which is what we expect from the
	 * server.
	 *
	 * We skip this for Windows, because Windows is a bit special in its EOF
	 * behaviour for TCP. Sometimes it will error with an ECONNRESET when
	 * there is a clean connection closure. See these threads for details:
	 * https://www.postgresql.org/message-id/flat/90b34057-4176-7bb0-0dbb-9822a5f6425b%40greiz-reinsdorf.de
	 *
	 * https://www.postgresql.org/message-id/flat/CA%2BhUKG%2BOeoETZQ%3DQw5Ub5h3tmwQhBmDA%3DnuNO3KG%3DzWfUypFAw%40mail.gmail.com
	 *
	 * PQcancel ignores such errors and reports success for the cancellation
	 * anyway, so even if this is not always correct we do the same here.
	 */
	if (n < 0 && errno != 0)
	{
		conn->status = CONNECTION_BAD;
		return PGRES_POLLING_FAILED;
	}
#endif

	/*
	 * We don't expect any data, only connection closure. So if we strangely
	 * do receive some data we consider that an error.
	 */
	if (n > 0)
	{
		libpq_append_conn_error(conn, "unexpected response from server");
		conn->status = CONNECTION_BAD;
		return PGRES_POLLING_FAILED;
	}

	/*
	 * Getting here means that we received an EOF, which is what we were
	 * expecting -- the cancel request has completed.
	 */
	cancelConn->conn.status = CONNECTION_OK;
	resetPQExpBuffer(&conn->errorMessage);
	return PGRES_POLLING_OK;
}

/*
 *		PQcancelStatus
 *
 * Get the status of a cancel connection.
 */
ConnStatusType
PQcancelStatus(const PGcancelConn *cancelConn)
{
	return PQstatus(&cancelConn->conn);
}

/*
 *		PQcancelSocket
 *
 * Get the socket of the cancel connection.
 */
int
PQcancelSocket(const PGcancelConn *cancelConn)
{
	return PQsocket(&cancelConn->conn);
}

/*
 *		PQcancelErrorMessage
 *
 * Returns the error message most recently generated by an operation on the
 * cancel connection.
 */
char *
PQcancelErrorMessage(const PGcancelConn *cancelConn)
{
	return PQerrorMessage(&cancelConn->conn);
}

/*
 *		PQcancelReset
 *
 * Resets the cancel connection, so it can be reused to send a new cancel
 * request.
 */
void
PQcancelReset(PGcancelConn *cancelConn)
{
	pqClosePGconn(&cancelConn->conn);
	cancelConn->conn.status = CONNECTION_ALLOCATED;
	cancelConn->conn.whichhost = 0;
	cancelConn->conn.whichaddr = 0;
	cancelConn->conn.try_next_host = false;
	cancelConn->conn.try_next_addr = false;
}

/*
 *		PQcancelFinish
 *
 * Closes and frees the cancel connection.
 */
void
PQcancelFinish(PGcancelConn *cancelConn)
{
	PQfinish(&cancelConn->conn);
}

/*
 * PQgetCancel: get a PGcancel structure corresponding to a connection.
 *
 * A copy is needed to be able to cancel a running query from a different
 * thread. If the same structure is used all structure members would have
 * to be individually locked (if the entire structure was locked, it would
 * be impossible to cancel a synchronous query because the structure would
 * have to stay locked for the duration of the query).
 */
PGcancel *
PQgetCancel(PGconn *conn)
{
	PGcancel   *cancel;
	int			cancel_req_len;
	CancelRequestPacket *req;

	if (!conn)
		return NULL;

	if (conn->sock == PGINVALID_SOCKET)
		return NULL;

	/* Check that we have received a cancellation key */
	if (conn->be_cancel_key_len == 0)
		return NULL;

	cancel_req_len = offsetof(CancelRequestPacket, cancelAuthCode) + conn->be_cancel_key_len;
	cancel = malloc(offsetof(PGcancel, cancel_req) + cancel_req_len);
	if (cancel == NULL)
		return NULL;

	memcpy(&cancel->raddr, &conn->raddr, sizeof(SockAddr));

	/* We use -1 to indicate an unset connection option */
	cancel->pgtcp_user_timeout = -1;
	cancel->keepalives = -1;
	cancel->keepalives_idle = -1;
	cancel->keepalives_interval = -1;
	cancel->keepalives_count = -1;
	if (conn->pgtcp_user_timeout != NULL)
	{
		if (!pqParseIntParam(conn->pgtcp_user_timeout,
							 &cancel->pgtcp_user_timeout,
							 conn, "tcp_user_timeout"))
			goto fail;
	}
	if (conn->keepalives != NULL)
	{
		if (!pqParseIntParam(conn->keepalives,
							 &cancel->keepalives,
							 conn, "keepalives"))
			goto fail;
	}
	if (conn->keepalives_idle != NULL)
	{
		if (!pqParseIntParam(conn->keepalives_idle,
							 &cancel->keepalives_idle,
							 conn, "keepalives_idle"))
			goto fail;
	}
	if (conn->keepalives_interval != NULL)
	{
		if (!pqParseIntParam(conn->keepalives_interval,
							 &cancel->keepalives_interval,
							 conn, "keepalives_interval"))
			goto fail;
	}
	if (conn->keepalives_count != NULL)
	{
		if (!pqParseIntParam(conn->keepalives_count,
							 &cancel->keepalives_count,
							 conn, "keepalives_count"))
			goto fail;
	}

	req = (CancelRequestPacket *) &cancel->cancel_req;
	req->cancelRequestCode = (MsgType) pg_hton32(CANCEL_REQUEST_CODE);
	req->backendPID = pg_hton32(conn->be_pid);
	memcpy(req->cancelAuthCode, conn->be_cancel_key, conn->be_cancel_key_len);
	/* include the length field itself in the length */
	cancel->cancel_pkt_len = pg_hton32(cancel_req_len + 4);

	return cancel;

fail:
	free(cancel);
	return NULL;
}

/*
 * PQsendCancelRequest
 *	 Submit a CancelRequest message, but don't wait for it to finish
 *
 * Returns: 1 if successfully submitted
 *			0 if error (conn->errorMessage is set)
 */
int
PQsendCancelRequest(PGconn *cancelConn)
{
	CancelRequestPacket req;

	/* Start the message. */
	if (pqPutMsgStart(0, cancelConn))
		return STATUS_ERROR;

	/* Send the message body. */
	memset(&req, 0, offsetof(CancelRequestPacket, cancelAuthCode));
	req.cancelRequestCode = (MsgType) pg_hton32(CANCEL_REQUEST_CODE);
	req.backendPID = pg_hton32(cancelConn->be_pid);
	if (pqPutnchar(&req, offsetof(CancelRequestPacket, cancelAuthCode), cancelConn))
		return STATUS_ERROR;
	if (pqPutnchar(cancelConn->be_cancel_key, cancelConn->be_cancel_key_len, cancelConn))
		return STATUS_ERROR;

	/* Finish the message. */
	if (pqPutMsgEnd(cancelConn))
		return STATUS_ERROR;

	/* Flush to ensure backend gets it. */
	if (pqFlush(cancelConn))
		return STATUS_ERROR;

	return STATUS_OK;
}

/* PQfreeCancel: free a cancel structure */
void
PQfreeCancel(PGcancel *cancel)
{
	free(cancel);
}


/*
 * Sets an integer socket option on a TCP socket, if the provided value is
 * not negative.  Returns false if setsockopt fails for some reason.
 *
 * CAUTION: This needs to be signal safe, since it's used by PQcancel.
 */
#if defined(TCP_USER_TIMEOUT) || !defined(WIN32)
static bool
optional_setsockopt(int fd, int protoid, int optid, int value)
{
	if (value < 0)
		return true;
	if (setsockopt(fd, protoid, optid, (char *) &value, sizeof(value)) < 0)
		return false;
	return true;
}
#endif


/*
 * PQcancel: old, non-encrypted, but signal-safe way of requesting query cancel
 *
 * The return value is true if the cancel request was successfully
 * dispatched, false if not (in which case an error message is available).
 * Note: successful dispatch is no guarantee that there will be any effect at
 * the backend.  The application must read the operation result as usual.
 *
 * On failure, an error message is stored in *errbuf, which must be of size
 * errbufsize (recommended size is 256 bytes).  *errbuf is not changed on
 * success return.
 *
 * CAUTION: we want this routine to be safely callable from a signal handler
 * (for example, an application might want to call it in a SIGINT handler).
 * This means we cannot use any C library routine that might be non-reentrant.
 * malloc/free are often non-reentrant, and anything that might call them is
 * just as dangerous.  We avoid sprintf here for that reason.  Building up
 * error messages with strcpy/strcat is tedious but should be quite safe.
 * We also save/restore errno in case the signal handler support doesn't.
 */
int
PQcancel(PGcancel *cancel, char *errbuf, int errbufsize)
{
	int			save_errno = SOCK_ERRNO;
	pgsocket	tmpsock = PGINVALID_SOCKET;
	int			maxlen;
	char		recvbuf;
	int			cancel_pkt_len;

	if (!cancel)
	{
		strlcpy(errbuf, "PQcancel() -- no cancel object supplied", errbufsize);
		/* strlcpy probably doesn't change errno, but be paranoid */
		SOCK_ERRNO_SET(save_errno);
		return false;
	}

	/*
	 * We need to open a temporary connection to the postmaster. Do this with
	 * only kernel calls.
	 */
	if ((tmpsock = socket(cancel->raddr.addr.ss_family, SOCK_STREAM, 0)) == PGINVALID_SOCKET)
	{
		strlcpy(errbuf, "PQcancel() -- socket() failed: ", errbufsize);
		goto cancel_errReturn;
	}

	/*
	 * Since this connection will only be used to send a single packet of
	 * data, we don't need NODELAY.  We also don't set the socket to
	 * nonblocking mode, because the API definition of PQcancel requires the
	 * cancel to be sent in a blocking way.
	 *
	 * We do set socket options related to keepalives and other TCP timeouts.
	 * This ensures that this function does not block indefinitely when
	 * reasonable keepalive and timeout settings have been provided.
	 */
	if (cancel->raddr.addr.ss_family != AF_UNIX &&
		cancel->keepalives != 0)
	{
#ifndef WIN32
		if (!optional_setsockopt(tmpsock, SOL_SOCKET, SO_KEEPALIVE, 1))
		{
			strlcpy(errbuf, "PQcancel() -- setsockopt(SO_KEEPALIVE) failed: ", errbufsize);
			goto cancel_errReturn;
		}

#ifdef PG_TCP_KEEPALIVE_IDLE
		if (!optional_setsockopt(tmpsock, IPPROTO_TCP, PG_TCP_KEEPALIVE_IDLE,
								 cancel->keepalives_idle))
		{
			strlcpy(errbuf, "PQcancel() -- setsockopt(" PG_TCP_KEEPALIVE_IDLE_STR ") failed: ", errbufsize);
			goto cancel_errReturn;
		}
#endif

#ifdef TCP_KEEPINTVL
		if (!optional_setsockopt(tmpsock, IPPROTO_TCP, TCP_KEEPINTVL,
								 cancel->keepalives_interval))
		{
			strlcpy(errbuf, "PQcancel() -- setsockopt(TCP_KEEPINTVL) failed: ", errbufsize);
			goto cancel_errReturn;
		}
#endif

#ifdef TCP_KEEPCNT
		if (!optional_setsockopt(tmpsock, IPPROTO_TCP, TCP_KEEPCNT,
								 cancel->keepalives_count))
		{
			strlcpy(errbuf, "PQcancel() -- setsockopt(TCP_KEEPCNT) failed: ", errbufsize);
			goto cancel_errReturn;
		}
#endif

#else							/* WIN32 */

#ifdef SIO_KEEPALIVE_VALS
		if (!pqSetKeepalivesWin32(tmpsock,
								  cancel->keepalives_idle,
								  cancel->keepalives_interval))
		{
			strlcpy(errbuf, "PQcancel() -- WSAIoctl(SIO_KEEPALIVE_VALS) failed: ", errbufsize);
			goto cancel_errReturn;
		}
#endif							/* SIO_KEEPALIVE_VALS */
#endif							/* WIN32 */

		/* TCP_USER_TIMEOUT works the same way on Unix and Windows */
#ifdef TCP_USER_TIMEOUT
		if (!optional_setsockopt(tmpsock, IPPROTO_TCP, TCP_USER_TIMEOUT,
								 cancel->pgtcp_user_timeout))
		{
			strlcpy(errbuf, "PQcancel() -- setsockopt(TCP_USER_TIMEOUT) failed: ", errbufsize);
			goto cancel_errReturn;
		}
#endif
	}

retry3:
	if (connect(tmpsock, (struct sockaddr *) &cancel->raddr.addr,
				cancel->raddr.salen) < 0)
	{
		if (SOCK_ERRNO == EINTR)
			/* Interrupted system call - we'll just try again */
			goto retry3;
		strlcpy(errbuf, "PQcancel() -- connect() failed: ", errbufsize);
		goto cancel_errReturn;
	}

	cancel_pkt_len = pg_ntoh32(cancel->cancel_pkt_len);

retry4:

	/*
	 * Send the cancel request packet. It starts with the message length at
	 * cancel_pkt_len, followed by the actual packet.
	 */
	if (send(tmpsock, (char *) &cancel->cancel_pkt_len, cancel_pkt_len, 0) != cancel_pkt_len)
	{
		if (SOCK_ERRNO == EINTR)
			/* Interrupted system call - we'll just try again */
			goto retry4;
		strlcpy(errbuf, "PQcancel() -- send() failed: ", errbufsize);
		goto cancel_errReturn;
	}

	/*
	 * Wait for the postmaster to close the connection, which indicates that
	 * it's processed the request.  Without this delay, we might issue another
	 * command only to find that our cancel zaps that command instead of the
	 * one we thought we were canceling.  Note we don't actually expect this
	 * read to obtain any data, we are just waiting for EOF to be signaled.
	 */
retry5:
	if (recv(tmpsock, &recvbuf, 1, 0) < 0)
	{
		if (SOCK_ERRNO == EINTR)
			/* Interrupted system call - we'll just try again */
			goto retry5;
		/* we ignore other error conditions */
	}

	/* All done */
	closesocket(tmpsock);
	SOCK_ERRNO_SET(save_errno);
	return true;

cancel_errReturn:

	/*
	 * Make sure we don't overflow the error buffer. Leave space for the \n at
	 * the end, and for the terminating zero.
	 */
	maxlen = errbufsize - strlen(errbuf) - 2;
	if (maxlen >= 0)
	{
		/*
		 * We can't invoke strerror here, since it's not signal-safe.  Settle
		 * for printing the decimal value of errno.  Even that has to be done
		 * the hard way.
		 */
		int			val = SOCK_ERRNO;
		char		buf[32];
		char	   *bufp;

		bufp = buf + sizeof(buf) - 1;
		*bufp = '\0';
		do
		{
			*(--bufp) = (val % 10) + '0';
			val /= 10;
		} while (val > 0);
		bufp -= 6;
		memcpy(bufp, "error ", 6);
		strncat(errbuf, bufp, maxlen);
		strcat(errbuf, "\n");
	}
	if (tmpsock != PGINVALID_SOCKET)
		closesocket(tmpsock);
	SOCK_ERRNO_SET(save_errno);
	return false;
}

/*
 * PQrequestCancel: old, not thread-safe function for requesting query cancel
 *
 * Returns true if able to send the cancel request, false if not.
 *
 * On failure, the error message is saved in conn->errorMessage; this means
 * that this can't be used when there might be other active operations on
 * the connection object.
 *
 * NOTE: error messages will be cut off at the current size of the
 * error message buffer, since we dare not try to expand conn->errorMessage!
 */
int
PQrequestCancel(PGconn *conn)
{
	int			r;
	PGcancel   *cancel;

	/* Check we have an open connection */
	if (!conn)
		return false;

	if (conn->sock == PGINVALID_SOCKET)
	{
		strlcpy(conn->errorMessage.data,
				"PQrequestCancel() -- connection is not open\n",
				conn->errorMessage.maxlen);
		conn->errorMessage.len = strlen(conn->errorMessage.data);
		conn->errorReported = 0;

		return false;
	}

	cancel = PQgetCancel(conn);
	if (cancel)
	{
		r = PQcancel(cancel, conn->errorMessage.data,
					 conn->errorMessage.maxlen);
		PQfreeCancel(cancel);
	}
	else
	{
		strlcpy(conn->errorMessage.data, "out of memory",
				conn->errorMessage.maxlen);
		r = false;
	}

	if (!r)
	{
		conn->errorMessage.len = strlen(conn->errorMessage.data);
		conn->errorReported = 0;
	}

	return r;
}
