/*
**  Copyright (c) 2010-2015, The Trusted Domain Project.  All rights reserved.
**
**  Copyright (c) 2026, PhoenixDKIM contributors. All rights reserved.
*/

#include "build-config.h"

/* system includes */
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <pwd.h>

/* openssl includes */
#include <openssl/pem.h>
#include <openssl/evp.h>
#include <openssl/bio.h>

#ifndef FALSE
# define FALSE		0
#endif /* ! FALSE */
#ifndef TRUE
# define TRUE		1
#endif /* ! TRUE */
#ifndef MIN
# define MIN(x,y)	((x) < (y) ? (x) : (y))
#endif /* ! MIN */

/* libopendkim includes */
#include <dkim.h>

/* opendkim includes */
#include "opendkim-db.h"
#include "config.h"
#include "opendkim-config.h"

/* definitions */
#define	BUFRSZ		1024
#define	CMDLINEOPTS	"C:d:DE:Fo:N:r:R:sSt:T:uvx:"
#define	DEFCONFFILE	CONFIG_BASE "/opendkim.conf"
#define	DEFEXPIRE	604800
#define	DEFREFRESH	10800
#define	DEFRETRY	1800
#define	DEFTTL		86400
#define	DKIMZONE	"._domainkey"
#define	HOSTMASTER	"hostmaster"
#define	LARGEBUFRSZ	8192
#define	MARGIN		75
#define	MAXNS		16

/* globals */
char *progname;

/*
**  STRFLEN -- determine length of a formatted string
**
**  Parameters:
**  	str -- string of interest
**
**  Return value:
**  	Rendered width (i.e. expand tabs, etc.).
*/

static int
strflen(char *str)
{
	int olen = 0;
	char *p;

	assert(str != NULL);

	for (p = str; *p != '\0'; p++)
	{
		if (*p == '\t')
			olen += 8 - (olen % 8);
		else
			olen++;
	}

	return olen;
}
	
/*
**  LOADKEY -- resolve a key
**
**  Parameters:
**  	buf -- key buffer
**  	buflen -- pointer to key buffer's length (updated)
**
**  Return value:
**  	TRUE on successful load, false otherwise
*/

static int
loadkey(char *buf, size_t *buflen)
{
	assert(buf != NULL);
	assert(buflen != NULL);

	if (buf[0] == '/' || (buf[0] == '.' && buf[1] == '/') ||
	    (buf[0] == '.' && buf[1] == '.' && buf[2] == '/'))
	{
		int fd;
		int status;
		ssize_t rlen;
		struct stat s;

		fd = open(buf, O_RDONLY);
		if (fd < 0)
			return FALSE;

		status = fstat(fd, &s);
		if (status != 0)
		{
			close(fd);
			return FALSE;
		}

		*buflen = MIN((size_t) s.st_size, *buflen);
		rlen = read(fd, buf, *buflen);
		close(fd);

		/* read() returns -1 on error; treat short reads (or any
		 * negative rlen) as failure rather than silently casting */
		if (rlen < 0 || (size_t) rlen < *buflen)
			return FALSE;
	}
	else
	{
		*buflen = strlen(buf);
	}

	return TRUE;
}

/*
**  DESPACE -- remove spaces from a string
**
**  Parameters:
**  	str -- string to update
**
**  Return value:
**  	None.
*/

static void
despace(char *str)
{
	char *p;
	char *q;

	assert(str != NULL);

	for (p = str, q = str; ; p++)
	{
		if (isascii(*p) && isspace(*p))
			continue;
		else
			*q++ = *p;
		if (*p == '\0')
			break;
	}
}
	
/*
**  USAGE -- print usage message and exit
**
**  Parameters:
**  	None.
**
**  Return value:
**  	EX_USAGE
*/

static int
usage(void)
{
	fprintf(stderr, "%s: usage: %s [opts] [dataset]\n"
	                "\t-C user@host\tcontact address to include in SOA\n"
	                "\t-d domain   \twrite keys for named domain only\n"
	                "\t-D          \tinclude '._domainkey' suffix\n"
	                "\t-E secs     \tuse specified expiration time in SOA\n"
	                "\t-F          \tinclude '._domainkey' suffix and domainname\n"
	                "\t-o file     \toutput file\n"
	                "\t-N ns[,...] \tlist NS records\n"
	                "\t-r secs     \tuse specified refresh time in SOA\n"
	                "\t-R secs     \tuse specified retry time in SOA\n"
	                "\t-s          \twith -d, also match subdomains\n"
	                "\t-S          \twrite an SOA record\n"
	                "\t-t secs     \tuse specified per-record TTL\n"
	                "\t-T secs     \tuse specified default TTL in SOA\n"
	                "\t-u          \tproduce output suitable for use by \"nsupdate\"\n"
	                "\t-v          \tverbose output\n"
	                "\t-x file     \tconfiguration file\n",
		progname, progname);

	return EX_USAGE;
}

/*
**  MAIN -- program mainline
**
**  Parameters:
**  	argc, argv -- the usual
**
**  Return value:
**  	Exit status.
*/

int
main(int argc, char **argv)
{
	_Bool seenlf;
	_Bool nsupdate = FALSE;
	_Bool suffix = FALSE;
	_Bool fqdnsuffix = FALSE;
	_Bool subdomains = FALSE;
	_Bool writesoa = FALSE;
	int c;
	int status;
	int verbose = 0;
	int olen;
	int ttl = -1;
	int defttl = DEFTTL;
	int expire = DEFEXPIRE;
	int refresh = DEFREFRESH;
	int retry = DEFRETRY;
	int nscount = 0;
	long len;
	time_t now;
	size_t keylen;
	size_t domain_len;
	size_t onlydomain_len;
	char *p;
	char *dataset = NULL;
	char *outfile = NULL;
	char *onlydomain = NULL;
	char *contact = NULL;
	char *nameservers = NULL;
	const char *configfile = NULL;
	const char *err = NULL;
	char *nslist[MAXNS];
	FILE *out;
	BIO *private;
	BIO *outbio = NULL;
	EVP_PKEY *pkey;
	DKIMF_DB db;
	char keyname[BUFRSZ + 1];
	char domain[BUFRSZ + 1];
	char selector[BUFRSZ + 1];
	char tmpbuf[LARGEBUFRSZ + 1];
	char hostname[DKIM_MAXHOSTNAMELEN + 1];
	char keydata[LARGEBUFRSZ];
	char derdata[LARGEBUFRSZ];
	struct dkimf_db_data dbd[3];

	progname = (p = strrchr(argv[0], '/')) == NULL ? argv[0] : p + 1;

	while ((c = getopt(argc, argv, CMDLINEOPTS)) != -1)
	{
		switch (c)
		{
		  case 'C':
			contact = strdup(optarg);
			break;

		  case 'd':
			onlydomain = optarg;
			break;

		  case 'D':
			suffix = TRUE;
			break;

		  case 'E':
			expire = strtol(optarg, &p, 10);
			if (*p != '\0' || expire < 0)
			{
				fprintf(stderr, "%s: invalid expire value\n",
				        progname);
				return EX_USAGE;
			}
			break;

		  case 'F':
			suffix = TRUE;
			fqdnsuffix = TRUE;
			break;

		  case 'N':
			nameservers = strdup(optarg);
			break;

		  case 'o':
			outfile = optarg;
			break;

		  case 'r':
			refresh = strtol(optarg, &p, 10);
			if (*p != '\0' || refresh < 0)
			{
				fprintf(stderr, "%s: invalid refresh value\n",
				        progname);
				return EX_USAGE;
			}
			break;

		  case 'R':
			retry = strtol(optarg, &p, 10);
			if (*p != '\0' || retry < 0)
			{
				fprintf(stderr, "%s: invalid retry value\n",
				        progname);
				return EX_USAGE;
			}
			break;

		  case 's':
			subdomains = TRUE;
			break;

		  case 'S':
			writesoa = TRUE;
			break;

		  case 't':
			ttl = strtol(optarg, &p, 10);
			if (*p != '\0' || ttl < 0)
			{
				fprintf(stderr, "%s: invalid TTL value\n",
				        progname);
				return EX_USAGE;
			}
			break;

		  case 'T':
			defttl = strtol(optarg, &p, 10);
			if (*p != '\0' || defttl < 0)
			{
				fprintf(stderr,
				        "%s: invalid default TTL value\n",
				        progname);
				return EX_USAGE;
			}
			break;

		  case 'u':
			nsupdate = TRUE;
			break;

		  case 'v':
			verbose++;
			break;

		  case 'x':
			configfile = optarg;
			break;

		  default:
			return usage();
		}
	}

	/* sanity check */
	if (subdomains && onlydomain == NULL)
	{
		fprintf(stderr, "%s: subdomain matching requires a domain\n",
		        progname);
		return EX_USAGE;
	}

	if (optind != argc)
		dataset = argv[optind];

	/* process config file */
	if (configfile == NULL && access(DEFCONFFILE, R_OK) == 0)
		configfile = DEFCONFFILE;
	if (configfile != NULL)
	{
		u_int line = 0;
		struct config *cfg;
		char path[MAXPATHLEN + 1];

		cfg = config_load(configfile, dkimf_config,
		                  &line, path, sizeof path, NULL);

		if (cfg == NULL)
		{
			fprintf(stderr,
			        "%s: %s: configuration error at line %u\n",
			        progname, path, line);
			return EX_CONFIG;
		}

		if (dataset == NULL)
		{
			(void) config_get(cfg, "KeyTable",
			                  &dataset, sizeof dataset);
		}
	}

	if (dataset == NULL)
		return usage();

	outbio = BIO_new(BIO_s_mem());
	if (outbio == NULL)
	{
		fprintf(stderr, "%s: BIO_new() failed\n", progname);
		return 1;
	}

	status = dkimf_db_open(&db, dataset, DKIMF_DB_FLAG_READONLY,
	                       NULL, &err);
	if (status != 0)
	{
		fprintf(stderr, "%s: dkimf_db_open(): %s\n", progname, err);
		(void) BIO_free(outbio);
		return 1;
	}

	if (dkimf_db_type(db) == DKIMF_DB_TYPE_REFILE)
	{
		fprintf(stderr, "%s: invalid data set type\n", progname);
		(void) BIO_free(outbio);
		(void) dkimf_db_close(db);
		return 1;
	}

	if (verbose > 0)
		fprintf(stderr, "%s: database opened\n", progname);

	if (outfile != NULL)
	{
		out = fopen(outfile, "w");
		if (out == NULL)
		{
			fprintf(stderr, "%s: %s: fopen(): %s\n",
			        progname, outfile, strerror(errno));
			(void) dkimf_db_close(db);
			(void) BIO_free(outbio);
			return 1;
		}
	}
	else
	{
		out = stdout;
	}

	if (nameservers != NULL)
	{
		for (p = strtok(nameservers, ",");
		     p != NULL && nscount < MAXNS;
		     p = strtok(NULL, ","))
			nslist[nscount++] = p;
	}

	memset(hostname, '\0', sizeof hostname);
	gethostname(hostname, sizeof hostname);

	if (nscount == 0)
		nslist[nscount++] = hostname;

	(void) time(&now);

	if (!nsupdate)
	{
		fprintf(out, "; DKIM public key zone data\n");
		if (onlydomain != NULL)
			fprintf(out, "; for %s\n", onlydomain);
	}

	if (writesoa && !nsupdate)
	{
		struct tm *tm;

		fprintf(out, "@\tIN\tSOA\t%s\t", nslist[0]);

		if (contact != NULL)
		{
			for (p = contact; *p != '\0'; p++)
			{
				if (*p == '@')
					*p = '.';
			}

			fprintf(out, "%s", contact);
		}
		else
		{
			struct passwd *pwd;

			pwd = getpwuid(getuid());

			fprintf(out, "%s.%s",
			        pwd == NULL ? HOSTMASTER : pwd->pw_name,
			        hostname);
		}

		tm = localtime(&now);

		fprintf(out,
		        "\t (\n"
		        "\t%04d%02d%02d%02d   ; Serial (yyyymmddhh)\n"
		        "\t%-10d   ; Refresh\n"
		        "\t%-10d   ; Retry\n"
		        "\t%-10d   ; Expire\n"
		        "\t%-10d ) ; Default\n\n",
		        tm->tm_year + 1900,
		        tm->tm_mon + 1,
		        tm->tm_mday,
		        tm->tm_hour,
		        refresh, retry, expire, defttl);
	}

	if (nameservers != NULL && !nsupdate)
	{
		for (c = 0; c < nscount; c++)
			fprintf(out, "\tIN\tNS\t%s\n", nslist[c]);

		fprintf(out, "\n");
	}

	if (nsupdate)
		fprintf(out, "server %s\n", nslist[0]);

	dbd[0].dbdata_buffer = domain;
	dbd[1].dbdata_buffer = selector;
	dbd[2].dbdata_buffer = keydata;

	for (c = 0; ; c++)
	{
		memset(keyname, '\0', sizeof keyname);
		memset(domain, '\0', sizeof domain);
		memset(selector, '\0', sizeof selector);
		memset(keydata, '\0', sizeof keydata);

		dbd[0].dbdata_buflen = sizeof domain;
		dbd[1].dbdata_buflen = sizeof selector;
		dbd[2].dbdata_buflen = sizeof keydata;

		keylen = sizeof keyname;

		status = dkimf_db_walk(db, c == 0, keyname, &keylen, dbd, 3);
		if (status == -1)
		{
			char errbuf[BUFRSZ];

			dkimf_db_strerror(db, errbuf, sizeof errbuf);
			fprintf(stderr, "%s: dkimf_db_walk(%d) failed: %s\n",
			        progname, c, errbuf);
			(void) dkimf_db_close(db);
			(void) BIO_free(outbio);
			return 1;
		}
		else if (status == 1)
		{
			break;
		}

		if (subdomains)
		{
			domain_len = strlen(domain);
			onlydomain_len = strlen(onlydomain);

			if (!((domain_len == onlydomain_len &&
			       strcasecmp(domain, onlydomain) == 0) ||
			      (domain_len > onlydomain_len &&
			       domain[domain_len - onlydomain_len - 1] == '.' &&
			       strcasecmp(domain + domain_len - onlydomain_len,
			                  onlydomain) == 0)))
			{
				fprintf(stderr, "%s: record %d for '%s' skipped\n",
				        progname, c, keyname);

				continue;
			}
		}
		else if (onlydomain != NULL &&
		         strcasecmp(domain, onlydomain) != 0)
		{
			fprintf(stderr, "%s: record %d for '%s' skipped\n",
			        progname, c, keyname);

			continue;
		}

		if (verbose > 1)
		{
			fprintf(stderr, "%s: record %d for '%s' retrieved\n",
			        progname, c, keyname);
		}

		keylen = sizeof keydata;
		if (!loadkey(keydata, &keylen))
		{
			fprintf(stderr, "%s: key for '%s' load failed\n",
			        progname, keyname);
			(void) dkimf_db_close(db);
			(void) BIO_free(outbio);
			return 1;
		}

		if (verbose > 1)
		{
			fprintf(stderr, "%s: key for '%s' loaded\n",
			        progname, keyname);
		}

		/* create a BIO for the private key */
		if (strncmp(keydata, "-----", 5) == 0)
		{
			private = BIO_new_mem_buf(keydata, keylen);
			if (private == NULL)
			{
				fprintf(stderr,
				        "%s: BIO_new_mem_buf() failed\n",
				        progname);
				(void) dkimf_db_close(db);
				(void) BIO_free(outbio);
				return 1;
			}

			pkey = PEM_read_bio_PrivateKey(private, NULL,
			                               NULL, NULL);
			if (pkey == NULL)
			{
				fprintf(stderr,
				        "%s: PEM_read_bio_PrivateKey() failed\n",
				        progname);
				(void) dkimf_db_close(db);
				(void) BIO_free(private);
				(void) BIO_free(outbio);
				return 1;
			}
		}
		else
		{
			int inlen;
			int outlen;
			BIO *b64;
			BIO *bio;
			BIO *decode;
			char buf[BUFRSZ];

			despace(keydata);

			b64 = BIO_new(BIO_f_base64());
			BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
			bio = BIO_new_mem_buf(keydata, -1);
			bio = BIO_push(b64, bio);

			decode = BIO_new(BIO_s_mem());

			for (;;)
			{
				inlen = BIO_read(bio, buf, sizeof buf);
				if (inlen == 0)
					break;
				BIO_write(decode, buf, inlen);
			}

			BIO_flush(decode);

			outlen = BIO_get_mem_data(decode, &p);
			memcpy(derdata, p, MIN(sizeof derdata, (size_t) outlen));

			BIO_free_all(b64);
			BIO_free(decode);

			private = BIO_new_mem_buf(derdata, outlen);
			if (private == NULL)
			{
				fprintf(stderr,
				        "%s: BIO_new_mem_buf() failed\n",
				        progname);
				(void) dkimf_db_close(db);
				(void) BIO_free(outbio);
				return 1;
			}

			pkey = d2i_PrivateKey_bio(private, NULL);
			if (pkey == NULL)
			{
				fprintf(stderr,
				        "%s: d2i_PrivateKey_bio() failed\n",
				        progname);
				(void) dkimf_db_close(db);
				(void) BIO_free(private);
				(void) BIO_free(outbio);
				return 1;
			}
		}

		/* convert private to public */
		status = PEM_write_bio_PUBKEY(outbio, pkey);
		if (status == 0)
		{
			fprintf(stderr,
			        "%s: PEM_write_bio_PUBKEY() failed\n",
			        progname);
			(void) dkimf_db_close(db);
			(void) BIO_free(private);
			(void) EVP_PKEY_free(pkey);
			(void) BIO_free(outbio);
			return 1;
		}

		/* write the record */
		if (nsupdate)
		{
			fprintf(out, "zone %s\n", domain);

			snprintf(tmpbuf, sizeof tmpbuf,
			         "update add %s%s%s%s%s %d TXT \"v=DKIM1; k=rsa; p=",
			         selector, suffix ? DKIMZONE : "",
			         fqdnsuffix ? "." : "",
			         fqdnsuffix ? domain : "",
			         fqdnsuffix ? "." : "",
			         ttl == -1 ? defttl : ttl);
		}
		else
		{
			if (ttl == -1)
			{
				snprintf(tmpbuf, sizeof tmpbuf,
				         "%s%s%s%s%s\tIN\tTXT\t( \"v=DKIM1; k=rsa; p=",
				         selector, suffix ? DKIMZONE : "",
				         fqdnsuffix ? "." : "",
				         fqdnsuffix ? domain : "",
				         fqdnsuffix ? "." : "");
			}
			else
			{
				snprintf(tmpbuf, sizeof tmpbuf,
				         "%s%s%s%s%s\t%d\tIN\tTXT\t( \"v=DKIM1; k=rsa; p=",
				         selector, suffix ? DKIMZONE : "",
				         fqdnsuffix ? "." : "",
				         fqdnsuffix ? domain : "",
				         fqdnsuffix ? "." : "",
				         ttl);
			}
		}

		fprintf(out, "%s", tmpbuf);

		if (nsupdate)
			olen = 0;
		else
			olen = strflen(tmpbuf);

		seenlf = FALSE;

		for (len = BIO_get_mem_data(outbio, &p); len > 0; len--, p++)
		{
			if (*p == '\n')
			{
				seenlf = TRUE;
			}
			else if (seenlf && *p == '-')
			{
				break;
			}
			else if (!seenlf)
			{
				continue;
			}
			else if (isascii(*p) && !isspace(*p))
			{
				if (olen >= MARGIN && !nsupdate)
				{
					fprintf(out, "\"\n\t\"");
					olen = 9;
				}
				else if (olen >= 255 && nsupdate)
				{
					fprintf(out, "\" \"");
					olen = 0;
				}

				(void) fputc(*p, out);
				olen++;
			}
		}

		if (nsupdate)
			fprintf(out, "\"\n");
		else
			fprintf(out, "\" )\n");

		/* prepare for the next one */
		(void) BIO_reset(outbio);
	}

	(void) BIO_flush(outbio);
	(void) BIO_free(outbio);
	(void) dkimf_db_close(db);

	if (nsupdate)
		fprintf(out, "send\nanswer\n");

	if (out != stdout)
		fclose(out);

	if (verbose > 0)
	{
		fprintf(stdout, "%s: %d record%s written\n",
		        progname, c, c == 1 ? "" : "s");
	}

	return 0;
}
