xref: /openbsd-src/usr.bin/sendbug/sendbug.c (revision a28daedfc357b214be5c701aa8ba8adb29a7f1c2)
1 /*	$OpenBSD: sendbug.c,v 1.60 2009/01/28 20:43:24 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 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 	"<PR category (one line)>",
48 	"<precise description of the problem (multiple lines)>",
49 	"<code/input/activities to reproduce the problem (multiple lines)>",
50 	"<how to correct or work around the problem, if known (multiple lines)>"
51 };
52 
53 struct passwd *pw;
54 char os[BUFSIZ], rel[BUFSIZ], mach[BUFSIZ], details[BUFSIZ];
55 char *fullname, *tmppath;
56 int Dflag, Pflag, wantcleanup;
57 
58 __dead void
59 usage(void)
60 {
61 	extern char *__progname;
62 
63 	fprintf(stderr, "usage: %s [-DPV]\n", __progname);
64 	exit(1);
65 }
66 
67 void
68 cleanup()
69 {
70 	if (wantcleanup && tmppath && unlink(tmppath) == -1)
71 		warn("unlink");
72 }
73 
74 
75 int
76 main(int argc, char *argv[])
77 {
78 	int ch, c, fd, ret = 1;
79 	const char *tmpdir;
80 	struct stat sb;
81 	char *pr_form;
82 	time_t mtime;
83 	FILE *fp;
84 
85 	while ((ch = getopt(argc, argv, "DPV")) != -1)
86 		switch (ch) {
87 		case 'D':
88 			Dflag = 1;
89 			break;
90 		case 'P':
91 			Pflag = 1;
92 			break;
93 		case 'V':
94 			printf("%s\n", version);
95 			exit(0);
96 		default:
97 			usage();
98 		}
99 	argc -= optind;
100 	argv += optind;
101 
102 	if (argc > 0)
103 		usage();
104 
105 	if (Pflag) {
106 		init();
107 		template(stdout);
108 		exit(0);
109 	}
110 
111 	if ((tmpdir = getenv("TMPDIR")) == NULL || tmpdir[0] == '\0')
112 		tmpdir = _PATH_TMP;
113 	if (asprintf(&tmppath, "%s%sp.XXXXXXXXXX", tmpdir,
114 	    tmpdir[strlen(tmpdir) - 1] == '/' ? "" : "/") == -1)
115 		err(1, "asprintf");
116 	if ((fd = mkstemp(tmppath)) == -1)
117 		err(1, "mkstemp");
118 	wantcleanup = 1;
119 	atexit(cleanup);
120 	if ((fp = fdopen(fd, "w+")) == NULL)
121 		err(1, "fdopen");
122 
123 	init();
124 
125 	pr_form = getenv("PR_FORM");
126 	if (pr_form) {
127 		char buf[BUFSIZ];
128 		size_t len;
129 		FILE *frfp;
130 
131 		frfp = fopen(pr_form, "r");
132 		if (frfp == NULL) {
133 			warn("can't seem to read your template file "
134 			    "(`%s'), ignoring PR_FORM", pr_form);
135 			template(fp);
136 		} else {
137 			while (!feof(frfp)) {
138 				len = fread(buf, 1, sizeof buf, frfp);
139 				if (len == 0)
140 					break;
141 				if (fwrite(buf, 1, len, fp) != len)
142 					break;
143 			}
144 			fclose(frfp);
145 		}
146 	} else
147 		template(fp);
148 
149 	if (fflush(fp) == EOF || fstat(fd, &sb) == -1 || fclose(fp) == EOF)
150 		err(1, "error creating template");
151 	mtime = sb.st_mtime;
152 
153  edit:
154 	if (editit(tmppath) == -1)
155 		err(1, "error running editor");
156 
157 	if (stat(tmppath, &sb) == -1)
158 		err(1, "stat");
159 	if (mtime == sb.st_mtime)
160 		errx(1, "report unchanged, nothing sent");
161 
162  prompt:
163 	if (!checkfile(tmppath))
164 		fprintf(stderr, "fields are blank, must be filled in\n");
165 	c = prompt();
166 	switch (c) {
167 	case 'a':
168 	case EOF:
169 		wantcleanup = 0;
170 		errx(1, "unsent report in %s", tmppath);
171 	case 'e':
172 		goto edit;
173 	case 's':
174 		if (sendmail(tmppath) == -1)
175 			goto quit;
176 		break;
177 	default:
178 		goto prompt;
179 	}
180 
181 	ret = 0;
182 quit:
183 	return (ret);
184 }
185 
186 void
187 dmesg(FILE *fp)
188 {
189 	char buf[BUFSIZ];
190 	FILE *dfp;
191 	off_t offset = -1;
192 
193 	dfp = fopen(_PATH_DMESG, "r");
194 	if (dfp == NULL) {
195 		warn("can't read dmesg");
196 		return;
197 	}
198 
199 	fputs("\n"
200 	    "SENDBUG: dmesg is attached.\n"
201 	    "SENDBUG: Feel free to delete or use the -D flag if it contains "
202 	    "sensitive information.\n", fp);
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 
229 /*
230  * Execute an editor on the specified pathname, which is interpreted
231  * from the shell.  This means flags may be included.
232  *
233  * Returns -1 on error, or the exit value on success.
234  */
235 int
236 editit(const char *pathname)
237 {
238 	char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p;
239 	sig_t sighup, sigint, sigquit, sigchld;
240 	pid_t pid;
241 	int saved_errno, st, ret = -1;
242 
243 	ed = getenv("VISUAL");
244 	if (ed == NULL || ed[0] == '\0')
245 		ed = getenv("EDITOR");
246 	if (ed == NULL || ed[0] == '\0')
247 		ed = _PATH_VI;
248 	if (asprintf(&p, "%s %s", ed, pathname) == -1)
249 		return (-1);
250 	argp[2] = p;
251 
252 	sighup = signal(SIGHUP, SIG_IGN);
253 	sigint = signal(SIGINT, SIG_IGN);
254 	sigquit = signal(SIGQUIT, SIG_IGN);
255 	sigchld = signal(SIGCHLD, SIG_DFL);
256 	if ((pid = fork()) == -1)
257 		goto fail;
258 	if (pid == 0) {
259 		execv(_PATH_BSHELL, argp);
260 		_exit(127);
261 	}
262 	while (waitpid(pid, &st, 0) == -1)
263 		if (errno != EINTR)
264 			goto fail;
265 	if (!WIFEXITED(st))
266 		errno = EINTR;
267 	else
268 		ret = WEXITSTATUS(st);
269 
270  fail:
271 	saved_errno = errno;
272 	(void)signal(SIGHUP, sighup);
273 	(void)signal(SIGINT, sigint);
274 	(void)signal(SIGQUIT, sigquit);
275 	(void)signal(SIGCHLD, sigchld);
276 	free(p);
277 	errno = saved_errno;
278 	return (ret);
279 }
280 
281 int
282 prompt(void)
283 {
284 	int c, ret;
285 
286 	fpurge(stdin);
287 	fprintf(stderr, "a)bort, e)dit, or s)end: ");
288 	fflush(stderr);
289 	ret = getchar();
290 	if (ret == EOF || ret == '\n')
291 		return (ret);
292 	do {
293 		c = getchar();
294 	} while (c != EOF && c != '\n');
295 	return (ret);
296 }
297 
298 int
299 sendmail(const char *pathname)
300 {
301 	int filedes[2];
302 
303 	if (pipe(filedes) == -1) {
304 		warn("pipe: unsent report in %s", pathname);
305 		return (-1);
306 	}
307 	switch (fork()) {
308 	case -1:
309 		warn("fork error: unsent report in %s",
310 		    pathname);
311 		return (-1);
312 	case 0:
313 		close(filedes[1]);
314 		if (dup2(filedes[0], STDIN_FILENO) == -1) {
315 			warn("dup2 error: unsent report in %s",
316 			    pathname);
317 			return (-1);
318 		}
319 		close(filedes[0]);
320 		execl(_PATH_SENDMAIL, "sendmail",
321 		    "-oi", "-t", (void *)NULL);
322 		warn("sendmail error: unsent report in %s",
323 		    pathname);
324 		return (-1);
325 	default:
326 		close(filedes[0]);
327 		/* Pipe into sendmail. */
328 		if (send_file(pathname, filedes[1]) == -1) {
329 			warn("send_file error: unsent report in %s",
330 			    pathname);
331 			return (-1);
332 		}
333 		close(filedes[1]);
334 		wait(NULL);
335 		break;
336 	}
337 	return (0);
338 }
339 
340 void
341 init(void)
342 {
343 	size_t amp, len, gecoslen, namelen;
344 	int sysname[2];
345 	char ch, *cp;
346 
347 	if ((pw = getpwuid(getuid())) == NULL)
348 		err(1, "getpwuid");
349 	namelen = strlen(pw->pw_name);
350 
351 	/* Count number of '&'. */
352 	for (amp = 0, cp = pw->pw_gecos; *cp && *cp != ','; ++cp)
353 		if (*cp == '&')
354 			++amp;
355 
356 	/* Truncate gecos to full name. */
357 	gecoslen = cp - pw->pw_gecos;
358 	pw->pw_gecos[gecoslen] = '\0';
359 
360 	/* Expanded str = orig str - '&' chars + concatenated logins. */
361 	len = gecoslen - amp + (amp * namelen) + 1;
362 	if ((fullname = malloc(len)) == NULL)
363 		err(1, "malloc");
364 
365 	/* Upper case first char of login. */
366 	ch = pw->pw_name[0];
367 	pw->pw_name[0] = toupper((unsigned char)pw->pw_name[0]);
368 
369 	cp = pw->pw_gecos;
370 	fullname[0] = '\0';
371 	while (cp != NULL) {
372 		char *token;
373 
374 		token = strsep(&cp, "&");
375 		if (token != pw->pw_gecos &&
376 		    strlcat(fullname, pw->pw_name, len) >= len)
377 			errx(1, "truncated string");
378 		if (strlcat(fullname, token, len) >= len)
379 			errx(1, "truncated string");
380 	}
381 	/* Restore case of first char of login. */
382 	pw->pw_name[0] = ch;
383 
384 	sysname[0] = CTL_KERN;
385 	sysname[1] = KERN_OSTYPE;
386 	len = sizeof(os) - 1;
387 	if (sysctl(sysname, 2, &os, &len, NULL, 0) == -1)
388 		err(1, "sysctl");
389 
390 	sysname[0] = CTL_KERN;
391 	sysname[1] = KERN_OSRELEASE;
392 	len = sizeof(rel) - 1;
393 	if (sysctl(sysname, 2, &rel, &len, NULL, 0) == -1)
394 		err(1, "sysctl");
395 
396 	sysname[0] = CTL_KERN;
397 	sysname[1] = KERN_VERSION;
398 	len = sizeof(details) - 1;
399 	if (sysctl(sysname, 2, &details, &len, NULL, 0) == -1)
400 		err(1, "sysctl");
401 
402 	cp = strchr(details, '\n');
403 	if (cp) {
404 		cp++;
405 		if (*cp)
406 			*cp++ = '\t';
407 		if (*cp)
408 			*cp++ = '\t';
409 		if (*cp)
410 			*cp++ = '\t';
411 	}
412 
413 	sysname[0] = CTL_HW;
414 	sysname[1] = HW_MACHINE;
415 	len = sizeof(mach) - 1;
416 	if (sysctl(sysname, 2, &mach, &len, NULL, 0) == -1)
417 		err(1, "sysctl");
418 }
419 
420 int
421 send_file(const char *file, int dst)
422 {
423 	size_t len;
424 	char *buf, *lbuf;
425 	FILE *fp;
426 	int rval = -1, saved_errno;
427 
428 	if ((fp = fopen(file, "r")) == NULL)
429 		return (-1);
430 	lbuf = NULL;
431 	while ((buf = fgetln(fp, &len))) {
432 		if (buf[len - 1] == '\n') {
433 			buf[len - 1] = '\0';
434 			--len;
435 		} else {
436 			/* EOF without EOL, copy and add the NUL */
437 			if ((lbuf = malloc(len + 1)) == NULL)
438 				goto end;
439 			memcpy(lbuf, buf, len);
440 			lbuf[len] = '\0';
441 			buf = lbuf;
442 		}
443 
444 		/* Skip lines starting with "SENDBUG". */
445 		if (strncmp(buf, "SENDBUG", sizeof("SENDBUG") - 1) == 0)
446 			continue;
447 		while (len) {
448 			char *sp = NULL, *ep = NULL;
449 			size_t copylen;
450 
451 			if ((sp = strchr(buf, '<')) != NULL) {
452 				size_t i;
453 
454 				for (i = 0; i < sizeof(comment) / sizeof(*comment); ++i) {
455 					size_t commentlen = strlen(comment[i]);
456 
457 					if (strncmp(sp, comment[i], commentlen) == 0) {
458 						ep = sp + commentlen - 1;
459 						break;
460 					}
461 				}
462 			}
463 			/* Length of string before comment. */
464 			if (ep)
465 				copylen = sp - buf;
466 			else
467 				copylen = len;
468 			if (atomicio(vwrite, dst, buf, copylen) != copylen)
469 				goto end;
470 			if (!ep)
471 				break;
472 			/* Skip comment. */
473 			len -= ep - buf + 1;
474 			buf = ep + 1;
475 		}
476 		if (atomicio(vwrite, dst, "\n", 1) != 1)
477 			goto end;
478 	}
479 	rval = 0;
480  end:
481 	saved_errno = errno;
482 	free(lbuf);
483 	fclose(fp);
484 	errno = saved_errno;
485 	return (rval);
486 }
487 
488 /*
489  * Does line start with `s' and end with non-comment and non-whitespace?
490  * Note: Does not treat `line' as a C string.
491  */
492 int
493 matchline(const char *s, const char *line, size_t linelen)
494 {
495 	size_t slen;
496 	int iscomment;
497 
498 	slen = strlen(s);
499 	/* Is line shorter than string? */
500 	if (linelen <= slen)
501 		return (0);
502 	/* Does line start with string? */
503 	if (memcmp(line, s, slen) != 0)
504 		return (0);
505 	/* Does line contain anything but comments and whitespace? */
506 	line += slen;
507 	linelen -= slen;
508 	iscomment = 0;
509 	while (linelen) {
510 		if (iscomment) {
511 			if (*line == '>')
512 				iscomment = 0;
513 		} else if (*line == '<')
514 			iscomment = 1;
515 		else if (!isspace((unsigned char)*line))
516 			return (1);
517 		++line;
518 		--linelen;
519 	}
520 	return (0);
521 }
522 
523 /*
524  * Are all required fields filled out?
525  */
526 int
527 checkfile(const char *pathname)
528 {
529 	FILE *fp;
530 	size_t len;
531 	int category = 0, synopsis = 0;
532 	char *buf;
533 
534 	if ((fp = fopen(pathname, "r")) == NULL) {
535 		warn("%s", pathname);
536 		return (0);
537 	}
538 	while ((buf = fgetln(fp, &len))) {
539 		if (matchline(">Category:", buf, len))
540 			category = 1;
541 		else if (matchline(">Synopsis:", buf, len))
542 			synopsis = 1;
543 	}
544 	fclose(fp);
545 	return (category && 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.\n");
554 	fprintf(fp, "SENDBUG:\n");
555 	fprintf(fp, "SENDBUG: Choose from the following categories:\n");
556 	fprintf(fp, "SENDBUG:\n");
557 	fprintf(fp, "SENDBUG: %s\n", categories);
558 	fprintf(fp, "SENDBUG:\n");
559 	fprintf(fp, "SENDBUG:\n");
560 	fprintf(fp, "To: %s\n", "gnats@openbsd.org");
561 	fprintf(fp, "Subject: \n");
562 	fprintf(fp, "From: %s\n", pw->pw_name);
563 	fprintf(fp, "Cc: %s\n", pw->pw_name);
564 	fprintf(fp, "Reply-To: %s\n", pw->pw_name);
565 	fprintf(fp, "\n");
566 	fprintf(fp, ">Synopsis:\t%s\n", comment[0]);
567 	fprintf(fp, ">Category:\t%s\n", comment[1]);
568 	fprintf(fp, ">Environment:\n");
569 	fprintf(fp, "\tSystem      : %s %s\n", os, rel);
570 	fprintf(fp, "\tDetails     : %s\n", details);
571 	fprintf(fp, "\tArchitecture: %s.%s\n", os, mach);
572 	fprintf(fp, "\tMachine     : %s\n", mach);
573 	fprintf(fp, ">Description:\n");
574 	fprintf(fp, "\t%s\n", comment[2]);
575 	fprintf(fp, ">How-To-Repeat:\n");
576 	fprintf(fp, "\t%s\n", comment[3]);
577 	fprintf(fp, ">Fix:\n");
578 	fprintf(fp, "\t%s\n", comment[4]);
579 
580 	if (!Dflag)
581 		dmesg(fp);
582 }
583