1/*	$OpenBSD: sendbug.c,v 1.80 2022/04/10 17:47:54 jca Exp $	*/
2
3/*
4 * Written by Ray Lai <ray@cyth.net>.
5 * Public domain.
6 */
7
8#include <sys/types.h>
9#include <sys/stat.h>
10#include <sys/sysctl.h>
11#include <sys/wait.h>
12
13#include <ctype.h>
14#include <err.h>
15#include <errno.h>
16#include <fcntl.h>
17#include <limits.h>
18#include <paths.h>
19#include <pwd.h>
20#include <signal.h>
21#include <stdio.h>
22#include <stdlib.h>
23#include <string.h>
24#include <unistd.h>
25
26#include "atomicio.h"
27
28#define _PATH_DMESG "/var/run/dmesg.boot"
29#define DMESG_START "OpenBSD "
30#define BEGIN64 "begin-base64 "
31#define END64 "===="
32
33void	checkfile(const char *);
34void	debase(void);
35void	dmesg(FILE *);
36int	editit(const char *);
37void	hwdump(FILE *);
38void	init(void);
39int	matchline(const char *, const char *, size_t);
40int	prompt(void);
41int	send_file(const char *, int);
42int	sendmail(const char *);
43void	template(FILE *);
44void	usbdevs(FILE *);
45
46const char *categories = "system user library documentation kernel "
47    "alpha aarch64 amd64 arm hppa i386 m88k mips64 mips64el powerpc powerpc64 "
48    "riscv64 sh sparc64";
49const char *comment[] = {
50	"<synopsis of the problem (one line)>",
51	"<PR category (one line)>",
52	"<precise description of the problem (multiple lines)>",
53	"<code/input/activities to reproduce the problem (multiple lines)>",
54	"<how to correct or work around the problem, if known (multiple lines)>"
55};
56
57struct passwd *pw;
58char os[BUFSIZ], rel[BUFSIZ], mach[BUFSIZ], details[BUFSIZ];
59const char *tmpdir = _PATH_TMP;
60char *tmppath;
61int Dflag, Pflag, wantcleanup;
62
63__dead void
64usage(void)
65{
66	extern char *__progname;
67
68	fprintf(stderr, "usage: %s [-DEP]\n", __progname);
69	exit(1);
70}
71
72void
73cleanup()
74{
75	if (wantcleanup && tmppath && unlink(tmppath) == -1)
76		warn("unlink");
77}
78
79
80int
81main(int argc, char *argv[])
82{
83	int ch, c, fd, ret = 1;
84	struct stat sb;
85	char *pr_form;
86	time_t mtime;
87	FILE *fp;
88
89	if (pledge("stdio rpath wpath cpath tmppath getpw proc exec", NULL) == -1)
90		err(1, "pledge");
91
92	while ((ch = getopt(argc, argv, "DEP")) != -1)
93		switch (ch) {
94		case 'D':
95			Dflag = 1;
96			break;
97		case 'E':
98			debase();
99			exit(0);
100		case 'P':
101			Pflag = 1;
102			break;
103		default:
104			usage();
105		}
106	argc -= optind;
107	argv += optind;
108
109	if (argc > 0)
110		usage();
111
112	if (Pflag) {
113		init();
114		template(stdout);
115		exit(0);
116	}
117
118	if (asprintf(&tmppath, "%s%sp.XXXXXXXXXX", tmpdir,
119	    tmpdir[strlen(tmpdir) - 1] == '/' ? "" : "/") == -1)
120		err(1, "asprintf");
121	if ((fd = mkstemp(tmppath)) == -1)
122		err(1, "mkstemp");
123	wantcleanup = 1;
124	atexit(cleanup);
125	if ((fp = fdopen(fd, "w+")) == NULL)
126		err(1, "fdopen");
127
128	init();
129
130	pr_form = getenv("PR_FORM");
131	if (pr_form) {
132		char buf[BUFSIZ];
133		size_t len;
134		FILE *frfp;
135
136		frfp = fopen(pr_form, "r");
137		if (frfp == NULL) {
138			warn("can't seem to read your template file "
139			    "(`%s'), ignoring PR_FORM", pr_form);
140			template(fp);
141		} else {
142			while (!feof(frfp)) {
143				len = fread(buf, 1, sizeof buf, frfp);
144				if (len == 0)
145					break;
146				if (fwrite(buf, 1, len, fp) != len)
147					break;
148			}
149			fclose(frfp);
150		}
151	} else
152		template(fp);
153
154	if (fflush(fp) == EOF || fstat(fd, &sb) == -1 || fclose(fp) == EOF)
155		err(1, "error creating template");
156	mtime = sb.st_mtime;
157
158 edit:
159	if (editit(tmppath) == -1)
160		err(1, "error running editor");
161
162	if (stat(tmppath, &sb) == -1)
163		err(1, "stat");
164	if (mtime == sb.st_mtime)
165		errx(1, "report unchanged, nothing sent");
166
167 prompt:
168	checkfile(tmppath);
169	c = prompt();
170	switch (c) {
171	case 'a':
172	case EOF:
173		wantcleanup = 0;
174		errx(1, "unsent report in %s", tmppath);
175	case 'e':
176		goto edit;
177	case 's':
178		if (sendmail(tmppath) == -1)
179			goto quit;
180		break;
181	default:
182		goto prompt;
183	}
184
185	ret = 0;
186quit:
187	return (ret);
188}
189
190void
191dmesg(FILE *fp)
192{
193	char buf[BUFSIZ];
194	FILE *dfp;
195	off_t offset = -1;
196
197	dfp = fopen(_PATH_DMESG, "r");
198	if (dfp == NULL) {
199		warn("can't read dmesg");
200		return;
201	}
202
203	/* Find last dmesg. */
204	for (;;) {
205		off_t o;
206
207		o = ftello(dfp);
208		if (fgets(buf, sizeof(buf), dfp) == NULL)
209			break;
210		if (!strncmp(DMESG_START, buf, sizeof(DMESG_START) - 1))
211			offset = o;
212	}
213	if (offset != -1) {
214		size_t len;
215
216		clearerr(dfp);
217		fseeko(dfp, offset, SEEK_SET);
218		while (offset != -1 && !feof(dfp)) {
219			len = fread(buf, 1, sizeof buf, dfp);
220			if (len == 0)
221				break;
222			if (fwrite(buf, 1, len, fp) != len)
223				break;
224		}
225	}
226	fclose(dfp);
227}
228
229void
230usbdevs(FILE *ofp)
231{
232	char buf[BUFSIZ];
233	FILE *ifp;
234	size_t len;
235
236	if ((ifp = popen("usbdevs -v", "r")) != NULL) {
237		while (!feof(ifp)) {
238			len = fread(buf, 1, sizeof buf, ifp);
239			if (len == 0)
240				break;
241			if (fwrite(buf, 1, len, ofp) != len)
242				break;
243		}
244		pclose(ifp);
245	}
246}
247
248/*
249 * Execute an editor on the specified pathname, which is interpreted
250 * from the shell.  This means flags may be included.
251 *
252 * Returns -1 on error, or the exit value on success.
253 */
254int
255editit(const char *pathname)
256{
257	char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p;
258	sig_t sighup, sigint, sigquit, sigchld;
259	pid_t pid;
260	int saved_errno, st, ret = -1;
261
262	ed = getenv("VISUAL");
263	if (ed == NULL || ed[0] == '\0')
264		ed = getenv("EDITOR");
265	if (ed == NULL || ed[0] == '\0')
266		ed = _PATH_VI;
267	if (asprintf(&p, "%s %s", ed, pathname) == -1)
268		return (-1);
269	argp[2] = p;
270
271	sighup = signal(SIGHUP, SIG_IGN);
272	sigint = signal(SIGINT, SIG_IGN);
273	sigquit = signal(SIGQUIT, SIG_IGN);
274	sigchld = signal(SIGCHLD, SIG_DFL);
275	if ((pid = fork()) == -1)
276		goto fail;
277	if (pid == 0) {
278		execv(_PATH_BSHELL, argp);
279		_exit(127);
280	}
281	while (waitpid(pid, &st, 0) == -1) {
282		if (errno != EINTR)
283			goto fail;
284	}
285	if (!WIFEXITED(st))
286		errno = EINTR;
287	else
288		ret = WEXITSTATUS(st);
289
290 fail:
291	saved_errno = errno;
292	(void)signal(SIGHUP, sighup);
293	(void)signal(SIGINT, sigint);
294	(void)signal(SIGQUIT, sigquit);
295	(void)signal(SIGCHLD, sigchld);
296	free(p);
297	errno = saved_errno;
298	return (ret);
299}
300
301int
302prompt(void)
303{
304	int c, ret;
305
306	fpurge(stdin);
307	fprintf(stderr, "a)bort, e)dit, or s)end: ");
308	fflush(stderr);
309	ret = getchar();
310	if (ret == EOF || ret == '\n')
311		return (ret);
312	do {
313		c = getchar();
314	} while (c != EOF && c != '\n');
315	return (ret);
316}
317
318int
319sendmail(const char *pathname)
320{
321	int filedes[2];
322	pid_t pid;
323
324	if (pipe(filedes) == -1) {
325		warn("pipe: unsent report in %s", pathname);
326		return (-1);
327	}
328	switch ((pid = fork())) {
329	case -1:
330		warn("fork error: unsent report in %s",
331		    pathname);
332		return (-1);
333	case 0:
334		close(filedes[1]);
335		if (dup2(filedes[0], STDIN_FILENO) == -1) {
336			warn("dup2 error: unsent report in %s",
337			    pathname);
338			return (-1);
339		}
340		close(filedes[0]);
341		execl(_PATH_SENDMAIL, "sendmail",
342		    "-oi", "-t", (char *)NULL);
343		warn("sendmail error: unsent report in %s",
344		    pathname);
345		return (-1);
346	default:
347		close(filedes[0]);
348		/* Pipe into sendmail. */
349		if (send_file(pathname, filedes[1]) == -1) {
350			warn("send_file error: unsent report in %s",
351			    pathname);
352			return (-1);
353		}
354		close(filedes[1]);
355		while (waitpid(pid, NULL, 0) == -1) {
356			if (errno != EINTR)
357				break;
358		}
359		break;
360	}
361	return (0);
362}
363
364void
365init(void)
366{
367	size_t len;
368	int sysname[2];
369	char *cp;
370
371	if ((pw = getpwuid(getuid())) == NULL)
372		err(1, "getpwuid");
373
374	sysname[0] = CTL_KERN;
375	sysname[1] = KERN_OSTYPE;
376	len = sizeof(os) - 1;
377	if (sysctl(sysname, 2, &os, &len, NULL, 0) == -1)
378		err(1, "sysctl");
379
380	sysname[0] = CTL_KERN;
381	sysname[1] = KERN_OSRELEASE;
382	len = sizeof(rel) - 1;
383	if (sysctl(sysname, 2, &rel, &len, NULL, 0) == -1)
384		err(1, "sysctl");
385
386	sysname[0] = CTL_KERN;
387	sysname[1] = KERN_VERSION;
388	len = sizeof(details) - 1;
389	if (sysctl(sysname, 2, &details, &len, NULL, 0) == -1)
390		err(1, "sysctl");
391
392	cp = strchr(details, '\n');
393	if (cp) {
394		cp++;
395		if (*cp)
396			*cp++ = '\t';
397		if (*cp)
398			*cp++ = '\t';
399		if (*cp)
400			*cp++ = '\t';
401	}
402
403	sysname[0] = CTL_HW;
404	sysname[1] = HW_MACHINE;
405	len = sizeof(mach) - 1;
406	if (sysctl(sysname, 2, &mach, &len, NULL, 0) == -1)
407		err(1, "sysctl");
408}
409
410int
411send_file(const char *file, int dst)
412{
413	size_t len;
414	char *buf, *lbuf;
415	FILE *fp;
416	int rval = -1, saved_errno;
417
418	if ((fp = fopen(file, "r")) == NULL)
419		return (-1);
420	lbuf = NULL;
421	while ((buf = fgetln(fp, &len))) {
422		if (buf[len - 1] == '\n') {
423			buf[len - 1] = '\0';
424			--len;
425		} else {
426			/* EOF without EOL, copy and add the NUL */
427			if ((lbuf = malloc(len + 1)) == NULL)
428				goto end;
429			memcpy(lbuf, buf, len);
430			lbuf[len] = '\0';
431			buf = lbuf;
432		}
433
434		/* Skip lines starting with "SENDBUG". */
435		if (strncmp(buf, "SENDBUG", sizeof("SENDBUG") - 1) == 0)
436			continue;
437		while (len) {
438			char *sp = NULL, *ep = NULL;
439			size_t copylen;
440
441			if ((sp = strchr(buf, '<')) != NULL) {
442				size_t i;
443
444				for (i = 0; i < sizeof(comment) / sizeof(*comment); ++i) {
445					size_t commentlen = strlen(comment[i]);
446
447					if (strncmp(sp, comment[i], commentlen) == 0) {
448						ep = sp + commentlen - 1;
449						break;
450					}
451				}
452			}
453			/* Length of string before comment. */
454			if (ep)
455				copylen = sp - buf;
456			else
457				copylen = len;
458			if (atomicio(vwrite, dst, buf, copylen) != copylen)
459				goto end;
460			if (!ep)
461				break;
462			/* Skip comment. */
463			len -= ep - buf + 1;
464			buf = ep + 1;
465		}
466		if (atomicio(vwrite, dst, "\n", 1) != 1)
467			goto end;
468	}
469	rval = 0;
470 end:
471	saved_errno = errno;
472	free(lbuf);
473	fclose(fp);
474	errno = saved_errno;
475	return (rval);
476}
477
478/*
479 * Does line start with `s' and end with non-comment and non-whitespace?
480 * Note: Does not treat `line' as a C string.
481 */
482int
483matchline(const char *s, const char *line, size_t linelen)
484{
485	size_t slen;
486	int iscomment;
487
488	slen = strlen(s);
489	/* Is line shorter than string? */
490	if (linelen <= slen)
491		return (0);
492	/* Does line start with string? */
493	if (memcmp(line, s, slen) != 0)
494		return (0);
495	/* Does line contain anything but comments and whitespace? */
496	line += slen;
497	linelen -= slen;
498	iscomment = 0;
499	while (linelen) {
500		if (iscomment) {
501			if (*line == '>')
502				iscomment = 0;
503		} else if (*line == '<')
504			iscomment = 1;
505		else if (!isspace((unsigned char)*line))
506			return (1);
507		++line;
508		--linelen;
509	}
510	return (0);
511}
512
513/*
514 * Are all required fields filled out?
515 */
516void
517checkfile(const char *pathname)
518{
519	FILE *fp;
520	size_t len;
521	int category = 0, synopsis = 0, subject = 0;
522	char *buf;
523
524	if ((fp = fopen(pathname, "r")) == NULL) {
525		warn("%s", pathname);
526		return;
527	}
528	while ((buf = fgetln(fp, &len))) {
529		if (matchline(">Category:", buf, len))
530			category = 1;
531		else if (matchline(">Synopsis:", buf, len))
532			synopsis = 1;
533		else if (matchline("Subject:", buf, len))
534			subject = 1;
535	}
536	fclose(fp);
537	if (!category || !synopsis || !subject) {
538		fprintf(stderr, "Some fields are blank, please fill them in: ");
539		if (!subject)
540			fprintf(stderr, "Subject ");
541		if (!synopsis)
542			fprintf(stderr, "Synopsis ");
543		if (!category)
544			fprintf(stderr, "Category ");
545		fputc('\n', stderr);
546	}
547}
548
549void
550template(FILE *fp)
551{
552	fprintf(fp, "SENDBUG: -*- sendbug -*-\n");
553	fprintf(fp, "SENDBUG: Lines starting with `SENDBUG' will"
554	    " be removed automatically.\n");
555	fprintf(fp, "SENDBUG:\n");
556	fprintf(fp, "SENDBUG: Choose from the following categories:\n");
557	fprintf(fp, "SENDBUG:\n");
558	fprintf(fp, "SENDBUG: %s\n", categories);
559	fprintf(fp, "SENDBUG:\n");
560	fprintf(fp, "SENDBUG:\n");
561	fprintf(fp, "To: %s\n", "bugs@openbsd.org");
562	fprintf(fp, "Subject: \n");
563	fprintf(fp, "From: %s\n", pw->pw_name);
564	fprintf(fp, "Cc: %s\n", pw->pw_name);
565	fprintf(fp, "Reply-To: %s\n", pw->pw_name);
566	fprintf(fp, "\n");
567	fprintf(fp, ">Synopsis:\t%s\n", comment[0]);
568	fprintf(fp, ">Category:\t%s\n", comment[1]);
569	fprintf(fp, ">Environment:\n");
570	fprintf(fp, "\tSystem      : %s %s\n", os, rel);
571	fprintf(fp, "\tDetails     : %s\n", details);
572	fprintf(fp, "\tArchitecture: %s.%s\n", os, mach);
573	fprintf(fp, "\tMachine     : %s\n", mach);
574	fprintf(fp, ">Description:\n");
575	fprintf(fp, "\t%s\n", comment[2]);
576	fprintf(fp, ">How-To-Repeat:\n");
577	fprintf(fp, "\t%s\n", comment[3]);
578	fprintf(fp, ">Fix:\n");
579	fprintf(fp, "\t%s\n", comment[4]);
580
581	if (!Dflag) {
582		int root;
583
584		fprintf(fp, "\n");
585		root = !geteuid();
586		if (!root)
587			fprintf(fp, "SENDBUG: Run sendbug as root "
588			    "if this is an ACPI report!\n");
589		fprintf(fp, "SENDBUG: dmesg%s and usbdevs are attached.\n"
590		    "SENDBUG: Feel free to delete or use the -D flag if they "
591		    "contain sensitive information.\n",
592		    root ? ", pcidump, acpidump" : "");
593		fputs("\ndmesg:\n", fp);
594		dmesg(fp);
595		fputs("\nusbdevs:\n", fp);
596		usbdevs(fp);
597		if (root)
598			hwdump(fp);
599	}
600}
601
602void
603hwdump(FILE *ofp)
604{
605	char buf[BUFSIZ];
606	FILE *ifp;
607	char *cmd, *acpidir;
608	size_t len;
609
610	if (asprintf(&acpidir, "%s%sp.XXXXXXXXXX", tmpdir,
611	    tmpdir[strlen(tmpdir) - 1] == '/' ? "" : "/") == -1)
612		err(1, "asprintf");
613	if (mkdtemp(acpidir) == NULL)
614		err(1, "mkdtemp");
615
616	if (asprintf(&cmd, "echo \"\\npcidump:\"; pcidump -xxv; "
617	    "echo \"\\nacpidump:\"; cd %s && cp /var/db/acpi/* .; "
618	    "for i in *; do b64encode $i $i; done; rm -rf %s",
619	    acpidir, acpidir) == -1)
620		err(1, "asprintf");
621
622	if ((ifp = popen(cmd, "r")) != NULL) {
623		while (!feof(ifp)) {
624			len = fread(buf, 1, sizeof buf, ifp);
625			if (len == 0)
626				break;
627			if (fwrite(buf, 1, len, ofp) != len)
628				break;
629		}
630		pclose(ifp);
631	}
632	free(cmd);
633	free(acpidir);
634}
635
636void
637debase(void)
638{
639	char buf[BUFSIZ];
640	FILE *fp = NULL;
641	size_t len;
642
643	while (fgets(buf, sizeof(buf), stdin) != NULL) {
644		len = strlen(buf);
645		if (!strncmp(buf, BEGIN64, sizeof(BEGIN64) - 1)) {
646			if (fp)
647				errx(1, "double begin");
648			fp = popen("b64decode", "w");
649			if (!fp)
650				errx(1, "popen b64decode");
651		}
652		if (fp && fwrite(buf, 1, len, fp) != len)
653			errx(1, "pipe error");
654		if (!strncmp(buf, END64, sizeof(END64) - 1)) {
655			if (pclose(fp) == -1)
656				errx(1, "pclose b64decode");
657			fp = NULL;
658		}
659	}
660}
661