/* SPDX-License-Identifier: LGPL-2.1-only */
/*
 * wpactrl.c - ctrl socket iface 
 *
 * Copyright (C) 2020-2025 Iopsys Software Solutions AB. All rights reserved.
 */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <sys/types.h>
#include <fcntl.h>
#include <net/if.h>
#include <time.h>
#include <syslog.h>
#include <unistd.h>
#include <stdlib.h>
#include <endian.h>
#include <dirent.h>

#include <easy/easy.h>
#include "wifiutils.h"
#include "wifi.h"
#include "debug.h"
#include "wpactrl.h"

#ifdef LIBWIFI_USE_CTRL_IFACE
#include "limits.h"
#include <ctype.h>
#include <sys/un.h>

#ifndef CONFIG_HOSTAPD_CTRL_IFACE_DIR
#define CONFIG_HOSTAPD_CTRL_IFACE_DIR "/var/run/hostapd"
#endif /* CONFIG_HOSTAPD_CTRL_IFACE_DIR */

#ifndef CONFIG_WPA_CTRL_IFACE_DIR
#define CONFIG_WPA_CTRL_IFACE_DIR "/var/run/wpa_supplicant"
#endif /* CONFIG_WPA_CTRL_IFACE_DIR */

#ifndef CONFIG_CTRL_IFACE_CLIENT_DIR
#define CONFIG_CTRL_IFACE_CLIENT_DIR "/tmp"
#endif /* CONFIG_CTRL_IFACE_CLIENT_DIR */

#ifndef CONFIG_CTRL_IFACE_CLIENT_PREFIX
#define CONFIG_CTRL_IFACE_CLIENT_PREFIX "wpa_ctrl_"
#endif /* CONFIG_CTRL_IFACE_CLIENT_PREFIX */

#define MAX_TRIES 5
#endif /*LIBWIFI_USE_CTRL_IFACE*/

#ifdef LIBWIFI_USE_CTRL_IFACE
struct wpa_ctrl {
	int s;
	struct sockaddr_un local;
	struct sockaddr_un dest;
};

static struct wpa_ctrl * wpa_ctrl_open(const char *ctrl_path)
{
	struct wpa_ctrl *ctrl;
	static int counter = 0;		/* FIXME-CR */
	int ret;
	int tries = 0;
	int flags;

	if (ctrl_path == NULL)
		return NULL;

	ctrl = malloc(sizeof(*ctrl));
	if (ctrl)
		memset(ctrl, 0, sizeof(*ctrl));
	else
		return NULL;

	ctrl->s = socket(PF_UNIX, SOCK_DGRAM, 0);
	if (ctrl->s < 0) {
		free(ctrl);
		return NULL;
	}

	ctrl->local.sun_family = AF_UNIX;
	counter++;
try_again:
	ret = snprintf(ctrl->local.sun_path,	/* Flawfinder: ignore */
				sizeof(ctrl->local.sun_path),
				CONFIG_CTRL_IFACE_CLIENT_DIR "/"
				CONFIG_CTRL_IFACE_CLIENT_PREFIX "%d-%d",
				(int) getpid(), counter);

	if (ret < 0 || (unsigned int) ret >= sizeof(ctrl->local.sun_path)) {
		close(ctrl->s);
		free(ctrl);
		return NULL;
	}
	tries++;

	if (bind(ctrl->s, (struct sockaddr *) &ctrl->local,
			sizeof(ctrl->local)) < 0) {
		if (errno == EADDRINUSE && tries < 2) {
			/*
			 * getpid() returns unique identifier for this instance,
			 * so the existing socket file must have
			 * been left by unclean termination of an earlier run.
			 * Remove the file and try again.
			 */
			unlink(ctrl->local.sun_path);
			goto try_again;
		}
		close(ctrl->s);
		free(ctrl);
		return NULL;
	}

	ctrl->dest.sun_family = AF_UNIX;

	if (strlen(ctrl_path) > sizeof(ctrl->dest.sun_path)) {
		close(ctrl->s);
		free(ctrl);
		return NULL;
	}

	memset(ctrl->dest.sun_path, '\0', sizeof(ctrl->dest.sun_path));
	strncpy(ctrl->dest.sun_path, ctrl_path, sizeof(ctrl->dest.sun_path) - 1);

	if (connect(ctrl->s, (struct sockaddr *) &ctrl->dest,
			sizeof(ctrl->dest)) < 0) {
		close(ctrl->s);
		unlink(ctrl->local.sun_path);
		free(ctrl);
		return NULL;
	}

	/*
	* Make socket non-blocking so that we don't hang forever if
	* target dies unexpectedly.
	*/
	flags = fcntl(ctrl->s, F_GETFL);
	if (flags >= 0) {
		flags |= O_NONBLOCK;
		if (fcntl(ctrl->s, F_SETFL, flags) < 0) {
			perror("fcntl(ctrl->s, O_NONBLOCK)");
			/* Not fatal, continue on.*/
		}
	}

	return ctrl;
}

static void wpa_ctrl_close(struct wpa_ctrl *ctrl)
{
	if (ctrl == NULL)
		return;
	unlink(ctrl->local.sun_path);
	if (ctrl->s >= 0)
		close(ctrl->s);
	free(ctrl);
}

static int wpa_ctrl_request(struct wpa_ctrl *ctrl, const char *cmd, size_t cmd_len,
			char *reply, size_t *reply_len)
{
	struct timeval tv;
	int res;
	fd_set rfds;
	int tries = 0;
	char *cmd_buf = NULL;

	errno = 0;
retry_send:
	tries++;
	if (send(ctrl->s, cmd, cmd_len, 0) < 0) {
		if (tries < MAX_TRIES && (errno == EAGAIN || errno == EBUSY || errno == EWOULDBLOCK)) {
			usleep(1 * 1000);
			goto retry_send;
		}
		free(cmd_buf);
		return -1;
	}
	free(cmd_buf);

	for (;;) {
		tv.tv_sec = 10;
		tv.tv_usec = 0;
		FD_ZERO(&rfds);
		FD_SET(ctrl->s, &rfds);
		res = select(ctrl->s + 1, &rfds, NULL, NULL, &tv);
		if (res < 0 && errno == EINTR)
			continue;
		if (res < 0)
			return res;
		if (FD_ISSET(ctrl->s, &rfds)) {
			res = recv(ctrl->s, reply, *reply_len, 0);
			if (res < 0)
				return res;
			if ((res > 0 && reply[0] == '<') ||
				(res > 6 && strncmp(reply, "IFNAME=", 7) == 0)) {
				// unsolicited is not the response to the command sent
				continue;
			}
			*reply_len = res;
			break;
		} else {
			return -2;
		}
	}
	return 0;
}

static int wpa_ctrl_cmd(char *buf, size_t buflen, const char *ifname,
			const char *cmd, const char *ctrl_iface_dir)
{
	int ret;
	struct wpa_ctrl * conn_ptr;
	char cfile[PATH_MAX] = {};

	if (ifname == NULL)
		return -1;

	snprintf(cfile, sizeof(cfile), "%s/%s", ctrl_iface_dir, ifname);
	conn_ptr = wpa_ctrl_open(cfile);
	if (conn_ptr == NULL)
		return -1;

	ret = wpa_ctrl_request(conn_ptr, cmd, strlen(cmd), buf, &buflen);
	wpa_ctrl_close(conn_ptr);

	if (ret == -2) {
		libwifi_dbg("%s %s '%s' command timed out.\n\n", ifname, __func__, cmd);
		return -2;
	} else if (ret < 0) {
		libwifi_dbg("%s %s '%s' command failed.\n\n", ifname, __func__, cmd);
		return -1;
	}

	return 0;
}

static char *upstr_cmd(char *str)
{
	char *p = str;
	/* uppercase only the command, not the arguments */
	while (*p && *p != ' ') {
		*p = (char)toupper((unsigned char) *p);
		p++;
	}
	return str;
}

static char *del_escapes(char *str)
{
	int start = 0, end = 0;

	if (!str)
		return NULL;

	end = strlen(str);
	if (end == 0)
		return str;

	while (start < end && str[start] != '\0') {
		if (str[start] == '\\') {
			memmove(str + start, str + start + 1, end - start);
			end--;
		}
		start++;
	}

	return str;
}

static int ctrl_iface_set(const char *ifname, const char *cmd, bool check_ok, bool isHostapd)
{
	char buf[128] = {};
	char *cmdbuf;
	int len;
	char *p;

	libwifi_dbg("%s %s set:\n%s\n", ifname, __func__, cmd);

	if (!ifname || !strlen(ifname))
		return -1;

	len = strlen(cmd) + 64;
	cmdbuf = calloc(len, sizeof(char));
	if (!cmdbuf)
		return -1;

	p = strstr(cmd, "raw");
	if (p)
		strncpy(cmdbuf, p + strlen("raw"), len);
	else
		strncpy(cmdbuf, cmd, len);

	trim(cmdbuf);
	upstr_cmd(cmdbuf);
	del_escapes(cmdbuf);

	if (wpa_ctrl_cmd(buf, sizeof(buf), ifname, cmdbuf,
			isHostapd ? CONFIG_HOSTAPD_CTRL_IFACE_DIR : CONFIG_WPA_CTRL_IFACE_DIR)) {
		free(cmdbuf);
		return -1;
	}

	remove_newline(buf);

	if(!check_ok) {
		free(cmdbuf);
		return 0;
	}

	if (!strcmp(buf, "OK")) {
		libwifi_dbg("OK\n");
		free(cmdbuf);
		return 0;
	}

	free(cmdbuf);
	return -1;
}

int wpa_ctrl_iface_get(const char *ifname, const char *cmd, char *out, size_t out_size, bool isHostapd)
{
	char cmdbuf[128] = {};
	char *p;

	libwifi_dbg("%s %s get:\n%s\n", ifname, __func__, cmd);

	if (!ifname || !strlen(ifname))
		return -1;

	p = strstr(cmd, "raw");
	if (p)
		strncpy(cmdbuf, p + strlen("raw"), sizeof(cmdbuf));
	else
		strncpy(cmdbuf, cmd, sizeof(cmdbuf));

	trim(cmdbuf);
	upstr_cmd(cmdbuf);

	if (wpa_ctrl_cmd(out, out_size, ifname,
			   cmdbuf,
			   isHostapd ? CONFIG_HOSTAPD_CTRL_IFACE_DIR : CONFIG_WPA_CTRL_IFACE_DIR)) {
		return -1;
	}

	remove_newline(out);

	/* STA-FIRST and STA-NEXT can return empty string and this is not error */
	if (out[0] == '\0' && !strstr(cmd, "STA-") && !strstr(cmd, "DENY_ACL SHOW")) {
		libwifi_err("%s %s failed\n", ifname, __func__);
		return -1;
	}

	libwifi_dbg("%s %s get:\n%s\n", ifname, __func__, out);
	return 0;
}

int hostapd_cli_set(const char *ifname, const char *cmd, bool check_ok)
{
	return ctrl_iface_set(ifname, cmd, check_ok, true);
}

int wpa_cli_set(const char *ifname, const char *cmd, bool check_ok)
{
	return ctrl_iface_set(ifname, cmd, check_ok, false);
}

int hostapd_cli_get(const char *ifname, const char *cmd, char *out, size_t out_size)
{
	return wpa_ctrl_iface_get(ifname, cmd, out, out_size, true);
}

int wpa_cli_get(const char *ifname, const char *cmd, char *out, size_t out_size)
{
	return wpa_ctrl_iface_get(ifname, cmd, out, out_size, false);
}

#else
int hostapd_cli_set(const char *ifname, const char *cmd, bool check_ok)
{
	char buf[128];

	libwifi_dbg("%s %s set:\n%s\n", ifname, __func__, cmd);

	if (!ifname)
		return -1;
	if (!strlen(ifname))
		return -1;

	chrCmd(buf, sizeof(buf), "timeout %d hostapd_cli -i %s %s", WPACTRL_TIMEOUT, ifname, cmd);

	if(!check_ok)
		return 0;

	if (!strcmp(buf, "OK")) {
		libwifi_dbg("OK\n");
		return 0;
	}

	return -1;
}

int hostapd_cli_get(const char *ifname, const char *cmd, char *out, size_t out_size)
{
	if (!ifname)
		return -1;
	if (!strlen(ifname))
		return -1;

	chrCmd(out, out_size, "timeout %d hostapd_cli -i %s %s", WPACTRL_TIMEOUT, ifname, cmd);

	if (out[0] == '\0') {
		libwifi_err("%s %s failed\n", ifname, __func__);
		return -1;
	}

	libwifi_dbg("%s %s get:\n%s\n", ifname, __func__, out);
	return 0;
}

int wpa_cli_set(const char *ifname, const char *cmd, bool check_ok)
{
	char buf[128];

	libwifi_dbg("%s %s set:\n%s\n", ifname, __func__, cmd);

	if (!ifname)
		return -1;
	if (!strlen(ifname))
		return -1;

	chrCmd(buf, sizeof(buf), "timeout %d wpa_cli -i %s %s", WPACTRL_TIMEOUT, ifname, cmd);

	if(!check_ok)
		return 0;

	if (!strcmp(buf, "OK")) {
		libwifi_dbg("OK\n");
		return 0;
	}

	return -1;
}

int wpa_cli_get(const char *ifname, const char *cmd, char *out, size_t out_size)
{
	if (!ifname)
		return -1;
	if (!strlen(ifname))
		return -1;

	chrCmd(out, out_size, "timeout %d wpa_cli -i %s %s", WPACTRL_TIMEOUT, ifname, cmd);

	if (out[0] == '\0') {
		libwifi_err("%s %s failed\n", ifname, __func__);
		return -1;
	}

	libwifi_dbg("%s %s get:\n%s\n", ifname, __func__, out);
	return 0;
}

int wpa_ctrl_iface_get(const char *ifname, const char *cmd, char *out, size_t out_size, bool isHostapd)
{
	(void) ifname;
	(void) cmd;
	(void) out;
	(void) out_size;
	(void) isHostapd;
	return -1;
}
#endif

/* input:
*  buf = "key1=val1\nkey2=val2\nkey3=val3"
*  param = "key2"
*  char value[x]
*  value_max = x
*  output:
*  value = "val2\0"
*/
int wpa_ctrl_get_param(const char *buf, const char *param, char *value, size_t value_max)
{
	const char *line, *loc;
	char *p, *origin_p;
	char search_str[256] = { 0 };
	int ret = -1;

	if (WARN_ON(!buf) || WARN_ON(!param) || WARN_ON(!value) || WARN_ON(value_max == 0))
		return -EINVAL;

	memset(value,0x00,value_max);
	p = strdup(buf);
	origin_p = p;

	while ((line = strsep(&p, "\n"))) {
		loc = strstr(line, param);
		if (!loc || loc != line)
			continue;
		snprintf(search_str, sizeof(search_str), "%64s=%%%zu[^\n]s", param, value_max);

		if (WARN_ON(sscanf(line, search_str, value) != 1)) {	/* Flawfinder: ignore */
			free(origin_p);
			return ret;
		}
		ret = 0;
		break;
	}
	free(origin_p);
	return ret;
}
