/*
 * DhcpClient.cpp
 *
 *  Created on: Mar 25, 2023
 *      Author: ZKSWE Develop Team
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <poll.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <linux/udp.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include "DhcpClient.h"
#include "utils/Log.h"

namespace net {

#define PORT_BOOTP_SERVER 67
#define PORT_BOOTP_CLIENT 68

#define OP_BOOTREQUEST 1
#define OP_BOOTREPLY   2

#define FLAGS_BROADCAST 0x8000

#define HTYPE_ETHER    1

#define DHCP_MSG_FIXED_SIZE 236

/* first four bytes of options are a cookie to indicate that
** the payload are DHCP options as opposed to some other BOOTP
** extension.
*/
#define OPT_COOKIE1          0x63
#define OPT_COOKIE2          0x82
#define OPT_COOKIE3          0x53
#define OPT_COOKIE4          0x63

/* BOOTP/DHCP options - see RFC 2132 */
#define OPT_PAD              0

#define OPT_SUBNET_MASK      1     /* 4 <ipaddr> */
#define OPT_TIME_OFFSET      2     /* 4 <seconds> */
#define OPT_GATEWAY          3     /* 4*n <ipaddr> * n */
#define OPT_DNS              6     /* 4*n <ipaddr> * n */
#define OPT_HOST_NAME        12
#define OPT_DOMAIN_NAME      15    /* n <domainnamestring> */
#define OPT_BROADCAST_ADDR   28    /* 4 <ipaddr> */

#define OPT_REQUESTED_IP     50    /* 4 <ipaddr> */
#define OPT_LEASE_TIME       51    /* 4 <seconds> */
#define OPT_MESSAGE_TYPE     53    /* 1 <msgtype> */
#define OPT_SERVER_ID        54    /* 4 <ipaddr> */
#define OPT_PARAMETER_LIST   55    /* n <optcode> * n */
#define OPT_MESSAGE          56    /* n <errorstring> */
#define OPT_CLASS_ID         60    /* n <opaque> */
#define OPT_CLIENT_ID        61    /* n <opaque> */
#define OPT_END              255

/* DHCP message types */
#define DHCPDISCOVER         1
#define DHCPOFFER            2
#define DHCPREQUEST          3
#define DHCPDECLINE          4
#define DHCPACK              5
#define DHCPNAK              6
#define DHCPRELEASE          7
#define DHCPINFORM           8

#ifndef TEMP_FAILURE_RETRY
#define TEMP_FAILURE_RETRY(exp) ({         \
    long int _rc;                          \
    do {                                   \
        _rc = (exp);                       \
    } while (_rc == -1 && errno == EINTR); \
    _rc; })
#endif

typedef struct {
	uint8_t op;           /* BOOTREQUEST / BOOTREPLY    */
	uint8_t htype;        /* hw addr type               */
	uint8_t hlen;         /* hw addr len                */
	uint8_t hops;         /* client set to 0            */

	uint32_t xid;         /* transaction id             */

	uint16_t secs;        /* seconds since start of acq */
	uint16_t flags;

	uint32_t ciaddr;      /* client IP addr             */
	uint32_t yiaddr;      /* your (client) IP addr      */
	uint32_t siaddr;      /* ip addr of next server     */
						  /* (DHCPOFFER and DHCPACK)    */
	uint32_t giaddr;      /* relay agent IP addr        */

	uint8_t chaddr[16];  /* client hw addr             */
	char sname[64];      /* asciiz server hostname     */
	char file[128];      /* asciiz boot file name      */

	uint8_t options[312];  /* optional parameters        */
} dhcp_msg;

typedef struct {
	uint32_t type;

	uint32_t ipaddr;
	uint32_t gateway;
	uint32_t prefixLength;

	uint32_t dns1;
	uint32_t dns2;

	uint32_t saddr;
	uint32_t lease;
} dhcp_info;

static bool _s_verbose = true;

static uint32_t get_sec() {
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec;
}

static int mask_to_prelen(in_addr_t mask) {
	int len = 0;
	uint32_t m = (uint32_t) ntohl(mask);
	while (m & 0x80000000) {
		len++;
		m = m << 1;
	}
	return len;
}

static void macaddr_to_hwaddr(const char *macaddr, uint8_t hwaddr[6]) {
	sscanf(macaddr, "%02x:%02x:%02x:%02x:%02x:%02x",
			hwaddr, hwaddr + 1, hwaddr + 2, hwaddr + 3, hwaddr + 4, hwaddr + 5);
}

static uint8_t *init_dhcp_msg(dhcp_msg *msg, int type, uint8_t *hwaddr, uint32_t xid) {
	uint8_t *x;

	memset(msg, 0, sizeof(dhcp_msg));

	msg->op = OP_BOOTREQUEST;
	msg->htype = HTYPE_ETHER;
	msg->hlen = 6;
	msg->hops = 0;
	msg->xid = xid;

	memcpy(msg->chaddr, hwaddr, 6);

	x = msg->options;

	*x++ = OPT_COOKIE1;
	*x++ = OPT_COOKIE2;
	*x++ = OPT_COOKIE3;
	*x++ = OPT_COOKIE4;

	*x++ = OPT_MESSAGE_TYPE;
	*x++ = 1;
	*x++ = type;

	return x;
}

static int init_dhcp_renew_msg(dhcp_msg *msg, uint8_t *hwaddr, uint32_t xid,
						uint32_t ipaddr, uint32_t saddr) {
	uint8_t *x;

	x = init_dhcp_msg(msg, DHCPREQUEST, hwaddr, xid);

	msg->ciaddr = ipaddr;

	*x++ = OPT_CLIENT_ID;
	*x++ = 7;
	*x++ = HTYPE_ETHER;
	memcpy(x, hwaddr, 6);
	x += 6;

	*x++ = OPT_PARAMETER_LIST;
	*x++ = 4;
	*x++ = OPT_SUBNET_MASK;
	*x++ = OPT_GATEWAY;
	*x++ = OPT_DNS;
	*x++ = OPT_BROADCAST_ADDR;

	*x++ = OPT_REQUESTED_IP;
	*x++ = 4;
	memcpy(x, &ipaddr, 4);
	x += 4;

	*x++ = OPT_SERVER_ID;
	*x++ = 4;
	memcpy(x, &saddr, 4);
	x += 4;

	*x++ = OPT_END;

	return DHCP_MSG_FIXED_SIZE + (x - msg->options);
}

static const char *dhcp_type_to_name(uint32_t type) {
	switch (type) {
	case DHCPDISCOVER: return "discover";
	case DHCPOFFER:    return "offer";
	case DHCPREQUEST:  return "request";
	case DHCPDECLINE:  return "decline";
	case DHCPACK:      return "ack";
	case DHCPNAK:      return "nak";
	case DHCPRELEASE:  return "release";
	case DHCPINFORM:   return "inform";
	default:           return "???";
	}
}

static void dump_dhcp_info(dhcp_info *info) {
	char addr[20], gway[20], dns[20];
	LOGD("[dhcp] %s (%d) ---\n", dhcp_type_to_name(info->type), info->type);
	inet_ntop(AF_INET, &info->ipaddr, addr, sizeof(addr));
	inet_ntop(AF_INET, &info->gateway, gway, sizeof(gway));
	LOGD("[dhcp] ip %s gw %s prefixLength %d\n", addr, gway, info->prefixLength);
	if (info->dns1) LOGD("[dhcp] dns1: %s\n", inet_ntop(AF_INET, &info->dns1, dns, sizeof(dns)));
	if (info->dns2) LOGD("[dhcp] dns2: %s\n", inet_ntop(AF_INET, &info->dns2, dns, sizeof(dns)));
	LOGD("[dhcp] server %s, lease %d seconds\n", inet_ntop(AF_INET, &info->saddr, addr, sizeof(addr)), info->lease);
}

static int decode_dhcp_msg(dhcp_msg *msg, int len, dhcp_info *info) {
	uint8_t *x;
	uint32_t opt;
	int optlen;

	memset(info, 0, sizeof(dhcp_info));
	if (len < (DHCP_MSG_FIXED_SIZE + 4)) return -1;

	len -= (DHCP_MSG_FIXED_SIZE + 4);

	if (msg->options[0] != OPT_COOKIE1) return -1;
	if (msg->options[1] != OPT_COOKIE2) return -1;
	if (msg->options[2] != OPT_COOKIE3) return -1;
	if (msg->options[3] != OPT_COOKIE4) return -1;

	x = msg->options + 4;

	while (len > 2) {
		opt = *x++;
		if (opt == OPT_PAD) {
			len--;
			continue;
		}
		if (opt == OPT_END) {
			break;
		}
		optlen = *x++;
		len -= 2;
		if (optlen > len) {
			break;
		}
		switch(opt) {
		case OPT_SUBNET_MASK:
			if (optlen >= 4) {
				in_addr_t mask;
				memcpy(&mask, x, 4);
				info->prefixLength = mask_to_prelen(mask);
			}
			break;
		case OPT_GATEWAY:
			if (optlen >= 4) memcpy(&info->gateway, x, 4);
			break;
		case OPT_DNS:
			if (optlen >= 4) memcpy(&info->dns1, x + 0, 4);
			if (optlen >= 8) memcpy(&info->dns2, x + 4, 4);
			break;
		case OPT_LEASE_TIME:
			if (optlen >= 4) {
				memcpy(&info->lease, x, 4);
				info->lease = ntohl(info->lease);
			}
			break;
		case OPT_SERVER_ID:
			if (optlen >= 4) memcpy(&info->saddr, x, 4);
			break;
		case OPT_MESSAGE_TYPE:
			info->type = *x;
			break;
		default:
			break;
		}
		x += optlen;
		len -= optlen;
	}

	info->ipaddr = msg->yiaddr;

	return 0;
}

static bool is_valid_reply(dhcp_msg *msg, dhcp_msg *reply, int sz) {
	if (sz < DHCP_MSG_FIXED_SIZE) {
		if (_s_verbose) {
			LOGD("netcfg: Wrong size %d != %d\n", sz, DHCP_MSG_FIXED_SIZE);
		}
		return false;
	}
	if (reply->op != OP_BOOTREPLY) {
		if (_s_verbose) {
			LOGD("netcfg: Wrong Op %d != %d\n", reply->op, OP_BOOTREPLY);
		}
		return false;
	}
	if (reply->xid != msg->xid) {
		if (_s_verbose) {
			LOGD("netcfg: Wrong Xid 0x%x != 0x%x\n", ntohl(reply->xid), ntohl(msg->xid));
		}
		return false;
	}
	if (reply->htype != msg->htype) {
		if (_s_verbose) {
			LOGD("netcfg: Wrong Htype %d != %d\n", reply->htype, msg->htype);
		}
		return false;
	}
	if (reply->hlen != msg->hlen) {
		if (_s_verbose) {
			LOGD("netcfg: Wrong Hlen %d != %d\n", reply->hlen, msg->hlen);
		}
		return false;
	}
	if (memcmp(msg->chaddr, reply->chaddr, msg->hlen)) {
		if (_s_verbose) {
			LOGD("netcfg: Wrong chaddr %x != %x\n", *(reply->chaddr),*(msg->chaddr));
		}
		return false;
	}
	return true;
}


DhcpClient::DhcpClient() : lease_cb_(NULL), record_time_(0), lease_(0), record_lease_(0) {
	wakeup_[0] = wakeup_[1] = -1;
}

DhcpClient::~DhcpClient() {
	stop();
}

void DhcpClient::set_lease_cb(dhcp_lease_cb cb) {
	lease_cb_ = cb;
}

bool DhcpClient::start(const char *ip, const char *macaddr, const char *gateway) {
	if (isRunning()) {
		return true;
	}

	if (pipe(wakeup_) < 0) {
		LOGE("[dhcp] create pipe fail\n");
		return false;
	}

	ip_ = ip;
	macaddr_ = macaddr;
	gateway_ = gateway;
	record_time_ = get_sec();
//	lease_ = 60 * 60;    // 1 hour
	lease_ = 100 * 60;    // 100分钟
	record_lease_ = 0;

	LOGD("开始请求dhcp，ip == %s", ip);
	return run("renew");
}

void DhcpClient::stop() {
	LOGD("[dhcp] stop +++\n");
	LOGD("正在断开dhcp，ip == %s", ip_.c_str());

	if (wakeup_[1] >= 0) {
		TEMP_FAILURE_RETRY(write(wakeup_[1], "W", 1));
	}

	requestExitAndWait();

	if (wakeup_[0] >= 0) {
		close(wakeup_[0]);
		wakeup_[0] = -1;
	}
	if (wakeup_[1] >= 0) {
		close(wakeup_[1]);
		wakeup_[1] = -1;
	}
	LOGD("[dhcp] stop ---\n");
}

bool DhcpClient::threadLoop() {
	uint32_t lease1_2 = lease_ / 2;    // 1/2
	uint32_t lease7_8 = lease_ - lease_ / 8;  // 7/8
	uint32_t past_time = get_sec() - record_time_;
	bool need_renew = false;

//	LOGD("lease_ %d\n", lease_);
//	LOGD("lease1_2 %d\n", lease1_2);
//	LOGD("lease7_8 %d\n", lease7_8);
//	LOGD("past_time %d\n", past_time);

	if (past_time > lease_) {
		LOGE("[dhcp] lease time out\n");
		if (lease_cb_) {
			lease_cb_(E_DHCP_LEASE_TYPE_TIMEOUT, this);
		}
		return false;
	} else if (past_time > lease7_8) {
		if (record_lease_ < lease7_8) {
			record_lease_ = lease7_8;
			need_renew = true;
		}
	} else if (past_time > lease1_2) {
		if (record_lease_ < lease1_2) {
			record_lease_ = lease1_2;
			need_renew = true;
		}
	}

	if (need_renew) {
		bool ret = false;
		for (int i = 0; i < 3; ++i) {
			if (renew()) {
				ret = true;
				break;
			}

			sleep(100);
		}

		if (ret) {
			record_lease_ = 0;
			record_time_ = get_sec();
			LOGD("[dhcp] lease ok\n");
			if (lease_cb_) {
				lease_cb_(E_DHCP_LEASE_TYPE_SUCCESS, this);
			}
		} else {
			LOGE("[dhcp] lease err\n");
			if (lease_cb_) {
				lease_cb_(E_DHCP_LEASE_TYPE_FAIL, this);
			}
		}
	}

	struct pollfd ufd = { wakeup_[0], POLLIN, 0 };
	poll(&ufd, 1, 3000);

	return true;
}

bool DhcpClient::renew() {
	int s = -1;
	sockaddr_in saddr;
	socklen_t saddr_len;
	sockaddr_in caddr;
	socklen_t caddr_len;
	struct timeval tv_out;
	uint8_t hwaddr[6];
	dhcp_msg renew_msg;
	dhcp_msg recv_msg;
	ssize_t renew_len;
	ssize_t recv_len;
	dhcp_info di;
	bool ret = false;

	s = socket(AF_INET, SOCK_DGRAM, 0);
	if (s < 0) {
		LOGE("[dhcp] create socket err\n");
		return false;
	}

	saddr_len = sizeof(struct sockaddr_in);
	memset(&saddr, 0, saddr_len);
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(PORT_BOOTP_SERVER);
	saddr.sin_addr.s_addr = inet_addr(gateway_.c_str());

	caddr_len = sizeof(struct sockaddr_in);
	memset(&caddr, 0, caddr_len);
	caddr.sin_family = AF_INET;
	caddr.sin_port = htons(PORT_BOOTP_CLIENT);
	caddr.sin_addr.s_addr = htonl(INADDR_ANY);

	if (bind(s, (sockaddr *) &caddr, caddr_len) < 0) {
		LOGE("[dhcp] bind err\n");
		goto END;
	}

	tv_out.tv_sec = 2;    // 等待2秒
	tv_out.tv_usec = 0;
	if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv_out, sizeof(tv_out)) < 0) {
		LOGE("[dhcp] setsockopt err\n");
		goto END;
	}

	macaddr_to_hwaddr(macaddr_.c_str(), hwaddr);
	renew_len = init_dhcp_renew_msg(&renew_msg, hwaddr, get_sec(), inet_addr(ip_.c_str()), inet_addr(gateway_.c_str()));
	if (sendto(s, &renew_msg, renew_len, 0, (struct sockaddr *) &saddr, saddr_len) < 0) {
		LOGE("[dhcp] sendto err\n");
		goto END;
	}

	recv_len = recvfrom(s, &recv_msg, sizeof(recv_msg), 0, (struct sockaddr *) &saddr, &saddr_len);
	if (recv_len < 0) {
		LOGE("[dhcp] recvfrom err\n");
		goto END;
	}

	LOGD("[dhcp] recv len %d\n", recv_len);
	if (!is_valid_reply(&renew_msg, &recv_msg, recv_len)) {
		goto END;
	}

	if (decode_dhcp_msg(&recv_msg, recv_len, &di) == -1) {
		LOGE("[dhcp] decode msg err\n");
		goto END;
	}

	dump_dhcp_info(&di);

	if (di.type != DHCPACK) {
		goto END;
	}

	lease_ = di.lease;
	ret = true;

END:
	close(s);

	return ret;
}

}
