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