xref: /openbsd-src/usr.bin/sendbug/sendbug.c (revision 62f9cc1070587911a3e9327846ad1f5b778eba0f)
1 /*	$OpenBSD: sendbug.c,v 1.57 2008/06/14 20:45:45 pvalchev 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 kernel "
43     "alpha amd64 arm hppa i386 m68k m88k mips64 powerpc sh 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(_PATH_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, *lbuf;
429 	FILE *fp;
430 	int rval = -1, saved_errno;
431 
432 	if ((fp = fopen(file, "r")) == NULL)
433 		return (-1);
434 	lbuf = NULL;
435 	while ((buf = fgetln(fp, &len))) {
436 		if (buf[len - 1] == '\n') {
437 			buf[len - 1] = '\0';
438 			--len;
439 		} else {
440 			/* EOF without EOL, copy and add the NUL */
441 			if ((lbuf = malloc(len + 1)) == NULL)
442 				goto end;
443 			memcpy(lbuf, buf, len);
444 			lbuf[len] = '\0';
445 			buf = lbuf;
446 		}
447 
448 		/* Skip lines starting with "SENDBUG". */
449 		if (strncmp(buf, "SENDBUG", sizeof("SENDBUG") - 1) == 0)
450 			continue;
451 		while (len) {
452 			char *sp = NULL, *ep = NULL;
453 			size_t copylen;
454 
455 			if ((sp = strchr(buf, '<')) != NULL) {
456 				size_t i;
457 
458 				for (i = 0; i < sizeof(comment) / sizeof(*comment); ++i) {
459 					size_t commentlen = strlen(comment[i]);
460 
461 					if (strncmp(sp, comment[i], commentlen) == 0) {
462 						ep = sp + commentlen - 1;
463 						break;
464 					}
465 				}
466 			}
467 			/* Length of string before comment. */
468 			if (ep)
469 				copylen = sp - buf;
470 			else
471 				copylen = len;
472 			if (atomicio(vwrite, dst, buf, copylen) != copylen)
473 				goto end;
474 			if (!ep)
475 				break;
476 			/* Skip comment. */
477 			len -= ep - buf + 1;
478 			buf = ep + 1;
479 		}
480 		if (atomicio(vwrite, dst, "\n", 1) != 1)
481 			goto end;
482 	}
483 	rval = 0;
484  end:
485 	saved_errno = errno;
486 	free(lbuf);
487 	fclose(fp);
488 	errno = saved_errno;
489 	return (rval);
490 }
491 
492 /*
493  * Does line start with `s' and end with non-comment and non-whitespace?
494  * Note: Does not treat `line' as a C string.
495  */
496 int
497 matchline(const char *s, const char *line, size_t linelen)
498 {
499 	size_t slen;
500 	int iscomment;
501 
502 	slen = strlen(s);
503 	/* Is line shorter than string? */
504 	if (linelen <= slen)
505 		return (0);
506 	/* Does line start with string? */
507 	if (memcmp(line, s, slen) != 0)
508 		return (0);
509 	/* Does line contain anything but comments and whitespace? */
510 	line += slen;
511 	linelen -= slen;
512 	iscomment = 0;
513 	while (linelen) {
514 		if (iscomment) {
515 			if (*line == '>')
516 				iscomment = 0;
517 		} else if (*line == '<')
518 			iscomment = 1;
519 		else if (!isspace((unsigned char)*line))
520 			return (1);
521 		++line;
522 		--linelen;
523 	}
524 	return (0);
525 }
526 
527 /*
528  * Are all required fields filled out?
529  */
530 int
531 checkfile(const char *pathname)
532 {
533 	FILE *fp;
534 	size_t len;
535 	int category, class, priority, release, severity, synopsis;
536 	char *buf;
537 
538 	if ((fp = fopen(pathname, "r")) == NULL) {
539 		warn("%s", pathname);
540 		return (0);
541 	}
542 	category = class = priority = release = severity = synopsis = 0;
543 	while ((buf = fgetln(fp, &len))) {
544 		if (matchline(">Category:", buf, len))
545 			category = 1;
546 		else if (matchline(">Class:", buf, len))
547 			class = 1;
548 		else if (matchline(">Priority:", buf, len))
549 			priority = 1;
550 		else if (matchline(">Release:", buf, len))
551 			release = 1;
552 		else if (matchline(">Severity:", buf, len))
553 			severity = 1;
554 		else if (matchline(">Synopsis:", buf, len))
555 			synopsis = 1;
556 	}
557 	fclose(fp);
558 	return (category && class && priority && release && severity &&
559 	    synopsis);
560 }
561 
562 void
563 template(FILE *fp)
564 {
565 	fprintf(fp, "SENDBUG: -*- sendbug -*-\n");
566 	fprintf(fp, "SENDBUG: Lines starting with `SENDBUG' will"
567 	    " be removed automatically.\n");
568 	fprintf(fp, "SENDBUG:\n");
569 	fprintf(fp, "SENDBUG: Choose from the following categories:\n");
570 	fprintf(fp, "SENDBUG:\n");
571 	fprintf(fp, "SENDBUG: %s\n", categories);
572 	fprintf(fp, "SENDBUG:\n");
573 	fprintf(fp, "SENDBUG:\n");
574 	fprintf(fp, "To: %s\n", "gnats@openbsd.org");
575 	fprintf(fp, "Subject: \n");
576 	fprintf(fp, "From: %s\n", pw->pw_name);
577 	fprintf(fp, "Cc: %s\n", pw->pw_name);
578 	fprintf(fp, "Reply-To: %s\n", pw->pw_name);
579 	fprintf(fp, "X-sendbug-version: %s\n", version);
580 	fprintf(fp, "\n");
581 	fprintf(fp, "\n");
582 	fprintf(fp, ">Submitter-Id:\tnet\n");
583 	fprintf(fp, ">Originator:\t%s\n", fullname);
584 	fprintf(fp, ">Organization:\n");
585 	fprintf(fp, "net\n");
586 	fprintf(fp, ">Synopsis:\t%s\n", comment[0]);
587 	fprintf(fp, ">Severity:\t%s\n", comment[1]);
588 	fprintf(fp, ">Priority:\t%s\n", comment[2]);
589 	fprintf(fp, ">Category:\t%s\n", comment[3]);
590 	fprintf(fp, ">Class:\t\t%s\n", comment[4]);
591 	fprintf(fp, ">Release:\t%s\n", comment[5]);
592 	fprintf(fp, ">Environment:\n");
593 	fprintf(fp, "\t%s\n", comment[6]);
594 	fprintf(fp, "\tSystem      : %s %s\n", os, rel);
595 	fprintf(fp, "\tDetails     : %s\n", details);
596 	fprintf(fp, "\tArchitecture: %s.%s\n", os, mach);
597 	fprintf(fp, "\tMachine     : %s\n", mach);
598 	fprintf(fp, ">Description:\n");
599 	fprintf(fp, "\t%s\n", comment[7]);
600 	fprintf(fp, ">How-To-Repeat:\n");
601 	fprintf(fp, "\t%s\n", comment[8]);
602 	fprintf(fp, ">Fix:\n");
603 	fprintf(fp, "\t%s\n", comment[9]);
604 
605 	if (!Dflag)
606 		dmesg(fp);
607 }
608