xref: /openbsd-src/usr.bin/sendbug/sendbug.c (revision c952560c19fc30c0537a7a9a9a0f740af57d0bcc)
1 /*	$OpenBSD: sendbug.c,v 1.53 2008/01/03 03:19:36 ray Exp $	*/
2 
3 /*
4  * Written by Ray Lai <ray@cyth.net>.
5  * Public domain.
6  */
7 
8 #include <sys/types.h>
9 #include <sys/param.h>
10 #include <sys/stat.h>
11 #include <sys/sysctl.h>
12 #include <sys/wait.h>
13 
14 #include <ctype.h>
15 #include <err.h>
16 #include <errno.h>
17 #include <fcntl.h>
18 #include <limits.h>
19 #include <paths.h>
20 #include <pwd.h>
21 #include <signal.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <unistd.h>
26 
27 #include "atomicio.h"
28 
29 #define _PATH_DMESG "/var/run/dmesg.boot"
30 #define DMESG_START "OpenBSD "
31 
32 int	checkfile(const char *);
33 void	dmesg(FILE *);
34 int	editit(const char *);
35 void	init(void);
36 int	matchline(const char *, const char *, size_t);
37 int	prompt(void);
38 int	send_file(const char *, int);
39 int	sendmail(const char *);
40 void	template(FILE *);
41 
42 const char *categories = "system user library documentation ports kernel "
43     "alpha amd64 arm i386 m68k m88k mips ppc sgi sparc sparc64 vax";
44 char *version = "4.2";
45 const char *comment[] = {
46 	"<synopsis of the problem (one line)>",
47 	"<[ non-critical | serious | critical ] (one line)>",
48 	"<[ low | medium | high ] (one line)>",
49 	"<PR category (one line)>",
50 	"<[ sw-bug | doc-bug | change-request | support ] (one line)>",
51 	"<release number or tag (one line)>",
52 	"<machine, os, target, libraries (multiple lines)>",
53 	"<precise description of the problem (multiple lines)>",
54 	"<code/input/activities to reproduce the problem (multiple lines)>",
55 	"<how to correct or work around the problem, if known (multiple lines)>"
56 };
57 
58 struct passwd *pw;
59 char os[BUFSIZ], rel[BUFSIZ], mach[BUFSIZ], details[BUFSIZ];
60 char *fullname, *tmppath;
61 int Dflag, wantcleanup;
62 
63 __dead void
64 usage(void)
65 {
66 	extern char *__progname;
67 
68 	fprintf(stderr, "usage: %s [-DLPV]\n", __progname);
69 	exit(1);
70 }
71 
72 void
73 cleanup()
74 {
75 	if (wantcleanup && tmppath && unlink(tmppath) == -1)
76 		warn("unlink");
77 }
78 
79 
80 int
81 main(int argc, char *argv[])
82 {
83 	int ch, c, fd, ret = 1;
84 	const char *tmpdir;
85 	struct stat sb;
86 	char *pr_form;
87 	time_t mtime;
88 	FILE *fp;
89 
90 	while ((ch = getopt(argc, argv, "DLPV")) != -1)
91 		switch (ch) {
92 		case 'D':
93 			Dflag = 1;
94 			break;
95 		case 'L':
96 			printf("Known categories:\n");
97 			printf("%s\n\n", categories);
98 			exit(0);
99 		case 'P':
100 			init();
101 			template(stdout);
102 			exit(0);
103 		case 'V':
104 			printf("%s\n", version);
105 			exit(0);
106 		default:
107 			usage();
108 		}
109 	argc -= optind;
110 	argv += optind;
111 
112 	if (argc > 0)
113 		usage();
114 
115 	if ((tmpdir = getenv("TMPDIR")) == NULL || tmpdir[0] == '\0')
116 		tmpdir = _PATH_TMP;
117 	if (asprintf(&tmppath, "%s%sp.XXXXXXXXXX", tmpdir,
118 	    tmpdir[strlen(tmpdir) - 1] == '/' ? "" : "/") == -1)
119 		err(1, "asprintf");
120 	if ((fd = mkstemp(tmppath)) == -1)
121 		err(1, "mkstemp");
122 	wantcleanup = 1;
123 	atexit(cleanup);
124 	if ((fp = fdopen(fd, "w+")) == NULL)
125 		err(1, "fdopen");
126 
127 	init();
128 
129 	pr_form = getenv("PR_FORM");
130 	if (pr_form) {
131 		char buf[BUFSIZ];
132 		size_t len;
133 		FILE *frfp;
134 
135 		frfp = fopen(pr_form, "r");
136 		if (frfp == NULL) {
137 			warn("can't seem to read your template file "
138 			    "(`%s'), ignoring PR_FORM", pr_form);
139 			template(fp);
140 		} else {
141 			while (!feof(frfp)) {
142 				len = fread(buf, 1, sizeof buf, frfp);
143 				if (len == 0)
144 					break;
145 				if (fwrite(buf, 1, len, fp) != len)
146 					break;
147 			}
148 			fclose(frfp);
149 		}
150 	} else
151 		template(fp);
152 
153 	if (fflush(fp) == EOF || fstat(fd, &sb) == -1 || fclose(fp) == EOF)
154 		err(1, "error creating template");
155 	mtime = sb.st_mtime;
156 
157  edit:
158 	if (editit(tmppath) == -1)
159 		err(1, "error running editor");
160 
161 	if (stat(tmppath, &sb) == -1)
162 		err(1, "stat");
163 	if (mtime == sb.st_mtime)
164 		errx(1, "report unchanged, nothing sent");
165 
166  prompt:
167 	if (!checkfile(tmppath))
168 		fprintf(stderr, "fields are blank, must be filled in\n");
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;
186 quit:
187 	return (ret);
188 }
189 
190 void
191 dmesg(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 	fputs("\n"
204 	    "SENDBUG: dmesg is attached.\n"
205 	    "SENDBUG: Feel free to delete or use the -D flag if it contains "
206 	    "sensitive information.\n", fp);
207 	/* Find last dmesg. */
208 	for (;;) {
209 		off_t o;
210 
211 		o = ftello(dfp);
212 		if (fgets(buf, sizeof(buf), dfp) == NULL)
213 			break;
214 		if (!strncmp(DMESG_START, buf, sizeof(DMESG_START) - 1))
215 			offset = o;
216 	}
217 	if (offset != -1) {
218 		size_t len;
219 
220 		clearerr(dfp);
221 		fseeko(dfp, offset, SEEK_SET);
222 		while (offset != -1 && !feof(dfp)) {
223 			len = fread(buf, 1, sizeof buf, dfp);
224 			if (len == 0)
225 				break;
226 			if (fwrite(buf, 1, len, fp) != len)
227 				break;
228 		}
229 	}
230 	fclose(dfp);
231 }
232 
233 /*
234  * Execute an editor on the specified pathname, which is interpreted
235  * from the shell.  This means flags may be included.
236  *
237  * Returns -1 on error, or the exit value on success.
238  */
239 int
240 editit(const char *pathname)
241 {
242 	char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p;
243 	sig_t sighup, sigint, sigquit, sigchld;
244 	pid_t pid;
245 	int saved_errno, st, ret = -1;
246 
247 	ed = getenv("VISUAL");
248 	if (ed == NULL || ed[0] == '\0')
249 		ed = getenv("EDITOR");
250 	if (ed == NULL || ed[0] == '\0')
251 		ed = _PATH_VI;
252 	if (asprintf(&p, "%s %s", ed, pathname) == -1)
253 		return (-1);
254 	argp[2] = p;
255 
256 	sighup = signal(SIGHUP, SIG_IGN);
257 	sigint = signal(SIGINT, SIG_IGN);
258 	sigquit = signal(SIGQUIT, SIG_IGN);
259 	sigchld = signal(SIGCHLD, SIG_DFL);
260 	if ((pid = fork()) == -1)
261 		goto fail;
262 	if (pid == 0) {
263 		execv(_PATH_BSHELL, argp);
264 		_exit(127);
265 	}
266 	while (waitpid(pid, &st, 0) == -1)
267 		if (errno != EINTR)
268 			goto fail;
269 	if (!WIFEXITED(st))
270 		errno = EINTR;
271 	else
272 		ret = WEXITSTATUS(st);
273 
274  fail:
275 	saved_errno = errno;
276 	(void)signal(SIGHUP, sighup);
277 	(void)signal(SIGINT, sigint);
278 	(void)signal(SIGQUIT, sigquit);
279 	(void)signal(SIGCHLD, sigchld);
280 	free(p);
281 	errno = saved_errno;
282 	return (ret);
283 }
284 
285 int
286 prompt(void)
287 {
288 	int c, ret;
289 
290 	fpurge(stdin);
291 	fprintf(stderr, "a)bort, e)dit, or s)end: ");
292 	fflush(stderr);
293 	ret = getchar();
294 	if (ret == EOF || ret == '\n')
295 		return (ret);
296 	do {
297 		c = getchar();
298 	} while (c != EOF && c != '\n');
299 	return (ret);
300 }
301 
302 int
303 sendmail(const char *pathname)
304 {
305 	int filedes[2];
306 
307 	if (pipe(filedes) == -1) {
308 		warn("pipe: unsent report in %s", pathname);
309 		return (-1);
310 	}
311 	switch (fork()) {
312 	case -1:
313 		warn("fork error: unsent report in %s",
314 		    pathname);
315 		return (-1);
316 	case 0:
317 		close(filedes[1]);
318 		if (dup2(filedes[0], STDIN_FILENO) == -1) {
319 			warn("dup2 error: unsent report in %s",
320 			    pathname);
321 			return (-1);
322 		}
323 		close(filedes[0]);
324 		execl("/usr/sbin/sendmail", "sendmail",
325 		    "-oi", "-t", (void *)NULL);
326 		warn("sendmail error: unsent report in %s",
327 		    pathname);
328 		return (-1);
329 	default:
330 		close(filedes[0]);
331 		/* Pipe into sendmail. */
332 		if (send_file(pathname, filedes[1]) == -1) {
333 			warn("send_file error: unsent report in %s",
334 			    pathname);
335 			return (-1);
336 		}
337 		close(filedes[1]);
338 		wait(NULL);
339 		break;
340 	}
341 	return (0);
342 }
343 
344 void
345 init(void)
346 {
347 	size_t amp, len, gecoslen, namelen;
348 	int sysname[2];
349 	char ch, *cp;
350 
351 	if ((pw = getpwuid(getuid())) == NULL)
352 		err(1, "getpwuid");
353 	namelen = strlen(pw->pw_name);
354 
355 	/* Count number of '&'. */
356 	for (amp = 0, cp = pw->pw_gecos; *cp && *cp != ','; ++cp)
357 		if (*cp == '&')
358 			++amp;
359 
360 	/* Truncate gecos to full name. */
361 	gecoslen = cp - pw->pw_gecos;
362 	pw->pw_gecos[gecoslen] = '\0';
363 
364 	/* Expanded str = orig str - '&' chars + concatenated logins. */
365 	len = gecoslen - amp + (amp * namelen) + 1;
366 	if ((fullname = malloc(len)) == NULL)
367 		err(1, "malloc");
368 
369 	/* Upper case first char of login. */
370 	ch = pw->pw_name[0];
371 	pw->pw_name[0] = toupper((unsigned char)pw->pw_name[0]);
372 
373 	cp = pw->pw_gecos;
374 	fullname[0] = '\0';
375 	while (cp != NULL) {
376 		char *token;
377 
378 		token = strsep(&cp, "&");
379 		if (token != pw->pw_gecos &&
380 		    strlcat(fullname, pw->pw_name, len) >= len)
381 			errx(1, "truncated string");
382 		if (strlcat(fullname, token, len) >= len)
383 			errx(1, "truncated string");
384 	}
385 	/* Restore case of first char of login. */
386 	pw->pw_name[0] = ch;
387 
388 	sysname[0] = CTL_KERN;
389 	sysname[1] = KERN_OSTYPE;
390 	len = sizeof(os) - 1;
391 	if (sysctl(sysname, 2, &os, &len, NULL, 0) == -1)
392 		err(1, "sysctl");
393 
394 	sysname[0] = CTL_KERN;
395 	sysname[1] = KERN_OSRELEASE;
396 	len = sizeof(rel) - 1;
397 	if (sysctl(sysname, 2, &rel, &len, NULL, 0) == -1)
398 		err(1, "sysctl");
399 
400 	sysname[0] = CTL_KERN;
401 	sysname[1] = KERN_VERSION;
402 	len = sizeof(details) - 1;
403 	if (sysctl(sysname, 2, &details, &len, NULL, 0) == -1)
404 		err(1, "sysctl");
405 
406 	cp = strchr(details, '\n');
407 	if (cp) {
408 		cp++;
409 		if (*cp)
410 			*cp++ = '\t';
411 		if (*cp)
412 			*cp++ = '\t';
413 		if (*cp)
414 			*cp++ = '\t';
415 	}
416 
417 	sysname[0] = CTL_HW;
418 	sysname[1] = HW_MACHINE;
419 	len = sizeof(mach) - 1;
420 	if (sysctl(sysname, 2, &mach, &len, NULL, 0) == -1)
421 		err(1, "sysctl");
422 }
423 
424 int
425 send_file(const char *file, int dst)
426 {
427 	size_t len;
428 	char *buf;
429 	FILE *fp;
430 	int blank = 0, dmesg_line = 0;
431 
432 	if ((fp = fopen(file, "r")) == NULL)
433 		return (-1);
434 	while ((buf = fgetln(fp, &len))) {
435 		/* Skip lines starting with "SENDBUG". */
436 		if (len >= sizeof("SENDBUG") - 1 &&
437 		    memcmp(buf, "SENDBUG", sizeof("SENDBUG") - 1) == 0)
438 			continue;
439 		/* Are we done with the headers? */
440 		if (!blank && len == 1 && buf[0] == '\n')
441 			blank = 1;
442 		/* Have we reached the dmesg? */
443 		if (blank && !dmesg_line &&
444 		    len >= sizeof(DMESG_START) - 1 &&
445 		    memcmp(buf, DMESG_START, sizeof(DMESG_START) - 1) == 0)
446 			dmesg_line = 1;
447 		/* Skip comments between headers and dmesg. */
448 		while (len) {
449 			char *sp = NULL, *ep = NULL;
450 			size_t copylen;
451 
452 			if (blank && !dmesg_line &&
453 			    (sp = memchr(buf, '<', len)) != NULL)
454 				ep = memchr(sp, '>', len - (sp - buf + 1));
455 			/* Length of string before comment. */
456 			if (ep)
457 				copylen = sp - buf;
458 			else
459 				copylen = len;
460 			if (atomicio(vwrite, dst, buf, copylen) != copylen) {
461 				int saved_errno = errno;
462 
463 				fclose(fp);
464 				errno = saved_errno;
465 				return (-1);
466 			}
467 			if (!ep)
468 				break;
469 			/* Skip comment. */
470 			len -= ep - buf + 1;
471 			buf = ep + 1;
472 		}
473 	}
474 	fclose(fp);
475 	return (0);
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  */
482 int
483 matchline(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  */
516 int
517 checkfile(const char *pathname)
518 {
519 	FILE *fp;
520 	size_t len;
521 	int category, class, priority, release, severity, synopsis;
522 	char *buf;
523 
524 	if ((fp = fopen(pathname, "r")) == NULL) {
525 		warn("%s", pathname);
526 		return (0);
527 	}
528 	category = class = priority = release = severity = synopsis = 0;
529 	while ((buf = fgetln(fp, &len))) {
530 		if (matchline(">Category:", buf, len))
531 			category = 1;
532 		else if (matchline(">Class:", buf, len))
533 			class = 1;
534 		else if (matchline(">Priority:", buf, len))
535 			priority = 1;
536 		else if (matchline(">Release:", buf, len))
537 			release = 1;
538 		else if (matchline(">Severity:", buf, len))
539 			severity = 1;
540 		else if (matchline(">Synopsis:", buf, len))
541 			synopsis = 1;
542 	}
543 	fclose(fp);
544 	return (category && class && priority && release && severity &&
545 	    synopsis);
546 }
547 
548 void
549 template(FILE *fp)
550 {
551 	fprintf(fp, "SENDBUG: -*- sendbug -*-\n");
552 	fprintf(fp, "SENDBUG: Lines starting with `SENDBUG' will"
553 	    " be removed automatically, as\n");
554 	fprintf(fp, "SENDBUG: will all comments (text enclosed in `<' and `>').\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", "gnats@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, "X-sendbug-version: %s\n", version);
567 	fprintf(fp, "\n");
568 	fprintf(fp, "\n");
569 	fprintf(fp, ">Submitter-Id:\tnet\n");
570 	fprintf(fp, ">Originator:\t%s\n", fullname);
571 	fprintf(fp, ">Organization:\n");
572 	fprintf(fp, "net\n");
573 	fprintf(fp, ">Synopsis:\t%s\n", comment[0]);
574 	fprintf(fp, ">Severity:\t%s\n", comment[1]);
575 	fprintf(fp, ">Priority:\t%s\n", comment[2]);
576 	fprintf(fp, ">Category:\t%s\n", comment[3]);
577 	fprintf(fp, ">Class:\t\t%s\n", comment[4]);
578 	fprintf(fp, ">Release:\t%s\n", comment[5]);
579 	fprintf(fp, ">Environment:\n");
580 	fprintf(fp, "\t%s\n", comment[6]);
581 	fprintf(fp, "\tSystem      : %s %s\n", os, rel);
582 	fprintf(fp, "\tDetails     : %s\n", details);
583 	fprintf(fp, "\tArchitecture: %s.%s\n", os, mach);
584 	fprintf(fp, "\tMachine     : %s\n", mach);
585 	fprintf(fp, ">Description:\n");
586 	fprintf(fp, "\t%s\n", comment[7]);
587 	fprintf(fp, ">How-To-Repeat:\n");
588 	fprintf(fp, "\t%s\n", comment[8]);
589 	fprintf(fp, ">Fix:\n");
590 	fprintf(fp, "\t%s\n", comment[9]);
591 
592 	if (!Dflag)
593 		dmesg(fp);
594 }
595