/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2022 RackTop Systems, Inc.
 */

#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/debug.h>
#include <sys/list.h>
#include <sys/types.h>
#include <sys/sysmacros.h>
#include <smbsrv/libsmb.h>
#include <ofmt.h>
#include <libintl.h>
#include <limits.h>
#include <locale.h>
#include <time.h>
#include <upanic.h>
#include "smbadm.h"

/*
 * Share types for shiX_type fields - duplicated from smb.h
 * Don't want to pull that in, and these are "carved in stone"
 * (from the SMB protocol definitions)
 */
#ifndef _SHARE_TYPES_DEFINED_
#define	_SHARE_TYPES_DEFINED_
#define	STYPE_DISKTREE		0x00000000
#define	STYPE_PRINTQ		0x00000001
#define	STYPE_DEVICE		0x00000002
#define	STYPE_IPC		0x00000003
#define	STYPE_MASK		0x0000000F
#endif /* _SHARE_TYPES_DEFINED_ */

#define	MINS		(60U)
#define	HRS		(60 * MINS)
#define	DAYS		(24 * HRS)
#define	TIME_FMT	"%F %T %Z"

#define	_(x) gettext(x)

struct flag_tbl {
	uint32_t	flag;
	const char	*name;
};

typedef enum user_field {
	UF_SESS_ID,
	UF_DOMAIN,
	UF_ACCOUNT,
	UF_USER,
	UF_UID,
	UF_WORKSTATION,
	UF_IP,
	UF_OS,
	UF_LOGON_TIME,
	UF_AGE,
	UF_NUMOPEN,
	UF_FLAGS,
} user_field_t;

typedef enum tree_field {
	TF_ID,
	TF_TYPE,
	TF_NUMOPEN,
	TF_NUMUSERS,
	TF_TIME,
	TF_AGE,
	TF_USERNAME,
	TF_SHARE,
} tree_field_t;

typedef enum netfileinfo_field {
	NFIF_FID,
	NFIF_UNIQID,
	NFIF_PERMS,
	NFIF_NUMLOCKS,
	NFIF_PATH,
	NFIF_USERNAME,
} netfileinfo_field_t;

static ofmt_handle_t cmd_create_handle(int, char **, const char *,
    ofmt_field_t *);

static boolean_t fmt_user(ofmt_arg_t *, char *, uint_t);
static boolean_t fmt_tree(ofmt_arg_t *, char *, uint_t);
static boolean_t fmt_netfileinfo(ofmt_arg_t *, char *, uint_t);

static void print_str(const char *restrict, char *restrict, uint_t);
static void print_u32(uint32_t, char *, uint_t);
static void print_age(time_t, char *, uint_t);
static void print_time(time_t, const char *, char *, uint_t);
static void print_flags(struct flag_tbl *, size_t, uint32_t, char *, uint_t);
static void print_perms(struct flag_tbl *, size_t, uint32_t, char *, uint_t);

static ofmt_field_t smb_user_fields[] = {
	{ "ID",		4,	UF_SESS_ID,	fmt_user },
	{ "DOMAIN",	32,	UF_DOMAIN,	fmt_user },
	{ "ACCT",	16,	UF_ACCOUNT,	fmt_user },
	{ "USER",	32,	UF_USER,	fmt_user },
	{ "UID",	12,	UF_UID,		fmt_user },
	{ "COMPUTER",	16,	UF_WORKSTATION,	fmt_user },
	{ "IP",		15,	UF_IP,		fmt_user },
	{ "OS",		8,	UF_OS,		fmt_user },
	{ "LOGON",	24,	UF_LOGON_TIME,	fmt_user },
	{ "AGE",	16,	UF_AGE,		fmt_user },
	{ "NOPEN",	5,	UF_NUMOPEN,	fmt_user },
	{ "FLAGS",	12,	UF_FLAGS,	fmt_user },
	{ NULL,		0,	0,		NULL }
};

static const char default_user_fields[] = "IP,USER,NOPEN,AGE,FLAGS";

struct flag_tbl user_flag_tbl[] = {
	{ SMB_ATF_GUEST, "GUEST" },
	{ SMB_ATF_ANON, "ANON" },
	{ SMB_ATF_ADMIN, "ADMIN" },
	{ SMB_ATF_POWERUSER, "POWERUSER" },
	{ SMB_ATF_BACKUPOP, "BACKUPOP" },
};

static ofmt_field_t smb_tree_fields[] = {
	{ "ID",		4,	TF_ID,		fmt_tree },
	{ "TYPE",	6,	TF_TYPE,	fmt_tree },
	{ "NOPEN",	6,	TF_NUMOPEN,	fmt_tree },
	{ "NUSER",	6,	TF_NUMUSERS,	fmt_tree },
	{ "TIME",	24,	TF_TIME,	fmt_tree },
	{ "AGE",	12,	TF_AGE,		fmt_tree },
	{ "USER",	32,	TF_USERNAME,	fmt_tree },
	{ "SHARE",	16,	TF_SHARE,	fmt_tree },
	{ NULL,		0,	0,		NULL }
};

static const char default_tree_fields[] = "TYPE,SHARE,USER,NOPEN,AGE";

static ofmt_field_t smb_netfileinfo_fields[] = {
	{ "ID",		4,	NFIF_FID,	fmt_netfileinfo },
	{ "UNIQID",	8,	NFIF_UNIQID,	fmt_netfileinfo },
	{ "PERM",	15,	NFIF_PERMS,	fmt_netfileinfo },
	{ "NLOCK",	6,	NFIF_NUMLOCKS,	fmt_netfileinfo },
	{ "PATH",	32,	NFIF_PATH,	fmt_netfileinfo },
	{ "USER",	16,	NFIF_USERNAME,	fmt_netfileinfo },
	{ NULL,		0,	0,		NULL }
};

static const char default_netfileinfo_fields[] = "UNIQID,PATH,USER,NLOCK,PERM";

/*
 * Flags are the same as "ls -V" and chmod ACLs:
 * eg:  everyone@:rwxpdDaARWcCos:fd----I:allow
 * See libsec:acltext.c
 */
static struct flag_tbl nfi_perm_tbl[] = {
	{ ACE_READ_DATA,		"r" },
	{ ACE_WRITE_DATA,		"w" },
	{ ACE_EXECUTE,			"x" },
	{ ACE_APPEND_DATA,		"p" },
	{ ACE_DELETE,			"d" },
	{ ACE_DELETE_CHILD,		"D" },
	{ ACE_READ_ATTRIBUTES,		"a" },
	{ ACE_WRITE_ATTRIBUTES,		"A" },
	{ ACE_READ_NAMED_ATTRS,		"R" },
	{ ACE_WRITE_NAMED_ATTRS,	"W" },
	{ ACE_READ_ACL,			"c" },
	{ ACE_WRITE_ACL,		"C" },
	{ ACE_WRITE_OWNER,		"o" },
	{ ACE_SYNCHRONIZE,		"s" },
};

static int do_enum(smb_svcenum_t *, ofmt_handle_t);
static void ofmt_fatal(ofmt_handle_t, ofmt_field_t *, ofmt_status_t)
    __NORETURN;
static void fatal(const char *, ...) __NORETURN;

time_t		now;
boolean_t	opt_p;
boolean_t	opt_x;

int
cmd_list_sess(int argc, char **argv)
{
	ofmt_handle_t	hdl;
	smb_svcenum_t	req = {
		.se_type = SMB_SVCENUM_TYPE_USER,
		.se_level = 1,
		.se_nlimit = UINT32_MAX,
	};
	int rc;

	hdl = cmd_create_handle(argc, argv, default_user_fields,
	    smb_user_fields);
	rc = do_enum(&req, hdl);
	ofmt_close(hdl);
	return (rc);
}

int
cmd_list_trees(int argc, char **argv)
{
	ofmt_handle_t	hdl;
	smb_svcenum_t	req = {
		.se_type = SMB_SVCENUM_TYPE_TREE,
		.se_level = 1,
		.se_nlimit = UINT32_MAX,
	};
	int rc;

	hdl = cmd_create_handle(argc, argv, default_tree_fields,
	    smb_tree_fields);
	rc = do_enum(&req, hdl);
	ofmt_close(hdl);
	return (rc);
}

int
cmd_list_ofiles(int argc, char **argv)
{
	ofmt_handle_t	hdl;
	smb_svcenum_t	req = {
		.se_type = SMB_SVCENUM_TYPE_FILE,
		.se_level = 1,
		.se_nlimit = UINT32_MAX,
	};
	int rc;

	hdl = cmd_create_handle(argc, argv, default_netfileinfo_fields,
	    smb_netfileinfo_fields);
	rc = do_enum(&req, hdl);
	ofmt_close(hdl);
	return (rc);
}

static ofmt_handle_t
cmd_create_handle(int argc, char **argv, const char *def, ofmt_field_t *templ)
{
	const char	*fields = def;
	ofmt_handle_t	hdl;
	ofmt_status_t	status;
	uint_t		flags = 0;
	int		c;

	while ((c = getopt(argc, argv, "Ho:px")) != -1) {
		switch (c) {
		case 'H':
			flags |= OFMT_NOHEADER;
			break;
		case 'o':
			fields = optarg;
			break;
		case 'p':
			opt_p = B_TRUE;
			flags |= OFMT_PARSABLE;
			break;
		case 'x':
			opt_x = B_TRUE;
			break;
		case '?':
			/* Note: getopt prints an error for us. */
			return (NULL);
		}
	}

	status = ofmt_open(fields, templ, flags, 0, &hdl);
	if (status != OFMT_SUCCESS)
		ofmt_fatal(hdl, templ, status);

	return (hdl);
}

int
cmd_close_ofile(int argc, char **argv)
{
	uint_t errs = 0;

	if (argc < 2) {
		warnx(_("Missing file id"));
		return (2);
	}

	for (int i = 1; i < argc; i++) {
		unsigned long ul;
		int rc;

		errno = 0;
		ul = strtoul(argv[i], NULL, 0);
		if (errno != 0) {
			warnx(_("Invalid file id '%s'"), argv[i]);
			return (2);
		}
#ifdef _LP64
		if (ul > UINT32_MAX) {
			warnx(_("File id %lu too large"), ul);
			return (2);
		}
#endif

		/*
		 * See SMB_IOC_FILE_CLOSE (ioc.uniqid)
		 * and smb_server_file_close()
		 */
		rc = smb_kmod_file_close((uint32_t)ul);
		if (rc != 0) {
			/*
			 * Since the user can specify the fid as a decimal
			 * or hex value, we use the string they gave us so
			 * the value displayed matches what we were given.
			 */
			warnc(rc, _("Closing fid %s failed"), argv[i]);
			errs++;
		}
	}

	if (errs > 0)
		return (1);
	return (0);
}

int
cmd_close_sess(int argc, char **argv)
{
	const char *client;
	const char *user = NULL;
	int rc;

	if (argc < 2) {
		warnx(_("clientname and username missing"));
		return (2);
	}
	client = argv[1];
	if (argc > 2) {
		user = argv[2];
	}

	/*
	 * See SMB_IOC_SESSION_CLOSE (ioc.client, ioc.username)
	 * and smb_server_session_close().  The "client" part
	 * can be EITHER the "workstation" or the IP address,
	 * as shown in the "COMPUTER" and "IP" fields in the
	 * output of "list-sessions".   The (optional) "user"
	 * part is as shown in "USER" part of that output.
	 */
	rc = smb_kmod_session_close(client, user);
	if (rc != 0) {
		rc = 1;
	}
	return (rc);
}

static boolean_t
fmt_user(ofmt_arg_t *arg, char *buf, uint_t buflen)
{
	smb_netuserinfo_t	*ui = arg->ofmt_cbarg;
	user_field_t		field = (user_field_t)arg->ofmt_id;

	switch (field) {
	case UF_SESS_ID:
		(void) snprintf(buf, buflen, "%" PRIu64, ui->ui_session_id);
		break;
	case UF_DOMAIN:
		print_str(ui->ui_domain, buf, buflen);
		break;
	case UF_ACCOUNT:
		print_str(ui->ui_account, buf, buflen);
		break;
	case UF_USER:
		(void) snprintf(buf, buflen, "%s\\%s", ui->ui_domain,
		    ui->ui_account);
		break;
	case UF_UID:
		VERIFY3U(arg->ofmt_width, <, INT_MAX);
		(void) snprintf(buf, buflen, "%u", ui->ui_posix_uid);
		break;
	case UF_WORKSTATION:
		print_str(ui->ui_workstation, buf, buflen);
		break;
	case UF_IP:
		(void) smb_inet_ntop(&ui->ui_ipaddr, buf, buflen);
		break;
	case UF_OS:
		/* XXX: Lookup string value */
		(void) snprintf(buf, buflen, "%" PRId32, ui->ui_native_os);
		break;
	case UF_LOGON_TIME:
		print_time(ui->ui_logon_time, TIME_FMT, buf, buflen);
		break;
	case UF_AGE:
		print_age(now - ui->ui_logon_time, buf, buflen);
		break;
	case UF_NUMOPEN:
		print_u32(ui->ui_numopens, buf, buflen);
		break;
	case UF_FLAGS:
		print_flags(user_flag_tbl, ARRAY_SIZE(user_flag_tbl),
		    ui->ui_flags, buf, buflen);
		break;
	default:
		fatal("%s: invalid field %d", __func__, field);
	}

	return (B_TRUE);
}

static boolean_t
fmt_tree_type(uint32_t type, char *buf, uint_t buflen)
{
	switch (type & STYPE_MASK) {
	case STYPE_DISKTREE:
		(void) strlcpy(buf, "DISK", buflen);
		break;
	case STYPE_PRINTQ:
		(void) strlcpy(buf, "PRINTQ", buflen);
		break;
	case STYPE_DEVICE:
		(void) strlcpy(buf, "DEVICE", buflen);
		break;
	case STYPE_IPC:
		(void) strlcpy(buf, "IPC", buflen);
		break;
	default:
		(void) snprintf(buf, buflen, "%" PRIx32, type & STYPE_MASK);
		break;
	}

	return (B_TRUE);
}

static boolean_t
fmt_tree(ofmt_arg_t *arg, char *buf, uint_t buflen)
{
	smb_netconnectinfo_t	*nc = arg->ofmt_cbarg;
	tree_field_t		field = (tree_field_t)arg->ofmt_id;

	switch (field) {
	case TF_ID:
		(void) snprintf(buf, buflen, "%" PRIu32, nc->ci_id);
		break;
	case TF_TYPE:
		return (fmt_tree_type(nc->ci_type, buf, buflen));
	case TF_NUMOPEN:
		print_u32(nc->ci_numopens, buf, buflen);
		break;
	case TF_NUMUSERS:
		print_u32(nc->ci_numusers, buf, buflen);
		break;
	case TF_TIME:
		print_time(now - nc->ci_time, TIME_FMT, buf, buflen);
		break;
	case TF_AGE:
		print_age(nc->ci_time, buf, buflen);
		break;
	case TF_USERNAME:
		print_str(nc->ci_username, buf, buflen);
		break;
	case TF_SHARE:
		print_str(nc->ci_share, buf, buflen);
		break;
	default:
		fatal("%s: invalid field %d", __func__, field);
	}

	return (B_TRUE);
}

static boolean_t
fmt_netfileinfo(ofmt_arg_t *arg, char *buf, uint_t buflen)
{
	smb_netfileinfo_t	*fi = arg->ofmt_cbarg;
	netfileinfo_field_t	field = (netfileinfo_field_t)arg->ofmt_id;

	switch (field) {
	case NFIF_FID:
		(void) snprintf(buf, buflen, "%" PRIu16, fi->fi_fid);
		break;
	case NFIF_UNIQID:
		(void) snprintf(buf, buflen, "%" PRIu32, fi->fi_uniqid);
		break;
	case NFIF_PERMS:
		print_perms(nfi_perm_tbl, ARRAY_SIZE(nfi_perm_tbl),
		    fi->fi_permissions, buf, buflen);
		break;
	case NFIF_NUMLOCKS:
		print_u32(fi->fi_numlocks, buf, buflen);
		break;
	case NFIF_PATH:
		print_str(fi->fi_path, buf, buflen);
		break;
	case NFIF_USERNAME:
		print_str(fi->fi_username, buf, buflen);
		break;
	default:
		fatal("%s: invalid field %d", __func__, field);
	}

	return (B_TRUE);
}

static int
do_enum(smb_svcenum_t *req, ofmt_handle_t hdl)
{
	smb_netsvc_t		*ns;
	smb_netsvcitem_t	*item;
	uint32_t		n = 0;
	int rc;

	if (hdl == NULL)
		return (2);	/* exit (2) -- usage */
	now = time(NULL);

	for (;;) {
		req->se_nskip = n;

		ns = smb_kmod_enum_init(req);
		if (ns == NULL) {
			warnx(_("SMB enum initialization failure"));
			return (1);
		}

		rc = smb_kmod_enum(ns);
		if (rc != 0) {
			/*
			 * When the SMB service is not running, expect ENXIO.
			 */
			if (rc == ENXIO) {
				warnx(_("Kernel SMB server not running"));
				return (1);
			}
			warnc(rc, _("SMB enumeration call failed"));
			return (1);
		}

		if (list_is_empty(&ns->ns_list))
			break;

		for (item = list_head(&ns->ns_list); item != NULL;
		    item = list_next(&ns->ns_list, item)) {
			ofmt_print(hdl, &item->nsi_un);
			n++;
		}

		smb_kmod_enum_fini(ns);
	}
	return (0);
}

static void
print_str(const char *restrict src, char *restrict buf, uint_t buflen)
{
	if (src == NULL) {
		buf[0] = '\0';
		return;
	}
	(void) strlcpy(buf, src, buflen);
}

static void
print_u32(uint32_t val, char *buf, uint_t buflen)
{
	const char *fmt = opt_p ? "%" PRIu32 : "%'" PRIu32;

	(void) snprintf(buf, buflen, fmt, val);
}

static void
print_age(time_t amt, char *buf, uint_t buflen)
{
	uint32_t days = 0, hours = 0, mins = 0;

	if (opt_p) {
		(void) snprintf(buf, buflen, "%" PRId64, (int64_t)amt);
		return;
	}

	if (amt >= DAYS) {
		days = amt / DAYS;
		amt %= DAYS;
	}
	if (amt >= HRS) {
		hours = amt / HRS;
		amt %= HRS;
	}
	if (amt >= MINS) {
		mins = amt / MINS;
		amt %= MINS;
	}

	if (days > 0) {
		int n = snprintf(buf, buflen, "%" PRIu32 " days%s",
		    days, (hours > 0 || mins > 0 || amt > 0) ? ", " : "");

		VERIFY3U(buflen, >, n);

		buf += n;
		buflen -= n;
	}

	(void) snprintf(buf, buflen, "%02" PRIu32 ":%02" PRIu32 ":%02" PRIu32,
	    hours, mins, amt);
}

static void
print_time(time_t when, const char *fmt, char *buf, uint_t buflen)
{
	const struct tm *tm;

	if (opt_p) {
		(void) snprintf(buf, buflen, "%" PRId64, (int64_t)when);
		return;
	}

	tm = localtime(&when);
	(void) strftime(buf, buflen - 1, fmt, tm);
}

static void
print_flags(struct flag_tbl *tbl, size_t nent, uint32_t val, char *buf,
    uint_t buflen)
{
	uint_t n = 0;
	uint_t i;

	if (opt_x) {
		(void) snprintf(buf, buflen, "%" PRIx32, val);
		return;
	}

	for (i = 0; i < nent; i++) {
		if ((val & tbl[i].flag) == 0)
			continue;
		if (n > 0)
			(void) strlcat(buf, ",", buflen);
		(void) strlcat(buf, tbl[i].name, buflen);
		n++;
	}

	if (n == 0)
		(void) strlcat(buf, "-", buflen);
}

static void
print_perms(struct flag_tbl *tbl, size_t nent, uint32_t val, char *buf,
    uint_t buflen)
{
	uint_t n = 0;
	uint_t i;

	if (opt_x) {
		(void) snprintf(buf, buflen, "%" PRIx32, val);
		return;
	}

	for (i = 0; i < nent; i++) {
		if ((val & tbl[i].flag) == 0) {
			(void) strlcat(buf, "-", buflen);
		} else {
			(void) strlcat(buf, tbl[i].name, buflen);
		}
		n++;
	}
}

__NORETURN static void
ofmt_fatal(ofmt_handle_t hdl, ofmt_field_t *templ, ofmt_status_t status)
{
	char buf[OFMT_BUFSIZE];
	char *msg = ofmt_strerror(hdl, status, buf, sizeof (buf));

	warnx(_("ofmt error: %s"), msg);

	if (status == OFMT_EBADFIELDS ||
	    status == OFMT_ENOFIELDS) {
		ofmt_field_t *f = templ;
		fprintf(stderr, _("Valid fields are: "));
		while (f->of_name != NULL) {
			fprintf(stderr, "%s", f->of_name);
			f++;
			if (f->of_name != NULL)
				fprintf(stderr, ",");
		}
		fprintf(stderr, "\n");
	}

	exit(EXIT_FAILURE);
}

__NORETURN static void
fatal(const char *msg, ...)
{
	char buf[128];
	va_list ap;
	size_t len;

	va_start(ap, msg);
	(void) vsnprintf(buf, sizeof (buf), msg, ap);
	va_end(ap);

	len = strlen(buf);
	upanic(buf, len);
}
