/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program 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, version 3.
**
** 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 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 <https://www.gnu.org/licenses/>.
**/

#include "ntp.h"
#include "../sysinfo.h"

#include "zbxcomms.h"
#include "zbxtime.h"

#define NTP_SCALE		4294967296.0	/* 2^32, of course! */

#define NTP_PACKET_SIZE		48		/* without authentication */
#define NTP_OFFSET_ORIGINATE	24		/* offset of originate timestamp */
#define NTP_OFFSET_TRANSMIT	40		/* offset of transmit timestamp */

#define NTP_VERSION		3		/* the current version */

#define NTP_MODE_CLIENT		3		/* NTP client request */
#define NTP_MODE_SERVER		4		/* NTP server response */

typedef struct
{
	unsigned char	version;
	unsigned char	mode;
	double		transmit;
}
ntp_data;

static void	make_packet(ntp_data *data)
{
	data->version = NTP_VERSION;
	data->mode = NTP_MODE_CLIENT;
	data->transmit = zbx_current_time();
}

static void	pack_ntp(const ntp_data *data, unsigned char *request, int length)
{
	/* Pack the essential data into an NTP packet, bypassing struct layout  */
	/* and endian problems. Note that it ignores fields irrelevant to SNTP. */

	memset(request, 0, length);

	request[0] = (data->version << 3) | data->mode;

	double	d = data->transmit / NTP_SCALE;

	for (int i = 0; i < 8; i++)
	{
		int	k = (int)(d *= 256.0);

		if (k >= 256)
			k = 255;

		request[NTP_OFFSET_TRANSMIT + i] = k;

		d -= k;
	}
}

static int	unpack_ntp(ntp_data *data, const unsigned char *request, const unsigned char *response, int length)
{
	/* Unpack the essential data from an NTP packet, bypassing struct layout */
	/* and endian problems. Note that it ignores fields irrelevant to SNTP.  */

	int	ret = FAIL;
	double	d;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__);

	if (NTP_PACKET_SIZE != length)
	{
		zabbix_log(LOG_LEVEL_DEBUG, "invalid response size: %d", length);
		goto out;
	}

	if (0 != memcmp(response + NTP_OFFSET_ORIGINATE, request + NTP_OFFSET_TRANSMIT, 8))
	{
		zabbix_log(LOG_LEVEL_DEBUG, "originate timestamp in the response does not match"
				" transmit timestamp in the request: 0x%04x%04x 0x%04x%04x",
				*(const unsigned int *)&response[NTP_OFFSET_ORIGINATE],
				*(const unsigned int *)&response[NTP_OFFSET_ORIGINATE + 4],
				*(const unsigned int *)&request[NTP_OFFSET_TRANSMIT],
				*(const unsigned int *)&request[NTP_OFFSET_TRANSMIT + 4]);
		goto out;
	}

	data->version = (response[0] >> 3) & 7;

	if (NTP_VERSION != data->version)
	{
		zabbix_log(LOG_LEVEL_DEBUG, "invalid NTP version in the response: %d", (int)data->version);
		goto out;
	}

	data->mode = response[0] & 7;

	if (NTP_MODE_SERVER != data->mode)
	{
		zabbix_log(LOG_LEVEL_DEBUG, "invalid mode in the response: %d", (int)data->mode);
		goto out;
	}

	if (15 < response[1])
	{
		zabbix_log(LOG_LEVEL_DEBUG, "invalid stratum in the response: %d", (int)response[1]);
		goto out;
	}

	d = 0.0;
	for (int i = 0; i < 8; i++)
		d = 256.0 * d + response[NTP_OFFSET_TRANSMIT + i];
	data->transmit = d / NTP_SCALE;

	if (0 == data->transmit)
	{
		zabbix_log(LOG_LEVEL_DEBUG, "invalid transmit timestamp in the response: " ZBX_FS_DBL, data->transmit);
		goto out;
	}

	ret = SUCCEED;
out:
	zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret));

	return ret;
}

int	check_ntp(char *host, unsigned short port, int timeout, int *value_int)
{
	zbx_socket_t	s;
	int		ret;
	char		request[NTP_PACKET_SIZE];
	ntp_data	data;

	*value_int = 0;

	if (SUCCEED == (ret = zbx_udp_connect(&s, sysinfo_get_config_source_ip(), host, port, timeout)))
	{
		make_packet(&data);

		pack_ntp(&data, (unsigned char *)request, sizeof(request));

		if (SUCCEED == (ret = zbx_udp_send(&s, request, sizeof(request), timeout)))
		{
			if (SUCCEED == (ret = zbx_udp_recv(&s, timeout)))
			{
				*value_int = (SUCCEED == unpack_ntp(&data, (unsigned char *)request,
						(unsigned char *)s.buffer, (int)s.read_bytes));
			}
		}

		zbx_udp_close(&s);
	}

	if (FAIL == ret)
		zabbix_log(LOG_LEVEL_DEBUG, "NTP check error: %s", zbx_socket_strerror());

	return SYSINFO_RET_OK;
}