/*
 * Copyright 2021 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include <fcntl.h>
#include <glib/gstdio.h>
#include <linux/ipmi.h>
#include <linux/ipmi_msgdefs.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "fu-ipmi-device.h"

#define FU_IPMI_DEVICE_TIMEOUT 1500 /* ms */

#define FU_IPMI_TRANSACTION_RETRY_COUNT 5
#define FU_IPMI_TRANSACTION_RETRY_DELAY 200 /* ms */

/* not defined in linux/ipmi_msgdefs.h */
#define IPMI_SET_USER_ACCESS   0x43
#define IPMI_SET_USER_NAME     0x45
#define IPMI_GET_USER_NAME     0x46
#define IPMI_SET_USER_PASSWORD 0x47

#define IPMI_PASSWORD_DISABLE_USER  0x00
#define IPMI_PASSWORD_ENABLE_USER   0x01
#define IPMI_PASSWORD_SET_PASSWORD  0x02
#define IPMI_PASSWORD_TEST_PASSWORD 0x03

/* these are not provided in ipmi_msgdefs.h,
 * nocheck:magic-defines=50 */
#define IPMI_INVALID_COMMAND_ON_LUN_ERR	 0xC2
#define IPMI_OUT_OF_SPACE_ERR		 0xC4
#define IPMI_CANCELLED_OR_INVALID_ERR	 0xC5
#define IPMI_OUT_OF_RANGE_ERR		 0xC9
#define IPMI_CANNOT_RETURN_DATA_ERR	 0xCA
#define IPMI_NOT_FOUND_ERR		 0xCB
#define IPMI_INVALID_DATA_FIELD_ERR	 0xCC
#define IPMI_COMMAND_ILLEGAL_ERR	 0xCD
#define IPMI_RESPONSE_NOT_PROVIDED_ERR	 0xCE
#define IPMI_DUPLICATED_REQUEST_ERR	 0xCF
#define IPMI_SDR_IN_UPDATE_MODE_ERR	 0xD0
#define IPMI_DESTINATION_UNAVAILABLE_ERR 0xD3
#define IPMI_INSUFFICIENT_PRIVILEGE_ERR	 0xD4
#define IPMI_COMMAND_DISABLED_ERR	 0xD6

#ifndef IPMI_DEVICE_IN_UPDATE_MODE_ERR
#define IPMI_DEVICE_IN_UPDATE_MODE_ERR 0xD1
#endif
#ifndef IPMI_DEVICE_IN_INIT_ERR
#define IPMI_DEVICE_IN_INIT_ERR 0xD2
#endif

struct _FuIpmiDevice {
	FuUdevDevice parent_instance;
	glong seq;
	guint8 device_id;
	guint8 device_rev;
	guint8 version_ipmi;
};

G_DEFINE_TYPE(FuIpmiDevice, fu_ipmi_device, FU_TYPE_UDEV_DEVICE)

#define FU_IPMI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */

static void
fu_ipmi_device_to_string(FuDevice *device, guint idt, GString *str)
{
	FuIpmiDevice *self = FU_IPMI_DEVICE(device);
	fwupd_codec_string_append_hex(str, idt, "DeviceId", self->device_id);
	fwupd_codec_string_append_hex(str, idt, "DeviceRev", self->device_rev);
	fwupd_codec_string_append_hex(str, idt, "VersionIpmi", self->version_ipmi);
}

static gboolean
fu_ipmi_device_ioctl_buffer_cb(FuIoctl *self,
			       gpointer ptr,
			       guint8 *buf,
			       gsize bufsz,
			       GError **error)
{
	struct ipmi_req *req = (struct ipmi_req *)ptr;
	req->msg.data = buf;
	req->msg.data_len = (guint16)bufsz;
	return TRUE;
}

static gboolean
fu_ipmi_device_ioctl_addr_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error)
{
	struct ipmi_req *req = (struct ipmi_req *)ptr;
	req->addr = buf;
	req->addr_len = bufsz;
	return TRUE;
}

static gboolean
fu_ipmi_device_send(FuIpmiDevice *self,
		    guint8 netfn,
		    guint8 cmd,
		    const guint8 *buf,
		    gsize bufsz,
		    GError **error)
{
	struct ipmi_system_interface_addr addr = {
	    .addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE,
	    .channel = IPMI_BMC_CHANNEL,
	};
	struct ipmi_req req = {
	    .msgid = self->seq++,
	    .msg.netfn = netfn,
	    .msg.cmd = cmd,
	};
	g_autofree guint8 *buf2 = NULL;
	g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self));

	if (buf != NULL) {
		buf2 = fu_memdup_safe(buf, bufsz, error);
		if (buf2 == NULL)
			return FALSE;
		fu_dump_raw(G_LOG_DOMAIN, "ipmi-send", buf2, bufsz);
	}

	/* include these when generating the emulation event */
	fu_ioctl_add_key_as_u16(ioctl, "Request", IPMICTL_SEND_COMMAND);
	fu_ioctl_add_key_as_u8(ioctl, "Msgid", req.msgid);
	fu_ioctl_add_key_as_u8(ioctl, "MsgNetfn", req.msg.netfn);
	fu_ioctl_add_key_as_u8(ioctl, "MsgCmd", req.msg.cmd);
	fu_ioctl_add_const_buffer(ioctl, NULL, buf2, bufsz, fu_ipmi_device_ioctl_buffer_cb);
	fu_ioctl_add_const_buffer(ioctl,
				  NULL,
				  (const guint8 *)&addr,
				  sizeof(addr),
				  fu_ipmi_device_ioctl_addr_cb);
	return fu_ioctl_execute(ioctl,
				IPMICTL_SEND_COMMAND,
				(guint8 *)&req,
				sizeof(req),
				NULL,
				FU_IPMI_DEVICE_IOCTL_TIMEOUT,
				FU_IOCTL_FLAG_NONE,
				error);
}

static gboolean
fu_ipmi_device_recv(FuIpmiDevice *self,
		    guint8 *netfn,
		    guint8 *cmd,
		    glong *seq,
		    guint8 *buf,
		    gsize bufsz,
		    gsize *len, /* optional, out */
		    GError **error)
{
	struct ipmi_addr addr = {0};
	struct ipmi_recv recv = {
	    .addr = (guint8 *)&addr,
	    .addr_len = sizeof(addr),
	    .msg.data = buf,
	    .msg.data_len = bufsz,
	};
	g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self));

	fu_ioctl_add_key_as_u16(ioctl, "Request", IPMICTL_RECEIVE_MSG_TRUNC);
	if (!fu_ioctl_execute(ioctl,
			      IPMICTL_RECEIVE_MSG_TRUNC,
			      (guint8 *)&recv,
			      sizeof(recv),
			      NULL,
			      FU_IPMI_DEVICE_IOCTL_TIMEOUT,
			      FU_IOCTL_FLAG_NONE,
			      error))
		return FALSE;
	fu_dump_raw(G_LOG_DOMAIN, "ipmi-recv", buf, bufsz);
	if (netfn != NULL)
		*netfn = recv.msg.netfn;
	if (cmd != NULL)
		*cmd = recv.msg.cmd;
	if (seq != NULL)
		*seq = recv.msgid;
	if (len != NULL)
		*len = (gsize)recv.msg.data_len;
	return TRUE;
}

static gboolean
fu_ipmi_device_lock_cb(FuDevice *device, GError **error)
{
	FuIpmiDevice *self = FU_IPMI_DEVICE(device);
	FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self));
	struct flock lock = {.l_type = F_WRLCK, .l_whence = SEEK_SET};
	if (fcntl(fu_io_channel_unix_get_fd(io_channel), F_SETLKW, &lock) == -1) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "error locking IPMI device: %m");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ipmi_device_unlock_cb(FuDevice *device, GError **error)
{
	FuIpmiDevice *self = FU_IPMI_DEVICE(device);
	FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self));
	struct flock lock = {.l_type = F_UNLCK};
	if (fcntl(fu_io_channel_unix_get_fd(io_channel), F_SETLKW, &lock) == -1) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "error unlocking IPMI device: %m");
		return FALSE;
	}
	return TRUE;
}

static const gchar *
fu_ipmi_device_errcode_to_string(guint8 errcode)
{
	if (errcode == IPMI_CC_NO_ERROR)
		return "no-error";
	if (errcode == IPMI_NODE_BUSY_ERR)
		return "node-busy";
	if (errcode == IPMI_INVALID_COMMAND_ERR)
		return "invalid-command";
	if (errcode == IPMI_TIMEOUT_ERR)
		return "timeout";
	if (errcode == IPMI_ERR_MSG_TRUNCATED)
		return "msg-truncated";
	if (errcode == IPMI_REQ_LEN_INVALID_ERR)
		return "req-len-invalid";
	if (errcode == IPMI_REQ_LEN_EXCEEDED_ERR)
		return "req-len-exceeded";
	if (errcode == IPMI_DEVICE_IN_UPDATE_MODE_ERR)
		return "device-in-update-mode";
	if (errcode == IPMI_DEVICE_IN_INIT_ERR)
		return "device-in-init";
	if (errcode == IPMI_NOT_IN_MY_STATE_ERR)
		return "not-in-my-state";
	if (errcode == IPMI_LOST_ARBITRATION_ERR)
		return "lost-arbitration";
	if (errcode == IPMI_BUS_ERR)
		return "bus-error";
	if (errcode == IPMI_NAK_ON_WRITE_ERR)
		return "nak-on-write";
	if (errcode == IPMI_ERR_UNSPECIFIED)
		return "unspecified";
	/* these are not defined in ipmi_msgdefs.h but used in reality */
	if (errcode == IPMI_INVALID_COMMAND_ON_LUN_ERR)
		return "invalid-command-on-lun";
	if (errcode == IPMI_OUT_OF_SPACE_ERR)
		return "out-of-space";
	if (errcode == IPMI_CANCELLED_OR_INVALID_ERR)
		return "cancelled-or-invalid";
	if (errcode == IPMI_OUT_OF_RANGE_ERR)
		return "out-of-range";
	if (errcode == IPMI_CANNOT_RETURN_DATA_ERR)
		return "cannot-return-data";
	if (errcode == IPMI_NOT_FOUND_ERR)
		return "not-found";
	if (errcode == IPMI_INVALID_DATA_FIELD_ERR)
		return "invalid-data-field";
	if (errcode == IPMI_COMMAND_ILLEGAL_ERR)
		return "command-illegal";
	if (errcode == IPMI_RESPONSE_NOT_PROVIDED_ERR)
		return "response-not-provided";
	if (errcode == IPMI_DUPLICATED_REQUEST_ERR)
		return "duplicated-request";
	if (errcode == IPMI_SDR_IN_UPDATE_MODE_ERR)
		return "sdr-in-update-mode";
	if (errcode == IPMI_DESTINATION_UNAVAILABLE_ERR)
		return "destination-unavailable";
	if (errcode == IPMI_INSUFFICIENT_PRIVILEGE_ERR)
		return "insufficient-privilege";
	if (errcode == IPMI_COMMAND_DISABLED_ERR)
		return "command-disabled";
	return "unknown";
}

static gboolean
fu_ipmi_device_errcode_to_error(guint8 errcode, GError **error)
{
	/* success */
	if (errcode == IPMI_CC_NO_ERROR)
		return TRUE;

	/* data not found, seemingly Lenovo specific */
	if (errcode == IPMI_INVALID_DATA_FIELD_ERR || errcode == IPMI_NOT_FOUND_ERR) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "CC error: %s [0x%02X]",
			    fu_ipmi_device_errcode_to_string(errcode),
			    errcode);
		return FALSE;
	}

	/* fallback */
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_INTERNAL,
		    "CC error: %s [0x%02X]",
		    fu_ipmi_device_errcode_to_string(errcode),
		    errcode);
	return FALSE;
}

typedef struct {
	guint8 netfn;
	guint8 cmd;
	const guint8 *req_buf;
	gsize req_bufsz;
	guint8 *resp_buf;
	gsize resp_bufsz;
	gsize *resp_len;
	gint timeout_ms;
} FuIpmiDeviceTransactionHelper;

static gboolean
fu_ipmi_device_transaction_cb(FuDevice *device, gpointer user_data, GError **error)
{
	FuIpmiDevice *self = FU_IPMI_DEVICE(device);
	FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self));
	FuIpmiDeviceTransactionHelper *helper = (FuIpmiDeviceTransactionHelper *)user_data;
	GPollFD pollfds[1];
	gsize resp_buf2sz = helper->resp_bufsz + 1;
	gsize resp_len2 = 0;
	g_autoptr(GTimer) timer = g_timer_new();
	g_autoptr(FuDeviceLocker) lock = NULL;
	g_autofree guint8 *resp_buf2 = g_malloc0(resp_buf2sz);

	lock = fu_device_locker_new_full(device,
					 fu_ipmi_device_lock_cb,
					 fu_ipmi_device_unlock_cb,
					 error);
	if (lock == NULL)
		return FALSE;

	if (!fu_ipmi_device_send(self,
				 helper->netfn,
				 helper->cmd,
				 helper->req_buf,
				 helper->req_bufsz,
				 error))
		return FALSE;

	pollfds[0].fd = fu_io_channel_unix_get_fd(io_channel);
	pollfds[0].events = POLLIN;

	for (;;) {
		guint8 resp_netfn = 0;
		guint8 resp_cmd = 0;
		glong seq = 0;
		gint rc;

		rc = g_poll(pollfds,
			    1,
			    helper->timeout_ms - (g_timer_elapsed(timer, NULL) * 1000.f));
		if (rc < 0) {
			g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "poll() error %m");
			return FALSE;
		}
		if (rc == 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_TIMED_OUT,
				    "timeout waiting for response "
				    "(netfn %d, cmd %d)",
				    helper->netfn,
				    helper->cmd);
			return FALSE;
		}

		if (!(pollfds[0].revents & POLLIN)) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "unexpected status");
			return FALSE;
		}

		if (!fu_ipmi_device_recv(self,
					 &resp_netfn,
					 &resp_cmd,
					 &seq,
					 resp_buf2,
					 resp_buf2sz,
					 &resp_len2,
					 error))
			return FALSE;

		if (seq != self->seq - 1) {
			g_debug("out-of-sequence reply: "
				"expected %ld, got %ld",
				self->seq,
				seq);
			if (g_timer_elapsed(timer, NULL) * 1000.f >= helper->timeout_ms) {
				g_set_error_literal(error,
						    FWUPD_ERROR,
						    FWUPD_ERROR_TIMED_OUT,
						    "timed out");
				return FALSE;
			}
		} else {
			if (!fu_ipmi_device_errcode_to_error(resp_buf2[0], error))
				return FALSE;
			if (helper->resp_buf != NULL) {
				if (!fu_memcpy_safe(helper->resp_buf,
						    helper->resp_bufsz,
						    0x0, /* dst */
						    resp_buf2,
						    resp_buf2sz,
						    0x01, /* src */
						    helper->resp_bufsz,
						    error))
					return FALSE;
			}
			if (helper->resp_len != NULL)
				*helper->resp_len = resp_len2 - 1;
			g_debug("IPMI netfn: %02x->%02x, cmd: %02x->%02x",
				helper->netfn,
				resp_netfn,
				helper->cmd,
				resp_cmd);
			break;
		}
	}
	return TRUE;
}

static gboolean
fu_ipmi_device_transaction(FuIpmiDevice *self,
			   guint8 netfn,
			   guint8 cmd,
			   const guint8 *req_buf,
			   gsize req_bufsz,
			   guint8 *resp_buf, /* optional */
			   gsize resp_bufsz,
			   gsize *resp_len, /* optional, out */
			   gint timeout_ms,
			   GError **error)
{
	FuIpmiDeviceTransactionHelper helper = {
	    .netfn = netfn,
	    .cmd = cmd,
	    .req_buf = req_buf,
	    .req_bufsz = req_bufsz,
	    .resp_buf = resp_buf,
	    .resp_bufsz = resp_bufsz,
	    .resp_len = resp_len,
	    .timeout_ms = timeout_ms,
	};
	fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, NULL);
	return fu_device_retry_full(FU_DEVICE(self),
				    fu_ipmi_device_transaction_cb,
				    FU_IPMI_TRANSACTION_RETRY_COUNT,
				    FU_IPMI_TRANSACTION_RETRY_DELAY,
				    &helper,
				    error);
}

static gboolean
fu_ipmi_device_probe(FuDevice *device, GError **error)
{
	FuIpmiDevice *self = FU_IPMI_DEVICE(device);
	const gchar *physical_ids[] = {"/dev/ipmi0", "/dev/ipmi/0", "/dev/ipmidev/0"};

	/* look for the IPMI device */
	for (guint i = 0; i < G_N_ELEMENTS(physical_ids); i++) {
		gboolean exists_fn = FALSE;
		if (!fu_device_query_file_exists(device, physical_ids[i], &exists_fn, error))
			return FALSE;
		if (exists_fn) {
			fu_device_set_physical_id(FU_DEVICE(self), physical_ids[i]);
			return TRUE;
		}
	}

	/* cannot continue */
	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no BMC device found");
	return FALSE;
}

static gboolean
fu_ipmi_device_setup(FuDevice *device, GError **error)
{
	FuIpmiDevice *self = FU_IPMI_DEVICE(device);
	gsize resp_len = 0;
	guint8 resp[16] = {0};

	/* get IPMI versions */
	if (!fu_ipmi_device_transaction(self,
					IPMI_NETFN_APP_REQUEST,
					IPMI_GET_DEVICE_ID_CMD,
					NULL,
					0,
					resp,
					sizeof(resp),
					&resp_len,
					FU_IPMI_DEVICE_TIMEOUT,
					error))
		return FALSE;
	if (resp_len == 11 || resp_len == 15) {
		guint8 bcd;
		g_autoptr(GString) str = g_string_new(NULL);

		self->device_id = resp[0];
		self->device_rev = resp[1];
		bcd = resp[3] & 0x0f;
		bcd += 10 * (resp[4] >> 3);
		/* rev1.rev2.aux_revision */
		g_string_append_printf(str, "%u.%02u", resp[2], bcd);
		if (resp_len == 15) {
			g_string_append_printf(str,
					       ".%02x%02x%02x%02x",
					       resp[11],
					       resp[12],
					       resp[13],
					       resp[14]);
		}
		fu_device_set_version(device, str->str);
		bcd = resp[4] & 0x0f;
		bcd += 10 * (resp[4] >> 4);
		self->version_ipmi = bcd;
	} else {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "failed to parse DEVICE_ID_CMD response (sz: %" G_GSIZE_FORMAT ")",
			    resp_len);
		return FALSE;
	}

	/* success */
	return TRUE;
}

gchar *
fu_ipmi_device_get_user_password(FuIpmiDevice *self, guint8 user_id, GError **error)
{
	const guint8 req[1] = {user_id};
	guint8 resp[0x10] = {0};
	gsize resp_len = 0;

	g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), NULL);
	g_return_val_if_fail(user_id != 0x0, NULL);

	/* run transaction */
	if (!fu_ipmi_device_transaction(self,
					IPMI_NETFN_APP_REQUEST,
					IPMI_GET_USER_NAME,
					req,
					sizeof(req),
					resp,
					sizeof(resp),
					&resp_len,
					FU_IPMI_DEVICE_TIMEOUT,
					error)) {
		g_prefix_error_literal(error, "failed to get username: ");
		return NULL;
	}
	if (resp_len != sizeof(resp)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "failed to retrieve username from IPMI, got 0x%x bytes",
			    (guint)resp_len);
		return NULL;
	}

	/* success */
	return fu_memstrsafe(resp, sizeof(resp), 0x0, resp_len, error);
}

gboolean
fu_ipmi_device_set_user_name(FuIpmiDevice *self,
			     guint8 user_id,
			     const gchar *username,
			     GError **error)
{
	guint8 req[0x11] = {user_id};
	gsize username_sz;

	g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
	g_return_val_if_fail(user_id != 0x0, FALSE);
	g_return_val_if_fail(username != NULL, FALSE);

	/* copy into buffer */
	username_sz = strlen(username);
	if (!fu_memcpy_safe(req,
			    sizeof(req),
			    0x1, /* dst */
			    (guint8 *)username,
			    username_sz,
			    0x0, /* src */
			    username_sz,
			    error)) {
		g_prefix_error_literal(error, "username invalid: ");
		return FALSE;
	}

	/* run transaction */
	if (!fu_ipmi_device_transaction(self,
					IPMI_NETFN_APP_REQUEST,
					IPMI_SET_USER_NAME,
					req,
					sizeof(req),
					NULL, /* resp */
					0,
					NULL,
					FU_IPMI_DEVICE_TIMEOUT,
					error)) {
		g_prefix_error(error, "failed to set user %02x name: ", user_id);
		return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
fu_ipmi_device_set_user_enable(FuIpmiDevice *self, guint8 user_id, gboolean value, GError **error)
{
	guint8 op = value ? IPMI_PASSWORD_ENABLE_USER : IPMI_PASSWORD_DISABLE_USER;
	const guint8 req[] = {user_id, op};

	g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
	g_return_val_if_fail(user_id != 0x0, FALSE);

	/* run transaction */
	if (!fu_ipmi_device_transaction(self,
					IPMI_NETFN_APP_REQUEST,
					IPMI_SET_USER_PASSWORD,
					req,
					sizeof(req),
					NULL, /* resp */
					0,
					NULL,
					FU_IPMI_DEVICE_TIMEOUT,
					error)) {
		g_prefix_error(error, "failed to set user %02x enable: ", user_id);
		return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
fu_ipmi_device_set_user_password(FuIpmiDevice *self,
				 guint8 user_id,
				 const gchar *password,
				 GError **error)
{
	guint8 req[0x12] = {user_id, IPMI_PASSWORD_SET_PASSWORD};
	gsize password_sz;

	g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
	g_return_val_if_fail(user_id != 0x0, FALSE);
	g_return_val_if_fail(password != NULL, FALSE);

	/* copy into buffer */
	password_sz = strlen(password);
	if (!fu_memcpy_safe(req,
			    sizeof(req),
			    0x2, /* dst */
			    (guint8 *)password,
			    password_sz,
			    0x0, /* src */
			    password_sz,
			    error)) {
		g_prefix_error_literal(error, "password invalid: ");
		return FALSE;
	}

	/* run transaction */
	if (!fu_ipmi_device_transaction(self,
					IPMI_NETFN_APP_REQUEST,
					IPMI_SET_USER_PASSWORD,
					req,
					sizeof(req),
					NULL, /* resp */
					0,
					NULL,
					FU_IPMI_DEVICE_TIMEOUT,
					error)) {
		g_prefix_error(error, "failed to set user %02x password: ", user_id);
		return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
fu_ipmi_device_set_user_priv(FuIpmiDevice *self,
			     guint8 user_id,
			     guint8 priv_limit,
			     guint8 channel,
			     GError **error)
{
	const guint8 req[] = {channel, user_id, priv_limit, 0x0};

	g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
	g_return_val_if_fail(user_id != 0x0, FALSE);
	g_return_val_if_fail(channel <= 0x0F, FALSE);
	g_return_val_if_fail(priv_limit <= 0x0F, FALSE);

	/* run transaction */
	if (!fu_ipmi_device_transaction(self,
					IPMI_NETFN_APP_REQUEST,
					IPMI_SET_USER_ACCESS,
					req,
					sizeof(req),
					NULL, /* resp */
					0,
					NULL,
					FU_IPMI_DEVICE_TIMEOUT,
					error)) {
		g_prefix_error(error,
			       "failed to set user %02x privs of 0x%02x, 0x%02x: ",
			       user_id,
			       priv_limit,
			       channel);
		return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
fu_ipmi_device_set_user_group_redfish_enable_advantech(FuIpmiDevice *self,
						       guint8 user_id,
						       GError **error)
{
	const guint8 req[] = {0x39, 0x28, 0x0, user_id, 0x3, 0x1};
	guint8 resp[0x3] = {0};
	gsize resp_len = 0;

	g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
	g_return_val_if_fail(user_id != 0x0, FALSE);

	/* run transaction */
	if (!fu_ipmi_device_transaction(self,
					0x2e,
					0x08,
					req,
					sizeof(req),
					resp,
					sizeof(resp),
					&resp_len,
					FU_IPMI_DEVICE_TIMEOUT,
					error)) {
		g_prefix_error(error, "failed to set user %02x redfish group enable: ", user_id);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static void
fu_ipmi_device_init(FuIpmiDevice *self)
{
	fu_device_set_name(FU_DEVICE(self), "IPMI");
	fu_device_set_summary(FU_DEVICE(self), "Intelligent Platform Management Interface");
	fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL);
	fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ);
	fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE);
}

static void
fu_ipmi_device_class_init(FuIpmiDeviceClass *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->probe = fu_ipmi_device_probe;
	device_class->setup = fu_ipmi_device_setup;
	device_class->to_string = fu_ipmi_device_to_string;
}

FuIpmiDevice *
fu_ipmi_device_new(FuContext *ctx)
{
	FuIpmiDevice *self;
	self = g_object_new(FU_TYPE_IPMI_DEVICE, "context", ctx, "device-file", "/dev/ipmi0", NULL);
	return self;
}
